442 lines
12 KiB
Rust
442 lines
12 KiB
Rust
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, errors: &mut Errors<'_>);
|
|
}
|
|
|
|
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>>,
|
|
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> {
|
|
fn new<S: 'static>(state: S) -> Self
|
|
where
|
|
S: Form<FieldName = 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(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<FieldName> FormState<FieldName>
|
|
where
|
|
FieldName: Eq + std::hash::Hash + 'static,
|
|
{
|
|
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
|
|
FieldName: Clone,
|
|
{
|
|
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 is_valid(&self) -> bool {
|
|
self.errors.is_empty()
|
|
}
|
|
|
|
pub(crate) fn disable(&mut self) -> &mut Self {
|
|
self.disabled = true;
|
|
self
|
|
}
|
|
|
|
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.values.get(name).cloned()
|
|
}
|
|
|
|
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(hm.values())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
#[inline_props]
|
|
#[allow(non_snake_case)]
|
|
fn InputErrors<'a, FieldName>(cx: Scope, name: &'a FieldName) -> Element
|
|
where
|
|
FieldName: Eq + std::hash::Hash + 'static,
|
|
{
|
|
let form_state = use_shared_state::<FormState<FieldName>>(cx);
|
|
|
|
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! {
|
|
ul {
|
|
class: "form-input--errors",
|
|
errors.map(|error| rsx! {
|
|
li {
|
|
class: "form-input--errors--error",
|
|
"{error}"
|
|
}
|
|
})
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
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>,
|
|
step: Option<f64>,
|
|
disabled: Option<bool>,
|
|
) -> Element
|
|
where
|
|
FieldName: Clone + Eq + std::hash::Hash + std::fmt::Display + 'static,
|
|
{
|
|
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>,
|
|
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(), disabled: disabled.unwrap_or(false) }
|
|
})
|
|
}
|
|
|
|
#[inline_props]
|
|
#[allow(non_snake_case)]
|
|
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(), 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)}"
|
|
},
|
|
},
|
|
},
|
|
}
|
|
})
|
|
}
|
|
|
|
#[inline_props]
|
|
#[allow(non_snake_case)]
|
|
pub(crate) fn Input<'a, FieldName>(
|
|
cx: Scope,
|
|
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! {
|
|
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 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}"
|
|
},
|
|
},
|
|
},
|
|
}
|
|
})
|
|
}
|
|
|
|
impl<T> Form for UseRef<T>
|
|
where
|
|
T: Form,
|
|
{
|
|
type FieldName = T::FieldName;
|
|
|
|
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 || {
|
|
let mut state = FormState::<FieldName>::new(form);
|
|
(init)(&mut state);
|
|
state
|
|
});
|
|
|
|
use_shared_state(cx).expect("Just set up")
|
|
}
|