organized

This commit is contained in:
Mitchell Marino 2024-06-05 12:41:15 -05:00
parent a3311b2b63
commit 63153d76a0
8 changed files with 274 additions and 276 deletions

View File

@ -1,10 +1,7 @@
use leptos::View; use leptos::View;
use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
use crate::{ use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
form::{FormBuilder, FormToolData},
styles::FormStyle,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct HeadingData { pub struct HeadingData {

View File

@ -1,10 +1,7 @@
use leptos::{Signal, View}; use leptos::{Signal, View};
use super::{ControlBuilder, ControlData, ControlRenderData}; use super::{ControlBuilder, ControlData, ControlRenderData};
use crate::{ use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
form::{FormBuilder, FormToolData},
styles::FormStyle,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SelectData { pub struct SelectData {

View File

@ -1,10 +1,7 @@
use leptos::View; use leptos::View;
use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
use crate::{ use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
form::{FormBuilder, FormToolData},
styles::FormStyle,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SubmitData { pub struct SubmitData {

View File

@ -1,8 +1,5 @@
use super::{ControlBuilder, ControlData, ControlRenderData}; use super::{ControlBuilder, ControlData, ControlRenderData};
use crate::{ use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
form::{FormBuilder, FormToolData},
styles::FormStyle,
};
use leptos::{Signal, View}; use leptos::{Signal, View};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]

View File

@ -1,10 +1,7 @@
use leptos::{Signal, View}; use leptos::{Signal, View};
use super::{ControlBuilder, ControlData, ControlRenderData}; use super::{ControlBuilder, ControlData, ControlRenderData};
use crate::{ use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
form::{FormBuilder, FormToolData},
styles::FormStyle,
};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TextInputData { pub struct TextInputData {

View File

@ -1,25 +1,18 @@
use crate::{ use crate::{controls::ValidationFn, form_builder::FormBuilder, styles::FormStyle};
controls::{
BuiltControlData, ControlBuilder, ControlData, FieldGetter, FieldSetter, ParseFn, RenderFn,
UnparseFn, ValidationCb, ValidationFn, VanityControlBuilder, VanityControlData,
},
styles::FormStyle,
};
use leptos::{ use leptos::{
server_fn::{client::Client, codec::PostUrl, request::ClientReq, ServerFn}, server_fn::{client::Client, codec::PostUrl, request::ClientReq, ServerFn},
*, *,
}; };
use leptos_router::{ActionForm, Form};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use std::rc::Rc; use std::rc::Rc;
use web_sys::{FormData, SubmitEvent}; use web_sys::FormData;
/// A type that can be used to validate the form data. /// A type that can be used to validate the form data.
/// ///
/// This can be useful to use the same validation logic on the front /// This can be useful to use the same validation logic on the front
/// end and backend without duplicating the logic. /// end and backend without duplicating the logic.
pub struct Validator<FD> { pub struct Validator<FD> {
validations: Vec<Rc<dyn ValidationFn<FD>>>, pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
} }
impl<FD: FormToolData> Validator<FD> { impl<FD: FormToolData> Validator<FD> {
@ -41,8 +34,8 @@ impl<FD: FormToolData> Validator<FD> {
/// a validator for the data. /// a validator for the data.
pub struct Form<FD: FormToolData> { pub struct Form<FD: FormToolData> {
pub fd: RwSignal<FD>, pub fd: RwSignal<FD>,
validations: Vec<Rc<dyn ValidationFn<FD>>>, pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
view: View, pub(crate) view: View,
} }
impl<FD: FormToolData> Form<FD> { impl<FD: FormToolData> Form<FD> {
@ -81,250 +74,6 @@ impl<FD: FormToolData> IntoView for Form<FD> {
} }
} }
/// A builder for laying out forms.
///
/// This builder allows you to specify what components should make up the form.
pub struct FormBuilder<FD: FormToolData, FS: FormStyle> {
/// 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> {
/// Creates a new [`FormBuilder`]
fn new(starting_data: FD, form_style: FS) -> FormBuilder<FD, FS> {
let fd = create_rw_signal(starting_data);
FormBuilder {
fd,
fs: form_style,
validations: Vec::new(),
render_fns: Vec::new(),
}
}
pub(crate) fn new_vanity<C: VanityControlData + Default>(
mut self,
builder: impl Fn(VanityControlBuilder<FS, C>) -> VanityControlBuilder<FS, C>,
) -> Self {
let vanity_builder = VanityControlBuilder::new(C::default());
let control = builder(vanity_builder);
self.add_vanity(control);
self
}
pub(crate) fn new_control<C: ControlData + Default, FDT: Clone + PartialEq + 'static>(
mut self,
builder: impl Fn(ControlBuilder<FD, FS, C, FDT>) -> ControlBuilder<FD, FS, C, FDT>,
) -> Self {
let control_builder = ControlBuilder::new(C::default());
let control = builder(control_builder);
self.add_control(control);
self
}
fn add_vanity<C: VanityControlData>(&mut self, vanity_control: VanityControlBuilder<FS, C>) {
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<C: ControlData, FDT: Clone + PartialEq + 'static>(
&mut self,
control: ControlBuilder<FD, FS, C, FDT>,
) {
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<FD>| {
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<C: ControlData, FDT: Clone + PartialEq + 'static>(
fd: RwSignal<FD>,
fs: &FS,
getter: Rc<dyn FieldGetter<FD, FDT>>,
setter: Rc<dyn FieldSetter<FD, FDT>>,
unparse_fn: Box<dyn UnparseFn<<C as ControlData>::ReturnType, FDT>>,
parse_fn: Box<dyn ParseFn<<C as ControlData>::ReturnType, FDT>>,
validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
render_data: crate::controls::ControlRenderData<FS, C>,
) -> (View, Box<dyn ValidationCb>) {
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<ServFn>(
self,
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
) -> Form<FD>
where
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
From<FormData>,
{
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! {
<ActionForm action=action on:submit=on_submit>
{elements}
</ActionForm>
};
Form {
fd: self.fd,
validations: self.validations,
view,
}
}
fn build_plain_form(self, url: String) -> Form<FD> {
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! {
<Form action=url on:submit=on_submit>
{elements}
</Form>
};
Form {
fd: self.fd,
validations: self.validations,
view,
}
}
fn validator(&self) -> Validator<FD> {
Validator {
validations: self.validations.clone(),
}
}
}
/// A trait allowing a form to be built around its containing data. /// 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 /// This trait defines a function that can be used to build all the data needed

260
src/form_builder.rs Normal file
View File

@ -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<FD: FormToolData, FS: FormStyle> {
/// 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> {
/// Creates a new [`FormBuilder`]
pub(crate) fn new(starting_data: FD, form_style: FS) -> FormBuilder<FD, FS> {
let fd = create_rw_signal(starting_data);
FormBuilder {
fd,
fs: form_style,
validations: Vec::new(),
render_fns: Vec::new(),
}
}
pub(crate) fn new_vanity<C: VanityControlData + Default>(
mut self,
builder: impl Fn(VanityControlBuilder<FS, C>) -> VanityControlBuilder<FS, C>,
) -> Self {
let vanity_builder = VanityControlBuilder::new(C::default());
let control = builder(vanity_builder);
self.add_vanity(control);
self
}
pub(crate) fn new_control<C: ControlData + Default, FDT: Clone + PartialEq + 'static>(
mut self,
builder: impl Fn(ControlBuilder<FD, FS, C, FDT>) -> ControlBuilder<FD, FS, C, FDT>,
) -> Self {
let control_builder = ControlBuilder::new(C::default());
let control = builder(control_builder);
self.add_control(control);
self
}
fn add_vanity<C: VanityControlData>(&mut self, vanity_control: VanityControlBuilder<FS, C>) {
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<C: ControlData, FDT: Clone + PartialEq + 'static>(
&mut self,
control: ControlBuilder<FD, FS, C, FDT>,
) {
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<FD>| {
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<C: ControlData, FDT: Clone + PartialEq + 'static>(
fd: RwSignal<FD>,
fs: &FS,
getter: Rc<dyn FieldGetter<FD, FDT>>,
setter: Rc<dyn FieldSetter<FD, FDT>>,
unparse_fn: Box<dyn UnparseFn<<C as ControlData>::ReturnType, FDT>>,
parse_fn: Box<dyn ParseFn<<C as ControlData>::ReturnType, FDT>>,
validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
render_data: crate::controls::ControlRenderData<FS, C>,
) -> (View, Box<dyn ValidationCb>) {
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<ServFn>(
self,
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
) -> Form<FD>
where
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
From<FormData>,
{
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! {
<ActionForm action=action on:submit=on_submit>
{elements}
</ActionForm>
};
Form {
fd: self.fd,
validations: self.validations,
view,
}
}
pub(crate) fn build_plain_form(self, url: String) -> Form<FD> {
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! {
<Form action=url on:submit=on_submit>
{elements}
</Form>
};
Form {
fd: self.fd,
validations: self.validations,
view,
}
}
pub(crate) fn validator(&self) -> Validator<FD> {
Validator {
validations: self.validations.clone(),
}
}
}

View File

@ -1,3 +1,7 @@
pub mod controls; pub mod controls;
pub mod form; pub mod form;
pub mod form_builder;
pub mod styles; pub mod styles;
pub use form::{Form, FormToolData, Validator};
pub use form_builder::FormBuilder;