From ead75f050ae40978a4bcdfb7a8c94e7dedd0048d Mon Sep 17 00:00:00 2001 From: Mitchell M Date: Wed, 5 Jun 2024 16:15:07 -0500 Subject: [PATCH] implemented some other controls --- src/controls/checkbox.rs | 47 +++++++ src/controls/group.rs | 30 +++++ src/controls/heading.rs | 3 +- src/controls/hidden.rs | 32 +++++ src/controls/mod.rs | 30 +++-- src/controls/output.rs | 32 +++++ src/controls/radio_buttons.rs | 61 +++++++++ src/controls/select.rs | 24 +++- src/controls/slider.rs | 78 +++++++++++ src/controls/stepper.rs | 74 +++++++++++ src/controls/text_area.rs | 3 +- src/controls/text_input.rs | 3 +- src/form_builder.rs | 9 +- src/styles/grid_form.rs | 237 ++++++++++++++++++++++++++++++++-- src/styles/mod.rs | 54 +++++++- 15 files changed, 679 insertions(+), 38 deletions(-) create mode 100644 src/controls/checkbox.rs create mode 100644 src/controls/group.rs create mode 100644 src/controls/hidden.rs create mode 100644 src/controls/output.rs create mode 100644 src/controls/radio_buttons.rs create mode 100644 src/controls/slider.rs create mode 100644 src/controls/stepper.rs diff --git a/src/controls/checkbox.rs b/src/controls/checkbox.rs new file mode 100644 index 0000000..67bd179 --- /dev/null +++ b/src/controls/checkbox.rs @@ -0,0 +1,47 @@ +use leptos::{Signal, View}; + +use super::{ControlBuilder, ControlData, ControlRenderData}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct CheckboxData { + pub(crate) name: String, + pub(crate) label: Option, +} + +impl ControlData for CheckboxData { + type ReturnType = bool; + + fn build_control( + fs: &FS, + control: ControlRenderData, + value_getter: Signal, + value_setter: Box, + _validation_state: Signal>, + ) -> View { + fs.checkbox(control, value_getter, value_setter) + } +} + +impl FormBuilder { + pub fn checkbox( + self, + builder: impl Fn( + ControlBuilder, + ) -> ControlBuilder, + ) -> Self { + self.new_control(builder) + } +} + +impl ControlBuilder { + pub fn named(mut self, control_name: impl ToString) -> Self { + self.data.name = control_name.to_string(); + self + } + + pub fn labeled(mut self, label: impl ToString) -> Self { + self.data.label = Some(label.to_string()); + self + } +} diff --git a/src/controls/group.rs b/src/controls/group.rs new file mode 100644 index 0000000..8baa308 --- /dev/null +++ b/src/controls/group.rs @@ -0,0 +1,30 @@ +use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; +use leptos::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) -> View { + fs.group(control) + } +} + +impl FormBuilder { + pub fn group( + self, + builder: impl Fn(VanityControlBuilder) -> VanityControlBuilder, + ) -> Self { + self.new_vanity(builder) + } +} + +impl VanityControlBuilder { + pub fn title(mut self, title: impl ToString) -> Self { + self.data.title = Some(title.to_string()); + self + } +} diff --git a/src/controls/heading.rs b/src/controls/heading.rs index 48c9bb4..0a66103 100644 --- a/src/controls/heading.rs +++ b/src/controls/heading.rs @@ -1,7 +1,6 @@ -use leptos::View; - use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; +use leptos::View; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct HeadingData { diff --git a/src/controls/hidden.rs b/src/controls/hidden.rs new file mode 100644 index 0000000..88bcafc --- /dev/null +++ b/src/controls/hidden.rs @@ -0,0 +1,32 @@ +use leptos::{Signal, View}; + +use super::{ControlBuilder, ControlData, ControlRenderData}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct HiddenData; + +impl ControlData for HiddenData { + type ReturnType = String; + + fn build_control( + fs: &FS, + control: ControlRenderData, + value_getter: Signal, + _value_setter: Box, + _validation_state: Signal>, + ) -> View { + fs.hidden(control, value_getter) + } +} + +impl FormBuilder { + pub fn hidden( + self, + builder: impl Fn( + ControlBuilder, + ) -> ControlBuilder, + ) -> Self { + self.new_control(builder) + } +} diff --git a/src/controls/mod.rs b/src/controls/mod.rs index 9328561..f05ebc2 100644 --- a/src/controls/mod.rs +++ b/src/controls/mod.rs @@ -1,10 +1,16 @@ -use std::{fmt::Display, rc::Rc, str::FromStr}; - use crate::{form::FormToolData, styles::FormStyle}; use leptos::{RwSignal, Signal, View}; +use std::{fmt::Display, rc::Rc, str::FromStr}; +pub mod checkbox; +pub mod group; pub mod heading; +pub mod hidden; +pub mod output; +pub mod radio_buttons; pub mod select; +pub mod slider; +pub mod stepper; pub mod submit; pub mod text_area; pub mod text_input; @@ -32,7 +38,9 @@ impl RenderFn for F where { } -/// A trait for the data needed to render an static control. +// TODO: vanity signals should have an optional getter. + +/// A trait for the data needed to render an read-only control. pub trait VanityControlData: 'static { /// Builds the control, returning the [`View`] that was built. fn build_control(fs: &FS, control: ControlRenderData) -> View; @@ -51,6 +59,7 @@ pub trait ControlData: 'static { validation_state: Signal>, ) -> View; } +pub trait ValidatedControlData: ControlData {} /// The data needed to render a interactive control of type `C`. pub struct ControlRenderData { @@ -58,7 +67,7 @@ pub struct ControlRenderData { pub style: Vec, } -/// The data needed to render a static control of type `C`. +/// The data needed to render a read-only control of type `C`. pub struct VanityControlBuilder { pub(crate) style_attributes: Vec, pub(crate) data: C, @@ -212,6 +221,14 @@ impl ControlBuilder Self { + self.style_attributes.push(attribute); + self + } +} + +impl ControlBuilder { /// Sets the validation function for this control /// /// This allows you to check if the parsed value is a valid value. @@ -229,11 +246,6 @@ impl ControlBuilder Self { - self.style_attributes.push(attribute); - self - } } impl ControlBuilder diff --git a/src/controls/output.rs b/src/controls/output.rs new file mode 100644 index 0000000..f83eec6 --- /dev/null +++ b/src/controls/output.rs @@ -0,0 +1,32 @@ +use leptos::{Signal, View}; + +use super::{ControlBuilder, ControlData, ControlRenderData}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct OutputData; + +impl ControlData for OutputData { + type ReturnType = String; + + fn build_control( + fs: &FS, + control: ControlRenderData, + value_getter: Signal, + _value_setter: Box, + _validation_state: Signal>, + ) -> View { + fs.output(control, value_getter) + } +} + +impl FormBuilder { + pub fn output( + self, + builder: impl Fn( + ControlBuilder, + ) -> ControlBuilder, + ) -> Self { + self.new_control(builder) + } +} diff --git a/src/controls/radio_buttons.rs b/src/controls/radio_buttons.rs new file mode 100644 index 0000000..6fa8961 --- /dev/null +++ b/src/controls/radio_buttons.rs @@ -0,0 +1,61 @@ +use leptos::{Signal, View}; + +use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct RadioButtonsData { + pub(crate) name: String, + pub(crate) label: Option, + pub(crate) options: Vec, +} + +impl ControlData for RadioButtonsData { + type ReturnType = String; + + fn build_control( + fs: &FS, + control: ControlRenderData, + value_getter: Signal, + value_setter: Box, + validation_state: Signal>, + ) -> View { + fs.radio_buttons(control, value_getter, value_setter, validation_state) + } +} +impl ValidatedControlData for RadioButtonsData {} + +impl FormBuilder { + pub fn radio_buttons( + self, + builder: impl Fn( + ControlBuilder, + ) -> ControlBuilder, + ) -> Self { + self.new_control(builder) + } +} + +impl ControlBuilder { + pub fn named(mut self, control_name: impl ToString) -> Self { + self.data.name = control_name.to_string(); + self + } + + pub fn labeled(mut self, label: impl ToString) -> Self { + self.data.label = Some(label.to_string()); + self + } + + pub fn with_option(mut self, option: impl ToString) -> Self { + self.data.options.push(option.to_string()); + self + } + + pub fn with_options(mut self, options: impl Iterator) -> Self { + for option in options { + self.data.options.push(option.to_string()); + } + self + } +} diff --git a/src/controls/select.rs b/src/controls/select.rs index c7f7c19..8d06488 100644 --- a/src/controls/select.rs +++ b/src/controls/select.rs @@ -1,11 +1,14 @@ use leptos::{Signal, View}; -use super::{ControlBuilder, ControlData, ControlRenderData}; +use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; +// TODO: have an option to have a display string and a value string in the options field + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] pub struct SelectData { pub(crate) name: String, + pub(crate) label: Option, pub(crate) options: Vec, } @@ -22,6 +25,7 @@ impl ControlData for SelectData { fs.select(control, value_getter, value_setter, validation_state) } } +impl ValidatedControlData for SelectData {} impl FormBuilder { pub fn select( @@ -35,13 +39,25 @@ impl FormBuilder { } impl ControlBuilder { - pub fn options(mut self, options: Vec) -> Self { - self.data.options = options; + pub fn named(mut self, control_name: impl ToString) -> Self { + self.data.name = control_name.to_string(); self } - pub fn and_option(mut self, option: impl ToString) -> Self { + pub fn labeled(mut self, label: impl ToString) -> Self { + self.data.label = Some(label.to_string()); + self + } + + pub fn with_option(mut self, option: impl ToString) -> Self { self.data.options.push(option.to_string()); self } + + pub fn with_options(mut self, options: impl Iterator) -> Self { + for option in options { + self.data.options.push(option.to_string()); + } + self + } } diff --git a/src/controls/slider.rs b/src/controls/slider.rs new file mode 100644 index 0000000..3cd5b5b --- /dev/null +++ b/src/controls/slider.rs @@ -0,0 +1,78 @@ +use std::ops::RangeInclusive; + +use leptos::{Signal, View}; + +use super::{ControlBuilder, ControlData, ControlRenderData}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct SliderData { + pub(crate) name: String, + pub(crate) label: Option, + pub(crate) min: i32, + pub(crate) max: i32, +} + +impl Default for SliderData { + fn default() -> Self { + SliderData { + name: String::new(), + label: None, + min: 0, + max: 1, + } + } +} + +impl ControlData for SliderData { + type ReturnType = i32; + + fn build_control( + fs: &FS, + control: ControlRenderData, + value_getter: Signal, + value_setter: Box, + validation_state: Signal>, + ) -> View { + fs.slider(control, value_getter, value_setter, validation_state) + } +} + +impl FormBuilder { + pub fn slider( + self, + builder: impl Fn( + ControlBuilder, + ) -> ControlBuilder, + ) -> Self { + self.new_control(builder) + } +} + +impl ControlBuilder { + pub fn named(mut self, control_name: impl ToString) -> Self { + self.data.name = control_name.to_string(); + self + } + + pub fn labeled(mut self, label: impl ToString) -> Self { + self.data.label = Some(label.to_string()); + self + } + + pub fn min(mut self, min: i32) -> Self { + self.data.min = min; + self + } + + pub fn max(mut self, max: i32) -> Self { + self.data.max = max; + self + } + + pub fn range(mut self, range: RangeInclusive) -> Self { + self.data.min = *range.start(); + self.data.max = *range.end(); + self + } +} diff --git a/src/controls/stepper.rs b/src/controls/stepper.rs new file mode 100644 index 0000000..f874d6f --- /dev/null +++ b/src/controls/stepper.rs @@ -0,0 +1,74 @@ +use std::ops::RangeInclusive; + +use leptos::{Signal, View}; + +use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData}; +use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; + +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] +pub struct StepperData { + pub(crate) name: String, + pub(crate) label: Option, + pub(crate) step: Option, + pub(crate) min: Option, + pub(crate) max: Option, +} + +impl ControlData for StepperData { + type ReturnType = String; + + fn build_control( + fs: &FS, + control: ControlRenderData, + value_getter: Signal, + value_setter: Box, + validation_state: Signal>, + ) -> View { + fs.stepper(control, value_getter, value_setter, validation_state) + } +} +impl ValidatedControlData for StepperData {} + +impl FormBuilder { + pub fn stepper( + self, + builder: impl Fn( + ControlBuilder, + ) -> ControlBuilder, + ) -> Self { + self.new_control(builder) + } +} + +impl ControlBuilder { + pub fn named(mut self, control_name: impl ToString) -> Self { + self.data.name = control_name.to_string(); + self + } + + pub fn labeled(mut self, label: impl ToString) -> Self { + self.data.label = Some(label.to_string()); + self + } + + pub fn step(mut self, step: i32) -> Self { + self.data.step = Some(step); + self + } + + pub fn min(mut self, min: i32) -> Self { + self.data.min = Some(min); + self + } + + pub fn max(mut self, max: i32) -> Self { + self.data.max = Some(max); + self + } + + pub fn range(mut self, range: RangeInclusive) -> Self { + self.data.min = Some(*range.start()); + self.data.max = Some(*range.end()); + self + } +} diff --git a/src/controls/text_area.rs b/src/controls/text_area.rs index dfff29d..ea1214f 100644 --- a/src/controls/text_area.rs +++ b/src/controls/text_area.rs @@ -1,4 +1,4 @@ -use super::{ControlBuilder, ControlData, ControlRenderData}; +use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use leptos::{Signal, View}; @@ -21,6 +21,7 @@ impl ControlData for TextAreaData { fs.text_area(control, value_getter, value_setter, validation_state) } } +impl ValidatedControlData for TextAreaData {} impl FormBuilder { pub fn text_area( diff --git a/src/controls/text_input.rs b/src/controls/text_input.rs index 1ac0b77..dd62869 100644 --- a/src/controls/text_input.rs +++ b/src/controls/text_input.rs @@ -1,6 +1,6 @@ use leptos::{Signal, View}; -use super::{ControlBuilder, ControlData, ControlRenderData}; +use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -37,6 +37,7 @@ impl ControlData for TextInputData { fs.text_input(control, value_getter, value_setter, validation_state) } } +impl ValidatedControlData for TextInputData {} impl FormBuilder { pub fn text_input( diff --git a/src/form_builder.rs b/src/form_builder.rs index 969a10e..71ca898 100644 --- a/src/form_builder.rs +++ b/src/form_builder.rs @@ -61,7 +61,11 @@ impl FormBuilder { self } - fn add_vanity(&mut self, vanity_control: VanityControlBuilder) { + // TODO: test this from a user context. A user adding a custom defined component. + pub fn add_vanity( + &mut self, + vanity_control: VanityControlBuilder, + ) { let render_data = vanity_control.build(); let render_fn = move |fs: &FS, _| { @@ -72,7 +76,8 @@ impl FormBuilder { self.render_fns.push(Box::new(render_fn)); } - fn add_control( + // TODO: test this from a user context. A user adding a custom defined component. + pub fn add_control( &mut self, control: ControlBuilder, ) { diff --git a/src/styles/grid_form.rs b/src/styles/grid_form.rs index 20844fa..f605c18 100644 --- a/src/styles/grid_form.rs +++ b/src/styles/grid_form.rs @@ -1,9 +1,24 @@ use super::FormStyle; use crate::controls::{ - heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData, - text_input::TextInputData, ControlData, ControlRenderData, + checkbox::CheckboxData, heading::HeadingData, hidden::HiddenData, output::OutputData, + select::SelectData, submit::SubmitData, text_area::TextAreaData, text_input::TextInputData, + ControlData, ControlRenderData, }; use leptos::*; +use std::rc::Rc; + +// TODO: move validation from happening on set, to happening on get +// I think. +// That might fix some issues where the field updates but the validation doesn't +/// I don't know if that will cause any loops or not... + +// TODO: send the server fn directly instead of parsing from form data perhaps. +// This would need a note in the docs about graceful degration. + +// TODO: some components dont have validation functions. They should not be able +// to specify one in the builder. + +// TODO: add button pub enum GridFormStylingAttributes { Width(u32), @@ -40,6 +55,7 @@ impl FormStyle for GridFormStyle { value_setter: Box::ReturnType)>, validation_state: Signal>, ) -> View { + // TODO: extract this to a common thing let mut width = 1; for style in control.style { match style { @@ -50,12 +66,12 @@ impl FormStyle for GridFormStyle { view! {
- - - {move || format!("{}", validation_state.get().err().unwrap_or_default())} - + + + {move || validation_state.get().err()} +
- *value + {value.clone()} } }) @@ -101,7 +117,8 @@ impl FormStyle for GridFormStyle { view! {
- {move || format!("{:?}", validation_state.get())} + {control.data.label} + {move || validation_state.get().err()} + } + .into_view() + } + + fn radio_buttons( + &self, + control: ControlRenderData, + value_getter: Signal< + ::ReturnType, + >, + value_setter: Box< + dyn Fn(::ReturnType), + >, + validation_state: Signal>, + ) -> View { + let mut width = 1; + for style in control.style { + match style { + GridFormStylingAttributes::Width(w) => width = w, + } + } + + let value_setter = Rc::new(value_setter); + let buttons_view = control + .data + .options + .into_iter() + .map(|o| { + let value_setter = value_setter.clone(); + let o_clone1 = o.clone(); + let o_clone2 = o.clone(); + view! { + + +
+ } + }) + .collect_view(); + + view! { +
+
+ + + {move || validation_state.get().err()} + +
+
+ {buttons_view} +
+
+ } + .into_view() + } + + fn checkbox( + &self, + control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, + ) -> View { + let mut width = 1; + for style in control.style { + match style { + GridFormStylingAttributes::Width(w) => width = w, + } + } + + view! { +
+ + +
+ } + .into_view() + } + + fn stepper( + &self, + control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box< + dyn Fn(::ReturnType), + >, + validation_state: Signal>, + ) -> View { + let mut width = 1; + for style in control.style { + match style { + GridFormStylingAttributes::Width(w) => width = w, + } + } + + view! { +
+
+ + + {move || validation_state.get().err()} + +
+ +
+ }.into_view() + } + + fn output( + &self, + _control: ControlRenderData, + value_getter: Signal, + ) -> View { + view! { +
{move || value_getter.get()}
+ } + .into_view() + } + + fn slider( + &self, + control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, + validation_state: Signal>, + ) -> View { + let mut width = 1; + for style in control.style { + match style { + GridFormStylingAttributes::Width(w) => width = w, + } + } + + view! { +
+
+ + + {move || validation_state.get().err()} + +
+ ().ok(); + if let Some(value) = value { + value_setter(value); + } + } + class="form_input" + /> +
+ } + .into_view() + } + + fn group(&self, _control: ControlRenderData) -> View { + todo!() + } } diff --git a/src/styles/mod.rs b/src/styles/mod.rs index 2cc5937..160fae6 100644 --- a/src/styles/mod.rs +++ b/src/styles/mod.rs @@ -2,8 +2,10 @@ mod grid_form; pub use grid_form::{GridFormStyle, GridFormStylingAttributes}; use crate::controls::{ - heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData, - text_input::TextInputData, ControlData, ControlRenderData, + 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, }; use leptos::{Signal, View}; @@ -19,6 +21,11 @@ pub trait FormStyle: Default + 'static { /// wrapping should be done with `div` or similar elements. fn form_frame(&self, children: View) -> View; fn heading(&self, control: ControlRenderData) -> View; + fn hidden( + &self, + control: ControlRenderData, + value_getter: Signal, + ) -> View; fn text_input( &self, control: ControlRenderData, @@ -26,6 +33,20 @@ pub trait FormStyle: Default + 'static { value_setter: Box::ReturnType)>, validation_state: Signal>, ) -> View; + fn text_area( + &self, + control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, + validation_state: Signal>, + ) -> View; + fn radio_buttons( + &self, + control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, + validation_state: Signal>, + ) -> View; fn select( &self, control: ControlRenderData, @@ -33,15 +54,34 @@ pub trait FormStyle: Default + 'static { value_setter: Box::ReturnType)>, validation_state: Signal>, ) -> View; - fn submit(&self, control: ControlRenderData) -> View; - fn text_area( + fn checkbox( &self, - control: ControlRenderData, - value_getter: Signal<::ReturnType>, - value_setter: Box::ReturnType)>, + control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, + ) -> View; + fn stepper( + &self, + control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, validation_state: Signal>, ) -> View; + fn output( + &self, + control: ControlRenderData, + value_getter: Signal, + ) -> View; + fn slider( + &self, + control: ControlRenderData, + value_getter: Signal<::ReturnType>, + value_setter: Box::ReturnType)>, + validation_state: Signal>, + ) -> View; + 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; }