yew_bootstrap/component/form/
form_control.rs

1use yew::prelude::*;
2use super::*;
3
4/// Validation type for a form control, with feedback message
5#[derive(Clone, PartialEq)]
6pub enum FormControlValidation {
7    /// Form field has not been validated or nothing to show
8    None,
9    /// Valid validation with optional feedback message
10    Valid(Option<AttrValue>),
11    /// Invalid validation with feedback message
12    Invalid(AttrValue),
13}
14
15
16/// # Properties for a FormControl
17#[derive(Properties, Clone, PartialEq)]
18pub struct FormControlProps {
19    /// Type of control
20    pub ctype: FormControlType,
21
22    /// Id for the form field
23    pub id: AttrValue,
24
25    /// CSS class
26    #[prop_or_default]
27    pub class: Classes,
28
29    /// Optional label for the control
30    #[prop_or_default]
31    pub label: Option<AttrValue>,
32
33    /// Optional placeholder, only used for text fields
34    #[prop_or_default]
35    pub placeholder: Option<AttrValue>,
36
37    /// Optional help text
38    #[prop_or_default]
39    pub help: Option<AttrValue>,
40
41    /// Autocomplete 
42    #[prop_or(FormAutocompleteType::Off)]
43    pub autocomplete: FormAutocompleteType,
44
45    /// Name for the form field.
46    /// For [FormControlType::Radio], set same name to create a group
47    #[prop_or_default]
48    pub name: AttrValue,
49
50    /// Value as string, ignored for checkbox (Use `checked` instead). For a radio,
51    /// indicates the value in the group
52    #[prop_or_default]
53    pub value: AttrValue,
54
55    /// Is this field required? Defaults to false.
56    #[prop_or_default]
57    pub required: bool,
58
59    /// Checked or default value:
60    ///
61    /// - For a checkbox, indicates the state (Checked or not)
62    /// - For a radio, indicates the default value (Only one in the group should have it)
63    #[prop_or_default]
64    pub checked: bool,
65
66    /// Disabled if true
67    #[prop_or_default]
68    pub disabled: bool,
69
70    /// If true, label is floating inside the input. Ignored for checkbox/radio, date/time,
71    /// color, range fields.
72    ///
73    /// When true, `label` is required and `placeholder` is ignored.
74    #[prop_or_default]
75    pub floating: bool,
76
77    /// Multiple select, only used for select form input
78    #[prop_or_default]
79    pub multiple: bool,
80
81    /// Children, only used for select form input
82    #[prop_or_default]
83    pub children: Children,
84
85    /// Form validation feedback
86    /// Note: you must always validate user input server-side as well, this is only provided for better user experience
87    #[prop_or(FormControlValidation::None)]
88    pub validation: FormControlValidation,
89
90    /// Optional onchange event applied on the input
91    /// For a text input, this is called when leaving the input field
92    #[prop_or_default]
93    pub onchange: Callback<Event>,
94
95    /// Optional oninput event applied on the input, only for text inputs
96    /// This is called each time an input is received, after each character
97    #[prop_or_default]
98    pub oninput: Callback<InputEvent>,
99
100    /// Optional onclick event applied on the input
101    #[prop_or_default]
102    pub onclick: Callback<MouseEvent>,
103
104    /// Reference to the [NodeRef] of the form control's underlying `<input>`,
105    /// `<select>`, `<textarea>` element.
106    ///
107    /// Used by components which add custom event handlers directly to the DOM.
108    ///
109    /// See [*Node Refs* in the Yew documentation][0] for more information.
110    ///
111    /// [0]: https://yew.rs/docs/concepts/function-components/node-refs
112    #[prop_or_default]
113    pub node_ref: NodeRef,
114}
115
116
117/// Convert an option (Typically integer) to an AttrValue option
118fn convert_to_string_option<T>(value: &Option<T>) -> Option<AttrValue>
119where T: std::fmt::Display {
120    value.as_ref().map(|v| AttrValue::from(v.to_string()))
121}
122
123/// # Form Control field
124///
125/// Global form control for most Bootstrap form fields. See:
126///
127/// - [FormControlType] to define the type of input
128/// - [FormControlProps] to list the properties for the component. Note that
129///   some fields are ignored or may not work correctly depending on the type
130///   of input (See Bootstrap documentation for details)
131///
132/// ## Examples
133///
134/// Basic text field:
135///
136/// ```rust
137/// use yew::prelude::*;
138/// use yew_bootstrap::component::form::*;
139/// fn test() -> Html {
140///   html! {
141///     <FormControl
142///         id="input-text"
143///         ctype={FormControlType::Text}
144///         class="mb-3"
145///         label="Text"
146///         value="Initial text"
147///     />
148///   }
149/// }
150/// ```
151///
152/// 
153/// Some input types need parameters for the `ctype` enum. Optional parameters use `Option` enums.
154/// ```rust
155/// use yew::prelude::*;
156/// use yew_bootstrap::component::form::*;
157/// fn test() -> Html {
158///   html! {
159///     <FormControl
160///         id="input-number"
161///         ctype={
162///             FormControlType::Number {
163///                 min: Some(10),
164///                 max: Some(20)
165///             }
166///         }
167///         class="mb-3"
168///         label="Number in range 10-20"
169///         value="12"
170///     />
171///   }
172/// }
173/// ```
174///
175/// Almost all properties are `AttrValue` type, and need to be converted into the
176/// correct format, as required by the input. For example for a DateTime with range
177/// input:
178/// ```rust
179/// use yew::prelude::*;
180/// use yew_bootstrap::component::form::*;
181/// fn test() -> Html {
182///   html! {
183///     <FormControl
184///         id="input-datetime2"
185///         ctype={
186///             FormControlType::DatetimeMinMax {
187///                 min: Some(AttrValue::from("2023-01-01T12:00")),
188///                 max: Some(AttrValue::from("2023-01-01T18:00"))
189///             }
190///         }
191///         class="mb-3"
192///         label="Date and time (1st Jan 2023, 12:00 to 18:00)"
193///         value="2023-01-01T15:00"
194///     />
195///   }
196/// }
197/// ```
198///
199/// Select input is the only input that can receive children, of type [SelectOption]
200/// or [SelectOptgroup]. For example:
201/// ```rust
202/// use yew::prelude::*;
203/// use yew_bootstrap::component::form::*;
204/// fn test() -> Html {
205///   html! {
206///     <FormControl
207///         id="input-select-feedback"
208///         ctype={ FormControlType::Select}
209///         class="mb-3"
210///         label={ "Form control invalid" }
211///         validation={
212///             FormControlValidation::Invalid(AttrValue::from("Select an option"))
213///         }
214///     >
215///       <SelectOption key=0 label="Select an option" selected=true />
216///       <SelectOption key=1 label="Option 1" value="1"/>
217///       <SelectOption key=2 label="Option 2" value="2"/>
218///       <SelectOption key=3 label="Option 3" value="3"/>
219///     </FormControl>
220///   }
221/// }
222/// ```
223///
224/// `onclick`, `oninput` and `onchange` events are available. `onchange` should be preferred
225/// for most inputs, but for text inputs (`Text`, `TextArea`, `Number`, etc), `onchange` is
226/// only called when the input looses focus, while `oninput` is called each time a key is
227/// pressed.
228///
229/// In the callback, the target needs to be converted to a descendent of `HtmlElement`
230/// to access the fields (Like `type`, `name` and `value`). All inputs can be converted
231/// to `HtmlInputElement` but `Select` and `TextArea`. This is an example of callback
232/// function to convert to the correct type; `checkbox` is special as the `checked`
233/// property should be used.
234///
235/// Note: `HtmlTextAreaElement` and `HtmlSelectElement` are not enabled by default
236/// and need the feature to be required:
237///
238/// ```toml
239/// web-sys = { version = "0.3.*", features = ["HtmlTextAreaElement", "HtmlSelectElement"] }
240/// ```
241///
242/// ```rust
243/// use wasm_bindgen::JsCast;
244/// use web_sys::{EventTarget, HtmlTextAreaElement, HtmlSelectElement, HtmlInputElement};
245/// use yew::prelude::*;
246///
247/// enum Msg {
248///   None,
249///   InputStrChanged { name: String, value: String },
250///   InputBoolChanged { name: String, value: bool },
251/// }
252///
253/// let onchange = Callback::from(move |event: Event| -> Msg {
254///   let target: Option<EventTarget> = event.target();
255///
256///   // Input element
257///   let input = target.clone().and_then(|t| t.dyn_into::<HtmlInputElement>().ok());
258///   if let Some(input) = input {
259///       let value = match &input.type_()[..] {
260///           "checkbox" => Msg::InputBoolChanged { name: input.name(), value: input.checked() },
261///           _ => Msg::InputStrChanged { name: input.name(), value: input.value() }
262///       };
263///       return value;
264///   }
265///
266///   // Select element
267///   let input = target.clone().and_then(|t| t.dyn_into::<HtmlSelectElement>().ok());
268///   if let Some(input) = input {
269///       return Msg::InputStrChanged { name: input.name(), value: input.value() }
270///   }
271///
272///   // TextArea element
273///   let input = target.and_then(|t| t.dyn_into::<HtmlTextAreaElement>().ok());
274///   if let Some(input) = input {
275///       return Msg::InputStrChanged { name: input.name(), value: input.value() }
276///   }
277///
278///   Msg::None
279/// });
280/// ```
281
282#[function_component]
283pub fn FormControl(props: &FormControlProps) -> Html {
284    let label = match props.label.clone() {
285        None => None,
286        Some(text) => {
287            let class = if props.floating { None } else { Some("form-label") };
288            Some(html! {
289                <label for={ props.id.clone() } class={ class }>{ text.clone() }</label>
290            })
291        }
292    };
293
294    let help = props.help.as_ref().map(|text| html! {
295        <div class="form-text">{ text.clone() }</div>
296    });
297
298    let (validation, validation_class) = match props.validation.clone() {
299        FormControlValidation::None => (None, None),
300        FormControlValidation::Valid(None) => (None, Some("is-valid")),
301        FormControlValidation::Valid(Some(text)) => (Some(html! {
302            <div class="valid-feedback"> { text.clone() }</div>
303        }), Some("is-valid")),
304        FormControlValidation::Invalid(text) => (Some(html! {
305            <div class="invalid-feedback"> { text.clone() }</div>
306        }), Some("is-invalid")),
307    };
308
309    let pattern = match &props.ctype {
310        FormControlType::Email{ pattern } => pattern,
311        FormControlType::Url{ pattern } => pattern,
312        _ => &None,
313    };
314
315    // Placeholder required when `floating` is set, assign to label
316    let mut placeholder = props.placeholder.clone();
317    if props.floating && placeholder.is_none() {
318        placeholder = Some(props.label.clone().expect("When floating is set, label cannot be None"));
319    }
320
321    match &props.ctype {
322        FormControlType::TextArea { cols, rows } => {
323            let mut classes = classes!(props.class.clone());
324            if props.floating {
325                classes.push("form-floating");
326            }
327
328            let input_classes = classes!("form-control", validation_class);
329
330            let cols_str = convert_to_string_option(cols);
331            let rows_str = convert_to_string_option(rows);
332            let (label_before, label_after) =
333                if props.floating { (None, label) } else { (label, None) };
334
335            html! {
336                <div class={ classes }>
337                    { label_before }
338                    <textarea
339                        class={ input_classes }
340                        id={ props.id.clone() }
341                        name={ props.name.clone() }
342                        cols={ cols_str }
343                        rows={ rows_str }
344                        placeholder={ placeholder }
345                        value={ props.value.clone() }
346                        disabled={ props.disabled }
347                        oninput={props.oninput.clone() }
348                        onchange={ props.onchange.clone() }
349                        onclick={ props.onclick.clone() }
350                        required={ props.required }
351                        autocomplete={ props.autocomplete.to_str() }
352                        ref={ props.node_ref.clone() }
353                    />
354                    { label_after }
355                    { help }
356                    { validation }
357                </div>
358            }
359        },
360        FormControlType::Select => {
361            let mut classes = classes!(props.class.clone());
362            if props.floating {
363                classes.push("form-floating");
364            }
365
366            let input_classes = classes!("form-select", validation_class);
367
368            let (label_before, label_after) =
369                if props.floating { (None, label) } else { (label, None) };
370
371            html! {
372                <div class={ classes }>
373                    { label_before }
374                    <select
375                        class={ input_classes }
376                        id={ props.id.clone()}
377                        name={ props.name.clone() }
378                        disabled={ props.disabled }
379                        onchange={ props.onchange.clone() }
380                        onclick={ props.onclick.clone() }
381                        required={ props.required }
382                        ref={ props.node_ref.clone() }
383                    >
384                        { for props.children.clone() }
385                    </select>
386                    { label_after }
387                    { help }
388                    { validation }
389                </div>
390            }
391        },
392        FormControlType::Checkbox | FormControlType::Radio => {
393            let mut classes = classes!("form-check");
394            classes.push(props.class.clone());
395
396            let input_classes = classes!("form-check-input", validation_class);
397
398            html! {
399                <div class={ classes }>
400                    <input
401                        type={ props.ctype.to_str() }
402                        class={ input_classes }
403                        id={ props.id.clone() }
404                        name={ props.name.clone() }
405                        checked={ props.checked }
406                        disabled={ props.disabled }
407                        value={ props.value.clone() }
408                        onchange={ props.onchange.clone() }
409                        onclick={ props.onclick.clone() }
410                        required={ props.required }
411                        ref={ props.node_ref.clone() }
412                    />
413                    { label }
414                    { help }
415                    { validation}
416                </div>
417            }
418        },
419        _ => {
420            let mut min_str = None;
421            let mut max_str = None;
422            let mut step_str = None;
423            let mut accept_str = None;
424            match &props.ctype {
425                FormControlType::Number { min, max } => {
426                    min_str = convert_to_string_option(min);
427                    max_str = convert_to_string_option(max);
428                },
429                FormControlType::Range { min, max, step } => {
430                    min_str = Some(AttrValue::from(min.to_string()));
431                    max_str = Some(AttrValue::from(max.to_string()));
432                    step_str = convert_to_string_option(step);
433                },
434                FormControlType::DateMinMax { min, max } |
435                FormControlType::DatetimeMinMax { min, max } |
436                FormControlType::TimeMinMax { min, max } => {
437                    min_str = min.clone();
438                    max_str = max.clone();
439                },
440                FormControlType::File { accept } => {
441                    let accept_vec : Vec<String> = accept.clone().iter().cloned().map(
442                        move |value| { value.to_string() }
443                    ).collect();
444                    accept_str = Some(accept_vec.join(", "));
445                }
446                _ => ()
447            }
448
449            let mut classes = classes!(props.class.clone());
450            if props.floating {
451                classes.push("form-floating");
452            }
453
454            let input_classes = classes!("form-control", validation_class);
455
456            let (label_before, label_after) =
457                if props.floating { (None, label) } else { (label, None) };
458
459            html! {
460                <div class={ classes }>
461                    { label_before }
462                    <input
463                        type={ props.ctype.to_str() }
464                        class={ input_classes }
465                        id={ props.id.clone() }
466                        name={ props.name.clone() }
467                        value={ props.value.clone() }
468                        pattern={ pattern }
469                        accept={ accept_str }
470                        placeholder={ placeholder }
471                        min={ min_str }
472                        max={ max_str }
473                        step={ step_str }
474                        disabled={ props.disabled }
475                        onchange={ props.onchange.clone() }
476                        onclick={ props.onclick.clone() }
477                        oninput={ props.oninput.clone() }
478                        required={ props.required }
479                        autocomplete={ props.autocomplete.to_str() }
480                        ref={ props.node_ref.clone() }
481                    />
482                    { label_after }
483                    { help }
484                    { validation }
485                </div>
486            }
487        }
488    }
489}