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