Skip to main content

dioxus_ui_system/molecules/
input_group.rs

1//! Input Group molecule component
2//!
3//! Combines Label, Input, and helper text/error messages into a cohesive form field.
4
5use crate::atoms::{AlignItems, Input, InputType, Label, SpacingSize, TextColor, TextSize, VStack};
6use crate::styles::Style;
7use crate::theme::use_style;
8use dioxus::prelude::*;
9
10/// Input Group properties
11#[derive(Props, Clone, PartialEq)]
12pub struct InputGroupProps {
13    /// Field label
14    pub label: String,
15    /// Current input value
16    #[props(default)]
17    pub value: String,
18    /// Placeholder text
19    #[props(default)]
20    pub placeholder: Option<String>,
21    /// Input type
22    #[props(default)]
23    pub input_type: InputType,
24    /// Error message (shows error state when Some)
25    #[props(default)]
26    pub error: Option<String>,
27    /// Help/hint text
28    #[props(default)]
29    pub hint: Option<String>,
30    /// Required field indicator
31    #[props(default)]
32    pub required: bool,
33    /// Disabled state
34    #[props(default)]
35    pub disabled: bool,
36    /// Change handler
37    #[props(default)]
38    pub onchange: Option<EventHandler<String>>,
39    /// Optional icon before input
40    #[props(default)]
41    pub leading_icon: Option<Element>,
42    /// Optional icon after input
43    #[props(default)]
44    pub trailing_icon: Option<Element>,
45    /// Custom inline styles
46    #[props(default)]
47    pub style: Option<String>,
48}
49
50/// Input Group molecule component
51///
52/// # Example
53/// ```rust,ignore
54/// use dioxus_ui_system::molecules::InputGroup;
55///
56/// let mut email = use_signal(|| String::new());
57///
58/// rsx! {
59///     InputGroup {
60///         label: "Email",
61///         value: email(),
62///         placeholder: "you@example.com",
63///         hint: "We'll never share your email.",
64///         onchange: move |v| email.set(v),
65///     }
66/// }
67/// ```
68#[component]
69pub fn InputGroup(props: InputGroupProps) -> Element {
70    let label = props.label.clone();
71    let value = props.value.clone();
72    let placeholder = props.placeholder.clone();
73    let input_type = props.input_type.clone();
74    let error = props.error.clone();
75    let hint = props.hint.clone();
76    let required = props.required;
77    let disabled = props.disabled;
78
79    let container_style = use_style(|_t| Style::new().w_full().build());
80
81    let input_wrapper_style = use_style(|_| Style::new().relative().build());
82
83    let label_element = if required {
84        rsx! {
85            Label {
86                size: TextSize::Small,
87                weight: crate::atoms::label::TextWeight::Medium,
88                "{label}"
89                Label {
90                    size: TextSize::Small,
91                    color: TextColor::Destructive,
92                    " *"
93                }
94            }
95        }
96    } else {
97        rsx! {
98            Label {
99                size: TextSize::Small,
100                weight: crate::atoms::label::TextWeight::Medium,
101                "{label}"
102            }
103        }
104    };
105
106    let helper_text = if let Some(err) = error {
107        Some(rsx! {
108            Label {
109                size: TextSize::ExtraSmall,
110                color: TextColor::Destructive,
111                "{err}"
112            }
113        })
114    } else if let Some(h) = hint {
115        Some(rsx! {
116            Label {
117                size: TextSize::ExtraSmall,
118                color: TextColor::Muted,
119                "{h}"
120            }
121        })
122    } else {
123        None
124    };
125
126    let custom_style = props.style.clone().unwrap_or_default();
127
128    rsx! {
129        div {
130            style: "{container_style} {custom_style}",
131
132            VStack {
133                gap: SpacingSize::Xs,
134                align: AlignItems::Stretch,
135
136                {label_element}
137
138                div {
139                    style: "{input_wrapper_style} display: flex; align-items: center;",
140
141                    if props.leading_icon.is_some() {
142                        div {
143                            style: "position: absolute; left: 12px; z-index: 1;",
144                            {props.leading_icon.unwrap()}
145                        }
146                    }
147
148                    Input {
149                        value: value,
150                        placeholder: placeholder,
151                        input_type: input_type,
152                        disabled: disabled,
153                        onchange: props.onchange.clone(),
154                    }
155
156                    if props.trailing_icon.is_some() {
157                        div {
158                            style: "position: absolute; right: 12px; z-index: 1;",
159                            {props.trailing_icon.unwrap()}
160                        }
161                    }
162                }
163
164                {helper_text}
165            }
166        }
167    }
168}