input_rs/dioxus.rs
1use crate::countries::COUNTRY_CODES;
2use dioxus::prelude::*;
3
4/// Props for a custom input component.
5/// This struct includes all possible attributes for an HTML `<input>` element.
6/// See [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) for more details.
7#[derive(Props, Clone)]
8pub struct InputProps {
9 /// The type of the input, e.g., "text", "password", etc.
10 #[props(default = "text")]
11 pub r#type: &'static str,
12
13 /// The label to be displayed for the input field.
14 #[props(default = "")]
15 pub label: &'static str,
16
17 /// The name of the input field, used for form submission and accessibility.
18 #[props(default = "")]
19 pub name: &'static str,
20
21 /// Indicates whether the input is required or not.
22 #[props(default = false)]
23 pub required: bool,
24
25 /// The error message to display when there is a validation error.
26 #[props(default = "")]
27 pub error_message: &'static str,
28
29 /// The CSS class to be applied to all inner elements.
30 #[props(default = "")]
31 pub input_class: &'static str,
32
33 /// The CSS class to be applied to the inner input element and icon.
34 #[props(default = "")]
35 pub field_class: &'static str,
36
37 /// The CSS class to be applied to the label for the input element.
38 #[props(default = "")]
39 pub label_class: &'static str,
40
41 /// The CSS class to be applied to the input element.
42 #[props(default = "")]
43 pub class: &'static str,
44
45 /// The CSS class to be applied to the error div element.
46 #[props(default = "")]
47 pub error_class: &'static str,
48
49 /// The CSS class to be applied to the icon element.
50 #[props(default = "")]
51 pub icon_class: &'static str,
52
53 /// The state handle for managing the value of the input.
54 pub handle: Signal<String>,
55
56 /// The state handle for managing the validity state of the input.
57 pub valid_handle: Signal<bool>,
58
59 /// A callback function to validate the input value. It takes a `String` as input and returns a `bool`.
60 pub validate_function: fn(String) -> bool,
61
62 /// The icon when the password is visible. Assuming fontawesome icons are used by default.
63 #[props(
64 default = "cursor-pointer right-4 top-1 text-2xl text-gray-600 toggle-button fa fa-eye"
65 )]
66 pub eye_active: &'static str,
67
68 /// The icon when the password is not visible. Assuming fontawesome icons are used by default.
69 #[props(
70 default = "cursor-pointer right-4 top-1 text-2xl text-gray-600 toggle-button fa fa-eye-slash"
71 )]
72 pub eye_disabled: &'static str,
73
74 // Accessibility and SEO-related attributes:
75 /// The ID attribute of the input element.
76 #[props(default = "")]
77 pub id: &'static str,
78
79 /// The placeholder text to be displayed in the input element.
80 #[props(default = "")]
81 pub placeholder: &'static str,
82
83 /// The aria-label attribute for screen readers, providing a label for accessibility.
84 #[props(default = "")]
85 pub aria_label: &'static str,
86
87 /// The aria-required attribute for screen readers, indicating whether the input is required.
88 #[props(default = "true")]
89 pub aria_required: &'static str,
90
91 /// The aria-invalid attribute for screen readers, indicating whether the input value is invalid.
92 #[props(default = "true")]
93 pub aria_invalid: &'static str,
94
95 /// The aria-describedby attribute for screen readers, describing the input element's error message.
96 #[props(default = "")]
97 pub aria_describedby: &'static str,
98
99 // Newly added attributes from MDN:
100 /// Hint for expected file type in file upload controls.
101 #[props(default = "")]
102 pub accept: &'static str,
103
104 /// The alternative text for `<input type="image">`. Required for accessibility.
105 #[props(default = "")]
106 pub alt: &'static str,
107
108 /// Controls automatic capitalization in inputted text.
109 #[props(default = "")]
110 pub autocapitalize: &'static str,
111
112 /// Hint for the browser's autofill feature.
113 #[props(default = "")]
114 pub autocomplete: &'static str,
115
116 /// Media capture input method in file upload controls.
117 #[props(default = "")]
118 pub capture: &'static str,
119
120 /// Whether the control is checked (for checkboxes or radio buttons).
121 #[props(default = false)]
122 pub checked: bool,
123
124 /// Name of the form field to use for sending the element's directionality in form submission.
125 #[props(default = "")]
126 pub dirname: &'static str,
127
128 /// Whether the form control is disabled.
129 #[props(default = false)]
130 pub disabled: bool,
131
132 /// Associates the input with a specific form element.
133 #[props(default = "")]
134 pub form: &'static str,
135
136 /// URL to use for form submission (for `<input type="image" | "submit">`).
137 #[props(default = "")]
138 pub formaction: &'static str,
139
140 /// Form data set encoding type for submission (for `<input type="image" | "submit">`).
141 #[props(default = "")]
142 pub formenctype: &'static str,
143
144 /// HTTP method to use for form submission (for `<input type="image" | "submit">`).
145 #[props(default = "")]
146 pub formmethod: &'static str,
147
148 /// Bypass form validation for submission (for `<input type="image" | "submit">`).
149 #[props(default = false)]
150 pub formnovalidate: bool,
151
152 /// Browsing context for form submission (for `<input type="image" | "submit">`).
153 #[props(default = "")]
154 pub formtarget: &'static str,
155
156 /// Same as the `height` attribute for `<img>` elements.
157 #[props(default = None)]
158 pub height: Option<u32>,
159
160 /// ID of the `<datalist>` element to use for autocomplete suggestions.
161 #[props(default = "")]
162 pub list: &'static str,
163
164 /// The maximum value for date, number, range, etc.
165 #[props(default = "")]
166 pub max: &'static str,
167
168 /// Maximum length of the input value (in characters).
169 #[props(default = None)]
170 pub maxlength: Option<usize>,
171
172 /// The minimum value for date, number, range, etc.
173 #[props(default = "")]
174 pub min: &'static str,
175
176 /// Minimum length of the input value (in characters).
177 #[props(default = None)]
178 pub minlength: Option<usize>,
179
180 /// Boolean indicating whether multiple values are allowed (for file inputs, emails, etc.).
181 #[props(default = false)]
182 pub multiple: bool,
183
184 /// Regex pattern the value must match to be valid.
185 #[props(default = ".*")]
186 pub pattern: &'static str,
187
188 /// Boolean indicating whether the input is read-only.
189 #[props(default = false)]
190 pub readonly: bool,
191
192 /// Size of the input field (e.g., character width).
193 #[props(default = None)]
194 pub size: Option<u32>,
195
196 /// Address of the image resource for `<input type="image">`.
197 #[props(default = "")]
198 pub src: &'static str,
199
200 /// Incremental values that are valid for the input.
201 #[props(default = "")]
202 pub step: &'static str,
203
204 /// The value of the control (used for two-way data binding).
205 #[props(default = "")]
206 pub value: &'static str,
207
208 /// Same as the `width` attribute for `<img>` elements.
209 #[props(default = None)]
210 pub width: Option<u32>,
211}
212
213impl PartialEq for InputProps {
214 fn eq(&self, other: &Self) -> bool {
215 self.r#type == other.r#type
216 && self.label == other.label
217 && self.name == other.name
218 && self.required == other.required
219 && self.error_message == other.error_message
220 && self.input_class == other.input_class
221 && self.field_class == other.field_class
222 && self.label_class == other.label_class
223 && self.class == other.class
224 && self.error_class == other.error_class
225 && self.icon_class == other.icon_class
226 && self.handle == other.handle
227 && self.valid_handle == other.valid_handle
228 && std::ptr::fn_addr_eq(self.validate_function, other.validate_function)
229 && self.eye_active == other.eye_active
230 && self.eye_disabled == other.eye_disabled
231 && self.id == other.id
232 && self.placeholder == other.placeholder
233 && self.aria_label == other.aria_label
234 && self.aria_required == other.aria_required
235 && self.aria_invalid == other.aria_invalid
236 && self.aria_describedby == other.aria_describedby
237 && self.accept == other.accept
238 && self.alt == other.alt
239 && self.autocapitalize == other.autocapitalize
240 && self.autocomplete == other.autocomplete
241 && self.capture == other.capture
242 && self.checked == other.checked
243 && self.dirname == other.dirname
244 && self.disabled == other.disabled
245 && self.form == other.form
246 && self.formaction == other.formaction
247 && self.formenctype == other.formenctype
248 && self.formmethod == other.formmethod
249 && self.formnovalidate == other.formnovalidate
250 && self.formtarget == other.formtarget
251 && self.height == other.height
252 && self.list == other.list
253 && self.max == other.max
254 && self.maxlength == other.maxlength
255 && self.min == other.min
256 && self.minlength == other.minlength
257 && self.multiple == other.multiple
258 && self.pattern == other.pattern
259 && self.readonly == other.readonly
260 && self.size == other.size
261 && self.src == other.src
262 && self.step == other.step
263 && self.value == other.value
264 && self.width == other.width
265 }
266}
267
268/// A custom input component that handles user input and validation.
269///
270/// # Arguments
271/// * `props` - The properties of the component.
272/// - `valid_handle` - A state hook to track the validity of the input.
273/// - `aria_invalid` - A string representing the 'aria-invalid' attribute value for accessibility. Defaults to "true".
274/// - `aria_required` - A string representing the 'aria-required' attribute value for accessibility. Defaults to "true".
275/// - `r#type` - The type of the input element. Defaults to "text".
276/// - `handle` - A state hook to set the value of the input.
277/// - `validate_function` - A function to validate the input value.
278///
279/// # Returns
280/// (Element): A Dioxus element representation of the input component.
281///
282/// # Examples
283/// ```rust
284/// use regex::Regex;
285/// use input_rs::dioxus::Input;
286/// use dioxus::prelude::*;
287///
288/// #[derive(Debug, Default, Clone)]
289/// struct LoginUserSchema {
290/// email: String,
291/// password: String,
292/// }
293///
294/// fn LoginFormOne() -> Element {
295/// let error_handle = use_signal(|| String::new());
296/// let email_valid_handle = use_signal(|| true);
297/// let password_valid_handle = use_signal(|| true);
298///
299/// let email_handle = use_signal(|| String::new());
300/// let password_handle = use_signal(|| String::new());
301///
302/// let validate_email = |email: String| {
303/// let pattern = Regex::new(r"^[^ ]+@[^ ]+\.[a-z]{2,3}$").unwrap();
304/// pattern.is_match(&email)
305/// };
306///
307/// let validate_password = |password: String| {
308/// !password.is_empty()
309/// };
310///
311/// let onsubmit = {
312/// move |e: FormEvent| {
313/// e.stop_propagation();
314/// // Custom logic for your endpoint goes here.
315/// }
316/// };
317///
318/// rsx! {
319/// div {
320/// class: "form-one-content",
321/// role: "main",
322/// aria_label: "Sign In Form",
323/// div {
324/// class: "text",
325/// h2 { "Sign In" }
326/// if !error_handle().is_empty() {
327/// div { class: "error", "{error_handle}" }
328/// }
329/// }
330/// form {
331/// onsubmit: onsubmit,
332/// aria_label: "Sign In Form",
333/// Input {
334/// r#type: "text",
335/// handle: email_handle,
336/// name: "email",
337/// placeholder: "Email",
338/// icon_class: "fas fa-user",
339/// error_message: "Enter a valid email address",
340/// field_class: "form-one-field",
341/// error_class: "error-txt",
342/// required: true,
343/// valid_handle: email_valid_handle,
344/// validate_function: validate_email,
345/// }
346/// Input {
347/// r#type: "password",
348/// handle: password_handle,
349/// name: "password",
350/// placeholder: "Password",
351/// icon_class: "fas fa-lock",
352/// error_message: "Password can't be blank!",
353/// field_class: "form-one-field",
354/// error_class: "error-txt",
355/// required: true,
356/// valid_handle: password_valid_handle,
357/// validate_function: validate_password,
358/// eye_active: "fa fa-eye",
359/// eye_disabled: "fa fa-eye-slash",
360/// }
361/// div {
362/// class: "form-one-forgot-pass",
363/// a {
364/// href: "#",
365/// aria_label: "Forgot Password?",
366/// "Forgot Password?"
367/// }
368/// }
369/// button {
370/// r#type: "submit",
371/// "Sign in"
372/// }
373/// div {
374/// class: "sign-up",
375/// "Not a member? ",
376/// a {
377/// href: "#",
378/// aria_label: "Sign up now",
379/// "Sign up now"
380/// }
381/// }
382/// }
383/// }
384/// }
385/// }
386/// ```
387#[component]
388pub fn Input(mut props: InputProps) -> Element {
389 let mut is_eye_active = use_signal(|| false);
390 let password_type = if is_eye_active() { "text" } else { "password" };
391 let mut country = use_signal(String::default);
392
393 let onchange = {
394 move |e: Event<FormData>| {
395 let value = e.value();
396 props.handle.set(value.clone());
397 props.valid_handle.set((props.validate_function)(value));
398 }
399 };
400
401 let on_select_change = {
402 move |e: Event<FormData>| {
403 let value = e.value();
404 country.set(value.clone());
405 props.handle.set(value);
406 }
407 };
408
409 let on_phone_number_input = {
410 move |e: Event<FormData>| {
411 let input_value = e.value();
412 for (code, _, _, _, _, _) in &COUNTRY_CODES {
413 if code.starts_with(&input_value) {
414 country.set(input_value);
415 break;
416 }
417 }
418 // Filter out non-numeric characters
419 let numeric_value: String = e.value().chars().filter(|c| c.is_numeric()).collect();
420 props.handle.set('+'.to_string() + &numeric_value);
421 }
422 };
423
424 let toggle_eye_icon = {
425 move |_| {
426 is_eye_active.set(!is_eye_active());
427 }
428 };
429
430 let input_field = match props.r#type {
431 "password" => rsx! {
432 input {
433 r#type: "{password_type}",
434 class: "{props.input_class}",
435 id: "{props.id}",
436 name: "{props.name}",
437 value: "{props.handle}",
438 placeholder: "{props.placeholder}",
439 aria_label: "{props.aria_label}",
440 aria_required: "{props.aria_required}",
441 aria_invalid: "{props.aria_invalid}",
442 aria_describedby: "{props.aria_describedby}",
443 oninput: onchange,
444 required: props.required,
445 autocomplete: props.autocomplete,
446 autocapitalize: props.autocapitalize,
447 readonly: "{props.readonly}",
448 disabled: "{props.disabled}",
449 minlength: props.minlength.map(|v| v.to_string()).unwrap_or_default(),
450 maxlength: props.maxlength.map(|v| v.to_string()).unwrap_or_default(),
451 pattern: "{props.pattern}",
452 size: props.size.map(|v| v.to_string()).unwrap_or_default(),
453 }
454 span {
455 class: if is_eye_active() { props.eye_active } else { props.eye_disabled },
456 onclick: toggle_eye_icon
457 }
458 },
459 "tel" => rsx! {
460 select {
461 style: "max-width: 55px; font-size: 14px; padding: 10px;",
462 onchange: on_select_change,
463 { COUNTRY_CODES.iter().map(|(code, emoji, _, name, _, _)| rsx! {
464 option { value: "{code}", selected: *code == country(), "{emoji} {name} ({code})" }
465 })}
466 }
467 input {
468 r#type: "tel",
469 class: "{props.input_class}",
470 id: "{props.id}",
471 name: "{props.name}",
472 value: "{props.handle}",
473 placeholder: "{props.placeholder}",
474 aria_label: "{props.aria_label}",
475 aria_required: "{props.aria_required}",
476 aria_invalid: "{props.aria_invalid}",
477 aria_describedby: "{props.aria_describedby}",
478 oninput: on_phone_number_input,
479 required: props.required,
480 autocomplete: props.autocomplete,
481 autocapitalize: props.autocapitalize,
482 readonly: "{props.readonly}",
483 disabled: "{props.disabled}",
484 minlength: props.minlength.map(|v| v.to_string()).unwrap_or_default(),
485 maxlength: props.maxlength.map(|v| v.to_string()).unwrap_or_default(),
486 pattern: "{props.pattern}",
487 size: props.size.map(|v| v.to_string()).unwrap_or_default(),
488 }
489 },
490 "textarea" => rsx! {
491 textarea {
492 class: "{props.input_class}",
493 id: "{props.id}",
494 name: "{props.name}",
495 value: "{props.handle}",
496 placeholder: "{props.placeholder}",
497 aria_label: "{props.aria_label}",
498 aria_required: "{props.aria_required}",
499 aria_invalid: "{props.aria_invalid}",
500 aria_describedby: "{props.aria_describedby}",
501 oninput: onchange,
502 required: props.required,
503 readonly: "{props.readonly}",
504 disabled: "{props.disabled}",
505 minlength: props.minlength.map(|v| v.to_string()).unwrap_or_default(),
506 maxlength: props.maxlength.map(|v| v.to_string()).unwrap_or_default(),
507 }
508 },
509 _ => rsx! {
510 input {
511 r#type: "{props.r#type}",
512 class: "{props.input_class}",
513 id: "{props.id}",
514 name: "{props.name}",
515 value: "{props.handle}",
516 placeholder: "{props.placeholder}",
517 aria_label: "{props.aria_label}",
518 aria_required: "{props.aria_required}",
519 aria_invalid: "{props.aria_invalid}",
520 aria_describedby: "{props.aria_describedby}",
521 oninput: onchange,
522 required: props.required,
523 autocomplete: props.autocomplete,
524 autocapitalize: props.autocapitalize,
525 readonly: "{props.readonly}",
526 disabled: "{props.disabled}",
527 minlength: props.minlength.map(|v| v.to_string()).unwrap_or_default(),
528 maxlength: props.maxlength.map(|v| v.to_string()).unwrap_or_default(),
529 pattern: "{props.pattern}",
530 size: props.size.map(|v| v.to_string()).unwrap_or_default(),
531 }
532 },
533 };
534
535 rsx! {
536 div {
537 class: "{props.class}",
538 label {
539 class: "{props.label_class}",
540 r#for: "{props.id}",
541 "{props.label}"
542 }
543 div {
544 class: "{props.field_class}",
545 {input_field}
546 span {class: "{props.icon_class}" }
547 }
548 if !(props.valid_handle)() {
549 div {
550 class: "{props.error_class}",
551 id: "{props.aria_describedby}",
552 "{props.error_message}"
553 }
554 }
555 }
556 }
557}