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 use wasm_bindgen::JsCast;
274
275 let size_class = match props.size {
276 Size::Md => String::new(),
277 s => format!(" form-select-{s}"),
278 };
279
280 let full_class = if props.class.is_empty() {
281 format!("form-select{size_class}")
282 } else {
283 format!("form-select{size_class} {}", props.class)
284 };
285
286 // A `<select>`'s selection is controlled by its `.value` property (or an
287 // `<option selected>`), NOT by a `value` content attribute — browsers ignore
288 // the latter on a select, so Dioxus's declarative `value` silently does
289 // nothing and the element shows its first option. Hold the mounted element
290 // and set `.value` imperatively on mount and whenever `value` changes, so the
291 // control reflects the value it is given.
292 let mut select_el = use_signal(|| None as Option<web_sys::HtmlSelectElement>);
293 let value = props.value.clone();
294 use_effect(use_reactive!(|value| {
295 if let Some(el) = select_el.peek().clone() {
296 el.set_value(&value);
297 }
298 }));
299
300 let mount_value = props.value.clone();
301 rsx! {
302 select {
303 class: "{full_class}",
304 disabled: props.disabled,
305 onmounted: move |evt: MountedEvent| {
306 if let Some(el) = evt
307 .downcast::<web_sys::Element>()
308 .and_then(|e| e.clone().dyn_into::<web_sys::HtmlSelectElement>().ok())
309 {
310 el.set_value(&mount_value);
311 select_el.set(Some(el));
312 }
313 },
314 onchange: move |evt| {
315 if let Some(handler) = &props.onchange {
316 handler.call(evt);
317 }
318 },
319 ..props.attributes,
320 {props.children}
321 }
322 }
323}
324
325/// Bootstrap Textarea component.
326///
327/// # Bootstrap HTML → Dioxus
328///
329/// | HTML | Dioxus |
330/// |---|---|
331/// | `<textarea class="form-control" rows="5">` | `Textarea { rows: 5 }` |
332/// | `<textarea class="form-control form-control-sm">` | `Textarea { size: Size::Sm }` |
333/// | `<textarea class="form-control" placeholder="..." disabled>` | `Textarea { placeholder: "...", disabled: true }` |
334///
335/// ```rust,no_run
336/// # use dioxus::prelude::*;
337/// # use dioxus_bootstrap_css::prelude::*;
338/// # fn _doctest() -> Element {
339/// rsx! {
340/// Textarea { rows: 5, placeholder: "Enter description..." }
341/// }
342/// # }
343/// ```
344#[derive(Clone, PartialEq, Props)]
345pub struct TextareaProps {
346 /// Current value.
347 #[props(default)]
348 pub value: String,
349 /// When `true`, the `value` attribute is omitted so the field is
350 /// *uncontrolled*: the DOM keeps whatever value the user or an external
351 /// script writes, instead of Dioxus forcing it back to `value` on every
352 /// render. Use for a field another script streams into (e.g. a live
353 /// transcript box).
354 #[props(default)]
355 pub uncontrolled: bool,
356 /// Number of visible rows.
357 #[props(default = 3)]
358 pub rows: u32,
359 /// Placeholder text.
360 #[props(default)]
361 pub placeholder: String,
362 /// Textarea size.
363 #[props(default)]
364 pub size: Size,
365 /// Disabled state.
366 #[props(default)]
367 pub disabled: bool,
368 /// Readonly state.
369 #[props(default)]
370 pub readonly: bool,
371 /// Input event handler.
372 #[props(default)]
373 pub oninput: Option<EventHandler<FormEvent>>,
374 /// Change event handler.
375 #[props(default)]
376 pub onchange: Option<EventHandler<FormEvent>>,
377 /// Focus event handler.
378 #[props(default)]
379 pub onfocus: Option<EventHandler<FocusEvent>>,
380 /// Blur event handler.
381 #[props(default)]
382 pub onblur: Option<EventHandler<FocusEvent>>,
383 /// Key down event handler.
384 #[props(default)]
385 pub onkeydown: Option<EventHandler<KeyboardEvent>>,
386 /// Key up event handler.
387 #[props(default)]
388 pub onkeyup: Option<EventHandler<KeyboardEvent>>,
389 /// Additional CSS classes.
390 #[props(default)]
391 pub class: String,
392 /// Any additional HTML attributes.
393 #[props(extends = GlobalAttributes)]
394 attributes: Vec<Attribute>,
395}
396
397#[component]
398pub fn Textarea(props: TextareaProps) -> Element {
399 let size_class = match props.size {
400 Size::Md => String::new(),
401 s => format!(" form-control-{s}"),
402 };
403 let full_class = if props.class.is_empty() {
404 format!("form-control{size_class}")
405 } else {
406 format!("form-control{size_class} {}", props.class)
407 };
408
409 rsx! {
410 textarea {
411 class: "{full_class}",
412 rows: "{props.rows}",
413 placeholder: "{props.placeholder}",
414 disabled: props.disabled,
415 readonly: props.readonly,
416 value: if props.uncontrolled { None } else { Some(props.value.clone()) },
417 oninput: move |evt| {
418 if let Some(handler) = &props.oninput {
419 handler.call(evt);
420 }
421 },
422 onchange: move |evt| {
423 if let Some(handler) = &props.onchange {
424 handler.call(evt);
425 }
426 },
427 onfocus: move |evt| {
428 if let Some(handler) = &props.onfocus {
429 handler.call(evt);
430 }
431 },
432 onblur: move |evt| {
433 if let Some(handler) = &props.onblur {
434 handler.call(evt);
435 }
436 },
437 onkeydown: move |evt| {
438 if let Some(handler) = &props.onkeydown {
439 handler.call(evt);
440 }
441 },
442 onkeyup: move |evt| {
443 if let Some(handler) = &props.onkeyup {
444 handler.call(evt);
445 }
446 },
447 ..props.attributes,
448 }
449 }
450}
451
452/// Bootstrap Checkbox component.
453///
454/// # Bootstrap HTML → Dioxus
455///
456/// ```html
457/// <!-- Bootstrap HTML -->
458/// <div class="form-check">
459/// <input class="form-check-input" type="checkbox" checked>
460/// <label class="form-check-label">Accept terms</label>
461/// </div>
462/// ```
463///
464/// ```rust,no_run
465/// # use dioxus::prelude::*;
466/// # use dioxus_bootstrap_css::prelude::*;
467/// # fn _doctest() -> Element {
468/// rsx! {
469/// Checkbox { checked: true, label: "Accept terms",
470/// onchange: move |evt| { /* handle */ },
471/// }
472/// }
473/// # }
474/// ```
475#[derive(Clone, PartialEq, Props)]
476pub struct CheckboxProps {
477 /// Whether the checkbox is checked.
478 #[props(default)]
479 pub checked: bool,
480 /// Optional id applied to the checkbox input.
481 #[props(default)]
482 pub input_id: Option<String>,
483 /// Label text.
484 #[props(default)]
485 pub label: String,
486 /// Disabled state.
487 #[props(default)]
488 pub disabled: bool,
489 /// Change event handler.
490 #[props(default)]
491 pub onchange: Option<EventHandler<FormEvent>>,
492 /// Click event handler for the checkbox input.
493 #[props(default)]
494 pub onclick: Option<EventHandler<MouseEvent>>,
495 /// Additional CSS classes for the wrapper.
496 #[props(default)]
497 pub class: String,
498 /// Any additional HTML attributes.
499 #[props(extends = GlobalAttributes)]
500 attributes: Vec<Attribute>,
501}
502
503#[component]
504pub fn Checkbox(props: CheckboxProps) -> Element {
505 let full_class = if props.class.is_empty() {
506 "form-check".to_string()
507 } else {
508 format!("form-check {}", props.class)
509 };
510 let label_for = props.input_id.clone().unwrap_or_default();
511
512 rsx! {
513 div { class: "{full_class}",
514 ..props.attributes,
515 input {
516 class: "form-check-input",
517 r#type: "checkbox",
518 id: props.input_id.unwrap_or_default(),
519 checked: props.checked,
520 disabled: props.disabled,
521 onclick: move |evt| {
522 if let Some(handler) = &props.onclick {
523 handler.call(evt);
524 }
525 },
526 onchange: move |evt| {
527 if let Some(handler) = &props.onchange {
528 handler.call(evt);
529 }
530 },
531 }
532 if !props.label.is_empty() {
533 label { class: "form-check-label", r#for: "{label_for}", "{props.label}" }
534 }
535 }
536 }
537}
538
539/// Bootstrap Switch (toggle) component.
540///
541/// # Bootstrap HTML → Dioxus
542///
543/// ```html
544/// <!-- Bootstrap HTML -->
545/// <div class="form-check form-switch">
546/// <input class="form-check-input" type="checkbox" role="switch" checked>
547/// <label class="form-check-label">Enable notifications</label>
548/// </div>
549/// ```
550///
551/// ```rust,no_run
552/// # use dioxus::prelude::*;
553/// # use dioxus_bootstrap_css::prelude::*;
554/// # fn _doctest() -> Element {
555/// rsx! {
556/// Switch { checked: true, label: "Enable notifications",
557/// onchange: move |evt| { /* handle */ },
558/// }
559/// }
560/// # }
561/// ```
562#[derive(Clone, PartialEq, Props)]
563pub struct SwitchProps {
564 /// Whether the switch is on.
565 #[props(default)]
566 pub checked: bool,
567 /// Label text.
568 #[props(default)]
569 pub label: String,
570 /// Disabled state.
571 #[props(default)]
572 pub disabled: bool,
573 /// Change event handler.
574 #[props(default)]
575 pub onchange: Option<EventHandler<FormEvent>>,
576 /// Additional CSS classes for the wrapper.
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 Switch(props: SwitchProps) -> Element {
586 let full_class = if props.class.is_empty() {
587 "form-check form-switch".to_string()
588 } else {
589 format!("form-check form-switch {}", props.class)
590 };
591
592 rsx! {
593 div { class: "{full_class}",
594 ..props.attributes,
595 input {
596 class: "form-check-input",
597 r#type: "checkbox",
598 role: "switch",
599 checked: props.checked,
600 disabled: props.disabled,
601 onchange: move |evt| {
602 if let Some(handler) = &props.onchange {
603 handler.call(evt);
604 }
605 },
606 }
607 if !props.label.is_empty() {
608 label { class: "form-check-label", "{props.label}" }
609 }
610 }
611 }
612}
613
614/// Bootstrap Range (slider) input.
615///
616/// # Bootstrap HTML → Dioxus
617///
618/// | HTML | Dioxus |
619/// |---|---|
620/// | `<input type="range" class="form-range" min="0" max="100">` | `Range { min: "0", max: "100" }` |
621/// | `<input type="range" class="form-range" step="5" disabled>` | `Range { step: "5".into(), disabled: true }` |
622///
623/// ```rust,no_run
624/// # use dioxus::prelude::*;
625/// # use dioxus_bootstrap_css::prelude::*;
626/// # fn _doctest() -> Element {
627/// rsx! {
628/// Range { value: "50", min: "0", max: "100" }
629/// }
630/// # }
631/// ```
632#[derive(Clone, PartialEq, Props)]
633pub struct RangeProps {
634 /// Current value.
635 #[props(default)]
636 pub value: String,
637 /// Minimum value.
638 #[props(default = "0".to_string())]
639 pub min: String,
640 /// Maximum value.
641 #[props(default = "100".to_string())]
642 pub max: String,
643 /// Step increment.
644 #[props(default)]
645 pub step: String,
646 /// Disabled state.
647 #[props(default)]
648 pub disabled: bool,
649 /// Input event handler.
650 #[props(default)]
651 pub oninput: Option<EventHandler<FormEvent>>,
652 /// Additional CSS classes.
653 #[props(default)]
654 pub class: String,
655 /// Any additional HTML attributes.
656 #[props(extends = GlobalAttributes)]
657 attributes: Vec<Attribute>,
658}
659
660#[component]
661pub fn Range(props: RangeProps) -> Element {
662 use wasm_bindgen::JsCast;
663
664 let full_class = if props.class.is_empty() {
665 "form-range".to_string()
666 } else {
667 format!("form-range {}", props.class)
668 };
669
670 // A range slider's thumb position is controlled by its `.value` DOM property,
671 // NOT by a `value` content attribute (the attribute only seeds the default).
672 // Dioxus's declarative `value` sets the attribute, so a server-reported value
673 // that differs from the default leaves the thumb at the default — the same
674 // property-vs-attribute gap the Select had. Hold the mounted element and set
675 // `.value` imperatively on mount and whenever `value` changes.
676 let mut range_el = use_signal(|| None as Option<web_sys::HtmlInputElement>);
677 let value = props.value.clone();
678 use_effect(use_reactive!(|value| {
679 if let Some(el) = range_el.peek().clone() {
680 el.set_value(&value);
681 }
682 }));
683
684 let mount_value = props.value.clone();
685 rsx! {
686 input {
687 class: "{full_class}",
688 r#type: "range",
689 min: "{props.min}",
690 max: "{props.max}",
691 step: if props.step.is_empty() { None } else { Some(props.step.clone()) },
692 disabled: props.disabled,
693 onmounted: move |evt: MountedEvent| {
694 if let Some(el) = evt
695 .downcast::<web_sys::Element>()
696 .and_then(|e| e.clone().dyn_into::<web_sys::HtmlInputElement>().ok())
697 {
698 el.set_value(&mount_value);
699 range_el.set(Some(el));
700 }
701 },
702 oninput: move |evt| {
703 if let Some(handler) = &props.oninput {
704 handler.call(evt);
705 }
706 },
707 ..props.attributes,
708 }
709 }
710}
711
712/// Bootstrap Floating Label wrapper.
713///
714/// Wraps an Input or Textarea with a floating label that moves
715/// above the control when focused or filled.
716///
717/// # Bootstrap HTML → Dioxus
718///
719/// | HTML | Dioxus |
720/// |---|---|
721/// | `<div class="form-floating"><input class="form-control" placeholder="..."><label>Email</label></div>` | `FloatingLabel { label: "Email", Input { placeholder: "..." } }` |
722///
723/// ```rust,no_run
724/// # use dioxus::prelude::*;
725/// # use dioxus_bootstrap_css::prelude::*;
726/// # fn _doctest() -> Element {
727/// rsx! {
728/// FloatingLabel { label: "Email address",
729/// Input { r#type: "email", placeholder: "name@example.com" }
730/// }
731/// }
732/// # }
733/// ```
734#[derive(Clone, PartialEq, Props)]
735pub struct FloatingLabelProps {
736 /// Label text.
737 pub label: String,
738 /// Additional CSS classes.
739 #[props(default)]
740 pub class: String,
741 /// Any additional HTML attributes.
742 #[props(extends = GlobalAttributes)]
743 attributes: Vec<Attribute>,
744 /// Child element (Input or Textarea).
745 pub children: Element,
746}
747
748#[component]
749pub fn FloatingLabel(props: FloatingLabelProps) -> Element {
750 let full_class = if props.class.is_empty() {
751 "form-floating".to_string()
752 } else {
753 format!("form-floating {}", props.class)
754 };
755
756 rsx! {
757 div { class: "{full_class}",
758 ..props.attributes,
759 {props.children}
760 label { "{props.label}" }
761 }
762 }
763}
764
765/// Bootstrap form validation feedback text.
766///
767/// # Bootstrap HTML → Dioxus
768///
769/// | HTML | Dioxus |
770/// |---|---|
771/// | `<div class="valid-feedback">Looks good!</div>` | `FormFeedback { valid: true, "Looks good!" }` |
772/// | `<div class="invalid-feedback">Required.</div>` | `FormFeedback { "Required." }` |
773///
774/// ```rust,no_run
775/// # use dioxus::prelude::*;
776/// # use dioxus_bootstrap_css::prelude::*;
777/// # fn _doctest() -> Element {
778/// rsx! {
779/// Input { class: "is-valid".to_string(), value: "correct" }
780/// FormFeedback { valid: true, "Looks good!" }
781/// }
782/// # }
783/// ```
784#[derive(Clone, PartialEq, Props)]
785pub struct FormFeedbackProps {
786 /// True for valid feedback, false for invalid.
787 #[props(default)]
788 pub valid: bool,
789 /// Additional CSS classes.
790 #[props(default)]
791 pub class: String,
792 /// Any additional HTML attributes.
793 #[props(extends = GlobalAttributes)]
794 attributes: Vec<Attribute>,
795 /// Feedback text.
796 pub children: Element,
797}
798
799#[component]
800pub fn FormFeedback(props: FormFeedbackProps) -> Element {
801 let base = if props.valid {
802 "valid-feedback"
803 } else {
804 "invalid-feedback"
805 };
806 let full_class = if props.class.is_empty() {
807 base.to_string()
808 } else {
809 format!("{base} {}", props.class)
810 };
811
812 rsx! {
813 div { class: "{full_class}", ..props.attributes, {props.children} }
814 }
815}
816
817/// Bootstrap form text (help text below a control).
818///
819/// # Bootstrap HTML → Dioxus
820///
821/// | HTML | Dioxus |
822/// |---|---|
823/// | `<div class="form-text">Must be 8-20 characters.</div>` | `FormText { "Must be 8-20 characters." }` |
824///
825/// ```rust,no_run
826/// # use dioxus::prelude::*;
827/// # use dioxus_bootstrap_css::prelude::*;
828/// # fn _doctest() -> Element {
829/// rsx! {
830/// Input { r#type: "password" }
831/// FormText { "Must be 8-20 characters long." }
832/// }
833/// # }
834/// ```
835#[derive(Clone, PartialEq, Props)]
836pub struct FormTextProps {
837 /// Additional CSS classes.
838 #[props(default)]
839 pub class: String,
840 /// Any additional HTML attributes.
841 #[props(extends = GlobalAttributes)]
842 attributes: Vec<Attribute>,
843 /// Help text content.
844 pub children: Element,
845}
846
847#[component]
848pub fn FormText(props: FormTextProps) -> Element {
849 let full_class = if props.class.is_empty() {
850 "form-text".to_string()
851 } else {
852 format!("form-text {}", props.class)
853 };
854
855 rsx! {
856 div { class: "{full_class}", ..props.attributes, {props.children} }
857 }
858}
859
860/// Bootstrap Radio button component.
861///
862/// # Bootstrap HTML → Dioxus
863///
864/// ```html
865/// <!-- Bootstrap HTML -->
866/// <div class="form-check">
867/// <input class="form-check-input" type="radio" name="color" checked>
868/// <label class="form-check-label">Red</label>
869/// </div>
870/// <div class="form-check">
871/// <input class="form-check-input" type="radio" name="color">
872/// <label class="form-check-label">Blue</label>
873/// </div>
874/// ```
875///
876/// ```rust,no_run
877/// # use dioxus::prelude::*;
878/// # use dioxus_bootstrap_css::prelude::*;
879/// # fn _doctest() -> Element {
880/// rsx! {
881/// Radio { name: "color", label: "Red", checked: true }
882/// Radio { name: "color", label: "Blue" }
883/// }
884/// # }
885/// ```
886#[derive(Clone, PartialEq, Props)]
887pub struct RadioProps {
888 /// Radio group name.
889 pub name: String,
890 /// Whether the radio is checked.
891 #[props(default)]
892 pub checked: bool,
893 /// Label text.
894 #[props(default)]
895 pub label: String,
896 /// Disabled state.
897 #[props(default)]
898 pub disabled: bool,
899 /// Change event handler.
900 #[props(default)]
901 pub onchange: Option<EventHandler<FormEvent>>,
902 /// Additional CSS classes for the wrapper.
903 #[props(default)]
904 pub class: String,
905 /// Any additional HTML attributes.
906 #[props(extends = GlobalAttributes)]
907 attributes: Vec<Attribute>,
908}
909
910#[component]
911pub fn Radio(props: RadioProps) -> Element {
912 let full_class = if props.class.is_empty() {
913 "form-check".to_string()
914 } else {
915 format!("form-check {}", props.class)
916 };
917
918 rsx! {
919 div { class: "{full_class}",
920 ..props.attributes,
921 input {
922 class: "form-check-input",
923 r#type: "radio",
924 name: "{props.name}",
925 checked: props.checked,
926 disabled: props.disabled,
927 onchange: move |evt| {
928 if let Some(handler) = &props.onchange {
929 handler.call(evt);
930 }
931 },
932 }
933 if !props.label.is_empty() {
934 label { class: "form-check-label", "{props.label}" }
935 }
936 }
937 }
938}