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}