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}