generated from mitchell/rust_template
working
This commit is contained in:
parent
fedc6d37bd
commit
161727ab7a
@ -6,4 +6,7 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
leptos = "0.6"
|
leptos = "0.6"
|
||||||
leptos_router = "0.6"
|
leptos_router = "0.6"
|
||||||
|
serde = { version = "1.0.203", features = ["derive"] }
|
||||||
|
# Leptos should pick the web_sys version for us
|
||||||
|
web-sys = "*"
|
||||||
|
|
||||||
|
|||||||
@ -40,7 +40,8 @@
|
|||||||
|
|
||||||
.form_input {
|
.form_input {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
box-sizing: border-box;
|
||||||
|
width: 100%;
|
||||||
background-color: #f7fafc;
|
background-color: #f7fafc;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
border-color: #e2e8f0;
|
border-color: #e2e8f0;
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use leptos::View;
|
|||||||
|
|
||||||
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
|
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
|
||||||
use crate::{
|
use crate::{
|
||||||
form::{FormBuilder, FormData},
|
form::{FormBuilder, FormToolData},
|
||||||
styles::FormStyle,
|
styles::FormStyle,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ impl VanityControlData for HeadingData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||||
pub fn heading(
|
pub fn heading(
|
||||||
self,
|
self,
|
||||||
builder: impl Fn(VanityControlBuilder<FS, HeadingData>) -> VanityControlBuilder<FS, HeadingData>,
|
builder: impl Fn(VanityControlBuilder<FS, HeadingData>) -> VanityControlBuilder<FS, HeadingData>,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use std::{fmt::Display, rc::Rc};
|
use std::{fmt::Display, rc::Rc};
|
||||||
|
|
||||||
use crate::{form::FormData, styles::FormStyle};
|
use crate::{form::FormToolData, styles::FormStyle};
|
||||||
use leptos::{Signal, View};
|
use leptos::{Signal, View};
|
||||||
|
|
||||||
pub mod heading;
|
pub mod heading;
|
||||||
@ -9,17 +9,20 @@ pub mod submit;
|
|||||||
pub mod text_area;
|
pub mod text_area;
|
||||||
pub mod text_input;
|
pub mod text_input;
|
||||||
|
|
||||||
// TODO: change to returning an Option<String>
|
|
||||||
pub trait ValidationFn<FDT>: Fn(&FDT) -> Result<(), String> + 'static {}
|
pub trait ValidationFn<FDT>: Fn(&FDT) -> Result<(), String> + 'static {}
|
||||||
|
pub trait ValidationCb: Fn() -> bool + 'static {}
|
||||||
pub trait ParseFn<CR, FDT>: Fn(CR) -> Result<FDT, String> + 'static {}
|
pub trait ParseFn<CR, FDT>: Fn(CR) -> Result<FDT, String> + 'static {}
|
||||||
pub trait UnparseFn<CR, FDT>: Fn(&FDT) -> CR + 'static {}
|
pub trait UnparseFn<CR, FDT>: Fn(&FDT) -> CR + 'static {}
|
||||||
pub trait FieldFn<FD, FDT>: Fn(&mut FD) -> &mut FDT + 'static {}
|
pub trait FieldGetter<FD, FDT>: Fn(FD) -> FDT + 'static {}
|
||||||
|
pub trait FieldSetter<FD, FDT>: Fn(&mut FD, FDT) + 'static {}
|
||||||
|
|
||||||
// implement the traits for all valid types
|
// implement the traits for all valid types
|
||||||
impl<FDT, T> ValidationFn<FDT> for T where T: Fn(&FDT) -> Result<(), String> + 'static {}
|
impl<FDT, T> ValidationFn<FDT> for T where T: Fn(&FDT) -> Result<(), String> + 'static {}
|
||||||
|
impl<T> ValidationCb for T where T: Fn() -> bool + 'static {}
|
||||||
impl<CR, FDT, F> ParseFn<CR, FDT> for F where F: Fn(CR) -> Result<FDT, String> + 'static {}
|
impl<CR, FDT, F> ParseFn<CR, FDT> for F where F: Fn(CR) -> Result<FDT, String> + 'static {}
|
||||||
impl<CR, FDT, F> UnparseFn<CR, FDT> for F where F: Fn(&FDT) -> CR + 'static {}
|
impl<CR, FDT, F> UnparseFn<CR, FDT> for F where F: Fn(&FDT) -> CR + 'static {}
|
||||||
impl<FD, FDT, F> FieldFn<FD, FDT> for F where F: Fn(&mut FD) -> &mut FDT + 'static {}
|
impl<FD, FDT, F> FieldGetter<FD, FDT> for F where F: Fn(FD) -> FDT + 'static {}
|
||||||
|
impl<FD, FDT, F> FieldSetter<FD, FDT> for F where F: Fn(&mut FD, FDT) + 'static {}
|
||||||
|
|
||||||
pub trait VanityControlData: 'static {
|
pub trait VanityControlData: 'static {
|
||||||
fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View;
|
fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View;
|
||||||
@ -84,16 +87,18 @@ impl Display for ControlBuildError {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct BuiltControlData<FD: FormData, FS: FormStyle, C: ControlData, FDT> {
|
pub(crate) struct BuiltControlData<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> {
|
||||||
pub(crate) render_data: ControlRenderData<FS, C>,
|
pub(crate) render_data: ControlRenderData<FS, C>,
|
||||||
pub(crate) field_fn: Rc<dyn FieldFn<FD, FDT>>,
|
pub(crate) getter: Rc<dyn FieldGetter<FD, FDT>>,
|
||||||
|
pub(crate) setter: Rc<dyn FieldSetter<FD, FDT>>,
|
||||||
pub(crate) parse_fn: Box<dyn ParseFn<C::ReturnType, FDT>>,
|
pub(crate) parse_fn: Box<dyn ParseFn<C::ReturnType, FDT>>,
|
||||||
pub(crate) unparse_fn: Box<dyn UnparseFn<C::ReturnType, FDT>>,
|
pub(crate) unparse_fn: Box<dyn UnparseFn<C::ReturnType, FDT>>,
|
||||||
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ControlBuilder<FD: FormData, FS: FormStyle, C: ControlData, FDT> {
|
pub struct ControlBuilder<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> {
|
||||||
pub(crate) field_fn: Option<Rc<dyn FieldFn<FD, FDT>>>,
|
pub(crate) getter: Option<Rc<dyn FieldGetter<FD, FDT>>>,
|
||||||
|
pub(crate) setter: Option<Rc<dyn FieldSetter<FD, FDT>>>,
|
||||||
pub(crate) parse_fn: Option<Box<dyn ParseFn<C::ReturnType, FDT>>>,
|
pub(crate) parse_fn: Option<Box<dyn ParseFn<C::ReturnType, FDT>>>,
|
||||||
pub(crate) unparse_fn: Option<Box<dyn UnparseFn<C::ReturnType, FDT>>>,
|
pub(crate) unparse_fn: Option<Box<dyn UnparseFn<C::ReturnType, FDT>>>,
|
||||||
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
||||||
@ -101,11 +106,12 @@ pub struct ControlBuilder<FD: FormData, FS: FormStyle, C: ControlData, FDT> {
|
|||||||
pub(crate) data: C,
|
pub(crate) data: C,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C, FDT> {
|
impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C, FDT> {
|
||||||
pub(crate) fn new(data: C) -> Self {
|
pub(crate) fn new(data: C) -> Self {
|
||||||
ControlBuilder {
|
ControlBuilder {
|
||||||
data,
|
data,
|
||||||
field_fn: None,
|
getter: None,
|
||||||
|
setter: None,
|
||||||
parse_fn: None,
|
parse_fn: None,
|
||||||
unparse_fn: None,
|
unparse_fn: None,
|
||||||
validation_fn: None,
|
validation_fn: None,
|
||||||
@ -115,18 +121,19 @@ impl<FD: FormData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C,
|
|||||||
|
|
||||||
pub(crate) fn build(self) -> Result<BuiltControlData<FD, FS, C, FDT>, ControlBuildError> {
|
pub(crate) fn build(self) -> Result<BuiltControlData<FD, FS, C, FDT>, ControlBuildError> {
|
||||||
// either all 3 should be specified or none of them due to the possible `field` for `field_with` calls.
|
// either all 3 should be specified or none of them due to the possible `field` for `field_with` calls.
|
||||||
let (field_fn, parse_fn, unparse_fn) = match (self.field_fn, self.parse_fn, self.unparse_fn)
|
let (getter, setter, parse_fn, unparse_fn) =
|
||||||
{
|
match (self.getter, self.setter, self.parse_fn, self.unparse_fn) {
|
||||||
(Some(f), Some(p), Some(u)) => (f, p, u),
|
(Some(g), Some(s), Some(p), Some(u)) => (g, s, p, u),
|
||||||
_ => return Err(ControlBuildError::MissingField),
|
_ => return Err(ControlBuildError::MissingField),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(BuiltControlData {
|
Ok(BuiltControlData {
|
||||||
render_data: ControlRenderData {
|
render_data: ControlRenderData {
|
||||||
data: Box::new(self.data),
|
data: Box::new(self.data),
|
||||||
style: self.style_attributes,
|
style: self.style_attributes,
|
||||||
},
|
},
|
||||||
field_fn,
|
getter,
|
||||||
|
setter,
|
||||||
parse_fn,
|
parse_fn,
|
||||||
unparse_fn,
|
unparse_fn,
|
||||||
validation_fn: self.validation_fn,
|
validation_fn: self.validation_fn,
|
||||||
@ -137,11 +144,13 @@ impl<FD: FormData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C,
|
|||||||
// TryInto<C::ReturnValue> and TryFrom<C::ReturnVlaue>
|
// TryInto<C::ReturnValue> and TryFrom<C::ReturnVlaue>
|
||||||
pub fn field_with(
|
pub fn field_with(
|
||||||
mut self,
|
mut self,
|
||||||
field_fn: impl FieldFn<FD, FDT>,
|
getter: impl FieldGetter<FD, FDT>,
|
||||||
|
setter: impl FieldSetter<FD, FDT>,
|
||||||
parse_fn: impl ParseFn<C::ReturnType, FDT>,
|
parse_fn: impl ParseFn<C::ReturnType, FDT>,
|
||||||
unparse_fn: impl UnparseFn<C::ReturnType, FDT>,
|
unparse_fn: impl UnparseFn<C::ReturnType, FDT>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.field_fn = Some(Rc::new(field_fn));
|
self.getter = Some(Rc::new(getter));
|
||||||
|
self.setter = Some(Rc::new(setter));
|
||||||
self.parse_fn = Some(Box::new(parse_fn));
|
self.parse_fn = Some(Box::new(parse_fn));
|
||||||
self.unparse_fn = Some(Box::new(unparse_fn));
|
self.unparse_fn = Some(Box::new(unparse_fn));
|
||||||
self
|
self
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use leptos::{Signal, View};
|
|||||||
|
|
||||||
use super::{ControlBuilder, ControlData, ControlRenderData};
|
use super::{ControlBuilder, ControlData, ControlRenderData};
|
||||||
use crate::{
|
use crate::{
|
||||||
form::{FormBuilder, FormData},
|
form::{FormBuilder, FormToolData},
|
||||||
styles::FormStyle,
|
styles::FormStyle,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -26,8 +26,8 @@ impl ControlData for SelectData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||||
pub fn select<FDT: 'static>(
|
pub fn select<FDT: Clone + PartialEq + 'static>(
|
||||||
self,
|
self,
|
||||||
builder: impl Fn(
|
builder: impl Fn(
|
||||||
ControlBuilder<FD, FS, SelectData, FDT>,
|
ControlBuilder<FD, FS, SelectData, FDT>,
|
||||||
@ -37,7 +37,7 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle, FDT> ControlBuilder<FD, FS, SelectData, FDT> {
|
impl<FD: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, SelectData, FDT> {
|
||||||
pub fn options(mut self, options: Vec<String>) -> Self {
|
pub fn options(mut self, options: Vec<String>) -> Self {
|
||||||
self.data.options = options;
|
self.data.options = options;
|
||||||
self
|
self
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use leptos::View;
|
|||||||
|
|
||||||
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
|
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
|
||||||
use crate::{
|
use crate::{
|
||||||
form::{FormBuilder, FormData},
|
form::{FormBuilder, FormToolData},
|
||||||
styles::FormStyle,
|
styles::FormStyle,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -17,7 +17,7 @@ impl VanityControlData for SubmitData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||||
pub fn submit(
|
pub fn submit(
|
||||||
self,
|
self,
|
||||||
builder: impl Fn(VanityControlBuilder<FS, SubmitData>) -> VanityControlBuilder<FS, SubmitData>,
|
builder: impl Fn(VanityControlBuilder<FS, SubmitData>) -> VanityControlBuilder<FS, SubmitData>,
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
use super::{ControlBuilder, ControlData, ControlRenderData};
|
use super::{ControlBuilder, ControlData, ControlRenderData};
|
||||||
use crate::{
|
use crate::{
|
||||||
form::{FormBuilder, FormData},
|
form::{FormBuilder, FormToolData},
|
||||||
styles::FormStyle,
|
styles::FormStyle,
|
||||||
};
|
};
|
||||||
use leptos::{Signal, View};
|
use leptos::{Signal, View};
|
||||||
@ -25,8 +25,8 @@ impl ControlData for TextAreaData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||||
pub fn text_area<FDT: 'static>(
|
pub fn text_area<FDT: Clone + PartialEq + 'static>(
|
||||||
self,
|
self,
|
||||||
builder: impl Fn(
|
builder: impl Fn(
|
||||||
ControlBuilder<FD, FS, TextAreaData, FDT>,
|
ControlBuilder<FD, FS, TextAreaData, FDT>,
|
||||||
@ -36,7 +36,7 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle, FDT> ControlBuilder<FD, FS, TextAreaData, FDT> {
|
impl<FD: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, TextAreaData, FDT> {
|
||||||
pub fn placeholder(mut self, placeholder: impl ToString) -> Self {
|
pub fn placeholder(mut self, placeholder: impl ToString) -> Self {
|
||||||
self.data.placeholder = Some(placeholder.to_string());
|
self.data.placeholder = Some(placeholder.to_string());
|
||||||
self
|
self
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use leptos::{Signal, View};
|
|||||||
|
|
||||||
use super::{ControlBuilder, ControlData, ControlRenderData};
|
use super::{ControlBuilder, ControlData, ControlRenderData};
|
||||||
use crate::{
|
use crate::{
|
||||||
form::{FormBuilder, FormData},
|
form::{FormBuilder, FormToolData},
|
||||||
styles::FormStyle,
|
styles::FormStyle,
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -41,8 +41,8 @@ impl ControlData for TextInputData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||||
pub fn text_input<FDT: 'static>(
|
pub fn text_input<FDT: Clone + PartialEq + 'static>(
|
||||||
self,
|
self,
|
||||||
builder: impl Fn(
|
builder: impl Fn(
|
||||||
ControlBuilder<FD, FS, TextInputData, FDT>,
|
ControlBuilder<FD, FS, TextInputData, FDT>,
|
||||||
@ -52,7 +52,7 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle, FDT> ControlBuilder<FD, FS, TextInputData, FDT> {
|
impl<FD: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, TextInputData, FDT> {
|
||||||
pub fn named(mut self, control_name: impl ToString) -> Self {
|
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||||
self.data.name = control_name.to_string();
|
self.data.name = control_name.to_string();
|
||||||
self
|
self
|
||||||
|
|||||||
215
src/form.rs
215
src/form.rs
@ -1,20 +1,32 @@
|
|||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
controls::{
|
controls::{
|
||||||
BuiltControlData, ControlBuilder, ControlData, FieldFn, ParseFn, UnparseFn, ValidationFn,
|
BuiltControlData, ControlBuilder, ControlData, FieldGetter, FieldSetter, ParseFn,
|
||||||
VanityControlBuilder, VanityControlData,
|
UnparseFn, ValidationCb, ValidationFn, VanityControlBuilder, VanityControlData,
|
||||||
},
|
},
|
||||||
styles::FormStyle,
|
styles::FormStyle,
|
||||||
};
|
};
|
||||||
use leptos::*;
|
use leptos::{
|
||||||
use leptos_router::Form;
|
server_fn::{client::Client, codec::PostUrl, request::ClientReq, ServerFn},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
use leptos_router::{ActionForm, Form};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use web_sys::{FormData, SubmitEvent};
|
||||||
|
|
||||||
pub struct Validator<FD: FormData> {
|
/// A type that can be used to validate the form data.
|
||||||
|
///
|
||||||
|
/// This can be useful to use the same validation logic on the front
|
||||||
|
/// end and backend without duplicating the logic.
|
||||||
|
pub struct Validator<FD> {
|
||||||
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData> Validator<FD> {
|
impl<FD: FormToolData> Validator<FD> {
|
||||||
|
/// Validates the given form data.
|
||||||
|
///
|
||||||
|
/// This runs all the validation functions for all the fields
|
||||||
|
/// in the form. The first falure to occur (if any) will be returned.
|
||||||
pub fn validate(&self, form_data: &FD) -> Result<(), String> {
|
pub fn validate(&self, form_data: &FD) -> Result<(), String> {
|
||||||
for v in self.validations.iter() {
|
for v in self.validations.iter() {
|
||||||
(*v)(form_data)?;
|
(*v)(form_data)?;
|
||||||
@ -27,13 +39,14 @@ impl<FD: FormData> Validator<FD> {
|
|||||||
///
|
///
|
||||||
/// With this, you can render the form, get the form data, or get
|
/// With this, you can render the form, get the form data, or get
|
||||||
/// a validator for the data.
|
/// a validator for the data.
|
||||||
pub struct Form<FD: FormData> {
|
pub struct Form<FD: FormToolData> {
|
||||||
pub fd: RwSignal<FD>,
|
pub fd: RwSignal<FD>,
|
||||||
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
||||||
view: View,
|
view: View,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData> Form<FD> {
|
impl<FD: FormToolData> Form<FD> {
|
||||||
|
/// Gets the [`Validator`] for this form.
|
||||||
pub fn validator(self) -> Validator<FD> {
|
pub fn validator(self) -> Validator<FD> {
|
||||||
Validator {
|
Validator {
|
||||||
validations: self.validations,
|
validations: self.validations,
|
||||||
@ -62,7 +75,7 @@ impl<FD: FormData> Form<FD> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData> IntoView for Form<FD> {
|
impl<FD: FormToolData> IntoView for Form<FD> {
|
||||||
fn into_view(self) -> View {
|
fn into_view(self) -> View {
|
||||||
self.view()
|
self.view()
|
||||||
}
|
}
|
||||||
@ -70,10 +83,11 @@ impl<FD: FormData> IntoView for Form<FD> {
|
|||||||
|
|
||||||
/// A version of the [`FormBuilder`] that contains all the data
|
/// A version of the [`FormBuilder`] that contains all the data
|
||||||
/// needed for full building of a [`Form`].
|
/// needed for full building of a [`Form`].
|
||||||
struct FullFormBuilder<FD: FormData, FS: FormStyle> {
|
struct FullFormBuilder<FD: FormToolData, FS: FormStyle> {
|
||||||
fd: RwSignal<FD>,
|
fd: RwSignal<FD>,
|
||||||
fs: FS,
|
fs: FS,
|
||||||
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
||||||
|
validation_cbs: Vec<Box<dyn ValidationCb>>,
|
||||||
views: Vec<View>,
|
views: Vec<View>,
|
||||||
}
|
}
|
||||||
/// The internal type for building forms
|
/// The internal type for building forms
|
||||||
@ -86,7 +100,7 @@ struct FullFormBuilder<FD: FormData, FS: FormStyle> {
|
|||||||
/// cannot or should not render the form. You can
|
/// cannot or should not render the form. You can
|
||||||
/// still get all the validation functions from the
|
/// still get all the validation functions from the
|
||||||
/// form data.
|
/// form data.
|
||||||
enum FormBuilderInner<FD: FormData, FS: FormStyle> {
|
enum FormBuilderInner<FD: FormToolData, FS: FormStyle> {
|
||||||
/// For building the form with views
|
/// For building the form with views
|
||||||
FullBuilder(FullFormBuilder<FD, FS>),
|
FullBuilder(FullFormBuilder<FD, FS>),
|
||||||
/// For building only the validations for the form
|
/// For building only the validations for the form
|
||||||
@ -95,11 +109,11 @@ enum FormBuilderInner<FD: FormData, FS: FormStyle> {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct FormBuilder<FD: FormData, FS: FormStyle> {
|
pub struct FormBuilder<FD: FormToolData, FS: FormStyle> {
|
||||||
inner: FormBuilderInner<FD, FS>,
|
inner: FormBuilderInner<FD, FS>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||||
// TODO: remove the Default trait bound
|
// TODO: remove the Default trait bound
|
||||||
fn new_full_builder(starting_data: FD, form_style: FS) -> FormBuilder<FD, FS> {
|
fn new_full_builder(starting_data: FD, form_style: FS) -> FormBuilder<FD, FS> {
|
||||||
let fd = create_rw_signal(starting_data);
|
let fd = create_rw_signal(starting_data);
|
||||||
@ -108,6 +122,7 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
fd,
|
fd,
|
||||||
fs: form_style,
|
fs: form_style,
|
||||||
validations: Vec::new(),
|
validations: Vec::new(),
|
||||||
|
validation_cbs: Vec::new(),
|
||||||
views: Vec::new(),
|
views: Vec::new(),
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
@ -131,7 +146,7 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_control<C: ControlData + Default, FDT: 'static>(
|
pub(crate) fn new_control<C: ControlData + Default, FDT: Clone + PartialEq + 'static>(
|
||||||
mut self,
|
mut self,
|
||||||
builder: impl Fn(ControlBuilder<FD, FS, C, FDT>) -> ControlBuilder<FD, FS, C, FDT>,
|
builder: impl Fn(ControlBuilder<FD, FS, C, FDT>) -> ControlBuilder<FD, FS, C, FDT>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@ -152,13 +167,14 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
builder.views.push(view);
|
builder.views.push(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_control<C: ControlData, FDT: 'static>(
|
fn add_control<C: ControlData, FDT: Clone + PartialEq + 'static>(
|
||||||
&mut self,
|
&mut self,
|
||||||
control: ControlBuilder<FD, FS, C, FDT>,
|
control: ControlBuilder<FD, FS, C, FDT>,
|
||||||
) {
|
) {
|
||||||
let BuiltControlData {
|
let BuiltControlData {
|
||||||
render_data,
|
render_data,
|
||||||
field_fn,
|
getter,
|
||||||
|
setter,
|
||||||
parse_fn,
|
parse_fn,
|
||||||
unparse_fn,
|
unparse_fn,
|
||||||
validation_fn,
|
validation_fn,
|
||||||
@ -182,9 +198,10 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
builder.validations.push(validation_fn.clone());
|
builder.validations.push(validation_fn.clone());
|
||||||
}
|
}
|
||||||
|
|
||||||
let view = Self::build_control_view(
|
let (validation_cb, view) = Self::build_control_view(
|
||||||
builder,
|
builder,
|
||||||
field_fn,
|
getter,
|
||||||
|
setter,
|
||||||
unparse_fn,
|
unparse_fn,
|
||||||
parse_fn,
|
parse_fn,
|
||||||
validation_fn,
|
validation_fn,
|
||||||
@ -192,33 +209,50 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
);
|
);
|
||||||
|
|
||||||
builder.views.push(view);
|
builder.views.push(view);
|
||||||
|
builder.validation_cbs.push(validation_cb);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build_control_view<C: ControlData, FDT: 'static>(
|
fn build_control_view<C: ControlData, FDT: Clone + PartialEq + 'static>(
|
||||||
builder: &FullFormBuilder<FD, FS>,
|
builder: &FullFormBuilder<FD, FS>,
|
||||||
field_fn: Rc<dyn FieldFn<FD, FDT>>,
|
getter: Rc<dyn FieldGetter<FD, FDT>>,
|
||||||
|
setter: Rc<dyn FieldSetter<FD, FDT>>,
|
||||||
unparse_fn: Box<dyn UnparseFn<<C as ControlData>::ReturnType, FDT>>,
|
unparse_fn: Box<dyn UnparseFn<<C as ControlData>::ReturnType, FDT>>,
|
||||||
parse_fn: Box<dyn ParseFn<<C as ControlData>::ReturnType, FDT>>,
|
parse_fn: Box<dyn ParseFn<<C as ControlData>::ReturnType, FDT>>,
|
||||||
validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
||||||
render_data: crate::controls::ControlRenderData<FS, C>,
|
render_data: crate::controls::ControlRenderData<FS, C>,
|
||||||
) -> View {
|
) -> (Box<dyn ValidationCb>, View) {
|
||||||
let (validation_signal, validation_signal_set) = create_signal(Ok(()));
|
let (validation_signal, validation_signal_set) = create_signal(Ok(()));
|
||||||
// TODO: The on-submit triggering a validation could be done here with a create_effect
|
let fd = builder.fd;
|
||||||
let field_fn2 = field_fn.clone();
|
|
||||||
let fd_getter = builder.fd.read_only();
|
|
||||||
let value_getter = move || {
|
let value_getter = move || {
|
||||||
|
let getter = getter.clone();
|
||||||
// TODO: ideally, this should not be borrowed. If we get a clone when we call `.get` we should pass in the clone, not borrow this clone
|
// TODO: ideally, this should not be borrowed. If we get a clone when we call `.get` we should pass in the clone, not borrow this clone
|
||||||
let mut fd = fd_getter.get();
|
let field = create_memo(move |_| getter(fd.get()));
|
||||||
let field = field_fn2(&mut fd);
|
unparse_fn(&field.get())
|
||||||
(unparse_fn)(field)
|
|
||||||
};
|
};
|
||||||
let value_getter = value_getter.into_signal();
|
let value_getter = value_getter.into_signal();
|
||||||
|
|
||||||
let fd = builder.fd.clone();
|
let validation_cb = move || {
|
||||||
|
let validation_fn = validation_fn.as_ref();
|
||||||
|
let validation_fn = match validation_fn {
|
||||||
|
Some(v) => v,
|
||||||
|
None => return true, // No validation function, so validation passes
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = fd.get();
|
||||||
|
let validation_result = validation_fn(&data);
|
||||||
|
let succeeded = validation_result.is_ok();
|
||||||
|
validation_signal_set.set(validation_result);
|
||||||
|
succeeded
|
||||||
|
};
|
||||||
|
let validation_cb = Box::new(validation_cb);
|
||||||
|
|
||||||
|
let validation_cb2 = validation_cb.clone();
|
||||||
let value_setter = move |value| {
|
let value_setter = move |value| {
|
||||||
// TODO: also do this on a submit somehow
|
|
||||||
let parsed = match parse_fn(value) {
|
let parsed = match parse_fn(value) {
|
||||||
Ok(p) => p,
|
Ok(p) => {
|
||||||
|
validation_signal_set.set(Ok(()));
|
||||||
|
p
|
||||||
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
validation_signal_set.set(Err(e));
|
validation_signal_set.set(Err(e));
|
||||||
return;
|
return;
|
||||||
@ -226,65 +260,93 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// parse succeeded, update value and validate
|
// parse succeeded, update value and validate
|
||||||
let field_fn = field_fn.clone(); // move
|
fd.update(|data| {
|
||||||
let validation_fn = validation_fn.clone(); // move
|
setter(data, parsed);
|
||||||
|
|
||||||
// TODO: change this to a get then set, so that if the validation fails,
|
|
||||||
// The value is not actually updated
|
|
||||||
fd.update(move |fd| {
|
|
||||||
let field = field_fn(fd);
|
|
||||||
*field = parsed;
|
|
||||||
|
|
||||||
if let Some(ref validation_fn) = validation_fn {
|
|
||||||
let validation_result = (validation_fn)(fd);
|
|
||||||
validation_signal_set.set(validation_result);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// run validation
|
||||||
|
(validation_cb2)();
|
||||||
};
|
};
|
||||||
let value_setter = Box::new(value_setter);
|
let value_setter = Box::new(value_setter);
|
||||||
// TODO: add a signal that triggers on submit to refresh the validation on_submit
|
|
||||||
|
|
||||||
C::build_control(
|
let view = C::build_control(
|
||||||
&builder.fs,
|
&builder.fs,
|
||||||
render_data,
|
render_data,
|
||||||
value_getter,
|
value_getter,
|
||||||
value_setter,
|
value_setter,
|
||||||
validation_signal.into(),
|
validation_signal.into(),
|
||||||
)
|
);
|
||||||
|
(validation_cb, view)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(
|
fn build_action_form<ServFn>(
|
||||||
self,
|
self,
|
||||||
action_location: String,
|
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
|
||||||
action: Option<Action<FD, Result<(), ServerFnError>>>,
|
) -> Option<Form<FD>>
|
||||||
) -> Option<Form<FD>> {
|
where
|
||||||
|
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
|
||||||
|
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
|
||||||
|
From<FormData>,
|
||||||
|
{
|
||||||
let builder = match self.inner {
|
let builder = match self.inner {
|
||||||
FormBuilderInner::FullBuilder(fb) => fb,
|
FormBuilderInner::FullBuilder(fb) => fb,
|
||||||
FormBuilderInner::ValidationBuilder { validations: _ } => return None,
|
FormBuilderInner::ValidationBuilder { validations: _ } => return None,
|
||||||
};
|
};
|
||||||
let validations = builder.validations.clone();
|
|
||||||
let validate_form = move || {
|
let elements = builder.fs.form_frame(builder.views.into_view());
|
||||||
let fd = builder.fd.get();
|
|
||||||
validations.iter().all(|v| v(&fd).is_ok())
|
let validation_cbs = builder.validation_cbs;
|
||||||
|
let on_submit = move |ev: SubmitEvent| {
|
||||||
|
let mut failed = false;
|
||||||
|
for validation in validation_cbs.iter() {
|
||||||
|
if !validation() {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failed {
|
||||||
|
ev.prevent_default();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let view = view! {
|
||||||
|
<ActionForm action=action on:submit=on_submit>
|
||||||
|
{elements}
|
||||||
|
</ActionForm>
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(Form {
|
||||||
|
fd: builder.fd,
|
||||||
|
validations: builder.validations,
|
||||||
|
view,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_plain_form(self, url: String) -> Option<Form<FD>> {
|
||||||
|
let builder = match self.inner {
|
||||||
|
FormBuilderInner::FullBuilder(fb) => fb,
|
||||||
|
FormBuilderInner::ValidationBuilder { validations: _ } => return None,
|
||||||
};
|
};
|
||||||
|
|
||||||
let elements = builder.fs.form_frame(builder.views.into_view());
|
let elements = builder.fs.form_frame(builder.views.into_view());
|
||||||
|
|
||||||
|
let validation_cbs = builder.validation_cbs;
|
||||||
|
let on_submit = move |ev: SubmitEvent| {
|
||||||
|
let mut failed = false;
|
||||||
|
for validation in validation_cbs.iter() {
|
||||||
|
if !validation() {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failed {
|
||||||
|
ev.prevent_default();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let view = view! {
|
let view = view! {
|
||||||
<Form action=action_location
|
<Form action=url on:submit=on_submit>
|
||||||
on:submit=move |ev| {
|
|
||||||
if !validate_form() {
|
|
||||||
ev.prevent_default();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if let Some(action) = action {
|
|
||||||
action.dispatch(builder.fd.get());
|
|
||||||
}
|
|
||||||
}>
|
|
||||||
{elements}
|
{elements}
|
||||||
</Form>
|
</Form>
|
||||||
}
|
};
|
||||||
.into_view();
|
|
||||||
|
|
||||||
Some(Form {
|
Some(Form {
|
||||||
fd: builder.fd,
|
fd: builder.fd,
|
||||||
@ -306,7 +368,7 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
///
|
///
|
||||||
/// This trait defines a function that can be used to build all the data needed
|
/// This trait defines a function that can be used to build all the data needed
|
||||||
/// to physically lay out a form, and how that data should be parsed and validated.
|
/// to physically lay out a form, and how that data should be parsed and validated.
|
||||||
pub trait FormData: Default + Clone + 'static {
|
pub trait FormToolData: Default + Clone + 'static {
|
||||||
type Style: FormStyle;
|
type Style: FormStyle;
|
||||||
|
|
||||||
/// Defines how the form should be layed out and how the data should be parsed and validated.
|
/// Defines how the form should be layed out and how the data should be parsed and validated.
|
||||||
@ -325,19 +387,24 @@ pub trait FormData: Default + Clone + 'static {
|
|||||||
let builder = FormBuilder::new_full_builder(self, style);
|
let builder = FormBuilder::new_full_builder(self, style);
|
||||||
let builder = Self::build_form(builder);
|
let builder = Self::build_form(builder);
|
||||||
builder
|
builder
|
||||||
.build(action.to_string(), None)
|
.build_plain_form(action.to_string())
|
||||||
.expect("builder should be full builder")
|
.expect("builder should be full builder")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_action_form(
|
fn get_action_form<ServFn>(
|
||||||
self,
|
self,
|
||||||
action: Action<Self, Result<(), ServerFnError>>,
|
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
|
||||||
style: Self::Style,
|
style: Self::Style,
|
||||||
) -> Form<Self> {
|
) -> Form<Self>
|
||||||
|
where
|
||||||
|
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
|
||||||
|
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
|
||||||
|
From<FormData>,
|
||||||
|
{
|
||||||
let builder = FormBuilder::new_full_builder(self, style);
|
let builder = FormBuilder::new_full_builder(self, style);
|
||||||
let builder = Self::build_form(builder);
|
let builder = Self::build_form(builder);
|
||||||
builder
|
builder
|
||||||
.build(action.url().unwrap_or(String::new()), Some(action))
|
.build_action_form(action)
|
||||||
.expect("builder should be full builder")
|
.expect("builder should be full builder")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -51,7 +51,6 @@ impl FormStyle for GridFormStyle {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
// TODO:
|
|
||||||
type=control.data.input_type
|
type=control.data.input_type
|
||||||
id=&control.data.name
|
id=&control.data.name
|
||||||
name=control.data.name
|
name=control.data.name
|
||||||
|
|||||||
Reference in New Issue
Block a user