Skip to main content

vertigo_forms/
login.rs

1use std::rc::Rc;
2use vertigo::{
3    AttrGroup, Css, DomNode, KeyDownEvent, Resource, Value, bind, bind_rc, component, css, dom,
4    transaction,
5};
6
7pub type OnSubmit = Rc<dyn Fn(&str, &str)>;
8
9/// Custom renderer for a login input. Receives the value bound to the form so
10/// submit/Enter handling keeps working.
11pub type RenderInput = Rc<dyn Fn(Value<String>) -> DomNode>;
12
13/// Custom renderer for the submit control. Receives the submit callback to wire up.
14pub type RenderSubmit = Rc<dyn Fn(Rc<dyn Fn()>) -> DomNode>;
15
16#[component]
17pub fn Login<T: Clone + PartialEq + 'static>(
18    on_submit: OnSubmit,
19    token_result: Value<Option<Resource<T>>>,
20    params: LoginParams,
21    /// Attributes passed to both user/pass inputs
22    i: AttrGroup,
23    /// Attributes passed to submit button
24    s: AttrGroup,
25) {
26    mount_login(on_submit, token_result, params, i, s)
27}
28
29pub struct LoginParams {
30    pub css: Css,
31    pub add_css: Css,
32    pub line_css: Css,
33    pub line_add_css: Css,
34    pub input_css: Css,
35    pub submit_css: Css,
36    pub submit_add_css: Css,
37    pub error_message: Rc<dyn Fn(String) -> String>,
38    pub username_label: String,
39    pub password_label: String,
40    pub button_label: String,
41    pub waiting_label: String,
42    /// Optional content rendered at the top of the form, before the message line.
43    pub header: Option<DomNode>,
44    /// Optional content rendered at the bottom of the form, after the submit button.
45    pub footer: Option<DomNode>,
46    /// Optional custom renderer for the username input. Receives the bound value
47    /// so submit/Enter handling keeps working. When `None`, a plain `<input>` is used.
48    pub render_username: Option<RenderInput>,
49    /// Optional custom renderer for the password input. Receives the bound value.
50    /// When `None`, a plain `<input type="password">` is used.
51    pub render_password: Option<RenderInput>,
52    /// Optional custom renderer for the submit control. Receives the submit callback
53    /// to wire up (e.g. `on_click`). When `None`, a plain `<input type="submit">` is used.
54    pub render_submit: Option<RenderSubmit>,
55}
56
57impl Default for LoginParams {
58    fn default() -> Self {
59        Self {
60            css: css! {"
61                width: 250px;
62                margin: auto;
63                padding: 10px;
64                margin-bottom: 10px;
65            "},
66            add_css: Css::default(),
67            line_css: css! {"
68                min-height: 1em;
69                margin-bottom: 5px;
70            "},
71            line_add_css: Css::default(),
72            input_css: Css::default(),
73            submit_css: css! {"
74                margin-top: 15px;
75            "},
76            submit_add_css: Css::default(),
77            error_message: Rc::new(|err| err),
78            username_label: "Username:".to_string(),
79            password_label: "Password:".to_string(),
80            button_label: "Login".to_string(),
81            waiting_label: "Logging in...".to_string(),
82            header: None,
83            footer: None,
84            render_username: None,
85            render_password: None,
86            render_submit: None,
87        }
88    }
89}
90
91pub fn mount_login<T: Clone + PartialEq + 'static>(
92    on_submit: OnSubmit,
93    token_result: Value<Option<Resource<T>>>,
94    params: LoginParams,
95    input_attrs: AttrGroup,
96    submit_attrs: AttrGroup,
97) -> DomNode {
98    let username = Value::<String>::default();
99    let password = Value::<String>::default();
100
101    let submit = bind_rc!(on_submit, username, password, || transaction(|ctx| {
102        on_submit(&username.get(ctx), &password.get(ctx));
103    }));
104
105    let on_key_down = bind!(submit, |key: KeyDownEvent| {
106        if key.code == "Enter" || key.code == "NumpadEnter" {
107            submit();
108            return true;
109        }
110        false
111    });
112
113    let css = &params.css + &params.add_css;
114    let line_css = &params.line_css + &params.line_add_css;
115    let submit_css = &params.submit_css + &params.submit_add_css;
116    let error_message = params.error_message.clone();
117    let waiting_label = params.waiting_label.clone();
118
119    let message_div = bind!(
120        line_css,
121        token_result.render_value(move |token_result| {
122            let css_error = line_css.clone().push_str("color: red;");
123
124            match token_result {
125                Some(Resource::Loading) => dom! {
126                    <div css={line_css.clone()}>{&waiting_label}</div>
127                },
128                Some(Resource::Error(err)) => dom! {
129                    <div css={css_error}>{error_message(err)}</div>
130                },
131                _ => dom! {
132                    <div css={line_css.clone()} />
133                },
134            }
135        })
136    );
137
138    let username_div = match &params.render_username {
139        Some(render) => render(username.clone()),
140        None => {
141            let on_username_change = bind!(username, |new_value: String| username.set(new_value));
142            dom! {
143                <div css={line_css.clone()}>
144                    <div>{&params.username_label}</div>
145                    <input
146                        css={&params.input_css}
147                        value={username.to_computed()}
148                        on_input={on_username_change}
149                        {..input_attrs.clone()}
150                    />
151                </div>
152            }
153        }
154    };
155
156    let password_div = match &params.render_password {
157        Some(render) => render(password.clone()),
158        None => {
159            let on_password_change = bind!(password, |new_value| password.set(new_value));
160            dom! {
161                <div css={line_css}>
162                    <div>{&params.password_label}</div>
163                    <input
164                        css={&params.input_css}
165                        value={password.to_computed()}
166                        on_input={on_password_change}
167                        type="password"
168                        {..input_attrs}
169                    />
170                </div>
171            }
172        }
173    };
174
175    let submit_div = match &params.render_submit {
176        Some(render) => render(submit.clone()),
177        None => dom! {
178            <div css={submit_css}>
179                <input
180                    type="submit"
181                    value={&params.button_label}
182                    on_click={move |_| submit()}
183                    {..submit_attrs}
184                />
185            </div>
186        },
187    };
188
189    let header = params.header;
190    let footer = params.footer;
191
192    dom! {
193        <div css={css} {on_key_down}>
194            { ..header }
195            { message_div }
196            { username_div }
197            { password_div }
198            { submit_div }
199            { ..footer }
200        </div>
201    }
202}