This commit is contained in:
Mitchell Marino 2024-06-04 16:00:19 -05:00
parent fedc6d37bd
commit 161727ab7a
10 changed files with 189 additions and 110 deletions

View File

@ -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 = "*"

View File

@ -40,6 +40,7 @@
.form_input { .form_input {
display: block; display: block;
box-sizing: border-box;
width: 100%; width: 100%;
background-color: #f7fafc; background-color: #f7fafc;
border-width: 2px; border-width: 2px;

View File

@ -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>,

View File

@ -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,9 +121,9 @@ 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),
}; };
@ -126,7 +132,8 @@ impl<FD: FormData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C,
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

View File

@ -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

View File

@ -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>,

View File

@ -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

View File

@ -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

View File

@ -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 view = view! { let validation_cbs = builder.validation_cbs;
<Form action=action_location let on_submit = move |ev: SubmitEvent| {
on:submit=move |ev| { let mut failed = false;
if !validate_form() { for validation in validation_cbs.iter() {
if !validation() {
failed = true;
}
}
if failed {
ev.prevent_default(); ev.prevent_default();
return;
} }
if let Some(action) = action { };
action.dispatch(builder.fd.get());
} let view = view! {
}> <Form action=url on:submit=on_submit>
{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")
} }

View File

@ -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