diff --git a/.bumpversion.toml b/.bumpversion.toml index 43f2411..77d7c3a 100755 --- a/.bumpversion.toml +++ b/.bumpversion.toml @@ -1,5 +1,5 @@ [bumpversion] -current_version = 0.2.2 +current_version = 0.2.3 [bumpversion:file:Cargo.toml] search = version = "{current_version}" diff --git a/Cargo.toml b/Cargo.toml index 7f3c177..9fb003d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "input-rs" -version = "0.2.2" +version = "0.2.3" edition = "2021" rust-version = "1.79" description = "A feature-rich, accessible, highly customizable, functional, reusable input component for Wasm Frameworks." @@ -18,6 +18,7 @@ exclude = ["assets", "examples"] web-sys = { version = "0.3", default-features = false } yew = { version = "0.21.0", default-features = false, optional = true } dioxus = { version = "0.5", optional = true } +leptos = { version = "0.7.0", optional = true } [dev-dependencies] bump2version = "0.1.4" @@ -27,6 +28,7 @@ serde = { version = "1.0.193", features = ["derive"] } [features] yew = ["dep:yew", ] dio = ["dioxus", ] +lep = ["leptos", ] [profile.release] opt-level = "z" diff --git a/README.md b/README.md index 85717c4..eb67240 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ -## 🎬 Yew Demo +## 🎬 Demo | Input Type | Demo | | ---------- | -------------------------------------------- | @@ -37,7 +37,7 @@ A reusable input component built for WASM frameworks like Yew, Dioxus, and Lepto 5. **♿ Accessible**: User-friendly and built for inclusivity. 6. **❌ Error Handling**: Displays clear error messages for invalid input. -## ⚙️ Installation Yew +## ⚙️ Yew Installation You can quickly integrate this Custom Reusable Input Component into your Yew project by following these simple steps: diff --git a/assets/favicon.ico b/assets/favicon.ico deleted file mode 100644 index 3805d87..0000000 Binary files a/assets/favicon.ico and /dev/null differ diff --git a/assets/favicon.png b/assets/favicon.png new file mode 100644 index 0000000..a3c2b74 Binary files /dev/null and b/assets/favicon.png differ diff --git a/assets/logo.png b/assets/logo.png old mode 100644 new mode 100755 index 203c07e..bde3ec5 Binary files a/assets/logo.png and b/assets/logo.png differ diff --git a/examples/leptos/Cargo.toml b/examples/leptos/Cargo.toml new file mode 100755 index 0000000..b82b9b9 --- /dev/null +++ b/examples/leptos/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "leptos-input-rs" +version = "0.1.0" +edition = "2021" + +[dependencies] +console_error_panic_hook = "0.1.7" +input-rs = { path = "../../", features = ["lep"] } +leptos = { version = "0.7.0", features = ["csr"] } +log = "0.4.22" +regex = "1.11.1" +serde = "1.0.215" +wasm-logger = "0.2.0" diff --git a/examples/leptos/README.md b/examples/leptos/README.md new file mode 100644 index 0000000..09b0312 --- /dev/null +++ b/examples/leptos/README.md @@ -0,0 +1,81 @@ +# 📚 Input RS Leptos Example + +## 🛠️ Pre-requisites: + +### 🐧 **Linux Users** + +1. **Install [`rustup`](https://www.rust-lang.org/tools/install)**: + + ```sh + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + ``` + +1. **Install [`trunk`](https://trunkrs.dev/)**: + + ```sh + cargo install --locked trunk + ``` + +1. **Add the Wasm target**: + + ```sh + rustup target add wasm32-unknown-unknown + ``` + +### 🪟 **Windows Users** + +1. **Download and install `rustup`**: Follow the installation instructions [here](https://www.rust-lang.org/tools/install). + +1. **Install [Windows Subsystem for Linux (WSL)](https://learn.microsoft.com/en-us/windows/wsl/install)**: Open PowerShell as administrator and run: + + ```sh + wsl --install + ``` + +1. **Reset Network Stack**: In PowerShell (administrator mode), run: + + ```sh + netsh int ip reset all + netsh winsock reset + ``` + +1. **Install Linux packages in WSL**: Once inside your WSL terminal, update and install required dependencies: + + ```sh + sudo apt update + sudo apt install build-essential pkg-config libudev-dev + ``` + +1. **Install `trunk`**: + + ```sh + cargo install --locked trunk + ``` + +1. **Add the Wasm target**: + + ```sh + rustup target add wasm32-unknown-unknown + ``` + +## 🚀 Building and Running + +1. Fork/Clone the GitHub repository. + + ```bash + git clone https://github.com/opensass/input-rs + ``` + +1. Navigate to the application directory. + + ```bash + cd input-rs/examples/leptos + ``` + +1. Run the client: + + ```sh + trunk serve --port 3000 + ``` + +Navigate to http://localhost:3000 to explore all available components. diff --git a/examples/leptos/index.html b/examples/leptos/index.html new file mode 100755 index 0000000..5b1dd65 --- /dev/null +++ b/examples/leptos/index.html @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/examples/leptos/src/main.rs b/examples/leptos/src/main.rs new file mode 100755 index 0000000..cbd3d1a --- /dev/null +++ b/examples/leptos/src/main.rs @@ -0,0 +1,155 @@ +use input_rs::leptos::Input; +use leptos::{prelude::*, task::spawn_local}; +use regex::Regex; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +struct LoginUserSchema { + email: String, + password: String, +} + +fn validate_email(email: String) -> bool { + let pattern = Regex::new(r"^[^ ]+@[^ ]+\.[a-z]{2,3}$").unwrap(); + pattern.is_match(&email) +} + +fn validate_password(password: String) -> bool { + !&password.is_empty() +} + +#[component] +pub fn App() -> impl IntoView { + view! { + + } +} + +#[component] +fn LoginForm() -> impl IntoView { + let error_handle = signal(String::default()); + let error = error_handle.0.get(); + + let email_valid_handle = signal(true); + let email_valid = email_valid_handle.0.get(); + + let password_valid_handle = signal(true); + let password_valid = password_valid_handle.0.get(); + + let email_handle = signal(String::default()); + let email = email_handle.0.get(); + + let password_handle = signal(String::default()); + let password = password_handle.0.get(); + + let onsubmit = move |ev: leptos::ev::SubmitEvent| { + ev.prevent_default(); + + let email_ref = email.clone(); + let password_ref = password.clone(); + let error_handle = error_handle.clone(); + + spawn_local(async move { + if email_valid && password_valid { + // API call + log::info!( + "Logged in with Email: {}, Password: {}", + email_ref, + password_ref + ); + } else { + error_handle + .1 + .set("Please provide a valid email and password!".to_string()); + } + }); + }; + + view! { +
+ // TODO: Why the flex styling is not applied? + //
+ //

{"Sign In"}

+ // { move || if !error.is_empty() { + // Some(view! {
error
}) + // } + // else {None} + // } + //
+
+ + +
+ +
+
+ {"Not a member?"} + {"Sign Up Now"} +
+
+
+ {"Or Sign In With"} +
+
+
+
+
+ + + + +
+
+
+
+ } +} + +fn main() { + console_error_panic_hook::set_once(); + wasm_logger::init(wasm_logger::Config::default()); + leptos::mount::mount_to_body(|| view! { }) +} diff --git a/src/leptos.rs b/src/leptos.rs new file mode 100644 index 0000000..21e85e2 --- /dev/null +++ b/src/leptos.rs @@ -0,0 +1,515 @@ +#![allow(unused)] + +use crate::countries::COUNTRY_CODES; +use leptos::{prelude::*, *}; + +/// A custom input component that handles user input and validation. +/// +/// # Arguments +/// * `props` - The properties of the component. +/// - `valid_handle` - A state hook to track the validity of the input. +/// - `aria_invalid` - A string representing the 'aria-invalid' attribute value for accessibility. Defaults to "true". +/// - `aria_required` - A string representing the 'aria-required' attribute value for accessibility. Defaults to "true". +/// - `r#type` - The type of the input element. Defaults to "text". +/// - `handle` - A state hook to set the value of the input. +/// - `validate_function` - A function to validate the input value. +/// +/// # Returns +/// (IntoView): A Leptos element representation of the input component. +/// +/// # Examples +/// ```rust +/// use leptos::{prelude::*, *}; +/// use regex::Regex; +/// use serde::{Deserialize, Serialize}; +/// use input_rs::leptos::Input; +/// +/// +/// #[derive(Debug, Default, Clone, Serialize, Deserialize)] +/// struct LoginUserSchema { +/// email: String, +/// password: String, +/// } +/// +/// fn validate_email(email: String) -> bool { +/// let pattern = Regex::new(r"^[^ ]+@[^ ]+\.[a-z]{2,3}$").unwrap(); +/// pattern.is_match(&email) +/// } +/// +/// fn validate_password(password: String) -> bool { +/// !&password.is_empty() +/// } +/// +/// #[component] +/// fn LoginForm() -> impl IntoView { +/// let error_handle = signal(String::default()); +/// let error = error_handle.0.get(); +/// +/// let email_valid_handle = signal(true); +/// let email_valid = email_valid_handle.0.get(); +/// +/// let password_valid_handle = signal(true); +/// let password_valid = password_valid_handle.0.get(); +/// +/// let email_handle = signal(String::default()); +/// let email = email_handle.0.get(); +/// +/// let password_handle = signal(String::default()); +/// let password = password_handle.0.get(); +/// +/// let onsubmit = move |ev: leptos::ev::SubmitEvent| { +/// ev.prevent_default(); +/// +/// let email_ref = email.clone(); +/// let password_ref = password.clone(); +/// let error_handle = error_handle.clone(); +/// +/// // Custom logic for your endpoint goes here +/// }; +/// +/// view! { +///
+///
+///

{"Sign In"}

+/// { move || if !error.is_empty() { +/// Some(view! {
error
}) +/// } +/// else {None} +/// } +///
+///
+/// +/// +/// +/// +/// +///
+///
+/// } +/// } +/// ``` +#[component] +pub fn Input( + /// Props for a custom input component. + /// This struct includes all possible attributes for an HTML `` element. + /// See [MDN docs](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input) for more details. + /// + /// The type of the input, e.g., "text", "password", etc. + #[prop(default = "text")] + r#type: &'static str, + + /// The label to be displayed for the input field. + #[prop(default = "")] + label: &'static str, + + /// The name of the input field, used for form submission and accessibility. + #[prop(default = "")] + name: &'static str, + + /// Indicates whether the input is required or not. + #[prop(default = false)] + required: bool, + + /// The error message to display when there is a validation error. + #[prop(default = "")] + error_message: &'static str, + + /// The CSS class to be applied to all inner elements. + #[prop(default = "")] + input_class: &'static str, + + /// The CSS class to be applied to the inner input element and icon. + #[prop(default = "")] + field_class: &'static str, + + /// The CSS class to be applied to the label for the input element. + #[prop(default = "")] + label_class: &'static str, + + /// The CSS class to be applied to the input element. + #[prop(default = "")] + class: &'static str, + + /// The CSS class to be applied to the error div element. + #[prop(default = "")] + error_class: &'static str, + + /// The CSS class to be applied to the icon element. + #[prop(default = "")] + icon_class: &'static str, + + /// The state handle for managing the value of the input. + handle: (ReadSignal, WriteSignal), + + /// The state handle for managing the validity state of the input. + valid_handle: (ReadSignal, WriteSignal), + + /// A callback function to validate the input value. It takes a `String` as input and returns a `bool`. + validate_function: fn(String) -> bool, + + /// The icon when the password is visible. Assuming fontawesome icons are used by default. + #[prop( + default = "cursor-pointer right-4 top-1 text-2xl text-gray-600 toggle-button fa fa-eye" + )] + eye_active: &'static str, + + /// The icon when the password is not visible. Assuming fontawesome icons are used by default. + #[prop( + default = "cursor-pointer right-4 top-1 text-2xl text-gray-600 toggle-button fa fa-eye-slash" + )] + eye_disabled: &'static str, + + // Accessibility and SEO-related attributes: + /// The ID attribute of the input element. + #[prop(default = "")] + id: &'static str, + + /// The placeholder text to be displayed in the input element. + #[prop(default = "")] + placeholder: &'static str, + + /// The aria-label attribute for screen readers, providing a label for accessibility. + #[prop(default = "")] + aria_label: &'static str, + + /// The aria-required attribute for screen readers, indicating whether the input is required. + #[prop(default = "true")] + aria_required: &'static str, + + /// The aria-invalid attribute for screen readers, indicating whether the input value is invalid. + #[prop(default = "true")] + aria_invalid: &'static str, + + /// The aria-describedby attribute for screen readers, describing the input element's error message. + #[prop(default = "")] + aria_describedby: &'static str, + + // Newly added attributes from MDN: + /// Hint for expected file type in file upload controls. + #[prop(default = "")] + accept: &'static str, + + /// The alternative text for ``. Required for accessibility. + #[prop(default = "")] + alt: &'static str, + + /// Controls automatic capitalization in inputted text. + #[prop(default = "")] + autocapitalize: &'static str, + + /// Hint for the browser's autofill feature. + #[prop(default = "")] + autocomplete: &'static str, + + /// Media capture input method in file upload controls. + #[prop(default = "")] + capture: &'static str, + + /// Whether the control is checked (for checkboxes or radio buttons). + #[prop(default = false)] + checked: bool, + + /// Name of the form field to use for sending the element's directionality in form submission. + #[prop(default = "")] + dirname: &'static str, + + /// Whether the form control is disabled. + #[prop(default = false)] + disabled: bool, + + /// Associates the input with a specific form element. + #[prop(default = "")] + form: &'static str, + + /// URL to use for form submission (for ``). + #[prop(default = "")] + formaction: &'static str, + + /// Form data set encoding type for submission (for ``). + #[prop(default = "")] + formenctype: &'static str, + + /// HTTP method to use for form submission (for ``). + #[prop(default = "")] + formmethod: &'static str, + + /// Bypass form validation for submission (for ``). + #[prop(default = false)] + formnovalidate: bool, + + /// Browsing context for form submission (for ``). + #[prop(default = "")] + formtarget: &'static str, + + /// Same as the `height` attribute for `` elements. + #[prop(default = None)] + height: Option, + + /// ID of the `` element to use for autocomplete suggestions. + #[prop(default = "")] + list: &'static str, + + /// The maximum value for date, number, range, etc. + #[prop(default = "")] + max: &'static str, + + /// Maximum length of the input value (in characters). + #[prop(default = None)] + maxlength: Option, + + /// The minimum value for date, number, range, etc. + #[prop(default = "")] + min: &'static str, + + /// Minimum length of the input value (in characters). + #[prop(default = None)] + minlength: Option, + + /// Boolean indicating whether multiple values are allowed (for file inputs, emails, etc.). + #[prop(default = false)] + multiple: bool, + + /// Regex pattern the value must match to be valid. + #[prop(default = ".*")] + pattern: &'static str, + + /// Boolean indicating whether the input is read-only. + #[prop(default = false)] + readonly: bool, + + /// Size of the input field (e.g., character width). + #[prop(default = None)] + size: Option, + + /// Address of the image resource for ``. + #[prop(default = "")] + src: &'static str, + + /// Incremental values that are valid for the input. + #[prop(default = "")] + step: &'static str, + + /// The value of the control (used for two-way data binding). + #[prop(default = "")] + value: &'static str, + + /// Same as the `width` attribute for `` elements. + #[prop(default = None)] + width: Option, +) -> impl IntoView { + let (eye_active_handle, set_eye_active_handle) = signal(false); + let (password_type, set_password_type) = signal("password".to_string()); + let valid = valid_handle; + let input_ref: NodeRef = NodeRef::new(); + + let onchange = { + let validate_function = validate_function.clone(); + move |ev: web_sys::Event| { + let input_value = input_ref.get().expect(" should be mounted").value(); + handle.1.set(input_value.clone()); + valid.1.set(validate_function(input_value)); + } + }; + + let on_toggle_password = { + move |ev: web_sys::Event| { + if eye_active_handle.get() { + set_password_type.set("password".to_string()); + } else { + set_password_type.set("text".to_string()); + } + set_eye_active_handle.set(!eye_active_handle.get()); + } + }; + + // TODO: Fix me. Matching logic in Leptos is such a pain. + let tag = { + move || match r#type { + "password" => Some(view! { + <> + + // + + }), + // "textarea" => Some(view! { + // <> + //