Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
105 changes: 75 additions & 30 deletions src/login.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,13 @@ use vertigo::{

pub type OnSubmit = Rc<dyn Fn(&str, &str)>;

/// Custom renderer for a login input. Receives the value bound to the form so
/// submit/Enter handling keeps working.
pub type RenderInput = Rc<dyn Fn(Value<String>) -> DomNode>;

/// Custom renderer for the submit control. Receives the submit callback to wire up.
pub type RenderSubmit = Rc<dyn Fn(Rc<dyn Fn()>) -> DomNode>;

#[component]
pub fn Login<T: Clone + PartialEq + 'static>(
on_submit: OnSubmit,
Expand All @@ -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<DomNode>,
/// Optional content rendered at the bottom of the form, after the submit button.
pub footer: Option<DomNode>,
/// Optional custom renderer for the username input. Receives the bound value
/// so submit/Enter handling keeps working. When `None`, a plain `<input>` is used.
pub render_username: Option<RenderInput>,
/// Optional custom renderer for the password input. Receives the bound value.
/// When `None`, a plain `<input type="password">` is used.
pub render_password: Option<RenderInput>,
/// Optional custom renderer for the submit control. Receives the submit callback
/// to wire up (e.g. `on_click`). When `None`, a plain `<input type="submit">` is used.
pub render_submit: Option<RenderSubmit>,
}

impl Default for LoginParams {
Expand Down Expand Up @@ -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,
}
}
}
Expand Down Expand Up @@ -110,40 +135,46 @@ pub fn mount_login<T: Clone + PartialEq + 'static>(
})
);

let on_username_change = bind!(username, |new_value: String| username.set(new_value));

let username_div = dom! {
<div css={line_css.clone()}>
<div>{&params.username_label}</div>
<input
css={&params.input_css}
value={username.to_computed()}
on_input={on_username_change}
{..input_attrs.clone()}
/>
</div>
let username_div = match &params.render_username {
Some(render) => render(username.clone()),
None => {
let on_username_change = bind!(username, |new_value: String| username.set(new_value));
dom! {
<div css={line_css.clone()}>
<div>{&params.username_label}</div>
<input
css={&params.input_css}
value={username.to_computed()}
on_input={on_username_change}
{..input_attrs.clone()}
/>
</div>
}
}
};

let on_password_change = bind!(password, |new_value| password.set(new_value));

let password_div = dom! {
<div css={line_css}>
<div>{&params.password_label}</div>
<input
css={&params.input_css}
value={password.to_computed()}
on_input={on_password_change}
type="password"
{..input_attrs}
/>
</div>
let password_div = match &params.render_password {
Some(render) => render(password.clone()),
None => {
let on_password_change = bind!(password, |new_value| password.set(new_value));
dom! {
<div css={line_css}>
<div>{&params.password_label}</div>
<input
css={&params.input_css}
value={password.to_computed()}
on_input={on_password_change}
type="password"
{..input_attrs}
/>
</div>
}
}
};

dom! {
<div css={css} {on_key_down}>
{ message_div }
{ username_div }
{ password_div }
let submit_div = match &params.render_submit {
Some(render) => render(submit.clone()),
None => dom! {
<div css={submit_css}>
<input
type="submit"
Expand All @@ -152,6 +183,20 @@ pub fn mount_login<T: Clone + PartialEq + 'static>(
{..submit_attrs}
/>
</div>
},
};

let header = params.header;
let footer = params.footer;

dom! {
<div css={css} {on_key_down}>
{ ..header }
{ message_div }
{ username_div }
{ password_div }
{ submit_div }
{ ..footer }
</div>
}
}
107 changes: 105 additions & 2 deletions storybook/src/login.rs
Original file line number Diff line number Diff line change
@@ -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());
Expand Down Expand Up @@ -27,12 +29,113 @@ pub fn login() -> DomNode {
<div>
<Login on_submit={on_submit.clone()} token_result={token_result.clone()} params={} />

<hr />

<Login {on_submit} {token_result} params={LoginParams {
username_label: "E-Mail".to_string(),
..Default::default()
}} />

{result_html}

<hr />

{ customized() }
</div>
}
}

/// 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! { <p>"Logging..."</p> },
Resource::Ready(token) => dom! { <p>"Login successful, token " {token}</p> },
Resource::Error(err) => dom! { <p>"Login error: " {err}</p> },
})
});

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<String>| {
dom! {
<div>
<label css={&label_css}>"👤 Username"</label>
<Input {value} />
</div>
}
})
};

// Hand-rolled input wiring for the password slot.
let render_password: RenderInput = {
let label_css = label_css.clone();
Rc::new(move |value: Value<String>| {
let on_input = bind!(value, |new_value: String| value.set(new_value));
dom! {
<div>
<label css={&label_css}>"🔒 Password"</label>
<input
type="password"
value={value.to_computed()}
{on_input}
/>
</div>
}
})
};

// Custom submit control wired to the form's submit callback.
let render_submit: RenderSubmit = Rc::new(|submit: Rc<dyn Fn()>| {
dom! {
<div css={css! {"margin-top: 15px;"}}>
<button
css={css! {"
padding: 6px 16px;
cursor: pointer;
"}}
on_click={move |_| submit()}
>
"Sign in →"
</button>
</div>
}
});

let params = LoginParams {
header: Some(dom! { <h3>"Custom login form"</h3> }),
footer: Some(dom! {
<p css={css! {"font-size: 0.85em; margin-top: 10px;"}}>"Forgot your password?"</p>
}),
render_username: Some(render_username),
render_password: Some(render_password),
render_submit: Some(render_submit),
..Default::default()
};

dom! {
<div>
<Login {on_submit} {token_result} {params} />
{result_html}
</div>
}
}
Loading