Add more input types, instant validation
This commit is contained in:
parent
a3bea8a62c
commit
29a045c86b
354
src/form.rs
354
src/form.rs
|
@ -1,18 +1,61 @@
|
|||
use std::collections::HashMap;
|
||||
use std::{
|
||||
collections::{HashMap, HashSet},
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use dioxus::prelude::*;
|
||||
|
||||
pub(crate) trait FormError: std::error::Error {
|
||||
fn ident(&self) -> &'static str;
|
||||
}
|
||||
|
||||
pub(crate) struct Errors<'a> {
|
||||
errors: &'a mut HashMap<&'static str, Rc<dyn FormError>>,
|
||||
}
|
||||
|
||||
pub(crate) trait Form {
|
||||
type FieldName: Eq + std::hash::Hash + 'static;
|
||||
|
||||
fn set(&mut self, field_name: Self::FieldName, value: &str);
|
||||
fn set(&mut self, field_name: Self::FieldName, value: &str, errors: &mut Errors<'_>);
|
||||
}
|
||||
|
||||
fn get(&self, field_name: &Self::FieldName) -> Option<String>;
|
||||
impl<'a> Errors<'a> {
|
||||
pub(crate) fn test<E: 'static>(&mut self, predicate: bool, error: E) -> &mut Self
|
||||
where
|
||||
E: FormError,
|
||||
{
|
||||
if predicate {
|
||||
self.add_error(error)
|
||||
} else {
|
||||
self.remove_error(&error)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn add_error<E: 'static>(&mut self, error: E) -> &mut Self
|
||||
where
|
||||
E: FormError,
|
||||
{
|
||||
self.errors.insert(error.ident(), Rc::new(error));
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn remove_error<E: 'static>(&mut self, error: &E) -> &mut Self
|
||||
where
|
||||
E: FormError,
|
||||
{
|
||||
self.errors.remove(&error.ident());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct FormState<FieldName> {
|
||||
state: Box<dyn Form<FieldName = FieldName>>,
|
||||
errors: HashMap<FieldName, Vec<Box<dyn std::error::Error>>>,
|
||||
values: HashMap<FieldName, String>,
|
||||
shown_errors: HashMap<FieldName, HashMap<&'static str, Rc<dyn FormError>>>,
|
||||
errors: HashMap<FieldName, HashMap<&'static str, Rc<dyn FormError>>>,
|
||||
always_show_errors: bool,
|
||||
disabled: bool,
|
||||
fields: HashSet<FieldName>,
|
||||
}
|
||||
|
||||
impl<FieldName> FormState<FieldName> {
|
||||
|
@ -22,7 +65,12 @@ impl<FieldName> FormState<FieldName> {
|
|||
{
|
||||
Self {
|
||||
state: Box::new(state),
|
||||
values: Default::default(),
|
||||
shown_errors: Default::default(),
|
||||
errors: Default::default(),
|
||||
always_show_errors: false,
|
||||
disabled: false,
|
||||
fields: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -31,33 +79,84 @@ impl<FieldName> FormState<FieldName>
|
|||
where
|
||||
FieldName: Eq + std::hash::Hash + 'static,
|
||||
{
|
||||
pub(crate) fn add_error<E: 'static>(&mut self, name: FieldName, error: E) -> &mut Self
|
||||
pub(crate) fn always_show_errors(&mut self) -> &mut Self {
|
||||
self.always_show_errors = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn validate(&mut self) -> &mut Self
|
||||
where
|
||||
E: std::error::Error,
|
||||
FieldName: Clone,
|
||||
{
|
||||
self.errors.entry(name).or_default().push(Box::new(error));
|
||||
for field in self
|
||||
.fields
|
||||
.iter()
|
||||
.filter(|field| !self.values.contains_key(field))
|
||||
{
|
||||
self.state.set(
|
||||
field.clone(),
|
||||
"",
|
||||
&mut Errors {
|
||||
errors: self.errors.entry(field.clone()).or_default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
self.shown_errors = self.errors.clone();
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn clear_errors(&mut self) -> &mut Self {
|
||||
self.errors.clear();
|
||||
pub(crate) fn is_valid(&self) -> bool {
|
||||
self.errors.is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn disable(&mut self) -> &mut Self {
|
||||
self.disabled = true;
|
||||
self
|
||||
}
|
||||
|
||||
fn set(&mut self, name: FieldName, value: &str) {
|
||||
self.state.set(name, value);
|
||||
pub(crate) fn enable(&mut self) -> &mut Self {
|
||||
self.disabled = false;
|
||||
self
|
||||
}
|
||||
|
||||
fn set(&mut self, name: FieldName, value: &str)
|
||||
where
|
||||
FieldName: Clone,
|
||||
{
|
||||
self.values.insert(name.clone(), value.to_owned());
|
||||
|
||||
self.state.set(
|
||||
name.clone(),
|
||||
value,
|
||||
&mut Errors {
|
||||
errors: self.errors.entry(name).or_default(),
|
||||
},
|
||||
);
|
||||
|
||||
if self.always_show_errors {
|
||||
for (name, error) in self.errors.drain() {
|
||||
if self.values.contains_key(&name) {
|
||||
self.shown_errors.insert(name, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get_value(&self, name: &FieldName) -> Option<String> {
|
||||
self.state.get(name)
|
||||
self.values.get(name).cloned()
|
||||
}
|
||||
|
||||
fn get_error(&self, name: &FieldName) -> Option<&[Box<dyn std::error::Error>]> {
|
||||
self.errors.get(name).and_then(|vec| {
|
||||
if vec.is_empty() {
|
||||
fn get_errors(
|
||||
&self,
|
||||
name: &FieldName,
|
||||
) -> Option<std::collections::hash_map::Values<'_, &'static str, Rc<dyn FormError>>> {
|
||||
self.shown_errors.get(name).and_then(|hm| {
|
||||
if hm.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(vec.as_slice())
|
||||
Some(hm.values())
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -69,17 +168,17 @@ fn InputErrors<'a, FieldName>(cx: Scope, name: &'a FieldName) -> Element
|
|||
where
|
||||
FieldName: Eq + std::hash::Hash + 'static,
|
||||
{
|
||||
let errors = use_shared_state::<FormState<FieldName>>(cx);
|
||||
let form_state = use_shared_state::<FormState<FieldName>>(cx);
|
||||
|
||||
if let Some(errors) = errors {
|
||||
let errors = errors.read();
|
||||
if let Some(errors) = errors.get_error(name) {
|
||||
if let Some(form_state) = form_state {
|
||||
let form_state = form_state.read();
|
||||
if let Some(errors) = form_state.get_errors(name) {
|
||||
return cx.render(rsx! {
|
||||
div {
|
||||
class: "text-input--errors",
|
||||
errors.iter().map(|error| rsx! {
|
||||
span {
|
||||
class: "text-input--errors--error",
|
||||
ul {
|
||||
class: "form-input--errors",
|
||||
errors.map(|error| rsx! {
|
||||
li {
|
||||
class: "form-input--errors--error",
|
||||
"{error}"
|
||||
}
|
||||
})
|
||||
|
@ -88,28 +187,49 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
None
|
||||
return cx.render(rsx! {
|
||||
ul {
|
||||
class: "form-input--errors",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn NumberInput<'a, FieldName>(cx: Scope, name: FieldName, label: Element<'a>) -> Element
|
||||
pub(crate) fn NumberInput<'a, FieldName>(
|
||||
cx: Scope,
|
||||
name: FieldName,
|
||||
label: Element<'a>,
|
||||
step: Option<f64>,
|
||||
disabled: Option<bool>,
|
||||
) -> Element
|
||||
where
|
||||
FieldName: Clone + Eq + std::hash::Hash + std::fmt::Display + 'static,
|
||||
{
|
||||
cx.render(rsx! {
|
||||
Input { kind: "number", name: name.clone(), label: label.clone() }
|
||||
})
|
||||
if let Some(step) = step {
|
||||
cx.render(rsx! {
|
||||
Input { kind: "number", name: name.clone(), label: label.clone(), step: *step, disabled: disabled.unwrap_or(false) }
|
||||
})
|
||||
} else {
|
||||
cx.render(rsx! {
|
||||
Input { kind: "number", name: name.clone(), label: label.clone(), disabled: disabled.unwrap_or(false) }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn TextInput<'a, FieldName>(cx: Scope, name: FieldName, label: Element<'a>) -> Element
|
||||
pub(crate) fn TextInput<'a, FieldName>(
|
||||
cx: Scope,
|
||||
name: FieldName,
|
||||
label: Element<'a>,
|
||||
disabled: Option<bool>,
|
||||
) -> Element
|
||||
where
|
||||
FieldName: Clone + Eq + std::hash::Hash + std::fmt::Display + 'static,
|
||||
{
|
||||
cx.render(rsx! {
|
||||
Input { kind: "text", name: name.clone(), label: label.clone() }
|
||||
Input { kind: "text", name: name.clone(), label: label.clone(), disabled: disabled.unwrap_or(false) }
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -119,12 +239,100 @@ pub(crate) fn PasswordInput<'a, FieldName>(
|
|||
cx: Scope,
|
||||
name: FieldName,
|
||||
label: Element<'a>,
|
||||
disabled: Option<bool>,
|
||||
) -> Element
|
||||
where
|
||||
FieldName: Clone + Eq + std::hash::Hash + std::fmt::Display + 'static,
|
||||
{
|
||||
cx.render(rsx! {
|
||||
Input { kind: "password", name: name.clone(), label: label.clone() }
|
||||
Input { kind: "password", name: name.clone(), label: label.clone(), disabled: disabled.unwrap_or(false) }
|
||||
})
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn InputWrapper<'a, FieldName>(
|
||||
cx: Scope,
|
||||
name: &'a FieldName,
|
||||
label: Element<'a>,
|
||||
children: Element<'a>,
|
||||
) -> Element
|
||||
where
|
||||
FieldName: Eq + std::hash::Hash + std::fmt::Display + 'static,
|
||||
{
|
||||
cx.render(rsx! {
|
||||
label {
|
||||
class: "form-input",
|
||||
span {
|
||||
class: "form-input--label",
|
||||
label,
|
||||
},
|
||||
children,
|
||||
InputErrors { name: *name, }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline_props]
|
||||
#[allow(non_snake_case)]
|
||||
pub(crate) fn Textarea<'a, FieldName>(
|
||||
cx: Scope,
|
||||
name: FieldName,
|
||||
label: Element<'a>,
|
||||
disabled: Option<bool>,
|
||||
) -> Element
|
||||
where
|
||||
FieldName: Clone + Eq + std::hash::Hash + std::fmt::Display + 'static,
|
||||
{
|
||||
cx.render(rsx! {
|
||||
InputWrapper {
|
||||
name: name,
|
||||
label: label.clone(),
|
||||
match use_shared_state::<FormState<FieldName>>(cx) {
|
||||
Some(form_state) => {
|
||||
cx.use_hook(|| {
|
||||
form_state.write().fields.insert(name.clone());
|
||||
});
|
||||
|
||||
let value = form_state.read().get_value(&name).unwrap_or_default();
|
||||
|
||||
let classes = if form_state.read().get_errors(&name).is_some() {
|
||||
"error"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let disabled = disabled.unwrap_or(false) || form_state.read().disabled;
|
||||
|
||||
if disabled {
|
||||
rsx! {
|
||||
textarea {
|
||||
class: classes,
|
||||
name: "{name}",
|
||||
value: "{value}",
|
||||
disabled: "{disabled}",
|
||||
},
|
||||
}
|
||||
} else {
|
||||
rsx! {
|
||||
textarea {
|
||||
class: classes,
|
||||
name: "{name}",
|
||||
value: "{value}",
|
||||
disabled: "{disabled}",
|
||||
oninput: move |event| form_state.write().set(name.clone(), &event.value),
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
None => rsx! {
|
||||
textarea {
|
||||
name: "{name}",
|
||||
disabled: "{disabled.unwrap_or(false)}"
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -135,38 +343,71 @@ pub(crate) fn Input<'a, FieldName>(
|
|||
kind: &'static str,
|
||||
name: FieldName,
|
||||
label: Element<'a>,
|
||||
step: Option<f64>,
|
||||
disabled: bool,
|
||||
) -> Element
|
||||
where
|
||||
FieldName: Clone + Eq + std::hash::Hash + std::fmt::Display + 'static,
|
||||
{
|
||||
cx.render(rsx! {
|
||||
label {
|
||||
class: "text-input",
|
||||
span {
|
||||
class: "text-input--label",
|
||||
label,
|
||||
},
|
||||
InputWrapper {
|
||||
name: name,
|
||||
label: label.clone(),
|
||||
match use_shared_state::<FormState<FieldName>>(cx) {
|
||||
Some(values) => {
|
||||
let value = values.read().get_value(&name).unwrap_or_else(|| String::new());
|
||||
Some(form_state) => {
|
||||
cx.use_hook(|| {
|
||||
form_state.write().fields.insert(name.clone());
|
||||
});
|
||||
|
||||
rsx! {
|
||||
input {
|
||||
name: "{name}",
|
||||
r#type: "{kind}",
|
||||
value: "{value}",
|
||||
oninput: move |event| values.write().set(name.clone(), &event.value),
|
||||
},
|
||||
let value = form_state.read().get_value(&name).unwrap_or_default();
|
||||
|
||||
let classes = if form_state.read().get_errors(&name).is_some() {
|
||||
"error"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let step = if *kind == "number" {
|
||||
*step
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let disabled = *disabled || form_state.read().disabled;
|
||||
|
||||
if disabled {
|
||||
rsx! {
|
||||
input {
|
||||
class: classes,
|
||||
name: "{name}",
|
||||
r#type: "{kind}",
|
||||
value: "{value}",
|
||||
step: step,
|
||||
disabled: "{disabled}",
|
||||
},
|
||||
}
|
||||
} else {
|
||||
rsx! {
|
||||
input {
|
||||
class: classes,
|
||||
name: "{name}",
|
||||
r#type: "{kind}",
|
||||
value: "{value}",
|
||||
step: step,
|
||||
disabled: "{disabled}",
|
||||
oninput: move |event| form_state.write().set(name.clone(), &event.value),
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
None => rsx! {
|
||||
input {
|
||||
name: "{name}",
|
||||
r#type: "{kind}",
|
||||
disabled: "{disabled}"
|
||||
},
|
||||
},
|
||||
},
|
||||
InputErrors { name: name, }
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -177,23 +418,24 @@ where
|
|||
{
|
||||
type FieldName = T::FieldName;
|
||||
|
||||
fn set(&mut self, field_name: Self::FieldName, value: &str) {
|
||||
self.with_mut(|form| form.set(field_name, value));
|
||||
}
|
||||
|
||||
fn get(&self, field_name: &Self::FieldName) -> Option<String> {
|
||||
self.with(|form| form.get(field_name))
|
||||
fn set(&mut self, field_name: Self::FieldName, value: &str, errors: &mut Errors<'_>) {
|
||||
self.with_mut(|form| form.set(field_name, value, errors));
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn use_form<FieldName, State>(
|
||||
cx: &ScopeState,
|
||||
form: UseRef<State>,
|
||||
init: impl FnOnce(&mut FormState<FieldName>),
|
||||
) -> UseSharedState<'_, FormState<FieldName>>
|
||||
where
|
||||
State: Form<FieldName = FieldName> + Default + 'static,
|
||||
{
|
||||
use_shared_state_provider(cx, move || FormState::<FieldName>::new(form));
|
||||
use_shared_state_provider(cx, move || {
|
||||
let mut state = FormState::<FieldName>::new(form);
|
||||
(init)(&mut state);
|
||||
state
|
||||
});
|
||||
|
||||
use_shared_state(cx).expect("Just set up")
|
||||
}
|
||||
|
|
383
src/main.rs
383
src/main.rs
|
@ -12,14 +12,14 @@ use dioxus_router::{Link, Redirect, Route, Router};
|
|||
|
||||
mod form;
|
||||
|
||||
use form::{use_form, NumberInput, PasswordInput, TextInput};
|
||||
use form::{use_form, Errors, FormError, NumberInput, PasswordInput, TextInput, Textarea};
|
||||
|
||||
#[derive(Clone, Debug, Default)]
|
||||
struct Form {
|
||||
username: String,
|
||||
password: String,
|
||||
password_confirmation: String,
|
||||
age: usize,
|
||||
age: f64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
|
||||
|
@ -37,37 +37,58 @@ enum Error {
|
|||
PasswordBlank,
|
||||
PasswordTooShort,
|
||||
PasswordMismatch,
|
||||
ParseAge,
|
||||
AgeTooYoung,
|
||||
AgeRequired,
|
||||
}
|
||||
|
||||
impl form::Form for Form {
|
||||
type FieldName = Field;
|
||||
|
||||
fn set(&mut self, field_name: Self::FieldName, value: &str) {
|
||||
fn set(&mut self, field_name: Self::FieldName, value: &str, errors: &mut Errors<'_>) {
|
||||
match field_name {
|
||||
Field::Username => {
|
||||
errors
|
||||
.test(value.is_empty(), Error::UsernameBlank)
|
||||
.test(value.len() < 5, Error::UsernameTooShort);
|
||||
|
||||
self.username = value.to_owned();
|
||||
}
|
||||
Field::Password => {
|
||||
errors
|
||||
.test(value.is_empty(), Error::PasswordBlank)
|
||||
.test(value.len() < 8, Error::PasswordTooShort);
|
||||
|
||||
self.password = value.to_owned();
|
||||
}
|
||||
Field::ConfirmPassword => {
|
||||
errors.test(value != self.password, Error::PasswordMismatch);
|
||||
|
||||
self.password_confirmation = value.to_owned();
|
||||
}
|
||||
Field::Age => {
|
||||
self.age = value.parse().unwrap_or(0);
|
||||
errors.test(value.is_empty(), Error::AgeRequired);
|
||||
|
||||
if value.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
match value.parse() {
|
||||
Ok(age) => {
|
||||
errors
|
||||
.remove_error(&Error::ParseAge)
|
||||
.test(age < 18.0, Error::AgeTooYoung);
|
||||
|
||||
self.age = age;
|
||||
}
|
||||
Err(_) => {
|
||||
errors.remove_error(&Error::AgeTooYoung);
|
||||
errors.add_error(Error::ParseAge);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn get(&self, field_name: &Self::FieldName) -> Option<String> {
|
||||
Some(match field_name {
|
||||
Field::Username => self.username.clone(),
|
||||
Field::Password => self.password.clone(),
|
||||
Field::ConfirmPassword => self.password_confirmation.clone(),
|
||||
Field::Age => self.age.to_string(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Field {
|
||||
|
@ -89,74 +110,242 @@ impl std::fmt::Display for Error {
|
|||
Self::PasswordBlank => write!(f, "Password is required"),
|
||||
Self::PasswordTooShort => write!(f, "Password is too short"),
|
||||
Self::PasswordMismatch => write!(f, "Passwords must match"),
|
||||
Self::ParseAge => write!(f, "Age is invalid"),
|
||||
Self::AgeTooYoung => write!(f, "You are not old enough"),
|
||||
Self::AgeRequired => write!(f, "Age is required"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl FormError for Error {
|
||||
fn ident(&self) -> &'static str {
|
||||
match self {
|
||||
Self::UsernameBlank => "username-blank",
|
||||
Self::UsernameTooShort => "username-too-short",
|
||||
Self::PasswordBlank => "password-blank",
|
||||
Self::PasswordTooShort => "password-too-shorrt",
|
||||
Self::PasswordMismatch => "password-mismatch",
|
||||
Self::ParseAge => "parse-age",
|
||||
Self::AgeTooYoung => "age-too-young",
|
||||
Self::AgeRequired => "age-required",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn Home(cx: Scope) -> Element {
|
||||
fn FirstForm(cx: Scope) -> Element {
|
||||
let form = use_ref(cx, Form::default);
|
||||
let form_state = use_form(cx, form.clone());
|
||||
let form_state = use_form(cx, form.clone(), |_| {});
|
||||
|
||||
cx.render(rsx! {
|
||||
form {
|
||||
onsubmit: move |_| {
|
||||
let form = form.read();
|
||||
let mut form_state = form_state.write();
|
||||
form_state.clear_errors();
|
||||
div {
|
||||
class: "form-container",
|
||||
form {
|
||||
onsubmit: move |_| {
|
||||
let valid = form_state.write().validate().is_valid();
|
||||
let form = form.read();
|
||||
|
||||
if form.username.is_empty() {
|
||||
form_state.add_error(Field::Username, Error::UsernameBlank);
|
||||
}
|
||||
if form.username.len() < 5 {
|
||||
form_state.add_error(Field::Username, Error::UsernameTooShort);
|
||||
}
|
||||
println!("{form:?} - valid: {valid}");
|
||||
},
|
||||
|
||||
if form.password.is_empty() {
|
||||
form_state.add_error(Field::Password, Error::PasswordBlank);
|
||||
}
|
||||
if form.password.len() < 8 {
|
||||
form_state.add_error(Field::Password, Error::PasswordTooShort);
|
||||
}
|
||||
TextInput {
|
||||
name: Field::Username,
|
||||
label: cx.render(rsx! { "Username" }),
|
||||
},
|
||||
|
||||
if form.password != form.password_confirmation {
|
||||
form_state.add_error(Field::ConfirmPassword, Error::PasswordMismatch);
|
||||
}
|
||||
PasswordInput {
|
||||
name: Field::Password,
|
||||
label: cx.render(rsx! { "Password" }),
|
||||
},
|
||||
|
||||
if form.age < 18 {
|
||||
form_state.add_error(Field::Age, Error::AgeTooYoung);
|
||||
}
|
||||
PasswordInput {
|
||||
name: Field::ConfirmPassword,
|
||||
label: cx.render(rsx! { "Confirm Password" }),
|
||||
},
|
||||
|
||||
println!("{:?}", form);
|
||||
NumberInput {
|
||||
name: Field::Age,
|
||||
label: cx.render(rsx! { "Age" }),
|
||||
step: 0.1,
|
||||
},
|
||||
|
||||
input {
|
||||
r#type: "submit",
|
||||
value: "Sign Up"
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
TextInput {
|
||||
name: Field::Username,
|
||||
label: cx.render(rsx! { "Username" }),
|
||||
#[allow(non_snake_case)]
|
||||
fn SecondForm(cx: Scope) -> Element {
|
||||
let form = use_ref(cx, Form::default);
|
||||
let form_state = use_form(cx, form.clone(), |form| {
|
||||
form.always_show_errors();
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
class: "form-container",
|
||||
form {
|
||||
onsubmit: move |_| {
|
||||
let valid = form_state.write().validate().is_valid();
|
||||
let form = form.read();
|
||||
|
||||
println!("{form:?} - valid: {valid}");
|
||||
},
|
||||
|
||||
TextInput {
|
||||
name: Field::Username,
|
||||
label: cx.render(rsx! { "Username" }),
|
||||
},
|
||||
|
||||
PasswordInput {
|
||||
name: Field::Password,
|
||||
label: cx.render(rsx! { "Password" }),
|
||||
},
|
||||
|
||||
PasswordInput {
|
||||
name: Field::ConfirmPassword,
|
||||
label: cx.render(rsx! { "Confirm Password" }),
|
||||
},
|
||||
|
||||
NumberInput {
|
||||
name: Field::Age,
|
||||
label: cx.render(rsx! { "Age" }),
|
||||
step: 0.1,
|
||||
},
|
||||
|
||||
input {
|
||||
r#type: "submit",
|
||||
value: "Sign Up"
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
PasswordInput {
|
||||
name: Field::Password,
|
||||
label: cx.render(rsx! { "Password" }),
|
||||
#[allow(non_snake_case)]
|
||||
fn ThirdForm(cx: Scope) -> Element {
|
||||
let form = use_ref(cx, Form::default);
|
||||
let form_state = use_form(cx, form.clone(), |form| {
|
||||
form.disable();
|
||||
});
|
||||
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
class: "form-container",
|
||||
form {
|
||||
onsubmit: move |_| {
|
||||
let valid = form_state.write().validate().is_valid();
|
||||
let form = form.read();
|
||||
|
||||
println!("{form:?} - valid: {valid}");
|
||||
},
|
||||
|
||||
TextInput {
|
||||
name: Field::Username,
|
||||
label: cx.render(rsx! { "Username" }),
|
||||
},
|
||||
|
||||
PasswordInput {
|
||||
name: Field::Password,
|
||||
label: cx.render(rsx! { "Password" }),
|
||||
},
|
||||
|
||||
PasswordInput {
|
||||
name: Field::ConfirmPassword,
|
||||
label: cx.render(rsx! { "Confirm Password" }),
|
||||
},
|
||||
|
||||
NumberInput {
|
||||
name: Field::Age,
|
||||
label: cx.render(rsx! { "Age" }),
|
||||
step: 0.1,
|
||||
},
|
||||
|
||||
input {
|
||||
r#type: "submit",
|
||||
value: "Sign Up"
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
PasswordInput {
|
||||
name: Field::ConfirmPassword,
|
||||
label: cx.render(rsx! { "Confirm Password" }),
|
||||
#[allow(non_snake_case)]
|
||||
fn FourthForm(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
class: "form-container",
|
||||
form {
|
||||
onsubmit: move |event| {
|
||||
println!("{:?}", event.values);
|
||||
},
|
||||
|
||||
TextInput {
|
||||
name: Field::Username,
|
||||
label: cx.render(rsx! { "Username" }),
|
||||
},
|
||||
|
||||
PasswordInput {
|
||||
name: Field::Password,
|
||||
label: cx.render(rsx! { "Password" }),
|
||||
},
|
||||
|
||||
PasswordInput {
|
||||
name: Field::ConfirmPassword,
|
||||
label: cx.render(rsx! { "Confirm Password" }),
|
||||
},
|
||||
|
||||
NumberInput {
|
||||
name: Field::Age,
|
||||
label: cx.render(rsx! { "Age" }),
|
||||
step: 0.1,
|
||||
disabled: true,
|
||||
}
|
||||
|
||||
input {
|
||||
r#type: "submit",
|
||||
value: "Sign Up"
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
NumberInput {
|
||||
name: Field::Age,
|
||||
label: cx.render(rsx! { "Age" }),
|
||||
}
|
||||
#[allow(non_snake_case)]
|
||||
fn FifthForm(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
class: "form-container",
|
||||
form {
|
||||
onsubmit: move |event| {
|
||||
println!("{:?}", event.values);
|
||||
},
|
||||
|
||||
input {
|
||||
r#type: "submit",
|
||||
value: "Sign Up"
|
||||
}
|
||||
Textarea {
|
||||
name: "post"
|
||||
label: cx.render(rsx! { "Text" })
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn Home(cx: Scope) -> Element {
|
||||
cx.render(rsx! {
|
||||
div {
|
||||
class: "form-list",
|
||||
FirstForm {},
|
||||
SecondForm {},
|
||||
ThirdForm {},
|
||||
FourthForm {},
|
||||
FifthForm {},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -187,11 +376,89 @@ async fn spa(addr: SocketAddr) -> Html<String> {
|
|||
box-sizing: border-box;
|
||||
}}
|
||||
body {{
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
background-color: #333;
|
||||
color: #f5f5f5;
|
||||
font-family: sans;
|
||||
margin: 0;
|
||||
min-height: 100vh;
|
||||
}}
|
||||
.form-list {{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-start;
|
||||
}}
|
||||
.form-container {{
|
||||
padding: 16px;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
}}
|
||||
form {{
|
||||
background-color: #fff;
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 8px;
|
||||
margin: 0 auto;
|
||||
color: #222;
|
||||
padding: 16px;
|
||||
width: 100%;
|
||||
}}
|
||||
.form-input {{
|
||||
display: block;
|
||||
}}
|
||||
.form-input--label {{
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
padding: 4px 0;
|
||||
}}
|
||||
.form-input input[type=text],
|
||||
.form-input input[type=password],
|
||||
.form-input input[type=number],
|
||||
.form-input textarea {{
|
||||
border: 1px solid #e5e5e5;
|
||||
border-radius: 3px;
|
||||
box-shadow: inset 0 0 4px rgba(50, 50, 50, 0.1);
|
||||
display: block;
|
||||
font-size: 16px;
|
||||
line-height: 22px;
|
||||
outline: none;
|
||||
padding: 8px 16px;
|
||||
width: 100%;
|
||||
}}
|
||||
.form-input input[type=text]:hover,
|
||||
.form-input input[type=password]:hover,
|
||||
.form-input input[type=number]:hover,
|
||||
.form-input textarea:hover {{
|
||||
border-color: #ff84ca;
|
||||
}}
|
||||
.form-input input[type=text]:focus,
|
||||
.form-input input[type=password]:focus,
|
||||
.form-input input[type=number]:focus,
|
||||
.form-input textarea:focus {{
|
||||
border-color: #ff84ca;
|
||||
box-shadow: inset 0 0 6px rgba(255, 132, 202, 0.4), 0 0 4px rgba(255, 132, 202, 0.2);
|
||||
}}
|
||||
.form-input input[type=text].error,
|
||||
.form-input input[type=password].error,
|
||||
.form-input input[type=number].error,
|
||||
.form-input textarea.error {{
|
||||
border-color: #ca3e3e;
|
||||
}}
|
||||
.form-input input[disabled=true],
|
||||
.form-input textarea[disabled=true] {{
|
||||
background-color: #f5f5f5;
|
||||
}}
|
||||
.form-input textarea {{
|
||||
min-height: 200px;
|
||||
}}
|
||||
.form-input--errors {{
|
||||
color: #ca3e3e;
|
||||
list-style: none;
|
||||
margin: 0;
|
||||
padding: 4px 0;
|
||||
}}
|
||||
.form-input--errors--error {{
|
||||
font-size: 14px;
|
||||
padding: 2px 0;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
|
|
Loading…
Reference in a new issue