input_yew/
lib.rs

1pub mod countries;
2
3use crate::countries::COUNTRY_CODES;
4use web_sys::HtmlInputElement;
5use yew::prelude::*;
6
7/// Props for a custom input component.
8#[derive(Properties, PartialEq, Clone)]
9pub struct Props {
10    /// The type of the input, e.g., "text", "password", etc.
11    #[prop_or("text")]
12    pub input_type: &'static str,
13
14    /// The label to be displayed for the input field.
15    #[prop_or_default]
16    pub label: &'static str,
17
18    /// The name of the input field, used for form submission and accessibility.
19    #[prop_or_default]
20    pub name: &'static str,
21
22    /// Indicates whether the input is required or not.
23    #[prop_or_default]
24    pub required: bool,
25
26    /// A reference to the DOM node of the input element.
27    pub input_ref: NodeRef,
28
29    /// The error message to display when there is a validation error.
30    #[prop_or_default]
31    pub error_message: &'static str,
32
33    /// The CSS class to be applied to all inner elements.
34    #[prop_or_default]
35    pub form_input_class: &'static str,
36
37    /// The CSS class to be applied to the inner input element and icon.
38    #[prop_or_default]
39    pub form_input_field_class: &'static str,
40
41    /// The CSS class to be applied to the label for the input element.
42    #[prop_or_default]
43    pub form_input_label_class: &'static str,
44
45    /// The CSS class to be applied to the input element.
46    #[prop_or_default]
47    pub form_input_input_class: &'static str,
48
49    /// The CSS class to be applied to the error div element.
50    #[prop_or_default]
51    pub form_input_error_class: &'static str,
52
53    /// The CSS class to be applied to the icon element.
54    #[prop_or_default]
55    pub icon_class: &'static str,
56
57    /// The state handle for managing the value of the input.
58    pub input_handle: UseStateHandle<String>,
59
60    /// The state handle for managing the validity state of the input.
61    pub input_valid_handle: UseStateHandle<bool>,
62
63    /// A callback function to validate the input value. It takes a `String` as input and returns a `bool`.
64    pub validate_function: Callback<String, bool>,
65
66    /// The icon when the password is visible. Assuming fontawesome icons is used by default.
67    #[prop_or("fa fa-eye")]
68    pub eye_active: &'static str,
69
70    /// The icon when the password is not visible. Assuming fontawesome icons is used by default.
71    #[prop_or("fa fa-eye-slash")]
72    pub eye_disabled: &'static str,
73
74    // Additional props for accessibility and SEO:
75    /// The ID attribute of the input element.
76    #[prop_or_default]
77    pub input_id: &'static str,
78
79    /// The placeholder text to be displayed in the input element.
80    #[prop_or_default]
81    pub input_placeholder: &'static str,
82
83    /// The aria-label attribute for screen readers, providing a label for accessibility.
84    #[prop_or_default]
85    pub aria_label: &'static str,
86
87    /// The aria-required attribute for screen readers, indicating whether the input is required.
88    #[prop_or("true")]
89    pub aria_required: &'static str,
90
91    /// The aria-invalid attribute for screen readers, indicating whether the input value is invalid.
92    #[prop_or("true")]
93    pub aria_invalid: &'static str,
94
95    /// The aria-describedby attribute for screen readers, describing the input element's error message.
96    #[prop_or_default]
97    pub aria_describedby: &'static str,
98}
99
100/// custom_input_component
101/// A custom input component that handles user input and validation.
102///
103/// # Arguments
104/// * `props` - The properties of the component.
105///   - `input_valid_handle` - A handle to track the validity of the input.
106///   - `aria_invalid` - A string representing the 'aria-invalid' attribute value for accessibility. Defaults to "true".
107///   - `aria_required` - A string representing the 'aria-required' attribute value for accessibility. Defaults to "true".
108///   - `input_type` - The type of the input element. Defaults to "text".
109///   - `input_ref` - A reference to the input element.
110///   - `input_handle` - A handle to set the value of the input.
111///   - `validate_function` - A callback function to validate the input value.
112///
113/// # Returns
114/// (Html): An HTML representation of the input component.
115///
116/// # Examples
117/// ```
118/// // Example of using the custom_input_component
119/// use regex::Regex;
120/// use serde::{Deserialize, Serialize};
121/// use input_yew::CustomInput;
122/// use yew::prelude::*;
123///
124/// #[derive(Debug, Default, Clone, Serialize, Deserialize)]
125/// struct LoginUserSchema {
126///     email: String,
127///     password: String,
128/// }
129///
130/// fn validate_email(email: String) -> bool {
131///     let pattern = Regex::new(r"^[^ ]+@[^ ]+\.[a-z]{2,3}$").unwrap();
132///     pattern.is_match(&email)
133/// }
134///
135/// fn validate_password(password: String) -> bool {
136///     !&password.is_empty()
137/// }
138///
139/// #[function_component(LoginFormOne)]
140/// pub fn login_form_one() -> Html {
141///     let error_handle = use_state(String::default);
142///     let error = (*error_handle).clone();;
143///
144///     let email_valid_handle = use_state(|| true);
145///     let email_valid = (*email_valid_handle).clone();;
146///
147///     let password_valid_handle = use_state(|| true);
148///     let password_valid = (*password_valid_handle).clone();;
149///
150///     let input_email_ref = use_node_ref();
151///     let input_email_handle = use_state(String::default);
152///     let input_email = (*input_email_handle).clone();;
153///
154///     let input_password_ref = use_node_ref();
155///     let input_password_handle = use_state(String::default);
156///     let input_password = (*input_password_handle).clone();;
157///
158///     let onsubmit = Callback::from(move |event: SubmitEvent| {
159///         event.prevent_default();
160///
161///         let email_ref = input_password.clone();
162///         let password_ref = input_password.clone();
163///         let error_handle = error_handle.clone();
164///
165///         // Custom logic for your endpoint goes here: `spawn_local`
166///     });
167///
168///     html! {
169///         <div class="form-one-content" role="main" aria-label="Sign In Form">
170///           <div class="text">
171///             <h2>{"Sign In"}</h2>
172///             if !error.is_empty() {
173///               <div class="error">{error}</div>
174///             }
175///           </div>
176///           <form action="#" aria-label="Sign In Form" onsubmit={onsubmit}>
177///               <CustomInput
178///                 input_type={"text"}
179///                 input_handle={input_email_handle}
180///                 name={"email"}
181///                 input_ref={input_email_ref}
182///                 input_placeholder={"Email"}
183///                 icon_class={"fas fa-user"}
184///                 error_message={"Enter a valid email address"}
185///                 form_input_field_class={"form-one-field"}
186///                 form_input_error_class={"error-txt"}
187///                 required={true}
188///                 input_valid_handle={email_valid_handle}
189///                 validate_function={validate_email}
190///               />
191///               <CustomInput
192///                 input_type={"password"}
193///                 input_handle={input_password_handle}
194///                 name={"password"}
195///                 input_ref={input_password_ref}
196///                 input_placeholder={"Password"}
197///                 icon_class={"fas fa-lock"}
198///                 error_message={"Password can't be blank!"}
199///                 form_input_field_class={"form-one-field"}
200///                 form_input_error_class={"error-txt"}
201///                 required={true}
202///                 input_valid_handle={password_valid_handle}
203///                 validate_function={validate_password}
204///                 eye_active={"fa fa-eye"}
205///                 eye_disabled={"fa fa-eye-slash"}
206///               />
207///             <div class="form-one-forgot-pass">
208///               <a href="#" aria-label="Forgot Password?">{"Forgot Password?"}</a>
209///             </div>
210///             <button type="submit">{"Sign in"}</button>
211///             <div class="sign-up">
212///               {"Not a member?"}
213///               <a href="#" aria-label="Sign up now">{"Sign up now"}</a>
214///             </div>
215///           </form>
216///         </div>
217///     }
218/// }
219/// ```
220#[function_component(CustomInput)]
221pub fn custom_input(props: &Props) -> Html {
222    let eye_active_handle = use_state(|| false);
223    let eye_active = *eye_active_handle;
224
225    let input_country_ref = use_node_ref();
226    let country_handle = use_state(String::default);
227    let country = (*country_handle).clone();
228
229    let password_type_handle = use_state(|| "password");
230    let password_type = *password_type_handle;
231
232    let input_valid = *props.input_valid_handle;
233
234    let aria_invalid = props.aria_invalid;
235
236    let eye_icon_active = props.eye_active;
237
238    let eye_icon_disabled = props.eye_disabled;
239
240    let aria_required = props.aria_required;
241
242    let input_type = props.input_type;
243
244    let onchange = {
245        let input_ref = props.input_ref.clone();
246        let input_handle = props.input_handle.clone();
247        let input_valid_handle = props.input_valid_handle.clone();
248        let validate_function = props.validate_function.clone();
249
250        Callback::from(move |_| {
251            if let Some(input) = input_ref.cast::<HtmlInputElement>() {
252                let value = input.value();
253                input_handle.set(value);
254                input_valid_handle.set(validate_function.emit(input.value()));
255            }
256        })
257    };
258
259    let on_select_change = {
260        let input_country_ref = input_country_ref.clone();
261        let input_handle = props.input_handle.clone();
262        let country_handle = country_handle.clone();
263        Callback::from(move |_| {
264            if let Some(input) = input_country_ref.cast::<HtmlInputElement>() {
265                let value = input.value();
266                country_handle.set(value);
267                input_handle.set(input.value());
268            }
269        })
270    };
271
272    let on_phone_number_input = {
273        let input_ref = props.input_ref.clone();
274        let input_handle = props.input_handle.clone();
275        let country_handle = country_handle;
276        Callback::from(move |_| {
277            if let Some(input) = input_ref.cast::<HtmlInputElement>() {
278                for (code, _, _, _, _, _) in &COUNTRY_CODES {
279                    if code.starts_with(&input.value()) {
280                        country_handle.set(input.value());
281                        break;
282                    }
283                }
284                // Filter out non-numeric characters
285                let numeric_value: String =
286                    input.value().chars().filter(|c| c.is_numeric()).collect();
287                input_handle.set('+'.to_string() + &numeric_value);
288            }
289        })
290    };
291
292    let on_toggle_password = {
293        Callback::from(move |_| {
294            if eye_active {
295                password_type_handle.set("password")
296            } else {
297                password_type_handle.set("text")
298            }
299            eye_active_handle.set(!eye_active);
300        })
301    };
302
303    let input_tag = match (*input_type).into() {
304        "password" => html! {
305            <>
306                <input
307                    type={password_type}
308                    class={props.form_input_input_class}
309                    id={props.input_id}
310                    name={props.name}
311                    value={(*props.input_handle).clone()}
312                    ref={props.input_ref.clone()}
313                    placeholder={props.input_placeholder}
314                    aria-label={props.aria_label}
315                    aria-required={aria_required}
316                    aria-invalid={aria_invalid}
317                    aria-describedby={props.aria_describedby}
318                    oninput={onchange}
319                    required={props.required}
320                />
321                <span
322                    class={format!("toggle-button {}", if eye_active { eye_icon_active } else { eye_icon_disabled })}
323                    onclick={on_toggle_password}
324                />
325            </>
326        },
327        "textarea" => html! {
328            <textarea
329                class={props.form_input_input_class}
330                id={props.input_id}
331                name={props.name}
332                value={(*props.input_handle).clone()}
333                ref={props.input_ref.clone()}
334                placeholder={props.input_placeholder}
335                aria-label={props.aria_label}
336                aria-required={aria_required}
337                aria-invalid={aria_invalid}
338                aria-describedby={props.aria_describedby}
339                oninput={onchange}
340                required={props.required}
341            />
342        },
343        "tel" => html! {
344            <>
345                <select ref={input_country_ref} onchange={on_select_change}>
346                    { for COUNTRY_CODES.iter().map(|(code, emoji, _, name, _, _)| {
347                            let selected = *code == country;
348                            html! {
349                                <option value={*code} selected={selected}>{ format!("{} {} {}", emoji, name, code) }</option>
350                            }
351                        }) }
352                </select>
353                <input
354                    type="tel"
355                    id="telNo"
356                    name="telNo"
357                    size="20"
358                    minlength="9"
359                    value={(*props.input_handle).clone()}
360                    maxlength="14"
361                    class={props.form_input_input_class}
362                    placeholder={props.input_placeholder}
363                    aria-label={props.aria_label}
364                    aria-required={aria_required}
365                    aria-invalid={aria_invalid}
366                    oninput={on_phone_number_input}
367                    ref={props.input_ref.clone()}
368                />
369            </>
370        },
371        _ => html! {
372            <input
373                type={input_type}
374                class={props.form_input_input_class}
375                id={props.input_id}
376                value={(*props.input_handle).clone()}
377                name={props.name}
378                ref={props.input_ref.clone()}
379                placeholder={props.input_placeholder}
380                aria-label={props.aria_label}
381                aria-required={aria_required}
382                aria-invalid={aria_invalid}
383                aria-describedby={props.aria_describedby}
384                oninput={onchange}
385                required={props.required}
386            />
387        },
388    };
389
390    html! {
391        <div class={props.form_input_class}>
392            <label class={props.form_input_label_class} for={props.input_id}>{ props.label }</label>
393            <div class={props.form_input_field_class}>
394                { input_tag }
395                <span class={props.icon_class} />
396            </div>
397            if !input_valid {
398                <div class={props.form_input_error_class} id={props.aria_describedby}>
399                    { &props.error_message }
400                </div>
401            }
402        </div>
403    }
404}