Skip to main content

dioxus_bootstrap_css/
form.rs

1use dioxus::prelude::*;
2
3use crate::types::Size;
4
5/// Bootstrap FormGroup — label + control wrapper.
6///
7/// # Bootstrap HTML → Dioxus
8///
9/// ```html
10/// <!-- Bootstrap HTML -->
11/// <div class="mb-3">
12///   <label class="form-label">Email</label>
13///   <input type="email" class="form-control" placeholder="you@example.com">
14/// </div>
15/// ```
16///
17/// ```rust,no_run
18/// rsx! {
19///     FormGroup { label: "Email",
20///         Input { r#type: "email", placeholder: "you@example.com" }
21///     }
22/// }
23/// ```
24#[derive(Clone, PartialEq, Props)]
25pub struct FormGroupProps {
26    /// Label text.
27    #[props(default)]
28    pub label: String,
29    /// Additional CSS classes for the wrapper div.
30    #[props(default)]
31    pub class: String,
32    /// Any additional HTML attributes.
33    #[props(extends = GlobalAttributes)]
34    attributes: Vec<Attribute>,
35    /// Child elements (form control).
36    pub children: Element,
37}
38
39#[component]
40pub fn FormGroup(props: FormGroupProps) -> Element {
41    let full_class = if props.class.is_empty() {
42        "mb-3".to_string()
43    } else {
44        format!("mb-3 {}", props.class)
45    };
46
47    rsx! {
48        div { class: "{full_class}",
49            ..props.attributes,
50            if !props.label.is_empty() {
51                label { class: "form-label", "{props.label}" }
52            }
53            {props.children}
54        }
55    }
56}
57
58/// Bootstrap Input component.
59///
60/// # Bootstrap HTML → Dioxus
61///
62/// | HTML | Dioxus |
63/// |---|---|
64/// | `<input class="form-control" type="text">` | `Input { r#type: "text" }` |
65/// | `<input class="form-control form-control-sm" type="email">` | `Input { r#type: "email", size: Size::Sm }` |
66/// | `<input class="form-control" disabled>` | `Input { disabled: true }` |
67///
68/// ```rust,no_run
69/// rsx! {
70///     Input { r#type: "text", value: "hello", placeholder: "Enter text" }
71///     Input { r#type: "email", size: Size::Sm, oninput: move |evt| { /* handle */ } }
72///     Input { r#type: "password", disabled: true }
73/// }
74/// ```
75#[derive(Clone, PartialEq, Props)]
76pub struct InputProps {
77    /// Input type (text, email, password, number, etc.).
78    #[props(default = "text".to_string())]
79    pub r#type: String,
80    /// Current value.
81    #[props(default)]
82    pub value: String,
83    /// Placeholder text.
84    #[props(default)]
85    pub placeholder: String,
86    /// Input size.
87    #[props(default)]
88    pub size: Size,
89    /// Disabled state.
90    #[props(default)]
91    pub disabled: bool,
92    /// Readonly state.
93    #[props(default)]
94    pub readonly: bool,
95    /// Input event handler.
96    #[props(default)]
97    pub oninput: Option<EventHandler<FormEvent>>,
98    /// Additional CSS classes.
99    #[props(default)]
100    pub class: String,
101    /// Any additional HTML attributes.
102    #[props(extends = GlobalAttributes)]
103    attributes: Vec<Attribute>,
104}
105
106#[component]
107pub fn Input(props: InputProps) -> Element {
108    let size_class = match props.size {
109        Size::Md => String::new(),
110        s => format!(" form-control-{s}"),
111    };
112
113    let full_class = if props.class.is_empty() {
114        format!("form-control{size_class}")
115    } else {
116        format!("form-control{size_class} {}", props.class)
117    };
118
119    rsx! {
120        input {
121            class: "{full_class}",
122            r#type: "{props.r#type}",
123            value: "{props.value}",
124            placeholder: "{props.placeholder}",
125            disabled: props.disabled,
126            readonly: props.readonly,
127            oninput: move |evt| {
128                if let Some(handler) = &props.oninput {
129                    handler.call(evt);
130                }
131            },
132            ..props.attributes,
133        }
134    }
135}
136
137/// Bootstrap Select (dropdown) component.
138///
139/// # Bootstrap HTML → Dioxus
140///
141/// ```html
142/// <!-- Bootstrap HTML -->
143/// <select class="form-select">
144///   <option value="opt1">Option 1</option>
145///   <option value="opt2" selected>Option 2</option>
146/// </select>
147/// ```
148///
149/// ```rust,no_run
150/// rsx! {
151///     Select { value: "opt2", onchange: move |evt| { /* handle */ },
152///         option { value: "opt1", "Option 1" }
153///         option { value: "opt2", "Option 2" }
154///     }
155/// }
156/// ```
157#[derive(Clone, PartialEq, Props)]
158pub struct SelectProps {
159    /// Current selected value.
160    #[props(default)]
161    pub value: String,
162    /// Select size.
163    #[props(default)]
164    pub size: Size,
165    /// Disabled state.
166    #[props(default)]
167    pub disabled: bool,
168    /// Change event handler.
169    #[props(default)]
170    pub onchange: Option<EventHandler<FormEvent>>,
171    /// Additional CSS classes.
172    #[props(default)]
173    pub class: String,
174    /// Any additional HTML attributes.
175    #[props(extends = GlobalAttributes)]
176    attributes: Vec<Attribute>,
177    /// Child elements (option elements).
178    pub children: Element,
179}
180
181#[component]
182pub fn Select(props: SelectProps) -> Element {
183    let size_class = match props.size {
184        Size::Md => String::new(),
185        s => format!(" form-select-{s}"),
186    };
187
188    let full_class = if props.class.is_empty() {
189        format!("form-select{size_class}")
190    } else {
191        format!("form-select{size_class} {}", props.class)
192    };
193
194    rsx! {
195        select {
196            class: "{full_class}",
197            value: "{props.value}",
198            disabled: props.disabled,
199            onchange: move |evt| {
200                if let Some(handler) = &props.onchange {
201                    handler.call(evt);
202                }
203            },
204            ..props.attributes,
205            {props.children}
206        }
207    }
208}
209
210/// Bootstrap Textarea component.
211///
212/// # Bootstrap HTML → Dioxus
213///
214/// | HTML | Dioxus |
215/// |---|---|
216/// | `<textarea class="form-control" rows="5">` | `Textarea { rows: 5 }` |
217/// | `<textarea class="form-control" placeholder="..." disabled>` | `Textarea { placeholder: "...", disabled: true }` |
218///
219/// ```rust
220/// rsx! {
221///     Textarea { rows: 5, placeholder: "Enter description..." }
222/// }
223/// ```
224#[derive(Clone, PartialEq, Props)]
225pub struct TextareaProps {
226    /// Current value.
227    #[props(default)]
228    pub value: String,
229    /// Number of visible rows.
230    #[props(default = 3)]
231    pub rows: u32,
232    /// Placeholder text.
233    #[props(default)]
234    pub placeholder: String,
235    /// Disabled state.
236    #[props(default)]
237    pub disabled: bool,
238    /// Readonly state.
239    #[props(default)]
240    pub readonly: bool,
241    /// Input event handler.
242    #[props(default)]
243    pub oninput: Option<EventHandler<FormEvent>>,
244    /// Additional CSS classes.
245    #[props(default)]
246    pub class: String,
247    /// Any additional HTML attributes.
248    #[props(extends = GlobalAttributes)]
249    attributes: Vec<Attribute>,
250}
251
252#[component]
253pub fn Textarea(props: TextareaProps) -> Element {
254    let full_class = if props.class.is_empty() {
255        "form-control".to_string()
256    } else {
257        format!("form-control {}", props.class)
258    };
259
260    rsx! {
261        textarea {
262            class: "{full_class}",
263            rows: "{props.rows}",
264            placeholder: "{props.placeholder}",
265            disabled: props.disabled,
266            readonly: props.readonly,
267            value: "{props.value}",
268            oninput: move |evt| {
269                if let Some(handler) = &props.oninput {
270                    handler.call(evt);
271                }
272            },
273            ..props.attributes,
274        }
275    }
276}
277
278/// Bootstrap Checkbox component.
279///
280/// # Bootstrap HTML → Dioxus
281///
282/// ```html
283/// <!-- Bootstrap HTML -->
284/// <div class="form-check">
285///   <input class="form-check-input" type="checkbox" checked>
286///   <label class="form-check-label">Accept terms</label>
287/// </div>
288/// ```
289///
290/// ```rust,no_run
291/// rsx! {
292///     Checkbox { checked: true, label: "Accept terms",
293///         onchange: move |evt| { /* handle */ },
294///     }
295/// }
296/// ```
297#[derive(Clone, PartialEq, Props)]
298pub struct CheckboxProps {
299    /// Whether the checkbox is checked.
300    #[props(default)]
301    pub checked: bool,
302    /// Label text.
303    #[props(default)]
304    pub label: String,
305    /// Disabled state.
306    #[props(default)]
307    pub disabled: bool,
308    /// Change event handler.
309    #[props(default)]
310    pub onchange: Option<EventHandler<FormEvent>>,
311    /// Additional CSS classes for the wrapper.
312    #[props(default)]
313    pub class: String,
314    /// Any additional HTML attributes.
315    #[props(extends = GlobalAttributes)]
316    attributes: Vec<Attribute>,
317}
318
319#[component]
320pub fn Checkbox(props: CheckboxProps) -> Element {
321    let full_class = if props.class.is_empty() {
322        "form-check".to_string()
323    } else {
324        format!("form-check {}", props.class)
325    };
326
327    rsx! {
328        div { class: "{full_class}",
329            ..props.attributes,
330            input {
331                class: "form-check-input",
332                r#type: "checkbox",
333                checked: props.checked,
334                disabled: props.disabled,
335                onchange: move |evt| {
336                    if let Some(handler) = &props.onchange {
337                        handler.call(evt);
338                    }
339                },
340            }
341            if !props.label.is_empty() {
342                label { class: "form-check-label", "{props.label}" }
343            }
344        }
345    }
346}
347
348/// Bootstrap Switch (toggle) component.
349///
350/// # Bootstrap HTML → Dioxus
351///
352/// ```html
353/// <!-- Bootstrap HTML -->
354/// <div class="form-check form-switch">
355///   <input class="form-check-input" type="checkbox" role="switch" checked>
356///   <label class="form-check-label">Enable notifications</label>
357/// </div>
358/// ```
359///
360/// ```rust,no_run
361/// rsx! {
362///     Switch { checked: true, label: "Enable notifications",
363///         onchange: move |evt| { /* handle */ },
364///     }
365/// }
366/// ```
367#[derive(Clone, PartialEq, Props)]
368pub struct SwitchProps {
369    /// Whether the switch is on.
370    #[props(default)]
371    pub checked: bool,
372    /// Label text.
373    #[props(default)]
374    pub label: String,
375    /// Disabled state.
376    #[props(default)]
377    pub disabled: bool,
378    /// Change event handler.
379    #[props(default)]
380    pub onchange: Option<EventHandler<FormEvent>>,
381    /// Additional CSS classes for the wrapper.
382    #[props(default)]
383    pub class: String,
384    /// Any additional HTML attributes.
385    #[props(extends = GlobalAttributes)]
386    attributes: Vec<Attribute>,
387}
388
389#[component]
390pub fn Switch(props: SwitchProps) -> Element {
391    let full_class = if props.class.is_empty() {
392        "form-check form-switch".to_string()
393    } else {
394        format!("form-check form-switch {}", props.class)
395    };
396
397    rsx! {
398        div { class: "{full_class}",
399            ..props.attributes,
400            input {
401                class: "form-check-input",
402                r#type: "checkbox",
403                role: "switch",
404                checked: props.checked,
405                disabled: props.disabled,
406                onchange: move |evt| {
407                    if let Some(handler) = &props.onchange {
408                        handler.call(evt);
409                    }
410                },
411            }
412            if !props.label.is_empty() {
413                label { class: "form-check-label", "{props.label}" }
414            }
415        }
416    }
417}
418
419/// Bootstrap Range (slider) input.
420///
421/// # Bootstrap HTML → Dioxus
422///
423/// | HTML | Dioxus |
424/// |---|---|
425/// | `<input type="range" class="form-range" min="0" max="100">` | `Range { min: "0", max: "100" }` |
426/// | `<input type="range" class="form-range" step="5" disabled>` | `Range { step: "5".into(), disabled: true }` |
427///
428/// ```rust
429/// rsx! {
430///     Range { value: "50", min: "0", max: "100" }
431/// }
432/// ```
433#[derive(Clone, PartialEq, Props)]
434pub struct RangeProps {
435    /// Current value.
436    #[props(default)]
437    pub value: String,
438    /// Minimum value.
439    #[props(default = "0".to_string())]
440    pub min: String,
441    /// Maximum value.
442    #[props(default = "100".to_string())]
443    pub max: String,
444    /// Step increment.
445    #[props(default)]
446    pub step: String,
447    /// Disabled state.
448    #[props(default)]
449    pub disabled: bool,
450    /// Input event handler.
451    #[props(default)]
452    pub oninput: Option<EventHandler<FormEvent>>,
453    /// Additional CSS classes.
454    #[props(default)]
455    pub class: String,
456    /// Any additional HTML attributes.
457    #[props(extends = GlobalAttributes)]
458    attributes: Vec<Attribute>,
459}
460
461#[component]
462pub fn Range(props: RangeProps) -> Element {
463    let full_class = if props.class.is_empty() {
464        "form-range".to_string()
465    } else {
466        format!("form-range {}", props.class)
467    };
468
469    rsx! {
470        input {
471            class: "{full_class}",
472            r#type: "range",
473            value: "{props.value}",
474            min: "{props.min}",
475            max: "{props.max}",
476            step: if props.step.is_empty() { None } else { Some(props.step.clone()) },
477            disabled: props.disabled,
478            oninput: move |evt| {
479                if let Some(handler) = &props.oninput {
480                    handler.call(evt);
481                }
482            },
483            ..props.attributes,
484        }
485    }
486}
487
488/// Bootstrap Floating Label wrapper.
489///
490/// Wraps an Input or Textarea with a floating label that moves
491/// above the control when focused or filled.
492///
493/// # Bootstrap HTML → Dioxus
494///
495/// | HTML | Dioxus |
496/// |---|---|
497/// | `<div class="form-floating"><input class="form-control" placeholder="..."><label>Email</label></div>` | `FloatingLabel { label: "Email", Input { placeholder: "..." } }` |
498///
499/// ```rust
500/// rsx! {
501///     FloatingLabel { label: "Email address",
502///         Input { r#type: "email", placeholder: "name@example.com" }
503///     }
504/// }
505/// ```
506#[derive(Clone, PartialEq, Props)]
507pub struct FloatingLabelProps {
508    /// Label text.
509    pub label: String,
510    /// Additional CSS classes.
511    #[props(default)]
512    pub class: String,
513    /// Any additional HTML attributes.
514    #[props(extends = GlobalAttributes)]
515    attributes: Vec<Attribute>,
516    /// Child element (Input or Textarea).
517    pub children: Element,
518}
519
520#[component]
521pub fn FloatingLabel(props: FloatingLabelProps) -> Element {
522    let full_class = if props.class.is_empty() {
523        "form-floating".to_string()
524    } else {
525        format!("form-floating {}", props.class)
526    };
527
528    rsx! {
529        div { class: "{full_class}",
530            ..props.attributes,
531            {props.children}
532            label { "{props.label}" }
533        }
534    }
535}
536
537/// Bootstrap form validation feedback text.
538///
539/// # Bootstrap HTML → Dioxus
540///
541/// | HTML | Dioxus |
542/// |---|---|
543/// | `<div class="valid-feedback">Looks good!</div>` | `FormFeedback { valid: true, "Looks good!" }` |
544/// | `<div class="invalid-feedback">Required.</div>` | `FormFeedback { "Required." }` |
545///
546/// ```rust
547/// rsx! {
548///     Input { class: "is-valid".to_string(), value: "correct" }
549///     FormFeedback { valid: true, "Looks good!" }
550/// }
551/// ```
552#[derive(Clone, PartialEq, Props)]
553pub struct FormFeedbackProps {
554    /// True for valid feedback, false for invalid.
555    #[props(default)]
556    pub valid: bool,
557    /// Additional CSS classes.
558    #[props(default)]
559    pub class: String,
560    /// Any additional HTML attributes.
561    #[props(extends = GlobalAttributes)]
562    attributes: Vec<Attribute>,
563    /// Feedback text.
564    pub children: Element,
565}
566
567#[component]
568pub fn FormFeedback(props: FormFeedbackProps) -> Element {
569    let base = if props.valid {
570        "valid-feedback"
571    } else {
572        "invalid-feedback"
573    };
574    let full_class = if props.class.is_empty() {
575        base.to_string()
576    } else {
577        format!("{base} {}", props.class)
578    };
579
580    rsx! {
581        div { class: "{full_class}", ..props.attributes, {props.children} }
582    }
583}
584
585/// Bootstrap form text (help text below a control).
586///
587/// # Bootstrap HTML → Dioxus
588///
589/// | HTML | Dioxus |
590/// |---|---|
591/// | `<div class="form-text">Must be 8-20 characters.</div>` | `FormText { "Must be 8-20 characters." }` |
592///
593/// ```rust
594/// rsx! {
595///     Input { r#type: "password" }
596///     FormText { "Must be 8-20 characters long." }
597/// }
598/// ```
599#[derive(Clone, PartialEq, Props)]
600pub struct FormTextProps {
601    /// Additional CSS classes.
602    #[props(default)]
603    pub class: String,
604    /// Any additional HTML attributes.
605    #[props(extends = GlobalAttributes)]
606    attributes: Vec<Attribute>,
607    /// Help text content.
608    pub children: Element,
609}
610
611#[component]
612pub fn FormText(props: FormTextProps) -> Element {
613    let full_class = if props.class.is_empty() {
614        "form-text".to_string()
615    } else {
616        format!("form-text {}", props.class)
617    };
618
619    rsx! {
620        div { class: "{full_class}", ..props.attributes, {props.children} }
621    }
622}
623
624/// Bootstrap Radio button component.
625///
626/// # Bootstrap HTML → Dioxus
627///
628/// ```html
629/// <!-- Bootstrap HTML -->
630/// <div class="form-check">
631///   <input class="form-check-input" type="radio" name="color" checked>
632///   <label class="form-check-label">Red</label>
633/// </div>
634/// <div class="form-check">
635///   <input class="form-check-input" type="radio" name="color">
636///   <label class="form-check-label">Blue</label>
637/// </div>
638/// ```
639///
640/// ```rust,no_run
641/// rsx! {
642///     Radio { name: "color", label: "Red", checked: true }
643///     Radio { name: "color", label: "Blue" }
644/// }
645/// ```
646#[derive(Clone, PartialEq, Props)]
647pub struct RadioProps {
648    /// Radio group name.
649    pub name: String,
650    /// Whether the radio is checked.
651    #[props(default)]
652    pub checked: bool,
653    /// Label text.
654    #[props(default)]
655    pub label: String,
656    /// Disabled state.
657    #[props(default)]
658    pub disabled: bool,
659    /// Change event handler.
660    #[props(default)]
661    pub onchange: Option<EventHandler<FormEvent>>,
662    /// Additional CSS classes for the wrapper.
663    #[props(default)]
664    pub class: String,
665    /// Any additional HTML attributes.
666    #[props(extends = GlobalAttributes)]
667    attributes: Vec<Attribute>,
668}
669
670#[component]
671pub fn Radio(props: RadioProps) -> Element {
672    let full_class = if props.class.is_empty() {
673        "form-check".to_string()
674    } else {
675        format!("form-check {}", props.class)
676    };
677
678    rsx! {
679        div { class: "{full_class}",
680            ..props.attributes,
681            input {
682                class: "form-check-input",
683                r#type: "radio",
684                name: "{props.name}",
685                checked: props.checked,
686                disabled: props.disabled,
687                onchange: move |evt| {
688                    if let Some(handler) = &props.onchange {
689                        handler.call(evt);
690                    }
691                },
692            }
693            if !props.label.is_empty() {
694                label { class: "form-check-label", "{props.label}" }
695            }
696        }
697    }
698}