patternfly_yew/components/form/
select.rs1use 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#[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 #[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 #[prop_or_default]
49 pub ouia_id: Option<String>,
50 #[prop_or(OUIA.component_type())]
52 pub ouia_type: OuiaComponentType,
53 #[prop_or(OuiaSafe::TRUE)]
55 pub ouia_safe: OuiaSafe,
56}
57
58#[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#[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#[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}