Skip to main content

dioxus_bootstrap_css/
form.rs

1use dioxus::prelude::*;
2
3use crate::types::Size;
4
5/// Bootstrap FormGroup — label + control wrapper with spacing.
6///
7/// ```rust
8/// rsx! {
9///     FormGroup { label: "Email",
10///         Input { r#type: "email", placeholder: "you@example.com" }
11///     }
12/// }
13/// ```
14#[derive(Clone, PartialEq, Props)]
15pub struct FormGroupProps {
16    /// Label text.
17    #[props(default)]
18    pub label: String,
19    /// Additional CSS classes for the wrapper div.
20    #[props(default)]
21    pub class: String,
22    /// Child elements (form control).
23    pub children: Element,
24}
25
26#[component]
27pub fn FormGroup(props: FormGroupProps) -> Element {
28    let full_class = if props.class.is_empty() {
29        "mb-3".to_string()
30    } else {
31        format!("mb-3 {}", props.class)
32    };
33
34    rsx! {
35        div { class: "{full_class}",
36            if !props.label.is_empty() {
37                label { class: "form-label", "{props.label}" }
38            }
39            {props.children}
40        }
41    }
42}
43
44/// Bootstrap Input component.
45///
46/// ```rust
47/// rsx! {
48///     Input { r#type: "text", value: "hello", placeholder: "Enter text" }
49///     Input { r#type: "password", size: Size::Sm }
50/// }
51/// ```
52#[derive(Clone, PartialEq, Props)]
53pub struct InputProps {
54    /// Input type (text, email, password, number, etc.).
55    #[props(default = "text".to_string())]
56    pub r#type: String,
57    /// Current value.
58    #[props(default)]
59    pub value: String,
60    /// Placeholder text.
61    #[props(default)]
62    pub placeholder: String,
63    /// Input size.
64    #[props(default)]
65    pub size: Size,
66    /// Disabled state.
67    #[props(default)]
68    pub disabled: bool,
69    /// Readonly state.
70    #[props(default)]
71    pub readonly: bool,
72    /// Input event handler.
73    #[props(default)]
74    pub oninput: Option<EventHandler<FormEvent>>,
75    /// Additional CSS classes.
76    #[props(default)]
77    pub class: String,
78}
79
80#[component]
81pub fn Input(props: InputProps) -> Element {
82    let size_class = match props.size {
83        Size::Md => String::new(),
84        s => format!(" form-control-{s}"),
85    };
86
87    let full_class = if props.class.is_empty() {
88        format!("form-control{size_class}")
89    } else {
90        format!("form-control{size_class} {}", props.class)
91    };
92
93    rsx! {
94        input {
95            class: "{full_class}",
96            r#type: "{props.r#type}",
97            value: "{props.value}",
98            placeholder: "{props.placeholder}",
99            disabled: props.disabled,
100            readonly: props.readonly,
101            oninput: move |evt| {
102                if let Some(handler) = &props.oninput {
103                    handler.call(evt);
104                }
105            },
106        }
107    }
108}
109
110/// Bootstrap Select component.
111///
112/// ```rust
113/// rsx! {
114///     Select { value: "opt1",
115///         option { value: "opt1", "Option 1" }
116///         option { value: "opt2", "Option 2" }
117///     }
118/// }
119/// ```
120#[derive(Clone, PartialEq, Props)]
121pub struct SelectProps {
122    /// Current selected value.
123    #[props(default)]
124    pub value: String,
125    /// Select size.
126    #[props(default)]
127    pub size: Size,
128    /// Disabled state.
129    #[props(default)]
130    pub disabled: bool,
131    /// Change event handler.
132    #[props(default)]
133    pub onchange: Option<EventHandler<FormEvent>>,
134    /// Additional CSS classes.
135    #[props(default)]
136    pub class: String,
137    /// Child elements (option elements).
138    pub children: Element,
139}
140
141#[component]
142pub fn Select(props: SelectProps) -> Element {
143    let size_class = match props.size {
144        Size::Md => String::new(),
145        s => format!(" form-select-{s}"),
146    };
147
148    let full_class = if props.class.is_empty() {
149        format!("form-select{size_class}")
150    } else {
151        format!("form-select{size_class} {}", props.class)
152    };
153
154    rsx! {
155        select {
156            class: "{full_class}",
157            value: "{props.value}",
158            disabled: props.disabled,
159            onchange: move |evt| {
160                if let Some(handler) = &props.onchange {
161                    handler.call(evt);
162                }
163            },
164            {props.children}
165        }
166    }
167}
168
169/// Bootstrap Textarea component.
170///
171/// ```rust
172/// rsx! {
173///     Textarea { rows: 5, placeholder: "Enter description..." }
174/// }
175/// ```
176#[derive(Clone, PartialEq, Props)]
177pub struct TextareaProps {
178    /// Current value.
179    #[props(default)]
180    pub value: String,
181    /// Number of visible rows.
182    #[props(default = 3)]
183    pub rows: u32,
184    /// Placeholder text.
185    #[props(default)]
186    pub placeholder: String,
187    /// Disabled state.
188    #[props(default)]
189    pub disabled: bool,
190    /// Readonly state.
191    #[props(default)]
192    pub readonly: bool,
193    /// Input event handler.
194    #[props(default)]
195    pub oninput: Option<EventHandler<FormEvent>>,
196    /// Additional CSS classes.
197    #[props(default)]
198    pub class: String,
199}
200
201#[component]
202pub fn Textarea(props: TextareaProps) -> Element {
203    let full_class = if props.class.is_empty() {
204        "form-control".to_string()
205    } else {
206        format!("form-control {}", props.class)
207    };
208
209    rsx! {
210        textarea {
211            class: "{full_class}",
212            rows: "{props.rows}",
213            placeholder: "{props.placeholder}",
214            disabled: props.disabled,
215            readonly: props.readonly,
216            value: "{props.value}",
217            oninput: move |evt| {
218                if let Some(handler) = &props.oninput {
219                    handler.call(evt);
220                }
221            },
222        }
223    }
224}
225
226/// Bootstrap Checkbox component.
227///
228/// ```rust
229/// rsx! {
230///     Checkbox { checked: true, label: "Accept terms" }
231/// }
232/// ```
233#[derive(Clone, PartialEq, Props)]
234pub struct CheckboxProps {
235    /// Whether the checkbox is checked.
236    #[props(default)]
237    pub checked: bool,
238    /// Label text.
239    #[props(default)]
240    pub label: String,
241    /// Disabled state.
242    #[props(default)]
243    pub disabled: bool,
244    /// Change event handler.
245    #[props(default)]
246    pub onchange: Option<EventHandler<FormEvent>>,
247    /// Additional CSS classes for the wrapper.
248    #[props(default)]
249    pub class: String,
250}
251
252#[component]
253pub fn Checkbox(props: CheckboxProps) -> Element {
254    let full_class = if props.class.is_empty() {
255        "form-check".to_string()
256    } else {
257        format!("form-check {}", props.class)
258    };
259
260    rsx! {
261        div { class: "{full_class}",
262            input {
263                class: "form-check-input",
264                r#type: "checkbox",
265                checked: props.checked,
266                disabled: props.disabled,
267                onchange: move |evt| {
268                    if let Some(handler) = &props.onchange {
269                        handler.call(evt);
270                    }
271                },
272            }
273            if !props.label.is_empty() {
274                label { class: "form-check-label", "{props.label}" }
275            }
276        }
277    }
278}
279
280/// Bootstrap Radio component.
281///
282/// ```rust
283/// rsx! {
284///     Radio { name: "color", label: "Red", checked: true }
285///     Radio { name: "color", label: "Blue" }
286/// }
287/// ```
288#[derive(Clone, PartialEq, Props)]
289pub struct RadioProps {
290    /// Radio group name.
291    pub name: String,
292    /// Whether the radio is checked.
293    #[props(default)]
294    pub checked: bool,
295    /// Label text.
296    #[props(default)]
297    pub label: String,
298    /// Disabled state.
299    #[props(default)]
300    pub disabled: bool,
301    /// Change event handler.
302    #[props(default)]
303    pub onchange: Option<EventHandler<FormEvent>>,
304    /// Additional CSS classes for the wrapper.
305    #[props(default)]
306    pub class: String,
307}
308
309#[component]
310pub fn Radio(props: RadioProps) -> Element {
311    let full_class = if props.class.is_empty() {
312        "form-check".to_string()
313    } else {
314        format!("form-check {}", props.class)
315    };
316
317    rsx! {
318        div { class: "{full_class}",
319            input {
320                class: "form-check-input",
321                r#type: "radio",
322                name: "{props.name}",
323                checked: props.checked,
324                disabled: props.disabled,
325                onchange: move |evt| {
326                    if let Some(handler) = &props.onchange {
327                        handler.call(evt);
328                    }
329                },
330            }
331            if !props.label.is_empty() {
332                label { class: "form-check-label", "{props.label}" }
333            }
334        }
335    }
336}