From 857dcec00f3fc9e7d4c70db6baeabfb376f5ff2d Mon Sep 17 00:00:00 2001 From: Mitchell Marino Date: Wed, 20 Mar 2024 20:33:13 -0500 Subject: [PATCH] third round of dev --- src/controls/heading.rs | 9 +- src/controls/mod.rs | 101 ++++++++-------- src/controls/select.rs | 11 +- src/controls/submit.rs | 9 +- src/controls/text_area.rs | 11 +- src/controls/text_input.rs | 11 +- src/form.rs | 234 +++++++++++++++++++++++++++++++------ src/styles/mod.rs | 29 ++--- src/styles/tw_grid.rs | 84 ++++++++++--- 9 files changed, 353 insertions(+), 146 deletions(-) diff --git a/src/controls/heading.rs b/src/controls/heading.rs index 1162186..afb9b8d 100644 --- a/src/controls/heading.rs +++ b/src/controls/heading.rs @@ -1,6 +1,6 @@ use leptos::View; -use super::{VanityControl, VanityControlBuilder, VanityControlData}; +use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; use crate::{ form::{FormBuilder, FormData}, styles::FormStyle, @@ -12,10 +12,7 @@ pub struct HeadingData { } impl VanityControlData for HeadingData { - fn build_control( - fs: &FS, - control: VanityControl, - ) -> View { + fn build_control(fs: &FS, control: ControlRenderData) -> View { fs.heading(control) } } @@ -23,7 +20,7 @@ impl VanityControlData for HeadingData { impl FormBuilder { pub fn heading( self, - builder: impl Fn(VanityControlBuilder) -> VanityControl, + builder: impl Fn(VanityControlBuilder) -> VanityControlBuilder, ) -> Self { self.new_vanity(builder) } diff --git a/src/controls/mod.rs b/src/controls/mod.rs index e748ee0..5e1b3cc 100644 --- a/src/controls/mod.rs +++ b/src/controls/mod.rs @@ -7,40 +7,50 @@ pub mod submit; pub mod text_area; pub mod text_input; -pub type ValidationFn = dyn Fn(&FD) -> Result<(), String> + 'static; -pub type ParseFn = dyn Fn(CReturnType, &mut FD) -> Result<(), String> + 'static; +pub trait ValidationFn: Fn(&FD) -> Result<(), String> + 'static {} +pub trait ParseFn: + Fn(C::ReturnType, &mut FD) -> Result<(), String> + 'static +{ +} +// implement the trait for all valid types +impl ValidationFn for T +where + FD: FormData, + T: Fn(&FD) -> Result<(), String> + 'static, +{ +} +// implement the trait for all valid types +impl ParseFn for T +where + FD: FormData, + C: ControlData, + T: Fn(C::ReturnType, &mut FD) -> Result<(), String> + 'static, +{ +} pub trait VanityControlData: 'static { - fn build_control( - fs: &FS, - control: VanityControl, - ) -> View; + fn build_control(fs: &FS, control: ControlRenderData) -> View; } pub trait ControlData: 'static { type ReturnType: Clone; // TODO: this should also return a getter for the data - fn build_control( + fn build_control( fs: &FS, - control: Control, + control: ControlRenderData, + validation_state: Signal>, ) -> (View, Signal); } -pub struct VanityControl { +pub struct ControlRenderData { pub data: Box, pub style: Vec, } -pub struct Control { - pub data: Box, - pub parse_fn: Box>, - pub validation: Box>, - pub style: Vec, -} pub struct VanityControlBuilder { - style_attributes: Vec, - data: C, + pub(crate) style_attributes: Vec, + pub(crate) data: C, } impl VanityControlBuilder { @@ -51,15 +61,8 @@ impl VanityControlBuilder { } } - pub fn build(self) -> VanityControl { - self.into() - } -} -impl Into> - for VanityControlBuilder -{ - fn into(self) -> VanityControl { - VanityControl { + pub(crate) fn build(self) -> ControlRenderData { + ControlRenderData { data: Box::new(self.data), style: self.style_attributes, } @@ -67,10 +70,10 @@ impl Into> } pub struct ControlBuilder { - parse_fn: Box>, - validation_fn: Box>, - style_attributes: Vec, - data: C, + pub(crate) parse_fn: Box>, + pub(crate) validation_fn: Box>, + pub(crate) style_attributes: Vec, + pub(crate) data: C, } impl ControlBuilder { @@ -83,15 +86,25 @@ impl ControlBuilder { } } - pub fn build(self) -> Control { - self.into() + pub(crate) fn build( + self, + ) -> ( + ControlRenderData, + impl ParseFn, + impl ValidationFn, + ) { + ( + ControlRenderData { + data: Box::new(self.data), + style: self.style_attributes, + }, + self.parse_fn, + self.validation_fn, + ) } - pub fn parse_fn( - mut self, - parse_fn: impl Fn(C::ReturnType, &mut FD) -> Result<(), String> + 'static, - ) -> Self { - self.parse_fn = Box::new(parse_fn) as Box>; + pub fn parse_fn(mut self, parse_fn: impl ParseFn) -> Self { + self.parse_fn = Box::new(parse_fn) as Box>; self } @@ -99,7 +112,7 @@ impl ControlBuilder { mut self, validation_fn: impl Fn(&FD) -> Result<(), String> + 'static, ) -> Self { - self.validation_fn = Box::new(validation_fn) as Box>; + self.validation_fn = Box::new(validation_fn) as Box>; self } @@ -108,15 +121,3 @@ impl ControlBuilder { self } } -impl Into> - for ControlBuilder -{ - fn into(self) -> Control { - Control { - data: Box::new(self.data), - style: self.style_attributes, - parse_fn: self.parse_fn, - validation: self.validation_fn, - } - } -} diff --git a/src/controls/select.rs b/src/controls/select.rs index b7331d8..1bdfea5 100644 --- a/src/controls/select.rs +++ b/src/controls/select.rs @@ -1,6 +1,6 @@ use leptos::{Signal, View}; -use super::{Control, ControlBuilder, ControlData}; +use super::{ControlBuilder, ControlData, ControlRenderData}; use crate::{ form::{FormBuilder, FormData}, styles::FormStyle, @@ -15,18 +15,19 @@ pub struct SelectData { impl ControlData for SelectData { type ReturnType = String; - fn build_control( + fn build_control( fs: &FS, - control: Control, + control: ControlRenderData, + validation_state: Signal>, ) -> (View, Signal) { - fs.select(control) + fs.select(control, validation_state) } } impl FormBuilder { pub fn select( self, - builder: impl Fn(ControlBuilder) -> Control, + builder: impl Fn(ControlBuilder) -> ControlBuilder, ) -> Self { self.new_control(builder) } diff --git a/src/controls/submit.rs b/src/controls/submit.rs index ba97d3d..61bb6f9 100644 --- a/src/controls/submit.rs +++ b/src/controls/submit.rs @@ -1,6 +1,6 @@ use leptos::View; -use super::{VanityControl, VanityControlBuilder, VanityControlData}; +use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; use crate::{ form::{FormBuilder, FormData}, styles::FormStyle, @@ -12,10 +12,7 @@ pub struct SubmitData { } impl VanityControlData for SubmitData { - fn build_control( - fs: &FS, - control: VanityControl, - ) -> View { + fn build_control(fs: &FS, control: ControlRenderData) -> View { fs.submit(control) } } @@ -23,7 +20,7 @@ impl VanityControlData for SubmitData { impl FormBuilder { pub fn submit( self, - builder: impl Fn(VanityControlBuilder) -> VanityControl, + builder: impl Fn(VanityControlBuilder) -> VanityControlBuilder, ) -> Self { self.new_vanity(builder) } diff --git a/src/controls/text_area.rs b/src/controls/text_area.rs index 51690c9..d0683fb 100644 --- a/src/controls/text_area.rs +++ b/src/controls/text_area.rs @@ -1,4 +1,4 @@ -use super::{Control, ControlBuilder, ControlData}; +use super::{ControlBuilder, ControlData, ControlRenderData}; use crate::{ form::{FormBuilder, FormData}, styles::FormStyle, @@ -14,18 +14,19 @@ pub struct TextAreaData { impl ControlData for TextAreaData { type ReturnType = String; - fn build_control( + fn build_control( fs: &FS, - control: Control, + control: ControlRenderData, + validation_state: Signal>, ) -> (View, Signal) { - fs.text_area(control) + fs.text_area(control, validation_state) } } impl FormBuilder { pub fn text_area( self, - builder: impl Fn(ControlBuilder) -> Control, + builder: impl Fn(ControlBuilder) -> ControlBuilder, ) -> Self { self.new_control(builder) } diff --git a/src/controls/text_input.rs b/src/controls/text_input.rs index 1483123..ddc07d0 100644 --- a/src/controls/text_input.rs +++ b/src/controls/text_input.rs @@ -1,6 +1,6 @@ use leptos::{Signal, View}; -use super::{Control, ControlBuilder, ControlData}; +use super::{ControlBuilder, ControlData, ControlRenderData}; use crate::{ form::{FormBuilder, FormData}, styles::FormStyle, @@ -30,18 +30,19 @@ impl Default for TextInputData { impl ControlData for TextInputData { type ReturnType = String; - fn build_control( + fn build_control( fs: &FS, - control: Control, + control: ControlRenderData, + validation_state: Signal>, ) -> (View, Signal) { - fs.text_input(control) + fs.text_input(control, validation_state) } } impl FormBuilder { pub fn text_input( self, - builder: impl Fn(ControlBuilder) -> Control, + builder: impl Fn(ControlBuilder) -> ControlBuilder, ) -> Self { self.new_control(builder) } diff --git a/src/form.rs b/src/form.rs index facf581..bc6ed5f 100644 --- a/src/form.rs +++ b/src/form.rs @@ -1,7 +1,6 @@ use crate::{ controls::{ - Control, ControlBuilder, ControlData, ValidationFn, VanityControl, VanityControlBuilder, - VanityControlData, + ControlBuilder, ControlData, ValidationFn, VanityControlBuilder, VanityControlData, }, styles::FormStyle, }; @@ -10,31 +9,135 @@ use leptos::{ WriteSignal, }; -pub struct Form {} +pub struct Validator { + validations: Vec>>, +} -pub struct FormBuilder { +impl Validator { + pub fn validate(&self, form_data: &FD) -> Result<(), String> { + for v in self.validations.iter() { + (*v)(form_data)?; + } + Ok(()) + } +} + +/// A constructed form object. +/// +/// 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>>, + view: View, +} + +impl Form { + pub fn validator(self) -> Validator { + Validator { + validations: self.validations, + } + } + + pub fn validate(&self, form_data: &FD) -> Result<(), String> { + for v in self.validations.iter() { + (*v)(form_data)?; + } + Ok(()) + } + + pub fn view(&self) -> View { + self.view.clone() + } + + pub fn to_parts(self) -> (ReadSignal, Validator, View) { + ( + self.fd, + Validator { + validations: self.validations, + }, + self.view, + ) + } +} + +impl IntoView for Form { + fn into_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, fs: FS, - validations: Vec>>, + validations: Vec>>, views: Vec, } +/// The internal type for building forms +/// +/// This allows us to build either the full form +/// with views, validation and data. Or we can just +/// build the validation functions. +/// +/// This is useful in the context of a server that +/// cannot or should not render the form. You can +/// still get all the validation functions from the +/// form data. +enum FormBuilderInner { + /// For building the form with views + FullBuilder(FullFormBuilder), + /// For building only the validations for the form + ValidationBuilder { + validations: Vec>>, + }, +} + +pub struct FormBuilder { + inner: FormBuilderInner, +} impl FormBuilder { - pub fn new(form_style: FS) -> FormBuilder { - let (fs_get, fs_set) = create_signal(FD::default()); + // 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()); FormBuilder { - fd_get, - fd_set, - fs: form_style, - validations: Vec::new(), - views: Vec::new(), + inner: FormBuilderInner::FullBuilder(FullFormBuilder { + fd_get, + fd_set, + fs: form_style, + validations: Vec::new(), + views: Vec::new(), + }), + } + } + + fn new_full_builder_with(starting_data: FD, form_style: FS) -> FormBuilder { + let (fd_get, fd_set) = create_signal(starting_data); + FormBuilder { + inner: FormBuilderInner::FullBuilder(FullFormBuilder { + fd_get, + fd_set, + fs: form_style, + validations: Vec::new(), + views: Vec::new(), + }), + } + } + + fn new_validation_builder() -> FormBuilder { + FormBuilder { + inner: FormBuilderInner::ValidationBuilder { + validations: Vec::new(), + }, } } pub(crate) fn new_vanity( mut self, - builder: impl Fn(VanityControlBuilder) -> VanityControl, + builder: impl Fn(VanityControlBuilder) -> VanityControlBuilder, ) -> Self { let vanity_builder = VanityControlBuilder::new(C::default()); let control = builder(vanity_builder); @@ -44,7 +147,7 @@ impl FormBuilder { pub(crate) fn new_control( mut self, - builder: impl Fn(ControlBuilder) -> Control, + builder: impl Fn(ControlBuilder) -> ControlBuilder, ) -> Self { let control_builder = ControlBuilder::new(C::default()); let control = builder(control_builder); @@ -52,43 +155,102 @@ impl FormBuilder { self } - fn add_vanity(&mut self, vanity_control: VanityControl) { - let view = VanityControlData::build_control::(&self.fs, vanity_control); - self.views.push(view); + fn add_vanity(&mut self, vanity_control: VanityControlBuilder) { + let full_builder = match &mut self.inner { + 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); } - fn add_control(&mut self, control: Control) { + fn add_control(&mut self, control: ControlBuilder) { + let full_builder = match &mut self.inner { + FormBuilderInner::ValidationBuilder { validations } => { + validations.push(control.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(&self.fs, control); + 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 - // TODO: we might want a way to see if this is the first time this ran, which would - // prevent the form's validation to pop up before the user typed anything in - // TODO: add validation here that run on the input changing, and writes - // it to the fd signals + let fd_setter = full_builder.fd_set; create_effect(move |last_value| { let control_value = control_value.get(); - let mut validation_result; - self.fd_set.update(|v| { - validation_result = - (control.parse_fn)(control_value, v).and_then(|_| (*control.validation)(v)); + 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 Some(validation_result) != last_value { - validation_signal_set.set(validation_result); + if last_value.is_some_and(|last_value| last_value != validation_result) { + validation_signal_set.set(validation_result.clone()); } validation_result }); - self.views.push(view); + full_builder.views.push(view); } - // TODO: this should return a Form object - // The Form should have `form_view()`, and `validate(&FD)` functions. - pub fn build(self) -> View { - self.views.into_view() + fn build(self) -> Form { + match self.inner { + FormBuilderInner::FullBuilder(full_builder) => Form { + fd: full_builder.fd_get, + validations: full_builder.validations, + view: full_builder.views.into_view(), + }, + FormBuilderInner::ValidationBuilder { validations } => Form { + fd: create_signal(FD::default()).0, + validations, + view: ().into_view(), + }, + } + } + + fn validator(self) -> Validator { + match self.inner { + FormBuilderInner::FullBuilder(full_builder) => Validator { + validations: full_builder.validations, + }, + FormBuilderInner::ValidationBuilder { validations } => Validator { validations }, + } } } +/// A trait allowing a form to be built around its containing data. +/// +/// 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 { - // TODO: this should return a Form Object - fn create_form() -> View; + type Style: FormStyle; + + /// Defines how the form should be layed out and how the data should be parsed and validated. + /// + /// Uses the given form builder to specify what fields should be present + /// in the form, what properties those fields should have, and how that + /// data should be parsed and checked. + fn build_form(fb: FormBuilder) -> FormBuilder; + + /// Gets the [`Form`] for this FormData type. + /// + /// The [`Form`] provides the way to render the form + fn get_form(style: Self::Style) -> Form { + let builder = FormBuilder::new_full_builder(style); + let builder = Self::build_form(builder); + builder.build() + } + + 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() + } + + fn get_validator() -> Validator { + let builder = FormBuilder::new_validation_builder(); + let builder = Self::build_form(builder); + builder.validator() + } } diff --git a/src/styles/mod.rs b/src/styles/mod.rs index 74280dd..f5ec688 100644 --- a/src/styles/mod.rs +++ b/src/styles/mod.rs @@ -2,12 +2,9 @@ mod tw_grid; pub use tw_grid::{TailwindGridFormStyle, TailwindGridStylingAttributes}; -use crate::{ - controls::{ - heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData, - text_input::TextInputData, Control, ControlData, VanityControl, - }, - form::FormData, +use crate::controls::{ + heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData, + text_input::TextInputData, ControlData, ControlRenderData, }; use leptos::{Signal, View}; @@ -17,20 +14,24 @@ pub trait FormStyle: 'static { // 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 heading(&self, control: VanityControl) -> View; - fn text_input( + fn heading(&self, control: ControlRenderData) -> View; + fn text_input( &self, - control: Control, + control: ControlRenderData, + validation_state: Signal>, ) -> (View, Signal<::ReturnType>); - fn select( + fn select( &self, - control: Control, + control: ControlRenderData, + validation_state: Signal>, ) -> (View, Signal<::ReturnType>); - fn submit(&self, control: VanityControl) -> View; - fn text_area( + fn submit(&self, control: ControlRenderData) -> View; + fn text_area( &self, - control: Control, + control: ControlRenderData, + validation_state: Signal>, ) -> (View, Signal<::ReturnType>); + // 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 1afd4fa..750258e 100644 --- a/src/styles/tw_grid.rs +++ b/src/styles/tw_grid.rs @@ -1,10 +1,7 @@ use super::FormStyle; -use crate::{ - controls::{ - heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData, - text_input::TextInputData, Control, VanityControl, - }, - form::FormData, +use crate::controls::{ + heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData, + text_input::TextInputData, ControlRenderData, }; use leptos::*; @@ -17,7 +14,7 @@ pub struct TailwindGridFormStyle; impl FormStyle for TailwindGridFormStyle { type StylingAttributes = TailwindGridStylingAttributes; - fn heading(&self, control: VanityControl) -> View { + fn heading(&self, control: ControlRenderData) -> View { view! {

{&control.data.title} @@ -26,14 +23,17 @@ impl FormStyle for TailwindGridFormStyle { .into_view() } - fn text_input( + fn text_input( &self, - control: Control, - ) -> (View, ReadSignal) { + control: ControlRenderData, + validation_state: Signal>, + ) -> (View, Signal) { let (read, write) = create_signal(String::new()); + leptos::logging::log!("Rendering text input"); let view = view! {
+ {move || format!("{:?}", validation_state.get())}
}.into_view(); - (view, read) + (view, read.into()) } - fn select(&self, control: Control) -> View { - view! { + fn select( + &self, + control: ControlRenderData, + validation_state: Signal>, + ) -> (View, Signal) { + let (read, write) = create_signal(String::new()); + + let options_view = control + .data + .options + .into_iter() + .map(|value| { + // let value = value; + let cloned_value = value.clone(); + view! { + + } + }) + .collect_view(); + + let view = view! { +
+ {move || format!("{:?}", validation_state.get())} - }.into_view() +
+ }.into_view(); + + (view, read.into()) } - fn submit(&self, control: VanityControl) -> View { + fn submit(&self, control: ControlRenderData) -> View { view! { (&self, control: Control) -> View { - view! { + fn text_area( + &self, + control: ControlRenderData, + validation_state: Signal>, + ) -> (View, Signal) { + let (read, write) = create_signal(String::new()); + + let view = view! { +
+ {move || format!("{:?}", validation_state.get())}