From 31b24a23a1c2a6c9ac9a7ee35d1d9bbcbb5b9265 Mon Sep 17 00:00:00 2001 From: Mitchell M Date: Fri, 7 Jun 2024 14:21:14 -0500 Subject: [PATCH] impl groups --- grid_form.scss | 148 +++++++++++++++++++++------------------- src/controls/group.rs | 60 ++++++++-------- src/form_builder.rs | 74 ++++++++++++++------ src/styles/grid_form.rs | 18 +++-- src/styles/mod.rs | 16 ++--- 5 files changed, 182 insertions(+), 134 deletions(-) diff --git a/grid_form.scss b/grid_form.scss index 7877d6f..7af6f06 100644 --- a/grid_form.scss +++ b/grid_form.scss @@ -1,113 +1,121 @@ // form component as a grid .form_grid { - display: grid; - grid-template-columns: repeat(6, minmax(0, 1fr)); - row-gap: 1.5rem; - column-gap: 1rem; + display: grid; + grid-template-columns: repeat(6, minmax(0, 1fr)); + row-gap: 1.5rem; + column-gap: 1rem; } // size up to 12 columns on small or bigger devices @media (min-width: 640px) { - .form_grid { - grid-template-columns: repeat(12, minmax(0, 1fr)); - } + .form_grid { + grid-template-columns: repeat(12, minmax(0, 1fr)); + } } .form_heading { - font-size: 1.875rem; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - text-align: center; - font-weight: bold; - color: #374151; - border-bottom: 2px solid #374151; - margin-bottom: 2rem; - grid-column: span 12; + font-size: 1.875rem; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + text-align: center; + font-weight: bold; + color: #374151; + border-bottom: 2px solid #374151; + margin-bottom: 2rem; + grid-column: span 12; + display: grid; +} + +.form_group { + background-color: #0295f744; + border-radius: 25px; + padding: 20px; + grid-column: 1 / -1; } .form_label { - font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji; - text-transform: uppercase; - letter-spacing: 0.05em; - text-align: left; - color: #4a5568; - font-size: 1rem; - font-weight: bold; - margin-left: 0.5rem; - margin-bottom: 0.25rem; - display: inline; + font-family: ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Noto Sans,Ubuntu,Cantarell,Helvetica Neue,sans-serif,Apple Color Emoji,Segoe UI Emoji,Segoe UI Symbol,Noto Color Emoji; + text-transform: uppercase; + letter-spacing: 0.05em; + text-align: left; + color: #4a5568; + font-size: 1rem; + font-weight: bold; + margin-left: 0.5rem; + margin-bottom: 0.25rem; + display: inline; } .form_input { - display: block; - box-sizing: border-box; - width: 100%; - background-color: #f7fafc; - border-width: 2px; - border-color: #e2e8f0; - border-style: solid; - color: #4a5568; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; - border-radius: 0.375rem; - line-height: 1.25; - outline: none; + display: block; + box-sizing: border-box; + width: 100%; + background-color: #f7fafc; + border-width: 2px; + border-color: #e2e8f0; + border-style: solid; + color: #4a5568; + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + border-radius: 0.375rem; + line-height: 1.25; + outline: none; } .form_input:focus { - background-color: #ffffff; - border-color: #90cdf4; + background-color: #ffffff; + border-color: #90cdf4; } .form_input_invalid { - border: 2px solid #ef4444; - background-color: #ffd4d4; + border: 2px solid #ef4444; + background-color: #ffd4d4; } .form_error { - display: inline; - padding-left: 0.25rem; - color: #ef4444; + display: inline; + padding-left: 0.25rem; + color: #ef4444; } .form_button { - display: block; - outline: none; - border: none; - /* width: 100%; */ - border-radius: 1rem; - background-color: #0477d6; - color: #fff; - font-weight: bold; - cursor: pointer; - margin: 0 auto; - padding: 0.75rem 1.25rem; - font-size: 1rem; - appearance: none; - min-height: 40px; + display: block; + outline: none; + border: none; + /* width: 100%; */ + border-radius: 1rem; + background-color: #0477d6; + color: #fff; + font-weight: bold; + cursor: pointer; + margin: 0 auto; + padding: 0.75rem 1.25rem; + font-size: 1rem; + appearance: none; + min-height: 40px; } .form_submit:hover { - background-color: #005fb3; + background-color: #005fb3; } // column widths .col-span-full { - grid-column: 1 / -1; + grid-column: 1 / -1; } .col-span-12 { - grid-column: span 12 / span 12; + grid-column: span 12 / span 12; } .col-span-6 { - grid-column: span 6 / span 6; + grid-column: span 6 / span 6; } .col-span-4 { - grid-column: span 4 / span 4; + grid-column: span 4 / span 4; } .col-span-3 { - grid-column: span 3 / span 3; + grid-column: span 3 / span 3; } .col-span-2 { - grid-column: span 2 / span 2; + grid-column: span 2 / span 2; } .col-span-1 { - grid-column: span 1 / span 1; + grid-column: span 1 / span 1; } diff --git a/src/controls/group.rs b/src/controls/group.rs index c4db082..ef7a886 100644 --- a/src/controls/group.rs +++ b/src/controls/group.rs @@ -1,36 +1,38 @@ -use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; +use super::ValidationCb; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; -use leptos::{Signal, View}; - -#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] -pub struct GroupData { - pub(crate) title: Option, -} - -impl VanityControlData for GroupData { - fn build_control( - fs: &FS, - control: ControlRenderData, - _value_getter: Option>, - ) -> View { - fs.group(control) - } -} +use leptos::{CollectView, RwSignal}; impl FormBuilder { - pub fn group( - self, - builder: impl Fn( - VanityControlBuilder, - ) -> VanityControlBuilder, - ) -> Self { - self.new_vanity(builder) - } -} + pub fn group(mut self, builder: impl Fn(FormBuilder) -> FormBuilder) -> Self { + let mut group_builder = FormBuilder::new_group(self.fd, self.fs); + group_builder = builder(group_builder); + self.fs = group_builder.fs; // take the style back -impl VanityControlBuilder { - pub fn title(mut self, title: impl ToString) -> Self { - self.data.title = Some(title.to_string()); + for validation in group_builder.validations { + self.validations.push(validation); + } + + let render_fn = move |fs: &FS, fd: RwSignal| { + let (views, validation_cbs): (Vec<_>, Vec<_>) = group_builder + .render_fns + .into_iter() + .map(|r_fn| r_fn(&fs, fd)) + .unzip(); + + let view = fs.group(views.collect_view()); + let validation_cb = move || { + let mut success = true; + for validation in validation_cbs.iter().flatten() { + if !validation() { + success = false; + } + } + success + }; + (view, Some(Box::new(validation_cb) as Box)) + }; + + self.render_fns.push(Box::new(render_fn)); self } } diff --git a/src/form_builder.rs b/src/form_builder.rs index bf9a004..763fac9 100644 --- a/src/form_builder.rs +++ b/src/form_builder.rs @@ -23,11 +23,11 @@ pub struct FormBuilder { /// The [`ToolFormData`] signal. pub(crate) fd: RwSignal, /// The [`FormStyle`]. - fs: FS, + pub(crate) fs: FS, /// The list of [`ValidationFn`]s. - validations: Vec>>, + pub(crate) validations: Vec>>, /// The list of functions that will render the form. - render_fns: Vec>>, + pub(crate) render_fns: Vec>>, } impl FormBuilder { @@ -42,6 +42,15 @@ impl FormBuilder { } } + pub(crate) fn new_group(fd: RwSignal, fs: FS) -> FormBuilder { + FormBuilder { + fd, + fs, + validations: Vec::new(), + render_fns: Vec::new(), + } + } + pub(crate) fn new_vanity( mut self, builder: impl Fn(VanityControlBuilder) -> VanityControlBuilder, @@ -119,7 +128,7 @@ impl FormBuilder { self.render_fns.push(Box::new(render_fn)); } - fn build_control_view( + fn build_control_view( fd: RwSignal, fs: &FS, getter: Rc>, @@ -130,11 +139,21 @@ impl FormBuilder { render_data: crate::controls::ControlRenderData, ) -> (View, Box) { let (validation_signal, validation_signal_set) = create_signal(Ok(())); + let validation_fn_clone = validation_fn.clone(); 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 fd = fd.get(); + + // rerun validation if it is failing + if validation_signal.get_untracked().is_err() { + if let Some(ref validation_fn) = validation_fn_clone { + let validation_result = validation_fn(&fd); + // if validation succeeds this time, resolve the validation error + if validation_result.is_ok() { + validation_signal_set.set(Ok(())); + } + } + } + unparse_fn(getter(fd)) }; let value_getter = value_getter.into_signal(); @@ -153,7 +172,31 @@ impl FormBuilder { }; let validation_cb = Box::new(validation_cb); - let validation_cb2 = validation_cb.clone(); + let value_setter = Self::create_value_setter( + validation_cb.clone(), + validation_signal_set, + parse_fn, + setter, + fd, + ); + + let view = C::build_control( + fs, + render_data, + value_getter, + value_setter, + validation_signal.into(), + ); + (view, validation_cb) + } + + fn create_value_setter( + validation_cb: Box bool + 'static>, + validation_signal_set: WriteSignal>, + parse_fn: Box>, + setter: Rc>, + fd: RwSignal, + ) -> Box { let value_setter = move |value| { let parsed = match parse_fn(value) { Ok(p) => { @@ -172,18 +215,9 @@ impl FormBuilder { }); // run validation - (validation_cb2)(); + (validation_cb)(); }; - 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) + Box::new(value_setter) } pub(crate) fn build_action_form( diff --git a/src/styles/grid_form.rs b/src/styles/grid_form.rs index b79a0de..5949138 100644 --- a/src/styles/grid_form.rs +++ b/src/styles/grid_form.rs @@ -79,12 +79,11 @@ impl FormStyle for GridFormStyle { .options .into_iter() .map(|value| { - // let value = value; let cloned_value = value.clone(); view! { @@ -100,7 +99,7 @@ impl FormStyle for GridFormStyle { id=&control.data.name name=control.data.name class="form_input" - on:change=move |ev| { + on:input=move |ev| { value_setter(event_target_value(&ev)); } > @@ -353,10 +352,6 @@ impl FormStyle for GridFormStyle { .into_view() } - fn group(&self, _control: ControlRenderData) -> View { - todo!() - } - fn button( &self, control: ControlRenderData>, @@ -383,4 +378,13 @@ impl FormStyle for GridFormStyle { } .into_view() } + + fn group(&self, inner: View) -> View { + view! { +
+ {inner} +
+ } + .into_view() + } } diff --git a/src/styles/mod.rs b/src/styles/mod.rs index 6fb1812..7d7d9a7 100644 --- a/src/styles/mod.rs +++ b/src/styles/mod.rs @@ -1,17 +1,17 @@ mod grid_form; -pub use grid_form::{GridFormStyle, GridFormStylingAttributes}; - use crate::{ controls::{ - button::ButtonData, checkbox::CheckboxData, group::GroupData, heading::HeadingData, - hidden::HiddenData, output::OutputData, radio_buttons::RadioButtonsData, - select::SelectData, slider::SliderData, stepper::StepperData, submit::SubmitData, - text_area::TextAreaData, text_input::TextInputData, ControlData, ControlRenderData, + button::ButtonData, checkbox::CheckboxData, heading::HeadingData, hidden::HiddenData, + output::OutputData, radio_buttons::RadioButtonsData, select::SelectData, + slider::SliderData, stepper::StepperData, submit::SubmitData, text_area::TextAreaData, + text_input::TextInputData, ControlData, ControlRenderData, }, FormToolData, }; use leptos::{Signal, View}; +pub use grid_form::{GridFormStyle, GridFormStylingAttributes}; + pub trait FormStyle: Default + 'static { type StylingAttributes; @@ -86,6 +86,6 @@ pub trait FormStyle: Default + 'static { fn submit(&self, control: ControlRenderData) -> View; // TODO: test custom component fn custom_component(&self, view: View) -> View; - // TODO: add group - fn group(&self, control: ControlRenderData) -> View; + // TODO: test group + fn group(&self, inner: View) -> View; }