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