selectrs/
yew.rs

1use std::rc::Rc;
2use yew::prelude::*;
3
4/// Properties for configuring the `Select` component.
5///
6/// The `Select` component creates a customizable dropdown list that allows you to choose
7/// a single or multiple options. It can be styled with custom classes and inline styles,
8/// and supports additional behaviors like multiple selections, disabled options, and
9/// change events for updating the selected value.
10///
11/// It works in combination with `Group` and `Option` components to provide a rich UI for
12/// selecting options from a list.
13/// Refer to the [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/select#attributes) for more info.
14#[derive(Properties, PartialEq, Clone)]
15pub struct SelectProps {
16    /// The name of the select component.
17    ///
18    /// This represents the name attribute used in the underlying HTML `select` element.
19    /// It is important when the component is part of a form, as it defines the field name.
20    /// Defaults to an empty string if not provided.
21    #[prop_or_default]
22    pub name: &'static str,
23
24    /// The id of the select component.
25    ///
26    /// This represents the id attribute used in the underlying HTML `select` element.
27    /// It helps in uniquely identifying the component within the DOM.
28    /// Defaults to an empty string if not provided.
29    #[prop_or_default]
30    pub id: &'static str,
31
32    /// The placeholder text for the select component.
33    ///
34    /// This text is displayed when no option is selected and the `select` element is empty.
35    /// It provides a hint to the user on what to select. It is not visible after an option is chosen.
36    /// Defaults to an empty string if not provided.
37    #[prop_or_default]
38    pub placeholder: &'static str,
39
40    /// Whether the select component allows multiple selections.
41    ///
42    /// If set to `true`, the user can select more than one option. If set to `false`, only one option can be selected at a time.
43    /// Defaults to `false` if not provided.
44    #[prop_or_default]
45    pub multiple: bool,
46
47    /// Whether the select component is disabled.
48    ///
49    /// If set to `true`, the select component will be unresponsive and users will not be able to interact with it.
50    /// Defaults to `false` if not provided.
51    #[prop_or_default]
52    pub disabled: bool,
53
54    /// Whether the select option is required.
55    ///
56    /// This Boolean attribute indicates that a value must be selected from the dropdown.
57    /// If not selected, form submission will be blocked. Defaults to `false` if not provided.
58    #[prop_or_default]
59    pub required: bool,
60
61    /// The visible size of the select dropdown.
62    ///
63    /// If the `multiple` attribute is specified, this defines the number of visible rows in the list.
64    /// This is helpful when displaying a scrollable list of options.
65    /// Defaults to `0`, which means the default layout will be used.
66    #[prop_or(0)]
67    pub size: u64,
68
69    /// The form to associate the select element with.
70    ///
71    /// This attribute allows you to associate the select element with a form elsewhere in the document.
72    /// The value must be the `id` of a form element in the same document. If not provided, the `select`
73    /// element will be associated with its nearest ancestor form. Defaults to an empty string if not provided.
74    #[prop_or_default]
75    pub form: &'static str,
76
77    /// The autocomplete hint for the select element.
78    ///
79    /// This string provides a hint to the user agent's autocomplete feature, helping it to
80    /// pre-fill values based on the user's past selections. The value should match one of the
81    /// valid autocomplete values for the `<select>` element. Defaults to an empty string if not provided.
82    #[prop_or_default]
83    pub autocomplete: &'static str,
84
85    /// Automatically focuses the select element when the page loads.
86    ///
87    /// This Boolean attribute lets you specify that the select element should automatically
88    /// gain input focus when the page loads. Only one form element in a document can have this attribute.
89    /// Defaults to `false` if not provided.
90    #[prop_or_default]
91    pub autofocus: bool,
92
93    /// Callback triggered when the selected values change.
94    ///
95    /// This callback is executed whenever the user selects or deselects an option in the select box.
96    /// It receives a vector of strings representing the selected options. This is useful for updating
97    /// the selected values in the application state. Defaults to a no-op if not provided.
98    #[prop_or_default]
99    pub onchange: Callback<Vec<String>>,
100
101    /// Child components for the select component.
102    ///
103    /// This property allows you to pass one or more `Group` components as children of the `Select` component.
104    /// The `Group` components contain the `Option` components, which represent the individual selectable options.
105    /// Defaults to an empty list of children if not provided.
106    #[prop_or_default]
107    pub children: ChildrenWithProps<Group>,
108
109    /// Custom CSS class for the select container.
110    ///
111    /// This property allows for custom styling of the select container by specifying one or more CSS classes.
112    /// It is applied to the outer wrapper of the `select` element. Defaults to an empty string if not provided.
113    #[prop_or_default]
114    pub class: &'static str,
115
116    /// Inline styles for the select container.
117    ///
118    /// This property allows for custom inline styles to be applied directly to the select container.
119    /// It provides more granular control over the styling of the component, without the need for external CSS.
120    /// Defaults to an empty string if not provided.
121    #[prop_or_default]
122    pub style: &'static str,
123
124    /// Custom CSS class for the label container.
125    ///
126    /// This property allows for custom styling of the labels in the `Select` component. It applies to the wrapper
127    /// around the labels (for multi-selects or grouped selections). Defaults to an empty string if not provided.
128    #[prop_or_default]
129    pub labels_class: &'static str,
130
131    /// Inline styles for the label container.
132    ///
133    /// This property allows for custom inline styles to be applied directly to the label container. This is useful
134    /// for modifying the appearance of the labels within the select dropdown.
135    /// Defaults to an empty string if not provided.
136    #[prop_or_default]
137    pub labels_style: &'static str,
138
139    /// Custom CSS class for the individual labels.
140    ///
141    /// This property allows for custom styling of the labels within the dropdown options.
142    /// Defaults to an empty string if not provided.
143    #[prop_or_default]
144    pub label_class: &'static str,
145
146    /// Inline styles for the individual labels.
147    ///
148    /// This property allows for custom inline styles to be applied directly to the individual labels.
149    /// It can be used to adjust the style of each label element within the `select` component.
150    /// Defaults to an empty string if not provided.
151    #[prop_or_default]
152    pub label_style: &'static str,
153
154    /// Custom CSS class for the close button (for multi-select).
155    ///
156    /// This property allows for custom styling of the close button that appears next to selected values in a multi-select dropdown.
157    /// Defaults to an empty string if not provided.
158    #[prop_or_default]
159    pub close_class: &'static str,
160
161    /// Inline styles for the close button (for multi-select).
162    ///
163    /// This property allows for custom inline styles to be applied directly to the close button.
164    /// This can be used to change the appearance of the button that removes selected options in a multi-select.
165    /// Defaults to an empty string if not provided.
166    #[prop_or_default]
167    pub close_style: &'static str,
168
169    /// Custom CSS class for the select dropdown.
170    ///
171    /// This property allows for custom styling of the select dropdown box itself. This class is applied to the
172    /// `select` element in the rendered HTML. Defaults to an empty string if not provided.
173    #[prop_or_default]
174    pub select_class: &'static str,
175
176    /// Inline styles for the select dropdown.
177    ///
178    /// This property allows for custom inline styles to be applied directly to the select dropdown.
179    /// It gives more granular control over the dropdown's appearance, such as height, width, or border color.
180    /// Defaults to an empty string if not provided.
181    #[prop_or_default]
182    pub select_style: &'static str,
183}
184
185/// Select Component
186///
187/// A Yew component for creating a customizable select dropdown with support for single or multiple selections.
188/// The `Select` component can handle options, dynamically manage selection, and customize its appearance and behavior.
189///
190/// # Properties
191/// The component uses the `SelectProps` struct for its properties. Key properties include:
192///
193/// - **name**: The name of the select element (`&'static str`). Default: `""`.
194/// - **id**: The ID of the select element (`&'static str`). Default: `""`.
195/// - **placeholder**: Placeholder text for the select input when no options are selected (`&'static str`). Default: `""`.
196/// - **multiple**: Whether the select allows multiple selections (`bool`). Default: `false`.
197/// - **disabled**: Whether the select element is disabled (`bool`). Default: `false`.
198/// - **onchange**: Callback triggered when the selected values change (`Callback<Vec<String>>`). Default: no-op.
199/// - **children**: A collection of `Option` components as children (`ChildrenWithProps<Option>`). Default: empty.
200/// - **class**: Custom CSS class for the select container (`&'static str`). Default: `""`.
201/// - **style**: Inline styles for the select container (`&'static str`). Default: `""`.
202/// - **labels_class**: Custom class for the selected options' labels (`&'static str`). Default: `""`.
203/// - **labels_style**: Inline styles for the selected options' labels (`&'static str`). Default: `""`.
204/// - **label_class**: Custom class for each label when an option is selected (`&'static str`). Default: `""`.
205/// - **label_style**: Inline styles for each label when an option is selected (`&'static str`). Default: `""`.
206/// - **close_class**: Custom class for the close button (`&'static str`). Default: `""`.
207/// - **close_style**: Inline styles for the close button (`&'static str`). Default: `""`.
208/// - **select_class**: Custom CSS class for the select element itself (`&'static str`). Default: `""`.
209/// - **select_style**: Inline styles for the select element (`&'static str`). Default: `""`.
210/// - **size**: The number of visible options in a scrolling select (`usize`). Default: `0`.
211/// - **required**: Whether the select field is required (`bool`). Default: `false`.
212/// - **form**: The ID of the form that the select is associated with (`&'static str`). Default: `""`.
213/// - **autocomplete**: Hint for the browser's autocomplete feature (`&'static str`). Default: `""`.
214/// - **autofocus**: Whether the select should gain focus when the page loads (`bool`). Default: `false`.
215///
216/// # Features
217/// - Supports both single and multiple selection modes.
218/// - Customizable via CSS classes and inline styles.
219/// - Optionally displays a placeholder and manages selected items with chips (for multiple selections).
220/// - Trigger an `onchange` callback whenever the selection changes.
221///
222/// # Examples
223///
224/// ## Basic Usage
225/// ```rust
226/// use yew::prelude::*;
227/// use selectrs::yew::{Select, Option, Group};
228///
229/// #[function_component(App)]
230/// pub fn app() -> Html {
231///     let onchange = Callback::from(|selected_values: Vec<String>| {
232///         // Handle the selected values
233///         log::info!("Selected: {:?}", selected_values);
234///     });
235///
236///     html! {
237///         <Select onchange={onchange}>
238///             <Group>
239///                 <Option value="Option1" label="Option 1" />
240///                 <Option value="Option2" label="Option 2" />
241///                 <Option value="Option3" label="Option 3" />
242///             </Group>
243///         </Select>
244///     }
245/// }
246/// ```
247///
248/// ## Multiple Selection
249/// ```rust
250/// use yew::prelude::*;
251/// use selectrs::yew::{Select, Option, Group};
252///
253/// #[function_component(App)]
254/// pub fn app() -> Html {
255///     let onchange = Callback::from(|selected_values: Vec<String>| {
256///         // Handle the selected values
257///         log::info!("Selected: {:?}", selected_values);
258///     });
259///
260///     html! {
261///         <Select multiple=true onchange={onchange}>
262///             <Group>
263///                 <Option value="Option1" label="Option 1" />
264///                 <Option value="Option2" label="Option 2" />
265///                 <Option value="Option3" label="Option 3" />
266///             </Group>
267///         </Select>
268///     }
269/// }
270/// ```
271///
272/// ## Custom Styling and Placeholder
273/// ```rust
274/// use yew::prelude::*;
275/// use selectrs::yew::{Select, Option, Group};
276///
277/// #[function_component(App)]
278/// pub fn app() -> Html {
279///     html! {
280///         <Select
281///             placeholder="Select an option..."
282///             class="custom-select"
283///             style="width: 200px"
284///             size={5}
285///         >
286///             <Group>
287///                 <Option value="Option1" label="Option 1" />
288///                 <Option value="Option2" label="Option 2" />
289///             </Group>
290///         </Select>
291///     }
292/// }
293/// ```
294///
295/// ## With a Required Field
296/// ```rust
297/// use yew::prelude::*;
298/// use selectrs::yew::{Select, Option, Group};
299///
300/// #[function_component(App)]
301/// pub fn app() -> Html {
302///     html! {
303///         <Select
304///             required=true
305///             onchange={Callback::from(|selected_values: Vec<String>| {
306///                 log::info!("Selected: {:?}", selected_values);
307///             })}
308///         >
309///             <Group>
310///                 <Option value="Option1" label="Option 1" />
311///                 <Option value="Option2" label="Option 2" />
312///             </Group>
313///         </Select>
314///     }
315/// }
316/// ```
317///
318/// # Behavior
319/// - The `Select` component handles single and multiple selections dynamically.
320/// - The selected values are updated using the `onchange` callback whenever the user interacts with the select options.
321/// - When multiple selection mode is enabled, selected values are displayed as chips with a close button to remove individual selections.
322/// - A placeholder option is displayed when no value is selected and the select is not disabled.
323///
324/// # Notes
325/// - The `children` property must contain `Option` components to populate the select dropdown.
326/// - If the `multiple` property is `true`, multiple options can be selected at once. If `false`, only one option can be selected.
327/// - Custom styling can be applied to the select container, options, and labels via CSS classes or inline styles.
328#[function_component(Select)]
329pub fn select(props: &SelectProps) -> Html {
330    let SelectProps {
331        name,
332        id,
333        placeholder,
334        multiple,
335        disabled,
336        onchange,
337        children,
338        class,
339        style,
340        labels_class,
341        labels_style,
342        label_class,
343        label_style,
344        close_class,
345        close_style,
346        select_class,
347        select_style,
348        size,
349        required,
350        form,
351        autocomplete,
352        autofocus,
353    } = props.clone();
354
355    let selected_values = use_state(Vec::<String>::new);
356    let selected = (*selected_values).clone();
357
358    let handle_group_change = {
359        let selected_values = selected_values.clone();
360        let on_change = onchange.clone();
361        Callback::from(move |value: String| {
362            let mut current_values = (*selected_values).clone();
363            if multiple {
364                if current_values.contains(&value) {
365                    current_values.retain(|v| v != &value);
366                } else {
367                    current_values.push(value);
368                }
369            } else {
370                current_values = vec![value];
371            }
372            selected_values.set(current_values.clone());
373            on_change.emit(current_values);
374        })
375    };
376
377    let remove_chip = {
378        let selected_values = selected_values.clone();
379        let on_change = onchange.clone();
380        Callback::from(move |value: String| {
381            let mut current_values = (*selected_values).clone();
382            current_values.retain(|v| v != &value);
383            selected_values.set(current_values.clone());
384            on_change.emit(current_values);
385        })
386    };
387
388    html! {
389        <div class={class} style={style}>
390            { if multiple {
391                html! {
392                    <div class={labels_class} style={labels_style}>
393                        { for selected.clone().into_iter().map(|value| html! {
394                            <div class={label_class} style={label_style}>
395                                { value.clone() }
396                                <button class={close_class} style={close_style} onclick={remove_chip.clone().reform(move |_| value.clone())}>
397                                    { "x" }
398                                </button>
399                            </div>
400                        }) }
401                    </div>
402                }
403            } else {
404                html! {}
405            } }
406            <select
407                id={id}
408                name={name}
409                multiple={multiple}
410                class={select_class}
411                style={select_style}
412                disabled={disabled}
413                size={size.to_string()}
414                required={required}
415                form={form}
416                autocomplete={autocomplete}
417                autofocus={autofocus}
418            >
419                { if (!placeholder.is_empty() && selected.is_empty()) || disabled {
420                    html! { <option disabled=true selected=true>{ placeholder }</option> }
421                } else {
422                    html! {}
423                } }
424                if !disabled {
425                    { for children.iter().map(|mut child| {
426                    let props = Rc::make_mut(&mut child.props);
427
428                    props.selected = selected.first().cloned().unwrap_or_default();
429                    let handle_group_change = handle_group_change.clone();
430                    props.onchange = Callback::from(move |value| handle_group_change.emit(value));
431
432                    child
433                }) }
434                }
435            </select>
436        </div>
437    }
438}
439
440/// Properties for configuring the `Group` component.
441///
442/// The `Group` component allows you to group together `Option` elements.
443/// It provides customization for labels, selection behavior, and event handling. The group allows a common state and
444/// behavior across the contained options. This component supports customization of styles and classes, as well as
445/// interaction handling through the `onchange` callback.
446#[derive(Properties, PartialEq, Clone)]
447pub struct GroupProps {
448    /// The label for the group.
449    ///
450    /// This is the text that labels the entire group of options. This can be used to describe
451    /// the set of options the user is about to choose from, making it useful for accessibility.
452    /// Defaults to an empty string if not provided.
453    #[prop_or_default]
454    pub label: &'static str,
455
456    /// Indicates whether this is a group options.
457    ///
458    /// If `group` is set to `true`, the options in this group will be considered as part of the
459    /// `label` options group, where only one option can be selected at a time. If set to `false`, the
460    /// group is disabled. Defaults to `false` if not provided.
461    #[prop_or_default]
462    pub group: bool,
463
464    /// The currently selected option in the group.
465    ///
466    /// This represents the selected value within the group. It should be bound to a state to
467    /// reflect changes as the user selects different options. Defaults to an empty string if not provided.
468    #[prop_or_default]
469    pub selected: String,
470
471    /// Callback for when the selected option changes.
472    ///
473    /// This callback is triggered whenever the user selects a different option within the group.
474    /// The callback receives a string representing the newly selected option's value. Defaults to a no-op.
475    #[prop_or_default]
476    pub onchange: Callback<String>,
477
478    /// Child components of type `Option` for the group.
479    ///
480    /// This property allows you to pass one or more `Option` components as children of the `Group` component.
481    /// These `Option` components represent the individual selectable options within the group.
482    /// Defaults to an empty string if not provided.
483    #[prop_or_default]
484    pub children: ChildrenWithProps<Option>,
485
486    /// Custom CSS class for the group.
487    ///
488    /// This property allows for custom styling of the group container by specifying one or more CSS classes.
489    /// It is applied to the outer wrapper of the group, such as for styling the container element.
490    /// Defaults to an empty string if not provided.
491    #[prop_or_default]
492    pub class: &'static str,
493
494    /// Inline styles for the group.
495    ///
496    /// This property allows for custom inline styles to be applied directly to the group container.
497    /// It provides more granular control over the styling of the group and its elements without the need for external CSS.
498    /// Defaults to an empty string if not provided.
499    #[prop_or_default]
500    pub style: &'static str,
501}
502
503#[function_component(Group)]
504pub fn group(props: &GroupProps) -> Html {
505    let GroupProps {
506        label,
507        group,
508        selected,
509        onchange,
510        children,
511        class,
512        style,
513    } = props.clone();
514
515    if group {
516        html! {
517            <optgroup label={label} class={class} style={style}>
518                { for children.iter().map(|mut child| {
519                    let props = Rc::make_mut(&mut child.props);
520                    let is_selected = props.value == selected;
521                    let onchange = onchange.clone();
522                    let value = props.value;
523
524                    props.selected = is_selected;
525                    props.on_click = Callback::from(move |_| {
526                        onchange.emit(value.to_string());
527                    });
528
529                    child
530                }) }
531            </optgroup>
532        }
533    } else {
534        html! {
535            { for children.iter().map(|mut child| {
536                    let props = Rc::make_mut(&mut child.props);
537                    let is_selected = props.value == selected;
538                    let onchange = onchange.clone();
539                    let value = props.value;
540
541                    props.selected = is_selected;
542                    props.on_click = Callback::from(move |_| {
543                        onchange.emit(value.to_string());
544                    });
545
546                    child
547                }) }
548        }
549    }
550}
551
552/// Properties for configuring the `Option` component.
553///
554/// The `Option` component represents an individual selectable option within a group of options, such as a
555/// string, a radio button, checkbox, or a custom select element. It allows for customization of the option's value,
556/// label, selection state, and appearance. The component also supports event handling for user interactions
557/// (e.g., click events).
558#[derive(Properties, PartialEq, Clone)]
559pub struct OptionProps {
560    /// The value of the option.
561    ///
562    /// This is the underlying value associated with the option. It is typically used when the user selects
563    /// this option, and is submitted or processed based on the selected state of the option. Defaults to an
564    /// empty string if not provided.
565    #[prop_or_default]
566    pub value: &'static str,
567
568    /// The label displayed for the option.
569    ///
570    /// This property defines the content that is shown to the user as the label for the option. It can be
571    /// any valid child element, such as text, icons, or other components. Defaults to no label (empty).
572    #[prop_or_default]
573    pub label: Children,
574
575    /// Whether the option is selected.
576    ///
577    /// This property indicates if the option is currently selected. If set to `true`, the option is visually
578    /// marked as selected, and it may trigger related behavior such as updating state or submitting a form.
579    /// Defaults to `false` if not provided.
580    #[prop_or_default]
581    pub selected: bool,
582
583    /// Whether the option is disabled.
584    ///
585    /// If set to `true`, the option is considered disabled, meaning it cannot be interacted with by the user.
586    /// Disabled options may be visually different (e.g., grayed out). Defaults to `false` if not provided.
587    #[prop_or_default]
588    pub disabled: bool,
589
590    /// Callback for when the option is clicked.
591    ///
592    /// This callback is invoked when the user clicks on the option. It can be used to trigger actions such as
593    /// updating the selected state or performing any other interaction. Defaults to a no-op (no action).
594    #[prop_or_default]
595    pub on_click: Callback<()>,
596
597    /// Custom CSS class for the option.
598    ///
599    /// This property allows you to specify a custom CSS class for the option. This class is applied to the
600    /// individual option container, enabling you to style it differently from other options. Defaults to an empty
601    /// string if not provided.
602    #[prop_or_default]
603    pub class: &'static str,
604
605    /// Inline styles for the option.
606    ///
607    /// This property enables you to apply inline styles directly to the option element. It allows for precise
608    /// customization of the option's appearance without needing an external stylesheet. Defaults to an empty string
609    /// if not provided.
610    #[prop_or_default]
611    pub style: &'static str,
612
613    /// Custom class for a selected option.
614    ///
615    /// This property defines a custom CSS class that is applied when the option is selected. It enables you to
616    /// style the selected option differently, such as changing its background color or text style. Defaults to an
617    /// empty string if not provided.
618    #[prop_or_default]
619    pub selected_class: &'static str,
620
621    /// Inline styles for a selected option.
622    ///
623    /// This property defines inline styles applied when the option is selected. It provides direct control over
624    /// the selected state styling, allowing for unique visual differentiation between selected and non-selected
625    /// options. Defaults to an empty string if not provided.
626    #[prop_or_default]
627    pub selected_style: &'static str,
628}
629
630#[function_component(Option)]
631pub fn option(props: &OptionProps) -> Html {
632    let OptionProps {
633        label,
634        selected,
635        disabled,
636        on_click,
637        class,
638        style,
639        selected_style,
640        selected_class,
641        ..
642    } = props.clone();
643
644    let handle_click = {
645        let on_click = on_click.clone();
646        Callback::from(move |_| on_click.emit(()))
647    };
648
649    html! {
650        <option
651            class={format!("{} {}", class, if selected { selected_class } else { "" })}
652            style={format!("{} {}", style, if selected { selected_style } else { "" })}
653            onclick={move |ev: MouseEvent| {
654                ev.prevent_default();
655                handle_click.emit(ev);
656            }}
657            disabled={disabled}
658        >
659            { label }
660        </option>
661    }
662}