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/// ```rust
213/// rsx! {
214///     Textarea { rows: 5, placeholder: "Enter description..." }
215/// }
216/// ```
217#[derive(Clone, PartialEq, Props)]
218pub struct TextareaProps {
219    /// Current value.
220    #[props(default)]
221    pub value: String,
222    /// Number of visible rows.
223    #[props(default = 3)]
224    pub rows: u32,
225    /// Placeholder text.
226    #[props(default)]
227    pub placeholder: String,
228    /// Disabled state.
229    #[props(default)]
230    pub disabled: bool,
231    /// Readonly state.
232    #[props(default)]
233    pub readonly: bool,
234    /// Input event handler.
235    #[props(default)]
236    pub oninput: Option<EventHandler<FormEvent>>,
237    /// Additional CSS classes.
238    #[props(default)]
239    pub class: String,
240    /// Any additional HTML attributes.
241    #[props(extends = GlobalAttributes)]
242    attributes: Vec<Attribute>,
243}
244
245#[component]
246pub fn Textarea(props: TextareaProps) -> Element {
247    let full_class = if props.class.is_empty() {
248        "form-control".to_string()
249    } else {
250        format!("form-control {}", props.class)
251    };
252
253    rsx! {
254        textarea {
255            class: "{full_class}",
256            rows: "{props.rows}",
257            placeholder: "{props.placeholder}",
258            disabled: props.disabled,
259            readonly: props.readonly,
260            value: "{props.value}",
261            oninput: move |evt| {
262                if let Some(handler) = &props.oninput {
263                    handler.call(evt);
264                }
265            },
266            ..props.attributes,
267        }
268    }
269}
270
271/// Bootstrap Checkbox component.
272///
273/// # Bootstrap HTML → Dioxus
274///
275/// ```html
276/// <!-- Bootstrap HTML -->
277/// <div class="form-check">
278///   <input class="form-check-input" type="checkbox" checked>
279///   <label class="form-check-label">Accept terms</label>
280/// </div>
281/// ```
282///
283/// ```rust,no_run
284/// rsx! {
285///     Checkbox { checked: true, label: "Accept terms",
286///         onchange: move |evt| { /* handle */ },
287///     }
288/// }
289/// ```
290#[derive(Clone, PartialEq, Props)]
291pub struct CheckboxProps {
292    /// Whether the checkbox is checked.
293    #[props(default)]
294    pub checked: bool,
295    /// Label text.
296    #[props(default)]
297    pub label: String,
298    /// Disabled state.
299    #[props(default)]
300    pub disabled: bool,
301    /// Change event handler.
302    #[props(default)]
303    pub onchange: Option<EventHandler<FormEvent>>,
304    /// Additional CSS classes for the wrapper.
305    #[props(default)]
306    pub class: String,
307    /// Any additional HTML attributes.
308    #[props(extends = GlobalAttributes)]
309    attributes: Vec<Attribute>,
310}
311
312#[component]
313pub fn Checkbox(props: CheckboxProps) -> Element {
314    let full_class = if props.class.is_empty() {
315        "form-check".to_string()
316    } else {
317        format!("form-check {}", props.class)
318    };
319
320    rsx! {
321        div { class: "{full_class}",
322            ..props.attributes,
323            input {
324                class: "form-check-input",
325                r#type: "checkbox",
326                checked: props.checked,
327                disabled: props.disabled,
328                onchange: move |evt| {
329                    if let Some(handler) = &props.onchange {
330                        handler.call(evt);
331                    }
332                },
333            }
334            if !props.label.is_empty() {
335                label { class: "form-check-label", "{props.label}" }
336            }
337        }
338    }
339}
340
341/// Bootstrap Switch (toggle) component.
342///
343/// # Bootstrap HTML → Dioxus
344///
345/// ```html
346/// <!-- Bootstrap HTML -->
347/// <div class="form-check form-switch">
348///   <input class="form-check-input" type="checkbox" role="switch" checked>
349///   <label class="form-check-label">Enable notifications</label>
350/// </div>
351/// ```
352///
353/// ```rust,no_run
354/// rsx! {
355///     Switch { checked: true, label: "Enable notifications",
356///         onchange: move |evt| { /* handle */ },
357///     }
358/// }
359/// ```
360#[derive(Clone, PartialEq, Props)]
361pub struct SwitchProps {
362    /// Whether the switch is on.
363    #[props(default)]
364    pub checked: bool,
365    /// Label text.
366    #[props(default)]
367    pub label: String,
368    /// Disabled state.
369    #[props(default)]
370    pub disabled: bool,
371    /// Change event handler.
372    #[props(default)]
373    pub onchange: Option<EventHandler<FormEvent>>,
374    /// Additional CSS classes for the wrapper.
375    #[props(default)]
376    pub class: String,
377    /// Any additional HTML attributes.
378    #[props(extends = GlobalAttributes)]
379    attributes: Vec<Attribute>,
380}
381
382#[component]
383pub fn Switch(props: SwitchProps) -> Element {
384    let full_class = if props.class.is_empty() {
385        "form-check form-switch".to_string()
386    } else {
387        format!("form-check form-switch {}", props.class)
388    };
389
390    rsx! {
391        div { class: "{full_class}",
392            ..props.attributes,
393            input {
394                class: "form-check-input",
395                r#type: "checkbox",
396                role: "switch",
397                checked: props.checked,
398                disabled: props.disabled,
399                onchange: move |evt| {
400                    if let Some(handler) = &props.onchange {
401                        handler.call(evt);
402                    }
403                },
404            }
405            if !props.label.is_empty() {
406                label { class: "form-check-label", "{props.label}" }
407            }
408        }
409    }
410}
411
412/// Bootstrap Range (slider) input.
413///
414/// ```rust
415/// rsx! {
416///     Range { value: "50", min: "0", max: "100" }
417/// }
418/// ```
419#[derive(Clone, PartialEq, Props)]
420pub struct RangeProps {
421    /// Current value.
422    #[props(default)]
423    pub value: String,
424    /// Minimum value.
425    #[props(default = "0".to_string())]
426    pub min: String,
427    /// Maximum value.
428    #[props(default = "100".to_string())]
429    pub max: String,
430    /// Step increment.
431    #[props(default)]
432    pub step: String,
433    /// Disabled state.
434    #[props(default)]
435    pub disabled: bool,
436    /// Input event handler.
437    #[props(default)]
438    pub oninput: Option<EventHandler<FormEvent>>,
439    /// Additional CSS classes.
440    #[props(default)]
441    pub class: String,
442    /// Any additional HTML attributes.
443    #[props(extends = GlobalAttributes)]
444    attributes: Vec<Attribute>,
445}
446
447#[component]
448pub fn Range(props: RangeProps) -> Element {
449    let full_class = if props.class.is_empty() {
450        "form-range".to_string()
451    } else {
452        format!("form-range {}", props.class)
453    };
454
455    rsx! {
456        input {
457            class: "{full_class}",
458            r#type: "range",
459            value: "{props.value}",
460            min: "{props.min}",
461            max: "{props.max}",
462            step: if props.step.is_empty() { None } else { Some(props.step.clone()) },
463            disabled: props.disabled,
464            oninput: move |evt| {
465                if let Some(handler) = &props.oninput {
466                    handler.call(evt);
467                }
468            },
469            ..props.attributes,
470        }
471    }
472}
473
474/// Bootstrap Floating Label wrapper.
475///
476/// Wraps an Input or Textarea with a floating label that moves
477/// above the control when focused or filled.
478///
479/// ```rust
480/// rsx! {
481///     FloatingLabel { label: "Email address",
482///         Input { r#type: "email", placeholder: "name@example.com" }
483///     }
484/// }
485/// ```
486#[derive(Clone, PartialEq, Props)]
487pub struct FloatingLabelProps {
488    /// Label text.
489    pub label: String,
490    /// Additional CSS classes.
491    #[props(default)]
492    pub class: String,
493    /// Any additional HTML attributes.
494    #[props(extends = GlobalAttributes)]
495    attributes: Vec<Attribute>,
496    /// Child element (Input or Textarea).
497    pub children: Element,
498}
499
500#[component]
501pub fn FloatingLabel(props: FloatingLabelProps) -> Element {
502    let full_class = if props.class.is_empty() {
503        "form-floating".to_string()
504    } else {
505        format!("form-floating {}", props.class)
506    };
507
508    rsx! {
509        div { class: "{full_class}",
510            ..props.attributes,
511            {props.children}
512            label { "{props.label}" }
513        }
514    }
515}
516
517/// Bootstrap form validation feedback text.
518///
519/// ```rust
520/// rsx! {
521///     Input { class: "is-valid".to_string(), value: "correct" }
522///     FormFeedback { valid: true, "Looks good!" }
523/// }
524/// ```
525#[derive(Clone, PartialEq, Props)]
526pub struct FormFeedbackProps {
527    /// True for valid feedback, false for invalid.
528    #[props(default)]
529    pub valid: bool,
530    /// Additional CSS classes.
531    #[props(default)]
532    pub class: String,
533    /// Any additional HTML attributes.
534    #[props(extends = GlobalAttributes)]
535    attributes: Vec<Attribute>,
536    /// Feedback text.
537    pub children: Element,
538}
539
540#[component]
541pub fn FormFeedback(props: FormFeedbackProps) -> Element {
542    let base = if props.valid {
543        "valid-feedback"
544    } else {
545        "invalid-feedback"
546    };
547    let full_class = if props.class.is_empty() {
548        base.to_string()
549    } else {
550        format!("{base} {}", props.class)
551    };
552
553    rsx! {
554        div { class: "{full_class}", ..props.attributes, {props.children} }
555    }
556}
557
558/// Bootstrap form text (help text below a control).
559///
560/// ```rust
561/// rsx! {
562///     Input { r#type: "password" }
563///     FormText { "Must be 8-20 characters long." }
564/// }
565/// ```
566#[derive(Clone, PartialEq, Props)]
567pub struct FormTextProps {
568    /// Additional CSS classes.
569    #[props(default)]
570    pub class: String,
571    /// Any additional HTML attributes.
572    #[props(extends = GlobalAttributes)]
573    attributes: Vec<Attribute>,
574    /// Help text content.
575    pub children: Element,
576}
577
578#[component]
579pub fn FormText(props: FormTextProps) -> Element {
580    let full_class = if props.class.is_empty() {
581        "form-text".to_string()
582    } else {
583        format!("form-text {}", props.class)
584    };
585
586    rsx! {
587        div { class: "{full_class}", ..props.attributes, {props.children} }
588    }
589}
590
591/// Bootstrap Radio button component.
592///
593/// # Bootstrap HTML → Dioxus
594///
595/// ```html
596/// <!-- Bootstrap HTML -->
597/// <div class="form-check">
598///   <input class="form-check-input" type="radio" name="color" checked>
599///   <label class="form-check-label">Red</label>
600/// </div>
601/// <div class="form-check">
602///   <input class="form-check-input" type="radio" name="color">
603///   <label class="form-check-label">Blue</label>
604/// </div>
605/// ```
606///
607/// ```rust,no_run
608/// rsx! {
609///     Radio { name: "color", label: "Red", checked: true }
610///     Radio { name: "color", label: "Blue" }
611/// }
612/// ```
613#[derive(Clone, PartialEq, Props)]
614pub struct RadioProps {
615    /// Radio group name.
616    pub name: String,
617    /// Whether the radio is checked.
618    #[props(default)]
619    pub checked: bool,
620    /// Label text.
621    #[props(default)]
622    pub label: String,
623    /// Disabled state.
624    #[props(default)]
625    pub disabled: bool,
626    /// Change event handler.
627    #[props(default)]
628    pub onchange: Option<EventHandler<FormEvent>>,
629    /// Additional CSS classes for the wrapper.
630    #[props(default)]
631    pub class: String,
632    /// Any additional HTML attributes.
633    #[props(extends = GlobalAttributes)]
634    attributes: Vec<Attribute>,
635}
636
637#[component]
638pub fn Radio(props: RadioProps) -> Element {
639    let full_class = if props.class.is_empty() {
640        "form-check".to_string()
641    } else {
642        format!("form-check {}", props.class)
643    };
644
645    rsx! {
646        div { class: "{full_class}",
647            ..props.attributes,
648            input {
649                class: "form-check-input",
650                r#type: "radio",
651                name: "{props.name}",
652                checked: props.checked,
653                disabled: props.disabled,
654                onchange: move |evt| {
655                    if let Some(handler) = &props.onchange {
656                        handler.call(evt);
657                    }
658                },
659            }
660            if !props.label.is_empty() {
661                label { class: "form-check-label", "{props.label}" }
662            }
663        }
664    }
665}