diff --git a/src/controls/heading.rs b/src/controls/heading.rs index 4bcac4a..48c9bb4 100644 --- a/src/controls/heading.rs +++ b/src/controls/heading.rs @@ -1,10 +1,7 @@ use leptos::View; use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; -use crate::{ - form::{FormBuilder, FormToolData}, - styles::FormStyle, -}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct HeadingData { diff --git a/src/controls/select.rs b/src/controls/select.rs index 04f3c68..c7f7c19 100644 --- a/src/controls/select.rs +++ b/src/controls/select.rs @@ -1,10 +1,7 @@ use leptos::{Signal, View}; use super::{ControlBuilder, ControlData, ControlRenderData}; -use crate::{ - form::{FormBuilder, FormToolData}, - styles::FormStyle, -}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct SelectData { diff --git a/src/controls/submit.rs b/src/controls/submit.rs index db5f74e..c7cbf43 100644 --- a/src/controls/submit.rs +++ b/src/controls/submit.rs @@ -1,10 +1,7 @@ use leptos::View; use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; -use crate::{ - form::{FormBuilder, FormToolData}, - styles::FormStyle, -}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct SubmitData { diff --git a/src/controls/text_area.rs b/src/controls/text_area.rs index 5d4784f..dfff29d 100644 --- a/src/controls/text_area.rs +++ b/src/controls/text_area.rs @@ -1,8 +1,5 @@ use super::{ControlBuilder, ControlData, ControlRenderData}; -use crate::{ - form::{FormBuilder, FormToolData}, - styles::FormStyle, -}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use leptos::{Signal, View}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] diff --git a/src/controls/text_input.rs b/src/controls/text_input.rs index 06d702e..1ac0b77 100644 --- a/src/controls/text_input.rs +++ b/src/controls/text_input.rs @@ -1,10 +1,7 @@ use leptos::{Signal, View}; use super::{ControlBuilder, ControlData, ControlRenderData}; -use crate::{ - form::{FormBuilder, FormToolData}, - styles::FormStyle, -}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct TextInputData { diff --git a/src/form.rs b/src/form.rs index cb3fbcf..59cdf57 100644 --- a/src/form.rs +++ b/src/form.rs @@ -1,25 +1,18 @@ -use crate::{ - controls::{ - BuiltControlData, ControlBuilder, ControlData, FieldGetter, FieldSetter, ParseFn, RenderFn, - UnparseFn, ValidationCb, ValidationFn, VanityControlBuilder, VanityControlData, - }, - styles::FormStyle, -}; +use crate::{controls::ValidationFn, form_builder::FormBuilder, styles::FormStyle}; 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}; +use web_sys::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 { - validations: Vec>>, + pub(crate) validations: Vec>>, } impl Validator { @@ -41,8 +34,8 @@ impl Validator { /// a validator for the data. pub struct Form { pub fd: RwSignal, - validations: Vec>>, - view: View, + pub(crate) validations: Vec>>, + pub(crate) view: View, } impl Form { @@ -81,250 +74,6 @@ impl IntoView for Form { } } -/// A builder for laying out forms. -/// -/// This builder allows you to specify what components should make up the form. -pub struct FormBuilder { - /// The [`ToolFormData`] signal. - fd: RwSignal, - /// The [`FormStyle`]. - fs: FS, - /// The list of [`ValidationFn`]s. - validations: Vec>>, - /// The list of functions that will render the form. - render_fns: Vec>>, -} - -impl FormBuilder { - /// Creates a new [`FormBuilder`] - fn new(starting_data: FD, form_style: FS) -> FormBuilder { - let fd = create_rw_signal(starting_data); - FormBuilder { - fd, - fs: form_style, - validations: Vec::new(), - render_fns: Vec::new(), - } - } - - pub(crate) fn new_vanity( - mut self, - builder: impl Fn(VanityControlBuilder) -> VanityControlBuilder, - ) -> Self { - let vanity_builder = VanityControlBuilder::new(C::default()); - let control = builder(vanity_builder); - self.add_vanity(control); - self - } - - pub(crate) fn new_control( - mut self, - builder: impl Fn(ControlBuilder) -> ControlBuilder, - ) -> Self { - let control_builder = ControlBuilder::new(C::default()); - let control = builder(control_builder); - self.add_control(control); - self - } - - fn add_vanity(&mut self, vanity_control: VanityControlBuilder) { - let render_data = vanity_control.build(); - - let render_fn = move |fs: &FS, _| { - let view = VanityControlData::build_control(fs, render_data); - (view, None) - }; - - self.render_fns.push(Box::new(render_fn)); - } - - fn add_control( - &mut self, - control: ControlBuilder, - ) { - let BuiltControlData { - render_data, - getter, - setter, - parse_fn, - unparse_fn, - validation_fn, - } = match control.build() { - Ok(c) => c, - Err(e) => panic!("Invalid Component: {}", e), - }; - - if let Some(ref validation_fn) = validation_fn { - self.validations.push(validation_fn.clone()); - } - - let render_fn = move |fs: &FS, fd: RwSignal| { - let (view, cb) = Self::build_control_view( - fd, - fs, - getter, - setter, - unparse_fn, - parse_fn, - validation_fn, - render_data, - ); - (view, Some(cb)) - }; - - self.render_fns.push(Box::new(render_fn)); - } - - fn build_control_view( - fd: RwSignal, - fs: &FS, - getter: Rc>, - setter: Rc>, - unparse_fn: Box::ReturnType, FDT>>, - parse_fn: Box::ReturnType, FDT>>, - validation_fn: Option>>, - render_data: crate::controls::ControlRenderData, - ) -> (View, Box) { - let (validation_signal, validation_signal_set) = create_signal(Ok(())); - let value_getter = move || { - let getter = getter.clone(); - // memoize so that updating one field doesn't cause a re-render for all fields - let field = create_memo(move |_| getter(fd.get())); - unparse_fn(field.get()) - }; - let value_getter = value_getter.into_signal(); - - 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 parsed = match parse_fn(value) { - Ok(p) => { - validation_signal_set.set(Ok(())); - p - } - Err(e) => { - validation_signal_set.set(Err(e)); - return; - } - }; - - // parse succeeded, update value and validate - fd.update(|data| { - setter(data, parsed); - }); - - // run validation - (validation_cb2)(); - }; - let value_setter = Box::new(value_setter); - - let view = C::build_control( - fs, - render_data, - value_getter, - value_setter, - validation_signal.into(), - ); - (view, validation_cb) - } - - fn build_action_form( - self, - action: Action>>, - ) -> Form - where - ServFn: DeserializeOwned + ServerFn + 'static, - <>::Request as ClientReq>::FormData: - From, - { - let (views, validation_cbs): (Vec<_>, Vec<_>) = self - .render_fns - .into_iter() - .map(|r_fn| r_fn(&self.fs, self.fd)) - .unzip(); - - let elements = self.fs.form_frame(views.into_view()); - - let on_submit = move |ev: SubmitEvent| { - let mut failed = false; - for validation in validation_cbs.iter().flatten() { - if !validation() { - failed = true; - } - } - if failed { - ev.prevent_default(); - } - }; - - let view = view! { - - {elements} - - }; - - Form { - fd: self.fd, - validations: self.validations, - view, - } - } - - fn build_plain_form(self, url: String) -> Form { - let (views, validation_cbs): (Vec<_>, Vec<_>) = self - .render_fns - .into_iter() - .map(|r_fn| r_fn(&self.fs, self.fd)) - .unzip(); - - let elements = self.fs.form_frame(views.into_view()); - - let on_submit = move |ev: SubmitEvent| { - let mut failed = false; - for validation in validation_cbs.iter().flatten() { - if !validation() { - failed = true; - } - } - if failed { - ev.prevent_default(); - } - }; - - let view = view! { -
- {elements} -
- }; - - Form { - fd: self.fd, - validations: self.validations, - view, - } - } - - fn validator(&self) -> Validator { - Validator { - validations: self.validations.clone(), - } - } -} - /// 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 diff --git a/src/form_builder.rs b/src/form_builder.rs new file mode 100644 index 0000000..969a10e --- /dev/null +++ b/src/form_builder.rs @@ -0,0 +1,260 @@ +use crate::{ + controls::{ + BuiltControlData, ControlBuilder, ControlData, FieldGetter, FieldSetter, ParseFn, RenderFn, + UnparseFn, ValidationCb, ValidationFn, VanityControlBuilder, VanityControlData, + }, + form::{Form, FormToolData, Validator}, + styles::FormStyle, +}; +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}; + +/// A builder for laying out forms. +/// +/// This builder allows you to specify what components should make up the form. +pub struct FormBuilder { + /// The [`ToolFormData`] signal. + fd: RwSignal, + /// The [`FormStyle`]. + fs: FS, + /// The list of [`ValidationFn`]s. + validations: Vec>>, + /// The list of functions that will render the form. + render_fns: Vec>>, +} + +impl FormBuilder { + /// Creates a new [`FormBuilder`] + pub(crate) fn new(starting_data: FD, form_style: FS) -> FormBuilder { + let fd = create_rw_signal(starting_data); + FormBuilder { + fd, + fs: form_style, + validations: Vec::new(), + render_fns: Vec::new(), + } + } + + pub(crate) fn new_vanity( + mut self, + builder: impl Fn(VanityControlBuilder) -> VanityControlBuilder, + ) -> Self { + let vanity_builder = VanityControlBuilder::new(C::default()); + let control = builder(vanity_builder); + self.add_vanity(control); + self + } + + pub(crate) fn new_control( + mut self, + builder: impl Fn(ControlBuilder) -> ControlBuilder, + ) -> Self { + let control_builder = ControlBuilder::new(C::default()); + let control = builder(control_builder); + self.add_control(control); + self + } + + fn add_vanity(&mut self, vanity_control: VanityControlBuilder) { + let render_data = vanity_control.build(); + + let render_fn = move |fs: &FS, _| { + let view = VanityControlData::build_control(fs, render_data); + (view, None) + }; + + self.render_fns.push(Box::new(render_fn)); + } + + fn add_control( + &mut self, + control: ControlBuilder, + ) { + let BuiltControlData { + render_data, + getter, + setter, + parse_fn, + unparse_fn, + validation_fn, + } = match control.build() { + Ok(c) => c, + Err(e) => panic!("Invalid Component: {}", e), + }; + + if let Some(ref validation_fn) = validation_fn { + self.validations.push(validation_fn.clone()); + } + + let render_fn = move |fs: &FS, fd: RwSignal| { + let (view, cb) = Self::build_control_view( + fd, + fs, + getter, + setter, + unparse_fn, + parse_fn, + validation_fn, + render_data, + ); + (view, Some(cb)) + }; + + self.render_fns.push(Box::new(render_fn)); + } + + fn build_control_view( + fd: RwSignal, + fs: &FS, + getter: Rc>, + setter: Rc>, + unparse_fn: Box::ReturnType, FDT>>, + parse_fn: Box::ReturnType, FDT>>, + validation_fn: Option>>, + render_data: crate::controls::ControlRenderData, + ) -> (View, Box) { + let (validation_signal, validation_signal_set) = create_signal(Ok(())); + let value_getter = move || { + let getter = getter.clone(); + // memoize so that updating one field doesn't cause a re-render for all fields + let field = create_memo(move |_| getter(fd.get())); + unparse_fn(field.get()) + }; + let value_getter = value_getter.into_signal(); + + 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 parsed = match parse_fn(value) { + Ok(p) => { + validation_signal_set.set(Ok(())); + p + } + Err(e) => { + validation_signal_set.set(Err(e)); + return; + } + }; + + // parse succeeded, update value and validate + fd.update(|data| { + setter(data, parsed); + }); + + // run validation + (validation_cb2)(); + }; + let value_setter = Box::new(value_setter); + + let view = C::build_control( + fs, + render_data, + value_getter, + value_setter, + validation_signal.into(), + ); + (view, validation_cb) + } + + pub(crate) fn build_action_form( + self, + action: Action>>, + ) -> Form + where + ServFn: DeserializeOwned + ServerFn + 'static, + <>::Request as ClientReq>::FormData: + From, + { + let (views, validation_cbs): (Vec<_>, Vec<_>) = self + .render_fns + .into_iter() + .map(|r_fn| r_fn(&self.fs, self.fd)) + .unzip(); + + let elements = self.fs.form_frame(views.into_view()); + + let on_submit = move |ev: SubmitEvent| { + let mut failed = false; + for validation in validation_cbs.iter().flatten() { + if !validation() { + failed = true; + } + } + if failed { + ev.prevent_default(); + } + }; + + let view = view! { + + {elements} + + }; + + Form { + fd: self.fd, + validations: self.validations, + view, + } + } + + pub(crate) fn build_plain_form(self, url: String) -> Form { + let (views, validation_cbs): (Vec<_>, Vec<_>) = self + .render_fns + .into_iter() + .map(|r_fn| r_fn(&self.fs, self.fd)) + .unzip(); + + let elements = self.fs.form_frame(views.into_view()); + + let on_submit = move |ev: SubmitEvent| { + let mut failed = false; + for validation in validation_cbs.iter().flatten() { + if !validation() { + failed = true; + } + } + if failed { + ev.prevent_default(); + } + }; + + let view = view! { +
+ {elements} +
+ }; + + Form { + fd: self.fd, + validations: self.validations, + view, + } + } + + pub(crate) fn validator(&self) -> Validator { + Validator { + validations: self.validations.clone(), + } + } +} diff --git a/src/lib.rs b/src/lib.rs index 7e42530..8a9c08d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,7 @@ pub mod controls; pub mod form; +pub mod form_builder; pub mod styles; + +pub use form::{Form, FormToolData, Validator}; +pub use form_builder::FormBuilder;