Fix input, fix submit handle, add validations

This commit is contained in:
Adrian Wozniak 2020-04-17 15:31:55 +02:00
parent 2f870a5a0a
commit b22210d55a
10 changed files with 434 additions and 328 deletions

View File

@ -1,9 +1,8 @@
#login > .styledForm { #login > .styledForm {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
margin: 124.5px auto 24px; margin: 0 auto 24px;
width: 400px; width: 400px;
/*padding: 32px 40px;*/
background: rgb(255, 255, 255) none repeat scroll 0 0; background: rgb(255, 255, 255) none repeat scroll 0 0;
border-radius: 3px; border-radius: 3px;
box-shadow: rgba(0, 0, 0, 0.1) 0 0 10px; box-shadow: rgba(0, 0, 0, 0.1) 0 0 10px;
@ -11,6 +10,23 @@
color: var(--textMedium); color: var(--textMedium);
} }
#login > .styledForm:first-of-type {
margin-top: 124.5px;
margin-bottom: 0;
}
#login > .styledForm:last-of-type {
box-shadow: rgba(0, 0, 0, 0.1) 0 10px 10px;
}
#login > .styledForm:first-of-type > .formElement {
padding-bottom: 0;
}
#login > .styledForm:last-of-type > .formElement {
padding-top: 0;
}
#login > .styledForm > .formElement > .formHeading { #login > .styledForm > .formElement > .formHeading {
color: var(--textMedium); color: var(--textMedium);
font-size: 1em; font-size: 1em;

View File

@ -1,4 +1,9 @@
use std::str::FromStr;
use seed::{prelude::*, *}; use seed::{prelude::*, *};
use uuid::Uuid;
use jirs_data::WsMsg;
use crate::api::send_ws_msg; use crate::api::send_ws_msg;
use crate::model::{LoginPage, Page, PageContent}; use crate::model::{LoginPage, Page, PageContent};
@ -6,12 +11,9 @@ use crate::shared::styled_button::StyledButton;
use crate::shared::styled_field::StyledField; use crate::shared::styled_field::StyledField;
use crate::shared::styled_form::StyledForm; use crate::shared::styled_form::StyledForm;
use crate::shared::styled_icon::{Icon, StyledIcon}; use crate::shared::styled_icon::{Icon, StyledIcon};
use crate::shared::styled_textarea::StyledTextarea; use crate::shared::styled_input::StyledInput;
use crate::shared::{outer_layout, write_auth_token, ToNode}; use crate::shared::{outer_layout, write_auth_token, ToNode};
use crate::{model, FieldId, LoginFieldId, Msg}; use crate::{model, FieldId, LoginFieldId, Msg};
use jirs_data::WsMsg;
use std::str::FromStr;
use uuid::Uuid;
pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) { pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>) {
if model.page != Page::Login { if model.page != Page::Login {
@ -31,12 +33,15 @@ pub fn update(msg: Msg, model: &mut model::Model, orders: &mut impl Orders<Msg>)
match msg { match msg {
Msg::InputChanged(FieldId::Login(LoginFieldId::Username), value) => { Msg::InputChanged(FieldId::Login(LoginFieldId::Username), value) => {
page.username = value; page.username = value;
page.username_touched = true;
} }
Msg::InputChanged(FieldId::Login(LoginFieldId::Email), value) => { Msg::InputChanged(FieldId::Login(LoginFieldId::Email), value) => {
page.email = value; page.email = value;
page.email_touched = true;
} }
Msg::InputChanged(FieldId::Login(LoginFieldId::Token), value) => { Msg::InputChanged(FieldId::Login(LoginFieldId::Token), value) => {
page.token = value; page.token = value;
page.token_touched = true;
} }
Msg::SignInRequest => { Msg::SignInRequest => {
send_ws_msg(WsMsg::AuthenticateRequest( send_ws_msg(WsMsg::AuthenticateRequest(
@ -77,11 +82,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
_ => return empty![], _ => return empty![],
}; };
let username = StyledTextarea::build() let username = StyledInput::build(FieldId::Login(LoginFieldId::Username))
.one_line()
.update_on(Ev::Change)
.value(page.username.as_str()) .value(page.username.as_str())
.build(FieldId::Login(LoginFieldId::Username)) .valid(!page.username_touched || page.username.len() > 1)
.build()
.into_node(); .into_node();
let username_field = StyledField::build() let username_field = StyledField::build()
.label("Username") .label("Username")
@ -89,11 +93,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let email = StyledTextarea::build() let email = StyledInput::build(FieldId::Login(LoginFieldId::Email))
.one_line()
.update_on(Ev::Change)
.value(page.email.as_str()) .value(page.email.as_str())
.build(FieldId::Login(LoginFieldId::Email)) .valid(!page.email_touched || is_email(page.email.as_str()))
.build()
.into_node(); .into_node();
let email_field = StyledField::build() let email_field = StyledField::build()
.label("E-Mail") .label("E-Mail")
@ -102,7 +105,9 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.into_node(); .into_node();
let submit = if page.login_success { let submit = if page.login_success {
StyledButton::build().success().text("") StyledButton::build()
.success()
.text("✓ Please check your mail")
} else { } else {
StyledButton::build() StyledButton::build()
.primary() .primary()
@ -126,11 +131,24 @@ pub fn view(model: &model::Model) -> Node<Msg> {
span!["Why I don't see password?"] span!["Why I don't see password?"]
]; ];
let token = StyledTextarea::build() let sign_in_form = StyledForm::build()
.one_line() .heading("Sign In to your account")
.update_on(Ev::Input) .on_submit(ev(Ev::Submit, |ev| {
ev.stop_propagation();
ev.prevent_default();
Msg::SignInRequest
}))
.add_field(username_field)
.add_field(email_field)
.add_field(submit_field)
.add_field(no_pass_section)
.build()
.into_node();
let token = StyledInput::build(FieldId::Login(LoginFieldId::Token))
.value(page.token.as_str()) .value(page.token.as_str())
.build(FieldId::Login(LoginFieldId::Token)) .valid(!page.token_touched || is_token(page.token.as_str()))
.build()
.into_node(); .into_node();
let token_field = StyledField::build() let token_field = StyledField::build()
.label("Single use token") .label("Single use token")
@ -145,17 +163,42 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.into_node(); .into_node();
let submit_token_field = StyledField::build().input(submit_token).build().into_node(); let submit_token_field = StyledField::build().input(submit_token).build().into_node();
let form = StyledForm::build() let bind_token_form = StyledForm::build()
.heading("Sign In to your account") .on_submit(ev(Ev::Submit, |ev| {
.add_field(username_field) ev.stop_propagation();
.add_field(email_field) ev.prevent_default();
.add_field(submit_field) Msg::BindClientRequest
.add_field(no_pass_section) }))
.add_field(token_field) .add_field(token_field)
.add_field(submit_token_field) .add_field(submit_token_field)
.build() .build()
.into_node(); .into_node();
let children = vec![form]; let children = vec![sign_in_form, bind_token_form];
outer_layout(model, "login", children) outer_layout(model, "login", children)
} }
fn is_token(s: &str) -> bool {
uuid::Uuid::from_str(s).is_ok()
}
fn is_email(s: &str) -> bool {
let mut has_at = false;
let mut has_dot = false;
for c in s.chars() {
match c {
'\n' | ' ' | '\t' | '\r' => return false,
'@' if !has_at => {
has_at = true;
}
'@' if has_at => return false,
'.' if has_at => {
has_dot = true;
}
_ if has_dot => return true,
_ => (),
}
}
return false;
}

View File

@ -151,9 +151,9 @@ pub fn view(model: &Model, modal: &AddIssueModal) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let description = StyledTextarea::build() let description = StyledTextarea::build(FieldId::AddIssueModal(IssueFieldId::Description))
.height(110) .height(110)
.build(FieldId::AddIssueModal(IssueFieldId::Description)) .build()
.into_node(); .into_node();
let description_field = StyledField::build() let description_field = StyledField::build()
.label("Description") .label("Description")

View File

@ -372,14 +372,14 @@ fn left_modal_column(model: &Model, modal: &EditIssueModal) -> Node<Msg> {
.. ..
} = modal; } = modal;
let title = StyledTextarea::build() let title = StyledTextarea::build(FieldId::EditIssueModal(EditIssueModalSection::Issue(
IssueFieldId::Title,
)))
.value(payload.title.as_str()) .value(payload.title.as_str())
.add_class("textarea") .add_class("textarea")
.max_height(48) .max_height(48)
.height(0) .height(0)
.build(FieldId::EditIssueModal(EditIssueModalSection::Issue( .build()
IssueFieldId::Title,
)))
.into_node(); .into_node();
let description_text = payload.description.as_ref().cloned().unwrap_or_default(); let description_text = payload.description.as_ref().cloned().unwrap_or_default();
@ -468,12 +468,12 @@ fn build_comment_form(form: &CommentForm) -> Vec<Node<Msg>> {
)) ))
}); });
let text_area = StyledTextarea::build() let text_area = StyledTextarea::build(FieldId::EditIssueModal(EditIssueModalSection::Comment(
.value(form.body.as_str())
.placeholder("Add a comment...")
.build(FieldId::EditIssueModal(EditIssueModalSection::Comment(
CommentFieldId::Body, CommentFieldId::Body,
))) )))
.value(form.body.as_str())
.placeholder("Add a comment...")
.build()
.into_node(); .into_node();
let submit = StyledButton::build() let submit = StyledButton::build()

View File

@ -225,6 +225,10 @@ pub struct LoginPage {
pub token: String, pub token: String,
pub login_success: bool, pub login_success: bool,
pub bad_token: String, pub bad_token: String,
// touched
pub username_touched: bool,
pub email_touched: bool,
pub token_touched: bool,
} }
#[derive(Debug)] #[derive(Debug)]

View File

@ -84,12 +84,12 @@ pub fn view(model: &model::Model) -> Node<Msg> {
PageContent::ProjectSettings(page) => page, PageContent::ProjectSettings(page) => page,
_ => return empty![], _ => return empty![],
}; };
let name = StyledTextarea::build() let name = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Name))
.value(page.payload.name.as_ref().cloned().unwrap_or_default()) .value(page.payload.name.as_ref().cloned().unwrap_or_default())
.height(39) .height(39)
.max_height(39) .max_height(39)
.disable_auto_resize() .disable_auto_resize()
.build(FieldId::ProjectSettings(ProjectFieldId::Name)) .build()
.into_node(); .into_node();
let name_field = StyledField::build() let name_field = StyledField::build()
.label("Name") .label("Name")
@ -98,12 +98,12 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build() .build()
.into_node(); .into_node();
let url = StyledTextarea::build() let url = StyledTextarea::build(FieldId::ProjectSettings(ProjectFieldId::Url))
.height(39) .height(39)
.max_height(39) .max_height(39)
.disable_auto_resize() .disable_auto_resize()
.value(page.payload.url.as_ref().cloned().unwrap_or_default()) .value(page.payload.url.as_ref().cloned().unwrap_or_default())
.build(FieldId::ProjectSettings(ProjectFieldId::Url)) .build()
.into_node(); .into_node();
let url_field = StyledField::build() let url_field = StyledField::build()
.label("Url") .label("Url")

View File

@ -101,11 +101,11 @@ pub fn render(values: StyledEditor) -> Node<Msg> {
let view_id = format!("view-{}", id); let view_id = format!("view-{}", id);
let name = format!("styled-editor-{}", id); let name = format!("styled-editor-{}", id);
let text_area = StyledTextarea::build() let text_area = StyledTextarea::build(id)
.height(40) .height(40)
.update_on(update_event) .update_on(update_event)
.value(text.as_str()) .value(text.as_str())
.build(id) .build()
.into_node(); .into_node();
let parsed = comrak::markdown_to_html( let parsed = comrak::markdown_to_html(

View File

@ -7,6 +7,7 @@ use crate::Msg;
pub struct StyledForm { pub struct StyledForm {
heading: String, heading: String,
fields: Vec<Node<Msg>>, fields: Vec<Node<Msg>>,
on_submit: Option<EventHandler<Msg>>,
} }
impl StyledForm { impl StyledForm {
@ -25,6 +26,7 @@ impl ToNode for StyledForm {
pub struct StyledFormBuilder { pub struct StyledFormBuilder {
fields: Vec<Node<Msg>>, fields: Vec<Node<Msg>>,
heading: String, heading: String,
on_submit: Option<EventHandler<Msg>>,
} }
impl StyledFormBuilder { impl StyledFormBuilder {
@ -41,17 +43,32 @@ impl StyledFormBuilder {
self self
} }
pub fn on_submit(mut self, on_submit: EventHandler<Msg>) -> Self {
self.on_submit = Some(on_submit);
self
}
pub fn build(self) -> StyledForm { pub fn build(self) -> StyledForm {
StyledForm { StyledForm {
heading: self.heading, heading: self.heading,
fields: self.fields, fields: self.fields,
on_submit: self.on_submit,
} }
} }
} }
pub fn render(values: StyledForm) -> Node<Msg> { pub fn render(values: StyledForm) -> Node<Msg> {
let StyledForm { heading, fields } = values; let StyledForm {
div![ heading,
fields,
on_submit,
} = values;
let handlers = match on_submit {
Some(handler) => vec![handler],
_ => vec![],
};
seed::form![
handlers,
attrs![At::Class => "styledForm"], attrs![At::Class => "styledForm"],
div![ div![
class!["formElement"], class!["formElement"],

View File

@ -94,17 +94,17 @@ pub fn render(values: StyledInput) -> Node<Msg> {
_ => empty![], _ => empty![],
}; };
let field_id = id.clone(); let field_id = id.clone();
let change_handler = keyboard_ev(Ev::KeyUp, move |event| { let change_handler = ev(Ev::Input, move |event| {
use wasm_bindgen::JsCast;
event.stop_propagation(); event.stop_propagation();
let value = event let target = event.target().unwrap();
.target() let input = seed::to_input(&target);
.unwrap() let value = input.value();
.dyn_ref::<web_sys::HtmlInputElement>()
.unwrap()
.value();
Msg::InputChanged(field_id, value) Msg::InputChanged(field_id, value)
}); });
let key_handler = ev(Ev::KeyUp, move |event| {
event.stop_propagation();
Msg::NoOp
});
div![ div![
attrs!(At::Class => wrapper_class_list.join(" ")), attrs!(At::Class => wrapper_class_list.join(" ")),
@ -116,6 +116,7 @@ pub fn render(values: StyledInput) -> Node<Msg> {
At::Type => input_type.unwrap_or_else(|| "text".to_string()), At::Type => input_type.unwrap_or_else(|| "text".to_string()),
], ],
change_handler, change_handler,
key_handler,
], ],
] ]
} }

View File

@ -22,13 +22,24 @@ impl ToNode for StyledTextarea {
} }
impl StyledTextarea { impl StyledTextarea {
pub fn build() -> StyledTextareaBuilder { pub fn build(field_id: FieldId) -> StyledTextareaBuilder {
StyledTextareaBuilder::default() StyledTextareaBuilder {
id: field_id,
height: None,
max_height: None,
on_change: None,
value: "".to_string(),
class_list: vec![],
update_event: None,
placeholder: None,
disable_auto_resize: false,
}
} }
} }
#[derive(Debug, Default)] #[derive(Debug)]
pub struct StyledTextareaBuilder { pub struct StyledTextareaBuilder {
id: FieldId,
height: Option<usize>, height: Option<usize>,
max_height: Option<usize>, max_height: Option<usize>,
on_change: Option<EventHandler<Msg>>, on_change: Option<EventHandler<Msg>>,
@ -40,11 +51,6 @@ pub struct StyledTextareaBuilder {
} }
impl StyledTextareaBuilder { impl StyledTextareaBuilder {
#[inline]
pub fn one_line(self) -> Self {
self.disable_auto_resize().height(39).max_height(39)
}
#[inline] #[inline]
pub fn height(mut self, height: usize) -> Self { pub fn height(mut self, height: usize) -> Self {
self.height = Some(height); self.height = Some(height);
@ -94,9 +100,9 @@ impl StyledTextareaBuilder {
} }
#[inline] #[inline]
pub fn build(self, id: FieldId) -> StyledTextarea { pub fn build(self) -> StyledTextarea {
StyledTextarea { StyledTextarea {
id, id: self.id,
value: self.value, value: self.value,
height: self.height.unwrap_or(110), height: self.height.unwrap_or(110),
class_list: self.class_list, class_list: self.class_list,
@ -145,34 +151,51 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
if disable_auto_resize { if disable_auto_resize {
style_list.push("resize: none".to_string()); style_list.push("resize: none".to_string());
style_list.push(format!(
"height: {h}px; max-height: {h}px; min-height: {h}px",
h = max_height
));
} }
let mut handlers = vec![];
let handler_disable_auto_resize = disable_auto_resize; let handler_disable_auto_resize = disable_auto_resize;
let resize_handler = ev(Ev::KeyUp, move |event| { let resize_handler = ev(Ev::KeyUp, move |event| {
use wasm_bindgen::JsCast; event.stop_propagation();
if handler_disable_auto_resize {
return Msg::NoOp;
}
let target = match event.target() { let target = event.target().unwrap();
Some(el) => el, let textarea = seed::to_textarea(&target);
_ => return Msg::NoOp, let value = textarea.value();
};
let text_area = target.dyn_ref::<web_sys::HtmlTextAreaElement>().unwrap();
let value: String = text_area.value();
let min_height = get_min_height(value.as_str(), height as f64, handler_disable_auto_resize); let min_height = get_min_height(value.as_str(), height as f64, handler_disable_auto_resize);
text_area textarea
.style() .style()
.set_css_text(format!("height: {min_height}px", min_height = min_height).as_str()); .set_css_text(format!("height: {min_height}px", min_height = min_height).as_str());
Msg::NoOp Msg::NoOp
}); });
handlers.push(resize_handler);
let handler_disable_auto_resize = disable_auto_resize;
let text_input_handler = ev(update_event, move |event| { let text_input_handler = ev(update_event, move |event| {
event.stop_propagation();
let target = event.target().unwrap(); let target = event.target().unwrap();
let text = seed::to_textarea(&target).value(); let textarea = seed::to_textarea(&target);
Msg::InputChanged(id, text) let value = textarea.value();
if handler_disable_auto_resize && value.contains("\n") {
event.prevent_default();
}
Msg::InputChanged(
id,
if handler_disable_auto_resize {
value.trim().to_string()
} else {
value
},
)
}); });
handlers.push(text_input_handler);
class_list.push("textAreaInput".to_string()); class_list.push("textAreaInput".to_string());
@ -185,9 +208,11 @@ pub fn render(values: StyledTextarea) -> Node<Msg> {
At::AutoFocus => "true"; At::AutoFocus => "true";
At::Style => style_list.join(";"); At::Style => style_list.join(";");
At::Placeholder => placeholder.unwrap_or_default(); At::Placeholder => placeholder.unwrap_or_default();
At::Rows => if disable_auto_resize { "1" } else { "auto" }
], ],
value, value,
handlers, resize_handler,
text_input_handler,
] ]
] ]
} }