patternfly_yew/components/form/
select.rs

1use crate::ouia;
2use crate::prelude::{Icon, OuiaComponentType, ValidationContext};
3use crate::utils::{Ouia, OuiaSafe};
4use std::fmt::Display;
5use std::rc::Rc;
6use std::str::FromStr;
7use web_sys::HtmlSelectElement;
8use yew::{
9    html::ChildrenRenderer,
10    prelude::*,
11    virtual_dom::{VChild, VComp},
12};
13
14const OUIA: Ouia = ouia!("FormSelect");
15
16/// Properties for [`FormSelect`]
17#[derive(Clone, PartialEq, Properties)]
18pub struct FormSelectProperties<K: 'static + Clone + PartialEq + Display + FromStr> {
19    #[prop_or_default]
20    pub id: Option<AttrValue>,
21
22    #[prop_or_default]
23    pub name: Option<AttrValue>,
24
25    #[prop_or_default]
26    pub disabled: bool,
27
28    #[prop_or_default]
29    pub required: bool,
30
31    #[prop_or_default]
32    pub placeholder: Option<AttrValue>,
33
34    #[prop_or_default]
35    pub onchange: Callback<Option<K>>,
36
37    /// The selected value.
38    #[prop_or_default]
39    pub value: Option<K>,
40
41    #[prop_or_default]
42    pub children: ChildrenRenderer<FormSelectChildVariant<K>>,
43
44    #[prop_or_default]
45    pub onvalidate: Callback<ValidationContext<Vec<K>>>,
46
47    /// OUIA Component id
48    #[prop_or_default]
49    pub ouia_id: Option<String>,
50    /// OUIA Component Type
51    #[prop_or(OUIA.component_type())]
52    pub ouia_type: OuiaComponentType,
53    /// OUIA Component Safe
54    #[prop_or(OuiaSafe::TRUE)]
55    pub ouia_safe: OuiaSafe,
56}
57
58/// A select component in a [`Form`](crate::prelude::Form)
59#[function_component(FormSelect)]
60pub fn form_select<K>(props: &FormSelectProperties<K>) -> Html
61where
62    K: 'static + Clone + PartialEq + Display + FromStr,
63{
64    let ouia_id = use_memo(props.ouia_id.clone(), |id| {
65        id.clone().unwrap_or(OUIA.generated_id())
66    });
67    let node_ref = use_node_ref();
68
69    let oninput = {
70        let node_ref = node_ref.clone();
71        let onchange = props.onchange.clone();
72        Callback::from(move |_evt: InputEvent| {
73            if let Some(ele) = node_ref.cast::<HtmlSelectElement>() {
74                let value = ele.value();
75                if value.is_empty() {
76                    onchange.emit(None);
77                } else {
78                    onchange.emit(K::from_str(&value).ok());
79                }
80            }
81        })
82    };
83
84    {
85        let node_ref = node_ref.clone();
86
87        use_effect_with(
88            props
89                .value
90                .as_ref()
91                .map(ToString::to_string)
92                .unwrap_or_default(),
93            move |value| {
94                if let Some(ele) = node_ref.cast::<HtmlSelectElement>() {
95                    ele.set_value(value);
96                }
97            },
98        );
99    }
100
101    html! (
102        <div class="pf-v5-c-form-control">
103            <select
104                {oninput}
105                name={&props.name}
106                id={&props.id}
107                ref={node_ref}
108                required={props.required}
109                data-ouia-component-id={(*ouia_id).clone()}
110                data-ouia-component-type={props.ouia_type}
111                data-ouia-safe={props.ouia_safe}
112            >
113                if let Some(placeholder) = &props.placeholder {
114                    <option value="">{ placeholder }</option>
115                }
116                { for props.children.iter() }
117            </select>
118            <div class="pf-v5-c-form-control__utilities">
119                <div class="pf-v5-c-form-control__toggle-icon">
120                  {Icon::CaretDown}
121                </div>
122            </div>
123        </div>
124    )
125}
126
127#[derive(Clone, PartialEq)]
128pub enum FormSelectChild<K>
129where
130    K: 'static + Clone + PartialEq + Display,
131{
132    Option(Rc<<FormSelectOption<K> as BaseComponent>::Properties>),
133    Group(Rc<<FormSelectGroup<K> as BaseComponent>::Properties>),
134}
135
136impl<K> From<FormSelectOptionProperties<K>> for FormSelectChild<K>
137where
138    K: Clone + PartialEq + Display,
139{
140    fn from(props: FormSelectOptionProperties<K>) -> Self {
141        FormSelectChild::Option(Rc::new(props))
142    }
143}
144
145impl<K> From<FormSelectGroupProperties<K>> for FormSelectChild<K>
146where
147    K: Clone + PartialEq + Display,
148{
149    fn from(props: FormSelectGroupProperties<K>) -> Self {
150        FormSelectChild::Group(Rc::new(props))
151    }
152}
153
154// variant
155
156#[derive(PartialEq, Clone)]
157pub struct FormSelectChildVariant<K>
158where
159    K: 'static + Clone + PartialEq + Display,
160{
161    props: FormSelectChild<K>,
162}
163
164impl<K, CHILD> From<VChild<CHILD>> for FormSelectChildVariant<K>
165where
166    CHILD: BaseComponent,
167    CHILD::Properties: Into<FormSelectChild<K>> + Clone,
168    K: 'static + Clone + PartialEq + Display,
169{
170    fn from(vchild: VChild<CHILD>) -> Self {
171        Self {
172            props: (*vchild.props).clone().into(),
173        }
174    }
175}
176
177impl<K> From<FormSelectChildVariant<K>> for Html
178where
179    K: 'static + Clone + PartialEq + Display,
180{
181    fn from(value: FormSelectChildVariant<K>) -> Self {
182        match value.props {
183            FormSelectChild::Option(props) => VComp::new::<FormSelectOption<K>>(props, None).into(),
184            FormSelectChild::Group(props) => VComp::new::<FormSelectGroup<K>>(props, None).into(),
185        }
186    }
187}
188
189// Item
190
191#[derive(Clone, PartialEq, Properties)]
192pub struct FormSelectOptionProperties<K>
193where
194    K: Clone + PartialEq + Display,
195{
196    pub value: K,
197
198    #[prop_or_default]
199    pub id: Option<String>,
200
201    #[prop_or_default]
202    pub description: Option<String>,
203
204    #[prop_or_default]
205    pub selected: bool,
206}
207
208#[function_component(FormSelectOption)]
209pub fn form_select_option<K>(props: &FormSelectOptionProperties<K>) -> Html
210where
211    K: 'static + Clone + PartialEq + Display,
212{
213    html! (
214        <option
215            id={props.id.clone()}
216            selected={props.selected}
217            value={props.value.to_string()}
218        >
219            if let Some(description) = &props.description {
220                { &description }
221            } else {
222                { &props.value }
223            }
224        </option>
225    )
226}
227
228#[derive(Clone, PartialEq, Properties)]
229pub struct FormSelectGroupProperties<K>
230where
231    K: 'static + Clone + PartialEq + Display,
232{
233    pub label: String,
234    #[prop_or_default]
235    pub children: ChildrenRenderer<FormSelectChildVariant<K>>,
236}
237
238#[function_component(FormSelectGroup)]
239pub fn form_select_group<K>(props: &FormSelectGroupProperties<K>) -> Html
240where
241    K: 'static + Clone + PartialEq + Display,
242{
243    html! (
244        <optgroup label={props.label.clone()}>
245            { for props.children.iter() }
246        </optgroup>
247    )
248}