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