From 161727ab7a7a4b217690ed9a2a212feb3b477965 Mon Sep 17 00:00:00 2001 From: Mitchell M Date: Tue, 4 Jun 2024 16:00:19 -0500 Subject: [PATCH] working --- Cargo.toml | 3 + grid_form.scss | 3 +- src/controls/heading.rs | 4 +- src/controls/mod.rs | 45 ++++---- src/controls/select.rs | 8 +- src/controls/submit.rs | 4 +- src/controls/text_area.rs | 8 +- src/controls/text_input.rs | 8 +- src/form.rs | 215 ++++++++++++++++++++++++------------- src/styles/grid_form.rs | 1 - 10 files changed, 189 insertions(+), 110 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 7689197..3d31903 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,4 +6,7 @@ edition = "2021" [dependencies] leptos = "0.6" leptos_router = "0.6" +serde = { version = "1.0.203", features = ["derive"] } +# Leptos should pick the web_sys version for us +web-sys = "*" diff --git a/grid_form.scss b/grid_form.scss index 04e99f9..fe58d19 100644 --- a/grid_form.scss +++ b/grid_form.scss @@ -40,7 +40,8 @@ .form_input { display: block; - width: 100%; + box-sizing: border-box; + width: 100%; background-color: #f7fafc; border-width: 2px; border-color: #e2e8f0; diff --git a/src/controls/heading.rs b/src/controls/heading.rs index afb9b8d..4bcac4a 100644 --- a/src/controls/heading.rs +++ b/src/controls/heading.rs @@ -2,7 +2,7 @@ use leptos::View; use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; use crate::{ - form::{FormBuilder, FormData}, + form::{FormBuilder, FormToolData}, styles::FormStyle, }; @@ -17,7 +17,7 @@ impl VanityControlData for HeadingData { } } -impl FormBuilder { +impl FormBuilder { pub fn heading( self, builder: impl Fn(VanityControlBuilder) -> VanityControlBuilder, diff --git a/src/controls/mod.rs b/src/controls/mod.rs index a940c42..e042577 100644 --- a/src/controls/mod.rs +++ b/src/controls/mod.rs @@ -1,6 +1,6 @@ use std::{fmt::Display, rc::Rc}; -use crate::{form::FormData, styles::FormStyle}; +use crate::{form::FormToolData, styles::FormStyle}; use leptos::{Signal, View}; pub mod heading; @@ -9,17 +9,20 @@ pub mod submit; pub mod text_area; pub mod text_input; -// TODO: change to returning an Option pub trait ValidationFn: Fn(&FDT) -> Result<(), String> + 'static {} +pub trait ValidationCb: Fn() -> bool + 'static {} pub trait ParseFn: Fn(CR) -> Result + 'static {} pub trait UnparseFn: Fn(&FDT) -> CR + 'static {} -pub trait FieldFn: Fn(&mut FD) -> &mut FDT + 'static {} +pub trait FieldGetter: Fn(FD) -> FDT + 'static {} +pub trait FieldSetter: Fn(&mut FD, FDT) + 'static {} // implement the traits for all valid types impl ValidationFn for T where T: Fn(&FDT) -> Result<(), String> + 'static {} +impl ValidationCb for T where T: Fn() -> bool + 'static {} impl ParseFn for F where F: Fn(CR) -> Result + 'static {} impl UnparseFn for F where F: Fn(&FDT) -> CR + 'static {} -impl FieldFn for F where F: Fn(&mut FD) -> &mut FDT + 'static {} +impl FieldGetter for F where F: Fn(FD) -> FDT + 'static {} +impl FieldSetter for F where F: Fn(&mut FD, FDT) + 'static {} pub trait VanityControlData: 'static { fn build_control(fs: &FS, control: ControlRenderData) -> View; @@ -84,16 +87,18 @@ impl Display for ControlBuildError { } } -pub(crate) struct BuiltControlData { +pub(crate) struct BuiltControlData { pub(crate) render_data: ControlRenderData, - pub(crate) field_fn: Rc>, + pub(crate) getter: Rc>, + pub(crate) setter: Rc>, pub(crate) parse_fn: Box>, pub(crate) unparse_fn: Box>, pub(crate) validation_fn: Option>>, } -pub struct ControlBuilder { - pub(crate) field_fn: Option>>, +pub struct ControlBuilder { + pub(crate) getter: Option>>, + pub(crate) setter: Option>>, pub(crate) parse_fn: Option>>, pub(crate) unparse_fn: Option>>, pub(crate) validation_fn: Option>>, @@ -101,11 +106,12 @@ pub struct ControlBuilder { pub(crate) data: C, } -impl ControlBuilder { +impl ControlBuilder { pub(crate) fn new(data: C) -> Self { ControlBuilder { data, - field_fn: None, + getter: None, + setter: None, parse_fn: None, unparse_fn: None, validation_fn: None, @@ -115,18 +121,19 @@ impl ControlBuilder Result, ControlBuildError> { // 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) - { - (Some(f), Some(p), Some(u)) => (f, p, u), - _ => return Err(ControlBuildError::MissingField), - }; + let (getter, setter, parse_fn, unparse_fn) = + match (self.getter, self.setter, self.parse_fn, self.unparse_fn) { + (Some(g), Some(s), Some(p), Some(u)) => (g, s, p, u), + _ => return Err(ControlBuildError::MissingField), + }; Ok(BuiltControlData { render_data: ControlRenderData { data: Box::new(self.data), style: self.style_attributes, }, - field_fn, + getter, + setter, parse_fn, unparse_fn, validation_fn: self.validation_fn, @@ -137,11 +144,13 @@ impl ControlBuilder and TryFrom pub fn field_with( mut self, - field_fn: impl FieldFn, + getter: impl FieldGetter, + setter: impl FieldSetter, parse_fn: impl ParseFn, unparse_fn: impl UnparseFn, ) -> 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.unparse_fn = Some(Box::new(unparse_fn)); self diff --git a/src/controls/select.rs b/src/controls/select.rs index b1b51b5..04f3c68 100644 --- a/src/controls/select.rs +++ b/src/controls/select.rs @@ -2,7 +2,7 @@ use leptos::{Signal, View}; use super::{ControlBuilder, ControlData, ControlRenderData}; use crate::{ - form::{FormBuilder, FormData}, + form::{FormBuilder, FormToolData}, styles::FormStyle, }; @@ -26,8 +26,8 @@ impl ControlData for SelectData { } } -impl FormBuilder { - pub fn select( +impl FormBuilder { + pub fn select( self, builder: impl Fn( ControlBuilder, @@ -37,7 +37,7 @@ impl FormBuilder { } } -impl ControlBuilder { +impl ControlBuilder { pub fn options(mut self, options: Vec) -> Self { self.data.options = options; self diff --git a/src/controls/submit.rs b/src/controls/submit.rs index 61bb6f9..db5f74e 100644 --- a/src/controls/submit.rs +++ b/src/controls/submit.rs @@ -2,7 +2,7 @@ use leptos::View; use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; use crate::{ - form::{FormBuilder, FormData}, + form::{FormBuilder, FormToolData}, styles::FormStyle, }; @@ -17,7 +17,7 @@ impl VanityControlData for SubmitData { } } -impl FormBuilder { +impl FormBuilder { pub fn submit( self, builder: impl Fn(VanityControlBuilder) -> VanityControlBuilder, diff --git a/src/controls/text_area.rs b/src/controls/text_area.rs index 674dd38..5d4784f 100644 --- a/src/controls/text_area.rs +++ b/src/controls/text_area.rs @@ -1,6 +1,6 @@ use super::{ControlBuilder, ControlData, ControlRenderData}; use crate::{ - form::{FormBuilder, FormData}, + form::{FormBuilder, FormToolData}, styles::FormStyle, }; use leptos::{Signal, View}; @@ -25,8 +25,8 @@ impl ControlData for TextAreaData { } } -impl FormBuilder { - pub fn text_area( +impl FormBuilder { + pub fn text_area( self, builder: impl Fn( ControlBuilder, @@ -36,7 +36,7 @@ impl FormBuilder { } } -impl ControlBuilder { +impl ControlBuilder { pub fn placeholder(mut self, placeholder: impl ToString) -> Self { self.data.placeholder = Some(placeholder.to_string()); self diff --git a/src/controls/text_input.rs b/src/controls/text_input.rs index 4c76632..06d702e 100644 --- a/src/controls/text_input.rs +++ b/src/controls/text_input.rs @@ -2,7 +2,7 @@ use leptos::{Signal, View}; use super::{ControlBuilder, ControlData, ControlRenderData}; use crate::{ - form::{FormBuilder, FormData}, + form::{FormBuilder, FormToolData}, styles::FormStyle, }; @@ -41,8 +41,8 @@ impl ControlData for TextInputData { } } -impl FormBuilder { - pub fn text_input( +impl FormBuilder { + pub fn text_input( self, builder: impl Fn( ControlBuilder, @@ -52,7 +52,7 @@ impl FormBuilder { } } -impl ControlBuilder { +impl ControlBuilder { pub fn named(mut self, control_name: impl ToString) -> Self { self.data.name = control_name.to_string(); self diff --git a/src/form.rs b/src/form.rs index 82183db..9e41027 100644 --- a/src/form.rs +++ b/src/form.rs @@ -1,20 +1,32 @@ -use std::rc::Rc; - use crate::{ controls::{ - BuiltControlData, ControlBuilder, ControlData, FieldFn, ParseFn, UnparseFn, ValidationFn, - VanityControlBuilder, VanityControlData, + BuiltControlData, ControlBuilder, ControlData, FieldGetter, FieldSetter, ParseFn, + UnparseFn, ValidationCb, ValidationFn, VanityControlBuilder, VanityControlData, }, styles::FormStyle, }; -use leptos::*; -use leptos_router::Form; +use leptos::{ + 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 { +/// 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 { validations: Vec>>, } -impl Validator { +impl Validator { + /// 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> { for v in self.validations.iter() { (*v)(form_data)?; @@ -27,13 +39,14 @@ impl Validator { /// /// With this, you can render the form, get the form data, or get /// a validator for the data. -pub struct Form { +pub struct Form { pub fd: RwSignal, validations: Vec>>, view: View, } -impl Form { +impl Form { + /// Gets the [`Validator`] for this form. pub fn validator(self) -> Validator { Validator { validations: self.validations, @@ -62,7 +75,7 @@ impl Form { } } -impl IntoView for Form { +impl IntoView for Form { fn into_view(self) -> View { self.view() } @@ -70,10 +83,11 @@ impl IntoView for Form { /// A version of the [`FormBuilder`] that contains all the data /// needed for full building of a [`Form`]. -struct FullFormBuilder { +struct FullFormBuilder { fd: RwSignal, fs: FS, validations: Vec>>, + validation_cbs: Vec>, views: Vec, } /// The internal type for building forms @@ -86,7 +100,7 @@ struct FullFormBuilder { /// cannot or should not render the form. You can /// still get all the validation functions from the /// form data. -enum FormBuilderInner { +enum FormBuilderInner { /// For building the form with views FullBuilder(FullFormBuilder), /// For building only the validations for the form @@ -95,11 +109,11 @@ enum FormBuilderInner { }, } -pub struct FormBuilder { +pub struct FormBuilder { inner: FormBuilderInner, } -impl FormBuilder { +impl FormBuilder { // TODO: remove the Default trait bound fn new_full_builder(starting_data: FD, form_style: FS) -> FormBuilder { let fd = create_rw_signal(starting_data); @@ -108,6 +122,7 @@ impl FormBuilder { fd, fs: form_style, validations: Vec::new(), + validation_cbs: Vec::new(), views: Vec::new(), }), } @@ -131,7 +146,7 @@ impl FormBuilder { self } - pub(crate) fn new_control( + pub(crate) fn new_control( mut self, builder: impl Fn(ControlBuilder) -> ControlBuilder, ) -> Self { @@ -152,13 +167,14 @@ impl FormBuilder { builder.views.push(view); } - fn add_control( + fn add_control( &mut self, control: ControlBuilder, ) { let BuiltControlData { render_data, - field_fn, + getter, + setter, parse_fn, unparse_fn, validation_fn, @@ -182,9 +198,10 @@ impl FormBuilder { builder.validations.push(validation_fn.clone()); } - let view = Self::build_control_view( + let (validation_cb, view) = Self::build_control_view( builder, - field_fn, + getter, + setter, unparse_fn, parse_fn, validation_fn, @@ -192,33 +209,50 @@ impl FormBuilder { ); builder.views.push(view); + builder.validation_cbs.push(validation_cb); } - fn build_control_view( + fn build_control_view( builder: &FullFormBuilder, - field_fn: Rc>, + getter: Rc>, + setter: Rc>, unparse_fn: Box::ReturnType, FDT>>, parse_fn: Box::ReturnType, FDT>>, validation_fn: Option>>, render_data: crate::controls::ControlRenderData, - ) -> View { + ) -> (Box, View) { 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 field_fn2 = field_fn.clone(); - let fd_getter = builder.fd.read_only(); + let fd = builder.fd; 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 - let mut fd = fd_getter.get(); - let field = field_fn2(&mut fd); - (unparse_fn)(field) + let field = create_memo(move |_| getter(fd.get())); + unparse_fn(&field.get()) }; 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| { - // TODO: also do this on a submit somehow let parsed = match parse_fn(value) { - Ok(p) => p, + Ok(p) => { + validation_signal_set.set(Ok(())); + p + } Err(e) => { validation_signal_set.set(Err(e)); return; @@ -226,65 +260,93 @@ impl FormBuilder { }; // parse succeeded, update value and validate - let field_fn = field_fn.clone(); // move - let validation_fn = validation_fn.clone(); // move - - // 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); - } + fd.update(|data| { + setter(data, parsed); }); + + // run validation + (validation_cb2)(); }; 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, render_data, value_getter, value_setter, validation_signal.into(), - ) + ); + (validation_cb, view) } - fn build( + fn build_action_form( self, - action_location: String, - action: Option>>, - ) -> Option> { + action: Action>>, + ) -> Option> + where + ServFn: DeserializeOwned + ServerFn + 'static, + <>::Request as ClientReq>::FormData: + From, + { let builder = match self.inner { FormBuilderInner::FullBuilder(fb) => fb, FormBuilderInner::ValidationBuilder { validations: _ } => return None, }; - let validations = builder.validations.clone(); - let validate_form = move || { - let fd = builder.fd.get(); - validations.iter().all(|v| v(&fd).is_ok()) + + 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! { + + {elements} + + }; + + Some(Form { + fd: builder.fd, + validations: builder.validations, + view, + }) + } + + fn build_plain_form(self, url: String) -> Option> { + 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 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! { -
+ {elements}
- } - .into_view(); + }; Some(Form { fd: builder.fd, @@ -306,7 +368,7 @@ impl FormBuilder { /// /// 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. -pub trait FormData: Default + Clone + 'static { +pub trait FormToolData: Default + Clone + 'static { type Style: FormStyle; /// 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 = Self::build_form(builder); builder - .build(action.to_string(), None) + .build_plain_form(action.to_string()) .expect("builder should be full builder") } - fn get_action_form( + fn get_action_form( self, - action: Action>, + action: Action>>, style: Self::Style, - ) -> Form { + ) -> Form + where + ServFn: DeserializeOwned + ServerFn + 'static, + <>::Request as ClientReq>::FormData: + From, + { let builder = FormBuilder::new_full_builder(self, style); let builder = Self::build_form(builder); builder - .build(action.url().unwrap_or(String::new()), Some(action)) + .build_action_form(action) .expect("builder should be full builder") } diff --git a/src/styles/grid_form.rs b/src/styles/grid_form.rs index f097fd2..8fd8160 100644 --- a/src/styles/grid_form.rs +++ b/src/styles/grid_form.rs @@ -51,7 +51,6 @@ impl FormStyle for GridFormStyle {