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