yew_components/
select.rs

1//! This module contains the implementation of the `Select` component.
2
3use web_sys::HtmlSelectElement;
4use yew::callback::Callback;
5use yew::html::{ChangeData, Component, ComponentLink, Html, NodeRef, ShouldRender};
6use yew::{html, Properties};
7
8/// An alternative to the HTML `<select>` tag.
9///
10/// The display of options is handled by the `ToString` implementation on their
11/// type.
12///
13/// # Example
14///
15/// ```
16///# use std::fmt;
17///# use yew::{Html, Component, ComponentLink, html};
18///# use yew_components::Select;
19/// #[derive(PartialEq, Clone)]
20/// enum Scene {
21///     First,
22///     Second,
23/// }
24///# struct Model { link: ComponentLink<Self> };
25///# impl Component for Model {
26///#     type Message = ();type Properties = ();
27///#     fn create(props: Self::Properties,link: ComponentLink<Self>) -> Self {unimplemented!()}
28///#     fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()}
29///#     fn change(&mut self, _: Self::Properties) -> bool {unimplemented!()}
30///#     fn view(&self) -> Html {unimplemented!()}}
31/// impl fmt::Display for Scene {
32///     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
33///         match self {
34///             Scene::First => write!(f, "{}", "First"),
35///             Scene::Second => write!(f, "{}", "Second"),
36///         }
37///     }
38/// }
39///
40/// fn view(link: ComponentLink<Model>) -> Html {
41///     let scenes = vec![Scene::First, Scene::Second];
42///     html! {
43///         <Select<Scene> options=scenes on_change=link.callback(|_| ()) />
44///     }
45/// }
46/// ```
47///
48/// # Properties
49///
50/// Only the `on_change` property is mandatory. Other (optional) properties
51/// are `selected`, `disabled`, `options`, `class`, `id`, and `placeholder`.
52#[derive(Debug)]
53pub struct Select<T: ToString + PartialEq + Clone + 'static> {
54    props: Props<T>,
55    select_ref: NodeRef,
56    link: ComponentLink<Self>,
57}
58
59/// Messages sent internally as part of the select component
60#[derive(Debug)]
61pub enum Msg {
62    /// Sent when the user selects a new option.
63    Selected(Option<usize>),
64}
65
66/// Properties of the `Select` component.
67#[derive(PartialEq, Clone, Properties, Debug)]
68pub struct Props<T: Clone> {
69    /// Initially selected value.
70    #[prop_or_default]
71    pub selected: Option<T>,
72    /// Whether or not the selector should be disabled.
73    #[prop_or_default]
74    pub disabled: bool,
75    /// A vector of options which the end user can choose from.
76    #[prop_or_default]
77    pub options: Vec<T>,
78    /// Classes to be applied to the `<select>` tag
79    #[prop_or_default]
80    pub class: String,
81    /// The ID for the `<select>` tag
82    #[prop_or_default]
83    pub id: String,
84    /// Placeholder value, shown at the top as a disabled option
85    #[prop_or(String::from("↪"))]
86    pub placeholder: String,
87    /// A callback which is called when the value of the `<select>` changes.
88    pub on_change: Callback<T>,
89}
90
91impl<T> Component for Select<T>
92where
93    T: ToString + PartialEq + Clone + 'static,
94{
95    type Message = Msg;
96    type Properties = Props<T>;
97
98    fn create(props: Self::Properties, link: ComponentLink<Self>) -> Self {
99        Self {
100            props,
101            select_ref: NodeRef::default(),
102            link,
103        }
104    }
105
106    fn update(&mut self, msg: Self::Message) -> ShouldRender {
107        match msg {
108            Msg::Selected(value) => {
109                if let Some(idx) = value {
110                    let item = self.props.options.get(idx - 1);
111                    if let Some(value) = item {
112                        self.props.on_change.emit(value.clone());
113                    }
114                }
115            }
116        }
117        true
118    }
119
120    fn change(&mut self, props: Self::Properties) -> ShouldRender {
121        if self.props.selected != props.selected {
122            if let Some(select) = self.select_ref.cast::<HtmlSelectElement>() {
123                let val = props
124                    .selected
125                    .as_ref()
126                    .map(|v| v.to_string())
127                    .unwrap_or_default();
128                select.set_value(&val);
129            }
130        }
131        self.props = props;
132        true
133    }
134
135    fn view(&self) -> Html {
136        let selected = self.props.selected.as_ref();
137        let view_option = |value: &T| {
138            let flag = selected == Some(value);
139            html! {
140                <option value=value.to_string() selected=flag>{ value.to_string() }</option>
141            }
142        };
143
144        html! {
145            <select
146                ref=self.select_ref.clone()
147                id=self.props.id.clone()
148                class=self.props.class.clone()
149                disabled=self.props.disabled
150                onchange=self.on_change()
151            >
152                <option value="" disabled=true selected=selected.is_none()>
153                    { self.props.placeholder.clone() }
154                </option>
155                { for self.props.options.iter().map(view_option) }
156            </select>
157        }
158    }
159}
160
161impl<T> Select<T>
162where
163    T: ToString + PartialEq + Clone + 'static,
164{
165    fn on_change(&self) -> Callback<ChangeData> {
166        self.link.callback(|event| match event {
167            ChangeData::Select(elem) => {
168                let value = elem.selected_index();
169                Msg::Selected(Some(value as usize))
170            }
171            _ => unreachable!(),
172        })
173    }
174}
175
176#[cfg(test)]
177mod tests {
178    use super::*;
179
180    #[test]
181    fn can_create_select() {
182        let on_change = Callback::<u8>::default();
183        html! {
184            <Select<u8> on_change=on_change />
185        };
186    }
187
188    #[test]
189    fn can_create_select_with_class() {
190        let on_change = Callback::<u8>::default();
191        html! {
192            <Select<u8> on_change=on_change class="form-control" />
193        };
194    }
195
196    #[test]
197    fn can_create_select_with_id() {
198        let on_change = Callback::<u8>::default();
199        html! {
200            <Select<u8> on_change=on_change id="test-select" />
201        };
202    }
203
204    #[test]
205    fn can_create_select_with_placeholder() {
206        let on_change = Callback::<u8>::default();
207        html! {
208            <Select<u8> on_change=on_change placeholder="--Please choose an option--" />
209        };
210    }
211}