From 4c99fe55115f39a335935745530363037b899999 Mon Sep 17 00:00:00 2001 From: Michal Pokrywka Date: Thu, 18 Jun 2026 21:42:40 +0200 Subject: [PATCH] Added optional custom renderers and layout slots to LoginParams --- CHANGES.md | 1 + src/login.rs | 105 ++++++++++++++++++++++++++++------------ storybook/src/login.rs | 107 ++++++++++++++++++++++++++++++++++++++++- 3 files changed, 181 insertions(+), 32 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2141bf6..051889a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ * `ResourceTable` * `ImageUpload` helper struct to `DropImageFile` * "Select file" button to `DropImageFile` +* Optional render customization slots to `LoginParams` (`header`, `footer`, `render_username`, `render_password`, `render_submit`) ### Fixed diff --git a/src/login.rs b/src/login.rs index 8e63905..3e35d84 100644 --- a/src/login.rs +++ b/src/login.rs @@ -6,6 +6,13 @@ use vertigo::{ pub type OnSubmit = Rc; +/// Custom renderer for a login input. Receives the value bound to the form so +/// submit/Enter handling keeps working. +pub type RenderInput = Rc) -> DomNode>; + +/// Custom renderer for the submit control. Receives the submit callback to wire up. +pub type RenderSubmit = Rc) -> DomNode>; + #[component] pub fn Login( on_submit: OnSubmit, @@ -32,6 +39,19 @@ pub struct LoginParams { pub password_label: String, pub button_label: String, pub waiting_label: String, + /// Optional content rendered at the top of the form, before the message line. + pub header: Option, + /// Optional content rendered at the bottom of the form, after the submit button. + pub footer: Option, + /// Optional custom renderer for the username input. Receives the bound value + /// so submit/Enter handling keeps working. When `None`, a plain `` is used. + pub render_username: Option, + /// Optional custom renderer for the password input. Receives the bound value. + /// When `None`, a plain `` is used. + pub render_password: Option, + /// Optional custom renderer for the submit control. Receives the submit callback + /// to wire up (e.g. `on_click`). When `None`, a plain `` is used. + pub render_submit: Option, } impl Default for LoginParams { @@ -59,6 +79,11 @@ impl Default for LoginParams { password_label: "Password:".to_string(), button_label: "Login".to_string(), waiting_label: "Logging in...".to_string(), + header: None, + footer: None, + render_username: None, + render_password: None, + render_submit: None, } } } @@ -110,40 +135,46 @@ pub fn mount_login( }) ); - let on_username_change = bind!(username, |new_value: String| username.set(new_value)); - - let username_div = dom! { -
-
{¶ms.username_label}
- -
+ let username_div = match ¶ms.render_username { + Some(render) => render(username.clone()), + None => { + let on_username_change = bind!(username, |new_value: String| username.set(new_value)); + dom! { +
+
{¶ms.username_label}
+ +
+ } + } }; - let on_password_change = bind!(password, |new_value| password.set(new_value)); - - let password_div = dom! { -
-
{¶ms.password_label}
- -
+ let password_div = match ¶ms.render_password { + Some(render) => render(password.clone()), + None => { + let on_password_change = bind!(password, |new_value| password.set(new_value)); + dom! { +
+
{¶ms.password_label}
+ +
+ } + } }; - dom! { -
- { message_div } - { username_div } - { password_div } + let submit_div = match ¶ms.render_submit { + Some(render) => render(submit.clone()), + None => dom! {
( {..submit_attrs} />
+ }, + }; + + let header = params.header; + let footer = params.footer; + + dom! { +
+ { ..header } + { message_div } + { username_div } + { password_div } + { submit_div } + { ..footer }
} } diff --git a/storybook/src/login.rs b/storybook/src/login.rs index 08b9fb6..8c68f7d 100644 --- a/storybook/src/login.rs +++ b/storybook/src/login.rs @@ -1,5 +1,7 @@ -use vertigo::{DomNode, Resource, Value, bind_rc, dom}; -use vertigo_forms::login::{Login, LoginParams}; +use std::rc::Rc; +use vertigo::{DomNode, Resource, Value, bind, bind_rc, css, dom}; +use vertigo_forms::Input; +use vertigo_forms::login::{Login, LoginParams, RenderInput, RenderSubmit}; pub fn login() -> DomNode { let outcome_text = Value::new(String::new()); @@ -27,12 +29,113 @@ pub fn login() -> DomNode {
+
+ {result_html} + +
+ + { customized() } +
+ } +} + +/// Demonstrates the optional render-customization slots on `LoginParams`: +/// `header`, `footer`, `render_username`, `render_password` and `render_submit`. +fn customized() -> DomNode { + let token_result = Value::default(); + let on_submit = bind_rc!(token_result, |user: &str, pass: &str| { + if user == "test" && pass == "123" { + token_result.set(Some(Resource::Ready("qwerty1234".to_string()))); + } else { + token_result.set(Some(Resource::Error( + "Invalid password, try test/123".to_string(), + ))); + } + }); + + let result_html = token_result.render_value_option(|v| { + v.map(|v| match v { + Resource::Loading => dom! {

"Logging..."

}, + Resource::Ready(token) => dom! {

"Login successful, token " {token}

}, + Resource::Error(err) => dom! {

"Login error: " {err}

}, + }) + }); + + let label_css = css! {" + display: block; + font-size: 0.85em; + margin-bottom: 3px; + "}; + + // Reuse the existing `Input` component for the username slot. + let render_username: RenderInput = { + let label_css = label_css.clone(); + Rc::new(move |value: Value| { + dom! { +
+ + +
+ } + }) + }; + + // Hand-rolled input wiring for the password slot. + let render_password: RenderInput = { + let label_css = label_css.clone(); + Rc::new(move |value: Value| { + let on_input = bind!(value, |new_value: String| value.set(new_value)); + dom! { +
+ + +
+ } + }) + }; + + // Custom submit control wired to the form's submit callback. + let render_submit: RenderSubmit = Rc::new(|submit: Rc| { + dom! { +
+ +
+ } + }); + + let params = LoginParams { + header: Some(dom! {

"Custom login form"

}), + footer: Some(dom! { +

"Forgot your password?"

+ }), + render_username: Some(render_username), + render_password: Some(render_password), + render_submit: Some(render_submit), + ..Default::default() + }; + + dom! { +
+ + {result_html}
} }