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