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