From 40dc1d8cdc9dbbdfdc79d8318056d049e151d6fa Mon Sep 17 00:00:00 2001 From: Mitchell M Date: Fri, 19 Apr 2024 15:48:53 -0500 Subject: [PATCH] fourth round of dev --- src/controls/mod.rs | 96 +++++++++++------ src/controls/select.rs | 8 +- src/controls/text_area.rs | 8 +- src/controls/text_input.rs | 8 +- src/form.rs | 205 ++++++++++++++++++++++++------------- src/styles/mod.rs | 18 ++-- src/styles/tw_grid.rs | 60 ++++++----- 7 files changed, 262 insertions(+), 141 deletions(-) diff --git a/src/controls/mod.rs b/src/controls/mod.rs index d65e289..6b383cc 100644 --- a/src/controls/mod.rs +++ b/src/controls/mod.rs @@ -1,3 +1,5 @@ +use std::{fmt::Display, rc::Rc}; + use crate::{form::FormData, styles::FormStyle}; use leptos::{Signal, View}; @@ -7,15 +9,17 @@ 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 ParseFn: Fn(&CT) -> Result + 'static {} -pub trait FieldFn: Fn(&mut FD) -> FDT + 'static {} -// implement the trait for all valid types +pub trait ParseFn: Fn(&CR) -> Result + 'static {} +pub trait UnparseFn: Fn(&FDT) -> CR + 'static {} +pub trait FieldFn: Fn(&mut FD) -> &mut FDT + 'static {} + +// implement the traits for all valid types impl ValidationFn for T where T: Fn(&FDT) -> Result<(), String> + 'static {} -// implement the trait for all valid types impl ParseFn for F where F: Fn(&CR) -> Result + 'static {} -// implement the trait for all valid types -impl FieldFn for F where F: Fn(&mut FD) -> FDT + 'static {} +impl UnparseFn for F where F: Fn(&FDT) -> CR + 'static {} +impl FieldFn for F where F: Fn(&mut FD) -> &mut FDT + 'static {} pub trait VanityControlData: 'static { fn build_control(fs: &FS, control: ControlRenderData) -> View; @@ -34,8 +38,10 @@ pub trait ControlData: 'static { fn build_control( fs: &FS, control: ControlRenderData, + value_getter: Signal, + value_setter: Box, validation_state: Signal>, - ) -> (View, Signal); + ) -> View; } pub struct ControlRenderData { @@ -64,11 +70,33 @@ impl VanityControlBuilder { } } +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] +pub enum ControlBuildError { + /// The field that this control belongs to is not specified. + MissingField, +} +impl Display for ControlBuildError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let message = match self { + ControlBuildError::MissingField => "you must specify what field this control is for", + }; + write!(f, "{}", message) + } +} + +pub(crate) struct BuiltControlData { + pub(crate) render_data: ControlRenderData, + pub(crate) field_fn: 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(crate) field_fn: Option>>, pub(crate) parse_fn: Option>>, - pub(crate) unparse_fn: Option>>, - pub(crate) validation_fn: Option>>, + pub(crate) unparse_fn: Option>>, + pub(crate) validation_fn: Option>>, pub(crate) style_attributes: Vec, pub(crate) data: C, } @@ -85,21 +113,24 @@ impl ControlBuilder ( - ControlRenderData, - impl ParseFn, - impl ValidationFn, - ) { - ( - ControlRenderData { + pub(crate) fn build(self) -> 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), + }; + + Ok(BuiltControlData { + render_data: ControlRenderData { data: Box::new(self.data), style: self.style_attributes, }, - self.parse_fn, - self.validation_fn, - ) + field_fn, + parse_fn, + unparse_fn, + validation_fn: self.validation_fn, + }) } // TODO: add method that automatically does the parse and unparse using @@ -108,24 +139,31 @@ impl ControlBuilder, parse_fn: impl ParseFn, - unparse_fn: impl ParseFn, + unparse_fn: impl UnparseFn, ) -> Self { - self.field_fn = Box::new(field_fn); - self.parse_fn = Box::new(parse_fn); - self.unparse_fn = Box::new(unparse_fn); + self.field_fn = Some(Rc::new(field_fn)); + self.parse_fn = Some(Box::new(parse_fn)); + self.unparse_fn = Some(Box::new(unparse_fn)); self } - pub fn parse_fn(mut self, parse_fn: impl ParseFn) -> Self { - self.parse_fn = Box::new(parse_fn) as Box>; + /// Overrides the field's parse functions with the ones given. + pub fn parse_fns( + mut self, + parse_fn: impl ParseFn, + unparse_fn: impl UnparseFn, + ) -> Self { + self.parse_fn = Some(Box::new(parse_fn)); + self.unparse_fn = Some(Box::new(unparse_fn)); self } + /// Sets the validation function for this control pub fn validation_fn( mut self, validation_fn: impl Fn(&FD) -> Result<(), String> + 'static, ) -> Self { - self.validation_fn = Some(Box::new(validation_fn)) as _; + self.validation_fn = Some(Rc::new(validation_fn)); self } diff --git a/src/controls/select.rs b/src/controls/select.rs index eb05313..b1b51b5 100644 --- a/src/controls/select.rs +++ b/src/controls/select.rs @@ -18,14 +18,16 @@ impl ControlData for SelectData { fn build_control( fs: &FS, control: ControlRenderData, + value_getter: Signal, + value_setter: Box, validation_state: Signal>, - ) -> (View, Signal) { - fs.select(control, validation_state) + ) -> View { + fs.select(control, value_getter, value_setter, validation_state) } } impl FormBuilder { - pub fn select( + pub fn select( self, builder: impl Fn( ControlBuilder, diff --git a/src/controls/text_area.rs b/src/controls/text_area.rs index fc71195..674dd38 100644 --- a/src/controls/text_area.rs +++ b/src/controls/text_area.rs @@ -17,14 +17,16 @@ impl ControlData for TextAreaData { fn build_control( fs: &FS, control: ControlRenderData, + value_getter: Signal, + value_setter: Box, validation_state: Signal>, - ) -> (View, Signal) { - fs.text_area(control, validation_state) + ) -> View { + fs.text_area(control, value_getter, value_setter, validation_state) } } impl FormBuilder { - pub fn text_area( + pub fn text_area( self, builder: impl Fn( ControlBuilder, diff --git a/src/controls/text_input.rs b/src/controls/text_input.rs index 458f9e4..b8a87ae 100644 --- a/src/controls/text_input.rs +++ b/src/controls/text_input.rs @@ -33,14 +33,16 @@ impl ControlData for TextInputData { fn build_control( fs: &FS, control: ControlRenderData, + value_getter: Signal, + value_setter: Box, validation_state: Signal>, - ) -> (View, Signal) { - fs.text_input(control, validation_state) + ) -> View { + fs.text_input(control, value_getter, value_setter, validation_state) } } impl FormBuilder { - pub fn text_input( + pub fn text_input( self, builder: impl Fn( ControlBuilder, diff --git a/src/form.rs b/src/form.rs index 4c3d7a8..ad619d1 100644 --- a/src/form.rs +++ b/src/form.rs @@ -1,16 +1,19 @@ +use std::rc::Rc; + use crate::{ controls::{ - ControlBuilder, ControlData, ValidationFn, VanityControlBuilder, VanityControlData, + BuiltControlData, ControlBuilder, ControlData, FieldFn, ParseFn, UnparseFn, ValidationFn, + VanityControlBuilder, VanityControlData, }, styles::FormStyle, }; use leptos::{ - create_effect, create_signal, IntoView, ReadSignal, SignalGet, SignalSet, SignalUpdate, View, - WriteSignal, + create_rw_signal, create_signal, IntoSignal, IntoView, RwSignal, SignalGet, SignalSet, + SignalUpdate, View, }; pub struct Validator { - validations: Vec>>, + validations: Vec>>, } impl Validator { @@ -27,8 +30,8 @@ impl Validator { /// With this, you can render the form, get the form data, or get /// a validator for the data. pub struct Form { - pub fd: ReadSignal, - validations: Vec>>, + pub fd: RwSignal, + validations: Vec>>, view: View, } @@ -50,7 +53,7 @@ impl Form { self.view.clone() } - pub fn to_parts(self) -> (ReadSignal, Validator, View) { + pub fn to_parts(self) -> (RwSignal, Validator, View) { ( self.fd, Validator { @@ -63,17 +66,16 @@ impl Form { impl IntoView for Form { fn into_view(self) -> View { - self.view + self.view() } } /// A version of the [`FormBuilder`] that contains all the data /// needed for full building of a [`Form`]. struct FullFormBuilder { - fd_get: ReadSignal, - fd_set: WriteSignal, + fd: RwSignal, fs: FS, - validations: Vec>>, + validations: Vec>>, views: Vec, } /// The internal type for building forms @@ -91,7 +93,7 @@ enum FormBuilderInner { FullBuilder(FullFormBuilder), /// For building only the validations for the form ValidationBuilder { - validations: Vec>>, + validations: Vec>>, }, } @@ -102,11 +104,10 @@ pub struct FormBuilder { impl FormBuilder { // TODO: remove the Default trait bound and bind it to this function only fn new_full_builder(form_style: FS) -> FormBuilder { - let (fd_get, fd_set) = create_signal(FD::default()); + let fd = create_rw_signal(FD::default()); FormBuilder { inner: FormBuilderInner::FullBuilder(FullFormBuilder { - fd_get, - fd_set, + fd, fs: form_style, validations: Vec::new(), views: Vec::new(), @@ -115,11 +116,10 @@ impl FormBuilder { } fn new_full_builder_with(starting_data: FD, form_style: FS) -> FormBuilder { - let (fd_get, fd_set) = create_signal(starting_data); + let fd = create_rw_signal(starting_data); FormBuilder { inner: FormBuilderInner::FullBuilder(FullFormBuilder { - fd_get, - fd_set, + fd, fs: form_style, validations: Vec::new(), views: Vec::new(), @@ -145,7 +145,7 @@ impl FormBuilder { self } - pub(crate) fn new_control( + pub(crate) fn new_control( mut self, builder: impl Fn(ControlBuilder) -> ControlBuilder, ) -> Self { @@ -156,67 +156,134 @@ impl FormBuilder { } fn add_vanity(&mut self, vanity_control: VanityControlBuilder) { - let full_builder = match &mut self.inner { + let builder = match &mut self.inner { + FormBuilderInner::FullBuilder(fb) => fb, FormBuilderInner::ValidationBuilder { validations: _ } => return, - FormBuilderInner::FullBuilder(full_builder) => full_builder, }; + let render_data = vanity_control.build(); - let view = VanityControlData::build_control(&full_builder.fs, render_data); - full_builder.views.push(view); + let view = VanityControlData::build_control(&builder.fs, render_data); + builder.views.push(view); } - fn add_control(&mut self, control: ControlBuilder) { - let full_builder = match &mut self.inner { - FormBuilderInner::ValidationBuilder { validations } => { - validations.push(control.validation_fn); + fn add_control( + &mut self, + control: ControlBuilder, + ) { + let BuiltControlData { + render_data, + field_fn, + parse_fn, + unparse_fn, + validation_fn, + } = match control.build() { + Ok(c) => c, + Err(e) => panic!("Invalid Component: {}", e), + }; + + let builder = match &mut self.inner { + FormBuilderInner::FullBuilder(fb) => fb, + FormBuilderInner::ValidationBuilder { + ref mut validations, + } => { + if let Some(validation_fn) = validation_fn { + validations.push(validation_fn); + } return; } - FormBuilderInner::FullBuilder(full_builder) => full_builder, }; - let (render_data, parse_fn, validation_fn) = control.build(); - let (validation_signal, validation_signal_set) = create_signal(Ok(())); - let (view, control_value) = - ControlData::build_control(&full_builder.fs, render_data, validation_signal.into()); - // TODO: add a signal that triggers on submit to refresh the validation on_submit - let fd_setter = full_builder.fd_set; - create_effect(move |last_value| { - let control_value = control_value.get(); - let mut validation_result = Ok(()); - fd_setter.update(|v| { - validation_result = (parse_fn)(control_value, v).and_then(|_| (validation_fn)(v)); - }); - // TODO: or this happened on a submit - if last_value.is_some_and(|last_value| last_value != validation_result) { - validation_signal_set.set(validation_result.clone()); - } - validation_result - }); - full_builder.views.push(view); + if let Some(ref validation_fn) = validation_fn { + builder.validations.push(validation_fn.clone()); + } + + let view = Self::build_control_view( + builder, + field_fn, + unparse_fn, + parse_fn, + validation_fn, + render_data, + ); + + builder.views.push(view); } - fn build(self) -> Form { - match self.inner { - FormBuilderInner::FullBuilder(full_builder) => Form { - fd: full_builder.fd_get, - validations: full_builder.validations, - // TODO: wrap in the style's form wrapper - view: full_builder.views.into_view(), - }, - FormBuilderInner::ValidationBuilder { validations } => Form { - fd: create_signal(FD::default()).0, - validations, - view: ().into_view(), - }, - } + fn build_control_view( + builder: &FullFormBuilder, + field_fn: Rc>, + unparse_fn: Box::ReturnType, FDT>>, + parse_fn: Box::ReturnType, FDT>>, + validation_fn: Option>>, + render_data: crate::controls::ControlRenderData, + ) -> 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 value_getter = move || { + // 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 value_getter = value_getter.into_signal(); + + let fd = builder.fd.clone(); + let value_setter = move |value| { + // TODO: also do this on a submit somehow + let parsed = match parse_fn(&value) { + Ok(p) => p, + Err(e) => { + validation_signal_set.set(Err(e)); + return; + } + }; + + // parse succeeded, update value and validate + let field_fn = field_fn.clone(); // move + let validation_fn = validation_fn.clone(); // move + fd.update(move |fd| { + let field = field_fn(fd); + *field = parsed; + + validation_fn + .as_ref() + .and_then(|vfn| vfn(fd).err()) + .map(|e| validation_signal_set.set(Err(e))); + }); + }; + let value_setter = Box::new(value_setter); + // TODO: add a signal that triggers on submit to refresh the validation on_submit + + C::build_control( + &builder.fs, + render_data, + value_getter, + value_setter, + validation_signal.into(), + ) + } + + fn build(self) -> Option> { + let builder = match self.inner { + FormBuilderInner::FullBuilder(fb) => fb, + FormBuilderInner::ValidationBuilder { validations: _ } => return None, + }; + let view = builder.fs.form_frame(builder.views.into_view()); + + Some(Form { + fd: builder.fd, + validations: builder.validations, + view, + }) } fn validator(self) -> Validator { - match self.inner { - FormBuilderInner::FullBuilder(full_builder) => Validator { - validations: full_builder.validations, - }, - FormBuilderInner::ValidationBuilder { validations } => Validator { validations }, - } + let validations = match self.inner { + FormBuilderInner::FullBuilder(fb) => fb.validations, + FormBuilderInner::ValidationBuilder { validations } => validations, + }; + Validator { validations } } } @@ -240,13 +307,13 @@ pub trait FormData: Default + Clone + 'static { fn get_form(style: Self::Style) -> Form { let builder = FormBuilder::new_full_builder(style); let builder = Self::build_form(builder); - builder.build() + builder.build().expect("builder should be full builder") } fn get_form_with_starting_data(self, style: Self::Style) -> Form { let builder = FormBuilder::new_full_builder_with(self, style); let builder = Self::build_form(builder); - builder.build() + builder.build().expect("builder should be full builder") } fn get_validator() -> Validator { diff --git a/src/styles/mod.rs b/src/styles/mod.rs index f5ec688..dda4d0e 100644 --- a/src/styles/mod.rs +++ b/src/styles/mod.rs @@ -8,29 +8,33 @@ use crate::controls::{ }; use leptos::{Signal, View}; -pub trait FormStyle: 'static { +pub trait FormStyle: Default + 'static { type StylingAttributes; - // TODO: add form frame - // TODO: perhaps we don't want to send the full control type anymore. - // as the rendering shouldn't depend on parse or validate anymore. + fn form_frame(&self, children: View) -> View; fn heading(&self, control: ControlRenderData) -> View; fn text_input( &self, control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, validation_state: Signal>, - ) -> (View, Signal<::ReturnType>); + ) -> View; fn select( &self, control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, validation_state: Signal>, - ) -> (View, Signal<::ReturnType>); + ) -> View; fn submit(&self, control: ControlRenderData) -> View; fn text_area( &self, control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, validation_state: Signal>, - ) -> (View, Signal<::ReturnType>); + ) -> View; // TODO: test custom component fn custom_component(&self, view: View) -> View; // TODO: add group diff --git a/src/styles/tw_grid.rs b/src/styles/tw_grid.rs index 750258e..eee1cea 100644 --- a/src/styles/tw_grid.rs +++ b/src/styles/tw_grid.rs @@ -1,7 +1,7 @@ use super::FormStyle; use crate::controls::{ heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData, - text_input::TextInputData, ControlRenderData, + text_input::TextInputData, ControlData, ControlRenderData, }; use leptos::*; @@ -9,11 +9,22 @@ pub enum TailwindGridStylingAttributes { Width(u32), } +#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] pub struct TailwindGridFormStyle; impl FormStyle for TailwindGridFormStyle { type StylingAttributes = TailwindGridStylingAttributes; + // TODO: something about an on-submit thing + fn form_frame(&self, children: View) -> View { + view! { +
+ {children} +
+ } + .into_view() + } + fn heading(&self, control: ControlRenderData) -> View { view! {

@@ -26,12 +37,11 @@ impl FormStyle for TailwindGridFormStyle { fn text_input( &self, control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, validation_state: Signal>, - ) -> (View, Signal) { - let (read, write) = create_signal(String::new()); - - leptos::logging::log!("Rendering text input"); - let view = view! { + ) -> View { + view! {
{move || format!("{:?}", validation_state.get())}
- }.into_view(); - (view, read.into()) + }.into_view() } fn select( &self, control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, validation_state: Signal>, - ) -> (View, Signal) { - let (read, write) = create_signal(String::new()); - + ) -> View { let options_view = control .data .options @@ -74,7 +83,7 @@ impl FormStyle for TailwindGridFormStyle { view! { @@ -82,7 +91,7 @@ impl FormStyle for TailwindGridFormStyle { }) .collect_view(); - let view = view! { + view! {
{move || format!("{:?}", validation_state.get())}
- }.into_view(); - - (view, read.into()) + }.into_view() } fn submit(&self, control: ControlRenderData) -> View { @@ -115,11 +122,11 @@ impl FormStyle for TailwindGridFormStyle { fn text_area( &self, control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, validation_state: Signal>, - ) -> (View, Signal) { - let (read, write) = create_signal(String::new()); - - let view = view! { + ) -> View { + view! {
{move || format!("{:?}", validation_state.get())}