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 {
display: flex;
flex-direction: column;
margin: 124.5px auto 24px;
margin: 0 auto 24px;
width: 400px;
/*padding: 32px 40px;*/
background: rgb(255, 255, 255) none repeat scroll 0 0;
border-radius: 3px;
box-shadow: rgba(0, 0, 0, 0.1) 0 0 10px;
@ -11,6 +10,23 @@
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 {
color: var(--textMedium);
font-size: 1em;

View File

@ -1,4 +1,9 @@
use std::str::FromStr;
use seed::{prelude::*, *};
use uuid::Uuid;
use jirs_data::WsMsg;
use crate::api::send_ws_msg;
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_form::StyledForm;
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::{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>) {
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 {
Msg::InputChanged(FieldId::Login(LoginFieldId::Username), value) => {
page.username = value;
page.username_touched = true;
}
Msg::InputChanged(FieldId::Login(LoginFieldId::Email), value) => {
page.email = value;
page.email_touched = true;
}
Msg::InputChanged(FieldId::Login(LoginFieldId::Token), value) => {
page.token = value;
page.token_touched = true;
}
Msg::SignInRequest => {
send_ws_msg(WsMsg::AuthenticateRequest(
@ -77,11 +82,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
_ => return empty![],
};
let username = StyledTextarea::build()
.one_line()
.update_on(Ev::Change)
let username = StyledInput::build(FieldId::Login(LoginFieldId::Username))
.value(page.username.as_str())
.build(FieldId::Login(LoginFieldId::Username))
.valid(!page.username_touched || page.username.len() > 1)
.build()
.into_node();
let username_field = StyledField::build()
.label("Username")
@ -89,11 +93,10 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.build()
.into_node();
let email = StyledTextarea::build()
.one_line()
.update_on(Ev::Change)
let email = StyledInput::build(FieldId::Login(LoginFieldId::Email))
.value(page.email.as_str())
.build(FieldId::Login(LoginFieldId::Email))
.valid(!page.email_touched || is_email(page.email.as_str()))
.build()
.into_node();
let email_field = StyledField::build()
.label("E-Mail")
@ -102,7 +105,9 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.into_node();
let submit = if page.login_success {
StyledButton::build().success().text("")
StyledButton::build()
.success()
.text("✓ Please check your mail")
} else {
StyledButton::build()
.primary()
@ -126,11 +131,24 @@ pub fn view(model: &model::Model) -> Node<Msg> {
span!["Why I don't see password?"]
];
let token = StyledTextarea::build()
.one_line()
.update_on(Ev::Input)
let sign_in_form = StyledForm::build()
.heading("Sign In to your account")
.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())
.build(FieldId::Login(LoginFieldId::Token))
.valid(!page.token_touched || is_token(page.token.as_str()))
.build()
.into_node();
let token_field = StyledField::build()
.label("Single use token")
@ -145,17 +163,42 @@ pub fn view(model: &model::Model) -> Node<Msg> {
.into_node();
let submit_token_field = StyledField::build().input(submit_token).build().into_node();
let form = StyledForm::build()
.heading("Sign In to your account")
.add_field(username_field)
.add_field(email_field)
.add_field(submit_field)
.add_field(no_pass_section)
let bind_token_form = StyledForm::build()
.on_submit(ev(Ev::Submit, |ev| {
ev.stop_propagation();
ev.prevent_default();
Msg::BindClientRequest
}))
.add_field(token_field)
.add_field(submit_token_field)
.build()
.into_node();
let children = vec![form];
let children = vec![sign_in_form, bind_token_form];
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()
.into_node();
let description = StyledTextarea::build()
let description = StyledTextarea::build(FieldId::AddIssueModal(IssueFieldId::Description))
.height(110)
.build(FieldId::AddIssueModal(IssueFieldId::Description))
.build()
.into_node();
let description_field = StyledField::build()
.label("Description")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -94,17 +94,17 @@ pub fn render(values: StyledInput) -> Node<Msg> {
_ => empty![],
};
let field_id = id.clone();
let change_handler = keyboard_ev(Ev::KeyUp, move |event| {
use wasm_bindgen::JsCast;
let change_handler = ev(Ev::Input, move |event| {
event.stop_propagation();
let value = event
.target()
.unwrap()
.dyn_ref::<web_sys::HtmlInputElement>()
.unwrap()
.value();
let target = event.target().unwrap();
let input = seed::to_input(&target);
let value = input.value();
Msg::InputChanged(field_id, value)
});
let key_handler = ev(Ev::KeyUp, move |event| {
event.stop_propagation();
Msg::NoOp
});
div![
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()),
],
change_handler,
key_handler,
],
]
}

View File

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