Skip to main content

dioxus_element_plug/components/
input.rs

1use dioxus::prelude::*;
2use crate::components::common::{ClassBuilder, style_str, fire_event};
3
4// Input CSS class constants
5pub const INPUT: &str = "el-input";
6pub const INPUT_INNER: &str = "el-input__inner";
7pub const INPUT_WRAPPER: &str = "el-input__wrapper";
8pub const INPUT_PREFIX: &str = "el-input__prefix";
9pub const INPUT_SUFFIX: &str = "el-input__suffix";
10pub const INPUT_DISABLED: &str = "is-disabled";
11pub const INPUT_LARGE: &str = "el-input--large";
12pub const INPUT_MEDIUM: &str = "el-input--medium";
13pub const INPUT_SMALL: &str = "el-input--small";
14pub const INPUT_MINI: &str = "el-input--mini";
15pub const INPUT_ERROR: &str = "is-error";
16pub const INPUT_GROUP: &str = "el-input-group";
17pub const INPUT_GROUP_APPEND: &str = "el-input-group__append";
18pub const INPUT_GROUP_PREPEND: &str = "el-input-group__prepend";
19
20/// Input sizes matching theme-chalk
21#[derive(Clone, PartialEq)]
22pub enum InputSize {
23    Large,
24    Medium,
25    Small,
26    Mini,
27}
28
29impl InputSize {
30    pub fn as_class(&self) -> &'static str {
31        match self {
32            InputSize::Large => "el-input--large",
33            InputSize::Medium => "el-input--medium",
34            InputSize::Small => "el-input--small",
35            InputSize::Mini => "el-input--mini",
36        }
37    }
38}
39
40/// Input types
41#[derive(Clone, PartialEq)]
42pub enum InputType {
43    Text,
44    Password,
45    Email,
46    Url,
47    Number,
48    Tel,
49    Search,
50    Textarea,
51}
52
53impl InputType {
54    pub fn as_str(&self) -> &'static str {
55        match self {
56            InputType::Text => "text",
57            InputType::Password => "password",
58            InputType::Email => "email",
59            InputType::Url => "url",
60            InputType::Number => "number",
61            InputType::Tel => "tel",
62            InputType::Search => "search",
63            InputType::Textarea => "textarea",
64        }
65    }
66}
67
68/// Input props for the theme-chalk styled input component
69#[derive(Props, Clone, PartialEq)]
70pub struct InputProps {
71    /// Input value
72    #[props(default)]
73    pub value: Option<String>,
74
75    /// Input placeholder
76    #[props(default)]
77    pub placeholder: Option<String>,
78
79    /// Input type
80    #[props(default = InputType::Text)]
81    pub input_type: InputType,
82
83    /// Input size
84    #[props(default)]
85    pub size: Option<InputSize>,
86
87    /// Whether the input is disabled
88    #[props(default = false)]
89    pub disabled: bool,
90
91    /// Whether the input is readonly
92    #[props(default = false)]
93    pub readonly: bool,
94
95    /// Whether to show clear button
96    #[props(default = false)]
97    pub clearable: bool,
98
99    /// Whether to show password toggle (for password inputs)
100    #[props(default = false)]
101    pub show_password: bool,
102
103    /// Whether the input has error state
104    #[props(default = false)]
105    pub error: bool,
106
107    /// Input label
108    #[props(default)]
109    pub label: Option<String>,
110
111    /// Input prefix icon (CSS class)
112    #[props(default)]
113    pub prefix_icon: Option<String>,
114
115    /// Input suffix icon (CSS class)
116    #[props(default)]
117    pub suffix_icon: Option<String>,
118
119    /// Maximum length
120    #[props(default)]
121    pub maxlength: Option<u32>,
122
123    /// Minimum length
124    #[props(default)]
125    pub minlength: Option<u32>,
126
127    /// Whether to auto focus
128    #[props(default = false)]
129    pub autofocus: bool,
130
131    /// Name attribute
132    #[props(default)]
133    pub name: Option<String>,
134
135    /// Id attribute
136    #[props(default)]
137    pub id: Option<String>,
138
139    /// Input event handler
140    #[props(default)]
141    pub on_input: Option<EventHandler<FormEvent>>,
142
143    /// Change event handler
144    #[props(default)]
145    pub on_change: Option<EventHandler<FormEvent>>,
146
147    /// Focus event handler
148    #[props(default)]
149    pub on_focus: Option<EventHandler<FocusEvent>>,
150
151    /// Blur event handler
152    #[props(default)]
153    pub on_blur: Option<EventHandler<FocusEvent>>,
154
155    /// Key down event handler
156    #[props(default)]
157    pub on_keydown: Option<EventHandler<KeyboardEvent>>,
158
159    /// Additional CSS classes
160    #[props(default)]
161    pub class: Option<String>,
162
163    /// Inline styles
164    #[props(default)]
165    pub style: Option<String>,
166}
167
168/// An input component styled with theme-chalk CSS
169///
170/// This component wraps the native HTML input/textarea element and applies
171/// theme-chalk CSS classes for consistent styling.
172///
173/// ## Example
174///
175/// ```rust,ignore
176/// use dioxus_element_plug::components::input::{Input, InputType, InputSize};
177///
178/// rsx! {
179///     Input {
180///         input_type: InputType::Text,
181///         placeholder: "Enter your name",
182///         size: Some(InputSize::Medium),
183///         on_input: move |evt| println!("Input: {}", evt.value()),
184///     }
185/// }
186/// ```
187#[component]
188pub fn Input(props: InputProps) -> Element {
189    let mut focused = use_signal(|| false);
190
191    let mut outer_builder = ClassBuilder::new(INPUT);
192
193    if let Some(ref size) = props.size {
194        outer_builder = outer_builder.add_class(size.as_class());
195    }
196
197    let outer_class = outer_builder
198        .add_if("is-disabled", props.disabled)
199        .add_if("is-error", props.error)
200        .add_opt(props.class.as_ref())
201        .build();
202
203    let wrapper_class = ClassBuilder::new(INPUT_WRAPPER)
204        .add_if("is-focus", focused())
205        .add_if("is-disabled", props.disabled)
206        .build();
207
208    let inner_style = style_str(&props.style);
209    let value = props.value.clone().unwrap_or_default();
210    let placeholder = props.placeholder.clone().unwrap_or_default();
211    let name = props.name.clone().unwrap_or_default();
212    let id = props.id.clone().unwrap_or_default();
213    let maxlength = props.maxlength.map(|n| n.to_string()).unwrap_or_default();
214    let minlength = props.minlength.map(|n| n.to_string()).unwrap_or_default();
215
216    let on_input = props.on_input;
217    let on_change = props.on_change;
218    let on_focus = props.on_focus;
219    let on_blur = props.on_blur;
220    let on_keydown = props.on_keydown;
221
222    let input_element = if props.input_type == InputType::Textarea {
223        rsx! {
224            textarea {
225                class: INPUT_INNER,
226                value: "{value}",
227                placeholder: "{placeholder}",
228                disabled: props.disabled,
229                readonly: props.readonly,
230                maxlength: "{maxlength}",
231                minlength: "{minlength}",
232                autofocus: props.autofocus,
233                name: "{name}",
234                id: "{id}",
235                style: "{inner_style}",
236                oninput: move |event| fire_event(&on_input, event),
237                onchange: move |event| fire_event(&on_change, event),
238                onfocus: move |event| {
239                    focused.set(true);
240                    fire_event(&on_focus, event);
241                },
242                onblur: move |event| {
243                    focused.set(false);
244                    fire_event(&on_blur, event);
245                },
246                onkeydown: move |event| fire_event(&on_keydown, event),
247            }
248        }
249    } else {
250        rsx! {
251            input {
252                class: INPUT_INNER,
253                r#type: props.input_type.as_str(),
254                value: "{value}",
255                placeholder: "{placeholder}",
256                disabled: props.disabled,
257                readonly: props.readonly,
258                maxlength: "{maxlength}",
259                minlength: "{minlength}",
260                autofocus: props.autofocus,
261                name: "{name}",
262                id: "{id}",
263                style: "{inner_style}",
264                oninput: move |event| fire_event(&on_input, event),
265                onchange: move |event| fire_event(&on_change, event),
266                onfocus: move |event| {
267                    focused.set(true);
268                    fire_event(&on_focus, event);
269                },
270                onblur: move |event| {
271                    focused.set(false);
272                    fire_event(&on_blur, event);
273                },
274                onkeydown: move |event| fire_event(&on_keydown, event),
275            }
276        }
277    };
278
279    rsx! {
280        div {
281            class: "{outer_class}",
282
283            if let Some(ref label) = props.label {
284                label {
285                    class: "el-form-item__label",
286                    "{label}"
287                }
288            }
289
290            div {
291                class: "{wrapper_class}",
292
293                if let Some(ref prefix_icon) = props.prefix_icon {
294                    span {
295                        class: INPUT_PREFIX,
296                        i { class: "{prefix_icon} el-input__icon" }
297                    }
298                }
299
300                {input_element}
301
302                if props.clearable {
303                    span {
304                        class: INPUT_SUFFIX,
305                        i { class: "el-input__icon el-icon-circle-close" }
306                    }
307                }
308
309                if props.show_password && props.input_type == InputType::Password {
310                    span {
311                        class: INPUT_SUFFIX,
312                        i { class: "el-input__icon el-icon-view" }
313                    }
314                }
315
316                if let Some(ref suffix_icon) = props.suffix_icon {
317                    span {
318                        class: INPUT_SUFFIX,
319                        i { class: "{suffix_icon} el-input__icon" }
320                    }
321                }
322            }
323        }
324    }
325}
326
327/// Search input with built-in search icon
328#[component]
329pub fn SearchInput(props: InputProps) -> Element {
330    let mut props = props;
331    if props.suffix_icon.is_none() {
332        props.suffix_icon = Some("el-icon-search".to_string());
333    }
334    Input(props)
335}
336
337/// Password input with toggle visibility
338#[component]
339pub fn PasswordInput(props: InputProps) -> Element {
340    let mut props = props;
341    props.input_type = InputType::Password;
342    props.show_password = true;
343    Input(props)
344}