Skip to main content

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-v6-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-v6-c-form-control__utilities">
119                <div class="pf-v6-c-form-control__toggle-icon">{ Icon::CaretDown }</div>
120            </div>
121        </div>
122    )
123}
124
125#[derive(Clone, PartialEq)]
126pub enum FormSelectChild<K>
127where
128    K: 'static + Clone + PartialEq + Display,
129{
130    Option(Rc<<FormSelectOption<K> as BaseComponent>::Properties>),
131    Group(Rc<<FormSelectGroup<K> as BaseComponent>::Properties>),
132}
133
134impl<K> From<FormSelectOptionProperties<K>> for FormSelectChild<K>
135where
136    K: Clone + PartialEq + Display,
137{
138    fn from(props: FormSelectOptionProperties<K>) -> Self {
139        FormSelectChild::Option(Rc::new(props))
140    }
141}
142
143impl<K> From<FormSelectGroupProperties<K>> for FormSelectChild<K>
144where
145    K: Clone + PartialEq + Display,
146{
147    fn from(props: FormSelectGroupProperties<K>) -> Self {
148        FormSelectChild::Group(Rc::new(props))
149    }
150}
151
152// variant
153
154#[derive(PartialEq, Clone)]
155pub struct FormSelectChildVariant<K>
156where
157    K: 'static + Clone + PartialEq + Display,
158{
159    props: FormSelectChild<K>,
160}
161
162impl<K, CHILD> From<VChild<CHILD>> for FormSelectChildVariant<K>
163where
164    CHILD: BaseComponent,
165    CHILD::Properties: Into<FormSelectChild<K>> + Clone,
166    K: 'static + Clone + PartialEq + Display,
167{
168    fn from(vchild: VChild<CHILD>) -> Self {
169        Self {
170            props: (*vchild.props).clone().into(),
171        }
172    }
173}
174
175impl<K> From<FormSelectChildVariant<K>> for Html
176where
177    K: 'static + Clone + PartialEq + Display,
178{
179    fn from(value: FormSelectChildVariant<K>) -> Self {
180        match value.props {
181            FormSelectChild::Option(props) => VComp::new::<FormSelectOption<K>>(props, None).into(),
182            FormSelectChild::Group(props) => VComp::new::<FormSelectGroup<K>>(props, None).into(),
183        }
184    }
185}
186
187// Item
188
189#[derive(Clone, PartialEq, Properties)]
190pub struct FormSelectOptionProperties<K>
191where
192    K: Clone + PartialEq + Display,
193{
194    pub value: K,
195
196    #[prop_or_default]
197    pub id: Option<String>,
198
199    #[prop_or_default]
200    pub description: Option<String>,
201
202    #[prop_or_default]
203    pub selected: bool,
204}
205
206#[function_component(FormSelectOption)]
207pub fn form_select_option<K>(props: &FormSelectOptionProperties<K>) -> Html
208where
209    K: 'static + Clone + PartialEq + Display,
210{
211    html! (
212        <option id={props.id.clone()} selected={props.selected} value={props.value.to_string()}>
213            if let Some(description) = &props.description {
214                { &description }
215            } else {
216                { &props.value }
217            }
218        </option>
219    )
220}
221
222#[derive(Clone, PartialEq, Properties)]
223pub struct FormSelectGroupProperties<K>
224where
225    K: 'static + Clone + PartialEq + Display,
226{
227    pub label: String,
228    #[prop_or_default]
229    pub children: ChildrenRenderer<FormSelectChildVariant<K>>,
230}
231
232#[function_component(FormSelectGroup)]
233pub fn form_select_group<K>(props: &FormSelectGroupProperties<K>) -> Html
234where
235    K: 'static + Clone + PartialEq + Display,
236{
237    html! (<optgroup label={props.label.clone()}>{ for props.children.iter() }</optgroup>)
238}