Compare commits

..

2 Commits

Author SHA1 Message Date
a3311b2b63 remove vanity builder and add docs 2024-06-05 12:30:15 -05:00
3621e5fb7b differ rendering 2024-06-05 11:46:48 -05:00
2 changed files with 129 additions and 137 deletions

View File

@ -1,7 +1,7 @@
use std::{fmt::Display, rc::Rc, str::FromStr}; use std::{fmt::Display, rc::Rc, str::FromStr};
use crate::{form::FormToolData, styles::FormStyle}; use crate::{form::FormToolData, styles::FormStyle};
use leptos::{Signal, View}; use leptos::{RwSignal, Signal, View};
pub mod heading; pub mod heading;
pub mod select; pub mod select;
@ -15,6 +15,10 @@ pub trait ParseFn<CR, FDT>: Fn(CR) -> Result<FDT, String> + 'static {}
pub trait UnparseFn<CR, FDT>: Fn(FDT) -> CR + 'static {} pub trait UnparseFn<CR, FDT>: Fn(FDT) -> CR + 'static {}
pub trait FieldGetter<FD, FDT>: Fn(FD) -> FDT + 'static {} pub trait FieldGetter<FD, FDT>: Fn(FD) -> FDT + 'static {}
pub trait FieldSetter<FD, FDT>: Fn(&mut FD, FDT) + 'static {} pub trait FieldSetter<FD, FDT>: Fn(&mut FD, FDT) + 'static {}
pub trait RenderFn<FS, FD>:
FnOnce(&FS, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + 'static
{
}
// implement the traits for all valid types // implement the traits for all valid types
impl<FDT, T> ValidationFn<FDT> for T where T: Fn(&FDT) -> Result<(), String> + 'static {} impl<FDT, T> ValidationFn<FDT> for T where T: Fn(&FDT) -> Result<(), String> + 'static {}
@ -23,21 +27,22 @@ impl<CR, FDT, F> ParseFn<CR, FDT> for F where F: Fn(CR) -> Result<FDT, String> +
impl<CR, FDT, F> UnparseFn<CR, FDT> for F where F: Fn(FDT) -> CR + 'static {} impl<CR, FDT, F> UnparseFn<CR, FDT> for F where F: Fn(FDT) -> CR + 'static {}
impl<FD, FDT, F> FieldGetter<FD, FDT> for F where F: Fn(FD) -> FDT + 'static {} impl<FD, FDT, F> FieldGetter<FD, FDT> for F where F: Fn(FD) -> FDT + 'static {}
impl<FD, FDT, F> FieldSetter<FD, FDT> for F where F: Fn(&mut FD, FDT) + 'static {} impl<FD, FDT, F> FieldSetter<FD, FDT> for F where F: Fn(&mut FD, FDT) + 'static {}
impl<FS, FD, F> RenderFn<FS, FD> for F where
F: FnOnce(&FS, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + 'static
{
}
/// A trait for the data needed to render an static control.
pub trait VanityControlData: 'static { pub trait VanityControlData: 'static {
/// Builds the control, returning the [`View`] that was built.
fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View; fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View;
} }
// TODO: what if the `FS` parameter was extracted to the trait level. /// A trait for the data needed to render an interactive control.
// Then this would be trait object able.
// If this is trait object able, then we can store this in a list,
// and differ rendering the control until we actually need to form view.
// Which, in turn, would get rid of the Form Builder as an enum (which was
// done to avoid rendering on the server).
pub trait ControlData: 'static { pub trait ControlData: 'static {
type ReturnType: Clone; type ReturnType: Clone;
// TODO: this should also return a getter for the data /// Builds the control, returning the [`View`] that was built.
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: ControlRenderData<FS, Self>,
@ -47,17 +52,20 @@ pub trait ControlData: 'static {
) -> View; ) -> View;
} }
/// The data needed to render a interactive control of type `C`.
pub struct ControlRenderData<FS: FormStyle + ?Sized, C: ?Sized> { pub struct ControlRenderData<FS: FormStyle + ?Sized, C: ?Sized> {
pub data: Box<C>, pub data: Box<C>,
pub style: Vec<FS::StylingAttributes>, pub style: Vec<FS::StylingAttributes>,
} }
/// The data needed to render a static control of type `C`.
pub struct VanityControlBuilder<FS: FormStyle, C: VanityControlData> { pub struct VanityControlBuilder<FS: FormStyle, C: VanityControlData> {
pub(crate) style_attributes: Vec<FS::StylingAttributes>, pub(crate) style_attributes: Vec<FS::StylingAttributes>,
pub(crate) data: C, pub(crate) data: C,
} }
impl<FS: FormStyle, C: VanityControlData> VanityControlBuilder<FS, C> { impl<FS: FormStyle, C: VanityControlData> VanityControlBuilder<FS, C> {
/// Creates a new [`VanityControlBuilder`] with the given [`VanityControlData`].
pub(crate) fn new(data: C) -> Self { pub(crate) fn new(data: C) -> Self {
VanityControlBuilder { VanityControlBuilder {
data, data,
@ -65,6 +73,7 @@ impl<FS: FormStyle, C: VanityControlData> VanityControlBuilder<FS, C> {
} }
} }
/// Builds the builder into the data needed to render the control.
pub(crate) fn build(self) -> ControlRenderData<FS, C> { pub(crate) fn build(self) -> ControlRenderData<FS, C> {
ControlRenderData { ControlRenderData {
data: Box::new(self.data), data: Box::new(self.data),
@ -73,12 +82,16 @@ impl<FS: FormStyle, C: VanityControlData> VanityControlBuilder<FS, C> {
} }
} }
/// The possibilities for errors when building a control.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum ControlBuildError { pub enum ControlBuildError {
/// The field that this control belongs to is not specified. /// The getter field was not specified.
MissingGetter, MissingGetter,
/// The setter field was not specified.
MissingSetter, MissingSetter,
/// The parse function was not specified.
MissingParseFn, MissingParseFn,
/// The unparse function was not specified.
MissingUnParseFn, MissingUnParseFn,
} }
impl Display for ControlBuildError { impl Display for ControlBuildError {
@ -93,6 +106,7 @@ impl Display for ControlBuildError {
} }
} }
/// The data returned fomr a control's build function.
pub(crate) struct BuiltControlData<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> { pub(crate) struct BuiltControlData<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> {
pub(crate) render_data: ControlRenderData<FS, C>, pub(crate) render_data: ControlRenderData<FS, C>,
pub(crate) getter: Rc<dyn FieldGetter<FD, FDT>>, pub(crate) getter: Rc<dyn FieldGetter<FD, FDT>>,
@ -102,6 +116,7 @@ pub(crate) struct BuiltControlData<FD: FormToolData, FS: FormStyle, C: ControlDa
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>, pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
} }
/// A builder for a interactive control.
pub struct ControlBuilder<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> { pub struct ControlBuilder<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> {
pub(crate) getter: Option<Rc<dyn FieldGetter<FD, FDT>>>, pub(crate) getter: Option<Rc<dyn FieldGetter<FD, FDT>>>,
pub(crate) setter: Option<Rc<dyn FieldSetter<FD, FDT>>>, pub(crate) setter: Option<Rc<dyn FieldSetter<FD, FDT>>>,
@ -113,6 +128,7 @@ pub struct ControlBuilder<FD: FormToolData, FS: FormStyle, C: ControlData, FDT>
} }
impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C, FDT> { impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C, FDT> {
/// Creates a new [`ControlBuilder`] with the given [`ControlData`].
pub(crate) fn new(data: C) -> Self { pub(crate) fn new(data: C) -> Self {
ControlBuilder { ControlBuilder {
data, data,
@ -125,6 +141,9 @@ impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS
} }
} }
/// Builds the builder into the data needed to render the control.
///
/// This fails if a required field was not specified.
pub(crate) fn build(self) -> Result<BuiltControlData<FD, FS, C, FDT>, ControlBuildError> { pub(crate) fn build(self) -> Result<BuiltControlData<FD, FS, C, FDT>, ControlBuildError> {
let getter = match self.getter { let getter = match self.getter {
Some(getter) => getter, Some(getter) => getter,

View File

@ -1,6 +1,6 @@
use crate::{ use crate::{
controls::{ controls::{
BuiltControlData, ControlBuilder, ControlData, FieldGetter, FieldSetter, ParseFn, BuiltControlData, ControlBuilder, ControlData, FieldGetter, FieldSetter, ParseFn, RenderFn,
UnparseFn, ValidationCb, ValidationFn, VanityControlBuilder, VanityControlData, UnparseFn, ValidationCb, ValidationFn, VanityControlBuilder, VanityControlData,
}, },
styles::FormStyle, styles::FormStyle,
@ -53,17 +53,17 @@ impl<FD: FormToolData> Form<FD> {
} }
} }
pub fn validate(&self, form_data: &FD) -> Result<(), String> { /// Validates the [`ToolFormData`], returning the result
for v in self.validations.iter() { pub fn validate(&self) -> Result<(), String> {
(*v)(form_data)?; self.fd.get_untracked().validate()
}
Ok(())
} }
/// Gets the view associated with this [`Form`].
pub fn view(&self) -> View { pub fn view(&self) -> View {
self.view.clone() self.view.clone()
} }
/// Splits this [`Form`] into it's parts.
pub fn to_parts(self) -> (RwSignal<FD>, Validator<FD>, View) { pub fn to_parts(self) -> (RwSignal<FD>, Validator<FD>, View) {
( (
self.fd, self.fd,
@ -81,63 +81,29 @@ impl<FD: FormToolData> IntoView for Form<FD> {
} }
} }
/// A version of the [`FormBuilder`] that contains all the data
/// needed for full building of a [`Form`].
struct FullFormBuilder<FD: FormToolData, FS: FormStyle> {
fd: RwSignal<FD>,
fs: FS,
validations: Vec<Rc<dyn ValidationFn<FD>>>,
validation_cbs: Vec<Box<dyn ValidationCb>>,
views: Vec<View>,
}
/// 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<FD: FormToolData, FS: FormStyle> {
/// For building the form with views
FullBuilder(FullFormBuilder<FD, FS>),
/// For building only the validations for the form
ValidationBuilder {
validations: Vec<Rc<dyn ValidationFn<FD>>>,
},
}
/// A builder for laying out forms. /// A builder for laying out forms.
/// ///
/// This builder allows you to specify what component should make up the form. /// This builder allows you to specify what components should make up the form.
pub struct FormBuilder<FD: FormToolData, FS: FormStyle> { pub struct FormBuilder<FD: FormToolData, FS: FormStyle> {
inner: FormBuilderInner<FD, FS>, /// The [`ToolFormData`] signal.
fd: RwSignal<FD>,
/// The [`FormStyle`].
fs: FS,
/// The list of [`ValidationFn`]s.
validations: Vec<Rc<dyn ValidationFn<FD>>>,
/// The list of functions that will render the form.
render_fns: Vec<Box<dyn RenderFn<FS, FD>>>,
} }
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
/// Creates a new full builder. /// Creates a new [`FormBuilder`]
fn new_full_builder(starting_data: FD, form_style: FS) -> FormBuilder<FD, FS> { fn new(starting_data: FD, form_style: FS) -> FormBuilder<FD, FS> {
let fd = create_rw_signal(starting_data); let fd = create_rw_signal(starting_data);
FormBuilder { FormBuilder {
inner: FormBuilderInner::FullBuilder(FullFormBuilder {
fd, fd,
fs: form_style, fs: form_style,
validations: Vec::new(), validations: Vec::new(),
validation_cbs: Vec::new(), render_fns: Vec::new(),
views: Vec::new(),
}),
}
}
/// Creates a new builder that only collects the validation functions.
fn new_validation_builder() -> FormBuilder<FD, FS> {
FormBuilder {
inner: FormBuilderInner::ValidationBuilder {
validations: Vec::new(),
},
} }
} }
@ -162,14 +128,14 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
} }
fn add_vanity<C: VanityControlData>(&mut self, vanity_control: VanityControlBuilder<FS, C>) { fn add_vanity<C: VanityControlData>(&mut self, vanity_control: VanityControlBuilder<FS, C>) {
let builder = match &mut self.inner { let render_data = vanity_control.build();
FormBuilderInner::FullBuilder(fb) => fb,
FormBuilderInner::ValidationBuilder { validations: _ } => return, let render_fn = move |fs: &FS, _| {
let view = VanityControlData::build_control(fs, render_data);
(view, None)
}; };
let render_data = vanity_control.build(); self.render_fns.push(Box::new(render_fn));
let view = VanityControlData::build_control(&builder.fs, render_data);
builder.views.push(view);
} }
fn add_control<C: ControlData, FDT: Clone + PartialEq + 'static>( fn add_control<C: ControlData, FDT: Clone + PartialEq + 'static>(
@ -188,23 +154,14 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
Err(e) => panic!("Invalid Component: {}", e), 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;
}
};
if let Some(ref validation_fn) = validation_fn { if let Some(ref validation_fn) = validation_fn {
builder.validations.push(validation_fn.clone()); self.validations.push(validation_fn.clone());
} }
let (validation_cb, view) = Self::build_control_view( let render_fn = move |fs: &FS, fd: RwSignal<FD>| {
builder, let (view, cb) = Self::build_control_view(
fd,
fs,
getter, getter,
setter, setter,
unparse_fn, unparse_fn,
@ -212,22 +169,23 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
validation_fn, validation_fn,
render_data, render_data,
); );
(view, Some(cb))
};
builder.views.push(view); self.render_fns.push(Box::new(render_fn));
builder.validation_cbs.push(validation_cb);
} }
fn build_control_view<C: ControlData, FDT: Clone + PartialEq + 'static>( fn build_control_view<C: ControlData, FDT: Clone + PartialEq + 'static>(
builder: &FullFormBuilder<FD, FS>, fd: RwSignal<FD>,
fs: &FS,
getter: Rc<dyn FieldGetter<FD, FDT>>, getter: Rc<dyn FieldGetter<FD, FDT>>,
setter: Rc<dyn FieldSetter<FD, FDT>>, setter: Rc<dyn FieldSetter<FD, FDT>>,
unparse_fn: Box<dyn UnparseFn<<C as ControlData>::ReturnType, FDT>>, unparse_fn: Box<dyn UnparseFn<<C as ControlData>::ReturnType, FDT>>,
parse_fn: Box<dyn ParseFn<<C as ControlData>::ReturnType, FDT>>, parse_fn: Box<dyn ParseFn<<C as ControlData>::ReturnType, FDT>>,
validation_fn: Option<Rc<dyn ValidationFn<FD>>>, validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
render_data: crate::controls::ControlRenderData<FS, C>, render_data: crate::controls::ControlRenderData<FS, C>,
) -> (Box<dyn ValidationCb>, View) { ) -> (View, Box<dyn ValidationCb>) {
let (validation_signal, validation_signal_set) = create_signal(Ok(())); let (validation_signal, validation_signal_set) = create_signal(Ok(()));
let fd = builder.fd;
let value_getter = move || { let value_getter = move || {
let getter = getter.clone(); let getter = getter.clone();
// memoize so that updating one field doesn't cause a re-render for all fields // memoize so that updating one field doesn't cause a re-render for all fields
@ -275,35 +233,35 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
let value_setter = Box::new(value_setter); let value_setter = Box::new(value_setter);
let view = C::build_control( let view = C::build_control(
&builder.fs, fs,
render_data, render_data,
value_getter, value_getter,
value_setter, value_setter,
validation_signal.into(), validation_signal.into(),
); );
(validation_cb, view) (view, validation_cb)
} }
fn build_action_form<ServFn>( fn build_action_form<ServFn>(
self, self,
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>, action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
) -> Option<Form<FD>> ) -> Form<FD>
where where
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static, ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData: <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
From<FormData>, From<FormData>,
{ {
let builder = match self.inner { let (views, validation_cbs): (Vec<_>, Vec<_>) = self
FormBuilderInner::FullBuilder(fb) => fb, .render_fns
FormBuilderInner::ValidationBuilder { validations: _ } => return None, .into_iter()
}; .map(|r_fn| r_fn(&self.fs, self.fd))
.unzip();
let elements = builder.fs.form_frame(builder.views.into_view()); let elements = self.fs.form_frame(views.into_view());
let validation_cbs = builder.validation_cbs;
let on_submit = move |ev: SubmitEvent| { let on_submit = move |ev: SubmitEvent| {
let mut failed = false; let mut failed = false;
for validation in validation_cbs.iter() { for validation in validation_cbs.iter().flatten() {
if !validation() { if !validation() {
failed = true; failed = true;
} }
@ -319,25 +277,25 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
</ActionForm> </ActionForm>
}; };
Some(Form { Form {
fd: builder.fd, fd: self.fd,
validations: builder.validations, validations: self.validations,
view, view,
}) }
} }
fn build_plain_form(self, url: String) -> Option<Form<FD>> { fn build_plain_form(self, url: String) -> Form<FD> {
let builder = match self.inner { let (views, validation_cbs): (Vec<_>, Vec<_>) = self
FormBuilderInner::FullBuilder(fb) => fb, .render_fns
FormBuilderInner::ValidationBuilder { validations: _ } => return None, .into_iter()
}; .map(|r_fn| r_fn(&self.fs, self.fd))
.unzip();
let elements = builder.fs.form_frame(builder.views.into_view()); let elements = self.fs.form_frame(views.into_view());
let validation_cbs = builder.validation_cbs;
let on_submit = move |ev: SubmitEvent| { let on_submit = move |ev: SubmitEvent| {
let mut failed = false; let mut failed = false;
for validation in validation_cbs.iter() { for validation in validation_cbs.iter().flatten() {
if !validation() { if !validation() {
failed = true; failed = true;
} }
@ -353,19 +311,17 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
</Form> </Form>
}; };
Some(Form { Form {
fd: builder.fd, fd: self.fd,
validations: builder.validations, validations: self.validations,
view, view,
}) }
} }
fn validator(&self) -> Validator<FD> { fn validator(&self) -> Validator<FD> {
let validations = match &self.inner { Validator {
FormBuilderInner::FullBuilder(fb) => fb.validations.clone(), validations: self.validations.clone(),
FormBuilderInner::ValidationBuilder { validations } => validations.clone(), }
};
Validator { validations }
} }
} }
@ -387,15 +343,22 @@ pub trait FormToolData: Default + Clone + 'static {
/// Constructs a [`Form`] for this [`FormToolData`] type. /// Constructs a [`Form`] for this [`FormToolData`] type.
/// ///
/// The [`Form`] provides the way to render the form. /// This renders the form as a the leptos_router
/// [`Form`](leptos_router::Form)
/// component. Call [`get_action_form`]\() to get the
/// [`ActionForm`](leptos_router::ActionForm) version.
fn get_form(self, action: impl ToString, style: Self::Style) -> Form<Self> { fn get_form(self, action: impl ToString, style: Self::Style) -> Form<Self> {
let builder = FormBuilder::new_full_builder(self, style); let builder = FormBuilder::new(self, style);
let builder = Self::build_form(builder); let builder = Self::build_form(builder);
builder builder.build_plain_form(action.to_string())
.build_plain_form(action.to_string())
.expect("builder should be full builder")
} }
/// Constructs a [`Form`] for this [`FormToolData`] type.
///
/// This renders the form as a the leptos_router
/// [`ActionForm`](leptos_router::ActionForm)
/// component. Call [`get_form`]\() to get the plain
/// [`Form`](leptos_router::Form) version.
fn get_action_form<ServFn>( fn get_action_form<ServFn>(
self, self,
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>, action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
@ -406,19 +369,29 @@ pub trait FormToolData: Default + Clone + 'static {
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData: <<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
From<FormData>, From<FormData>,
{ {
let builder = FormBuilder::new_full_builder(self, style); let builder = FormBuilder::new(self, style);
let builder = Self::build_form(builder); let builder = Self::build_form(builder);
builder builder.build_action_form(action)
.build_action_form(action)
.expect("builder should be full builder")
} }
/// Gets a [`Validator`] for this [`ToolFormData`].
///
/// This doesn't render the view, but just collects all the validation
/// Functions from building the form. That means it can be called on the
/// Server and no rendering will be done.
///
/// However, the code to render the views are not configured out, it
/// simply doesn't run, so the view needs to compile even on the server.
fn get_validator() -> Validator<Self> { fn get_validator() -> Validator<Self> {
let builder = FormBuilder::new_validation_builder(); let builder = FormBuilder::new(Self::default(), Self::Style::default());
let builder = Self::build_form(builder); let builder = Self::build_form(builder);
builder.validator() builder.validator()
} }
/// Validates this [`FormToolData`] struct.
///
/// This is shorthand for creating a validator with [`get_validator`]\()
/// and then calling `validator.validate(&self)`.
fn validate(&self) -> Result<(), String> { fn validate(&self) -> Result<(), String> {
let validator = Self::get_validator(); let validator = Self::get_validator();
validator.validate(self) validator.validate(self)