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}