input_rs/
leptos.rs

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