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