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