generated from mitchell/rust_template
fourth round of dev
This commit is contained in:
parent
6edc59ecb7
commit
40dc1d8cdc
@ -1,3 +1,5 @@
|
|||||||
|
use std::{fmt::Display, rc::Rc};
|
||||||
|
|
||||||
use crate::{form::FormData, styles::FormStyle};
|
use crate::{form::FormData, styles::FormStyle};
|
||||||
use leptos::{Signal, View};
|
use leptos::{Signal, View};
|
||||||
|
|
||||||
@ -7,15 +9,17 @@ pub mod submit;
|
|||||||
pub mod text_area;
|
pub mod text_area;
|
||||||
pub mod text_input;
|
pub mod text_input;
|
||||||
|
|
||||||
|
// TODO: change to returning an Option<String>
|
||||||
pub trait ValidationFn<FDT>: Fn(&FDT) -> Result<(), String> + 'static {}
|
pub trait ValidationFn<FDT>: Fn(&FDT) -> Result<(), String> + 'static {}
|
||||||
pub trait ParseFn<CT, FDT>: Fn(&CT) -> Result<FDT, String> + 'static {}
|
pub trait ParseFn<CR, FDT>: Fn(&CR) -> Result<FDT, String> + 'static {}
|
||||||
pub trait FieldFn<FD, FDT>: Fn(&mut FD) -> FDT + 'static {}
|
pub trait UnparseFn<CR, FDT>: Fn(&FDT) -> CR + 'static {}
|
||||||
// implement the trait for all valid types
|
pub trait FieldFn<FD, FDT>: Fn(&mut FD) -> &mut FDT + 'static {}
|
||||||
|
|
||||||
|
// 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 {}
|
||||||
// implement the trait for all valid types
|
|
||||||
impl<CR, FDT, F> ParseFn<CR, FDT> for F where F: Fn(&CR) -> Result<FDT, String> + 'static {}
|
impl<CR, FDT, F> ParseFn<CR, FDT> for F where F: Fn(&CR) -> Result<FDT, String> + 'static {}
|
||||||
// implement the trait for all valid types
|
impl<CR, FDT, F> UnparseFn<CR, FDT> for F where F: Fn(&FDT) -> CR + 'static {}
|
||||||
impl<FD, FDT, F> FieldFn<FD, FDT> for F where F: Fn(&mut FD) -> FDT + 'static {}
|
impl<FD, FDT, F> FieldFn<FD, FDT> for F where F: Fn(&mut FD) -> &mut FDT + 'static {}
|
||||||
|
|
||||||
pub trait VanityControlData: 'static {
|
pub trait VanityControlData: 'static {
|
||||||
fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View;
|
fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View;
|
||||||
@ -34,8 +38,10 @@ pub trait ControlData: 'static {
|
|||||||
fn build_control<FS: FormStyle>(
|
fn build_control<FS: FormStyle>(
|
||||||
fs: &FS,
|
fs: &FS,
|
||||||
control: ControlRenderData<FS, Self>,
|
control: ControlRenderData<FS, Self>,
|
||||||
|
value_getter: Signal<Self::ReturnType>,
|
||||||
|
value_setter: Box<dyn Fn(Self::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> (View, Signal<Self::ReturnType>);
|
) -> View;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ControlRenderData<FS: FormStyle + ?Sized, C: ?Sized> {
|
pub struct ControlRenderData<FS: FormStyle + ?Sized, C: ?Sized> {
|
||||||
@ -64,11 +70,33 @@ impl<FS: FormStyle, C: VanityControlData> VanityControlBuilder<FS, C> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
|
pub enum ControlBuildError {
|
||||||
|
/// The field that this control belongs to is not specified.
|
||||||
|
MissingField,
|
||||||
|
}
|
||||||
|
impl Display for ControlBuildError {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
let message = match self {
|
||||||
|
ControlBuildError::MissingField => "you must specify what field this control is for",
|
||||||
|
};
|
||||||
|
write!(f, "{}", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct BuiltControlData<FD: FormData, FS: FormStyle, C: ControlData, FDT> {
|
||||||
|
pub(crate) render_data: ControlRenderData<FS, C>,
|
||||||
|
pub(crate) field_fn: Rc<dyn FieldFn<FD, FDT>>,
|
||||||
|
pub(crate) parse_fn: Box<dyn ParseFn<C::ReturnType, FDT>>,
|
||||||
|
pub(crate) unparse_fn: Box<dyn UnparseFn<C::ReturnType, FDT>>,
|
||||||
|
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ControlBuilder<FD: FormData, FS: FormStyle, C: ControlData, FDT> {
|
pub struct ControlBuilder<FD: FormData, FS: FormStyle, C: ControlData, FDT> {
|
||||||
pub(crate) field_fn: Option<Box<dyn FieldFn<FD, FDT>>>,
|
pub(crate) field_fn: Option<Rc<dyn FieldFn<FD, FDT>>>,
|
||||||
pub(crate) parse_fn: Option<Box<dyn ParseFn<C::ReturnType, FDT>>>,
|
pub(crate) parse_fn: Option<Box<dyn ParseFn<C::ReturnType, FDT>>>,
|
||||||
pub(crate) unparse_fn: Option<Box<dyn ParseFn<FDT, C::ReturnType>>>,
|
pub(crate) unparse_fn: Option<Box<dyn UnparseFn<C::ReturnType, FDT>>>,
|
||||||
pub(crate) validation_fn: Option<Box<dyn ValidationFn<FD>>>,
|
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
||||||
pub(crate) style_attributes: Vec<FS::StylingAttributes>,
|
pub(crate) style_attributes: Vec<FS::StylingAttributes>,
|
||||||
pub(crate) data: C,
|
pub(crate) data: C,
|
||||||
}
|
}
|
||||||
@ -85,21 +113,24 @@ impl<FD: FormData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build(
|
pub(crate) fn build(self) -> Result<BuiltControlData<FD, FS, C, FDT>, ControlBuildError> {
|
||||||
self,
|
// either all 3 should be specified or none of them due to the possible `field` for `field_with` calls.
|
||||||
) -> (
|
let (field_fn, parse_fn, unparse_fn) = match (self.field_fn, self.parse_fn, self.unparse_fn)
|
||||||
ControlRenderData<FS, C>,
|
{
|
||||||
impl ParseFn<FD, C>,
|
(Some(f), Some(p), Some(u)) => (f, p, u),
|
||||||
impl ValidationFn<FD>,
|
_ => return Err(ControlBuildError::MissingField),
|
||||||
) {
|
};
|
||||||
(
|
|
||||||
ControlRenderData {
|
Ok(BuiltControlData {
|
||||||
|
render_data: ControlRenderData {
|
||||||
data: Box::new(self.data),
|
data: Box::new(self.data),
|
||||||
style: self.style_attributes,
|
style: self.style_attributes,
|
||||||
},
|
},
|
||||||
self.parse_fn,
|
field_fn,
|
||||||
self.validation_fn,
|
parse_fn,
|
||||||
)
|
unparse_fn,
|
||||||
|
validation_fn: self.validation_fn,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add method that automatically does the parse and unparse using
|
// TODO: add method that automatically does the parse and unparse using
|
||||||
@ -108,24 +139,31 @@ impl<FD: FormData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C,
|
|||||||
mut self,
|
mut self,
|
||||||
field_fn: impl FieldFn<FD, FDT>,
|
field_fn: impl FieldFn<FD, FDT>,
|
||||||
parse_fn: impl ParseFn<C::ReturnType, FDT>,
|
parse_fn: impl ParseFn<C::ReturnType, FDT>,
|
||||||
unparse_fn: impl ParseFn<FDT, C::ReturnType>,
|
unparse_fn: impl UnparseFn<C::ReturnType, FDT>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.field_fn = Box::new(field_fn);
|
self.field_fn = Some(Rc::new(field_fn));
|
||||||
self.parse_fn = Box::new(parse_fn);
|
self.parse_fn = Some(Box::new(parse_fn));
|
||||||
self.unparse_fn = Box::new(unparse_fn);
|
self.unparse_fn = Some(Box::new(unparse_fn));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_fn(mut self, parse_fn: impl ParseFn<FD, C>) -> Self {
|
/// Overrides the field's parse functions with the ones given.
|
||||||
self.parse_fn = Box::new(parse_fn) as Box<dyn ParseFn<FD, C>>;
|
pub fn parse_fns(
|
||||||
|
mut self,
|
||||||
|
parse_fn: impl ParseFn<C::ReturnType, FDT>,
|
||||||
|
unparse_fn: impl UnparseFn<C::ReturnType, FDT>,
|
||||||
|
) -> Self {
|
||||||
|
self.parse_fn = Some(Box::new(parse_fn));
|
||||||
|
self.unparse_fn = Some(Box::new(unparse_fn));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the validation function for this control
|
||||||
pub fn validation_fn(
|
pub fn validation_fn(
|
||||||
mut self,
|
mut self,
|
||||||
validation_fn: impl Fn(&FD) -> Result<(), String> + 'static,
|
validation_fn: impl Fn(&FD) -> Result<(), String> + 'static,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.validation_fn = Some(Box::new(validation_fn)) as _;
|
self.validation_fn = Some(Rc::new(validation_fn));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,14 +18,16 @@ impl ControlData for SelectData {
|
|||||||
fn build_control<FS: FormStyle>(
|
fn build_control<FS: FormStyle>(
|
||||||
fs: &FS,
|
fs: &FS,
|
||||||
control: ControlRenderData<FS, Self>,
|
control: ControlRenderData<FS, Self>,
|
||||||
|
value_getter: Signal<Self::ReturnType>,
|
||||||
|
value_setter: Box<dyn Fn(Self::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> (View, Signal<Self::ReturnType>) {
|
) -> View {
|
||||||
fs.select(control, validation_state)
|
fs.select(control, value_getter, value_setter, validation_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||||
pub fn select<FDT>(
|
pub fn select<FDT: 'static>(
|
||||||
self,
|
self,
|
||||||
builder: impl Fn(
|
builder: impl Fn(
|
||||||
ControlBuilder<FD, FS, SelectData, FDT>,
|
ControlBuilder<FD, FS, SelectData, FDT>,
|
||||||
|
|||||||
@ -17,14 +17,16 @@ impl ControlData for TextAreaData {
|
|||||||
fn build_control<FS: FormStyle>(
|
fn build_control<FS: FormStyle>(
|
||||||
fs: &FS,
|
fs: &FS,
|
||||||
control: ControlRenderData<FS, Self>,
|
control: ControlRenderData<FS, Self>,
|
||||||
|
value_getter: Signal<Self::ReturnType>,
|
||||||
|
value_setter: Box<dyn Fn(Self::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> (View, Signal<Self::ReturnType>) {
|
) -> View {
|
||||||
fs.text_area(control, validation_state)
|
fs.text_area(control, value_getter, value_setter, validation_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||||
pub fn text_area<FDT>(
|
pub fn text_area<FDT: 'static>(
|
||||||
self,
|
self,
|
||||||
builder: impl Fn(
|
builder: impl Fn(
|
||||||
ControlBuilder<FD, FS, TextAreaData, FDT>,
|
ControlBuilder<FD, FS, TextAreaData, FDT>,
|
||||||
|
|||||||
@ -33,14 +33,16 @@ impl ControlData for TextInputData {
|
|||||||
fn build_control<FS: FormStyle>(
|
fn build_control<FS: FormStyle>(
|
||||||
fs: &FS,
|
fs: &FS,
|
||||||
control: ControlRenderData<FS, Self>,
|
control: ControlRenderData<FS, Self>,
|
||||||
|
value_getter: Signal<Self::ReturnType>,
|
||||||
|
value_setter: Box<dyn Fn(Self::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> (View, Signal<Self::ReturnType>) {
|
) -> View {
|
||||||
fs.text_input(control, validation_state)
|
fs.text_input(control, value_getter, value_setter, validation_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||||
pub fn text_input<FDT>(
|
pub fn text_input<FDT: 'static>(
|
||||||
self,
|
self,
|
||||||
builder: impl Fn(
|
builder: impl Fn(
|
||||||
ControlBuilder<FD, FS, TextInputData, FDT>,
|
ControlBuilder<FD, FS, TextInputData, FDT>,
|
||||||
|
|||||||
205
src/form.rs
205
src/form.rs
@ -1,16 +1,19 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
controls::{
|
controls::{
|
||||||
ControlBuilder, ControlData, ValidationFn, VanityControlBuilder, VanityControlData,
|
BuiltControlData, ControlBuilder, ControlData, FieldFn, ParseFn, UnparseFn, ValidationFn,
|
||||||
|
VanityControlBuilder, VanityControlData,
|
||||||
},
|
},
|
||||||
styles::FormStyle,
|
styles::FormStyle,
|
||||||
};
|
};
|
||||||
use leptos::{
|
use leptos::{
|
||||||
create_effect, create_signal, IntoView, ReadSignal, SignalGet, SignalSet, SignalUpdate, View,
|
create_rw_signal, create_signal, IntoSignal, IntoView, RwSignal, SignalGet, SignalSet,
|
||||||
WriteSignal,
|
SignalUpdate, View,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Validator<FD: FormData> {
|
pub struct Validator<FD: FormData> {
|
||||||
validations: Vec<Box<dyn ValidationFn<FD>>>,
|
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData> Validator<FD> {
|
impl<FD: FormData> Validator<FD> {
|
||||||
@ -27,8 +30,8 @@ impl<FD: FormData> Validator<FD> {
|
|||||||
/// With this, you can render the form, get the form data, or get
|
/// With this, you can render the form, get the form data, or get
|
||||||
/// a validator for the data.
|
/// a validator for the data.
|
||||||
pub struct Form<FD: FormData> {
|
pub struct Form<FD: FormData> {
|
||||||
pub fd: ReadSignal<FD>,
|
pub fd: RwSignal<FD>,
|
||||||
validations: Vec<Box<dyn ValidationFn<FD>>>,
|
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
||||||
view: View,
|
view: View,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -50,7 +53,7 @@ impl<FD: FormData> Form<FD> {
|
|||||||
self.view.clone()
|
self.view.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_parts(self) -> (ReadSignal<FD>, Validator<FD>, View) {
|
pub fn to_parts(self) -> (RwSignal<FD>, Validator<FD>, View) {
|
||||||
(
|
(
|
||||||
self.fd,
|
self.fd,
|
||||||
Validator {
|
Validator {
|
||||||
@ -63,17 +66,16 @@ impl<FD: FormData> Form<FD> {
|
|||||||
|
|
||||||
impl<FD: FormData> IntoView for Form<FD> {
|
impl<FD: FormData> IntoView for Form<FD> {
|
||||||
fn into_view(self) -> View {
|
fn into_view(self) -> View {
|
||||||
self.view
|
self.view()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A version of the [`FormBuilder`] that contains all the data
|
/// A version of the [`FormBuilder`] that contains all the data
|
||||||
/// needed for full building of a [`Form`].
|
/// needed for full building of a [`Form`].
|
||||||
struct FullFormBuilder<FD: FormData, FS: FormStyle> {
|
struct FullFormBuilder<FD: FormData, FS: FormStyle> {
|
||||||
fd_get: ReadSignal<FD>,
|
fd: RwSignal<FD>,
|
||||||
fd_set: WriteSignal<FD>,
|
|
||||||
fs: FS,
|
fs: FS,
|
||||||
validations: Vec<Box<dyn ValidationFn<FD>>>,
|
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
||||||
views: Vec<View>,
|
views: Vec<View>,
|
||||||
}
|
}
|
||||||
/// The internal type for building forms
|
/// The internal type for building forms
|
||||||
@ -91,7 +93,7 @@ enum FormBuilderInner<FD: FormData, FS: FormStyle> {
|
|||||||
FullBuilder(FullFormBuilder<FD, FS>),
|
FullBuilder(FullFormBuilder<FD, FS>),
|
||||||
/// For building only the validations for the form
|
/// For building only the validations for the form
|
||||||
ValidationBuilder {
|
ValidationBuilder {
|
||||||
validations: Vec<Box<dyn ValidationFn<FD>>>,
|
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,11 +104,10 @@ pub struct FormBuilder<FD: FormData, FS: FormStyle> {
|
|||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||||
// TODO: remove the Default trait bound and bind it to this function only
|
// TODO: remove the Default trait bound and bind it to this function only
|
||||||
fn new_full_builder(form_style: FS) -> FormBuilder<FD, FS> {
|
fn new_full_builder(form_style: FS) -> FormBuilder<FD, FS> {
|
||||||
let (fd_get, fd_set) = create_signal(FD::default());
|
let fd = create_rw_signal(FD::default());
|
||||||
FormBuilder {
|
FormBuilder {
|
||||||
inner: FormBuilderInner::FullBuilder(FullFormBuilder {
|
inner: FormBuilderInner::FullBuilder(FullFormBuilder {
|
||||||
fd_get,
|
fd,
|
||||||
fd_set,
|
|
||||||
fs: form_style,
|
fs: form_style,
|
||||||
validations: Vec::new(),
|
validations: Vec::new(),
|
||||||
views: Vec::new(),
|
views: Vec::new(),
|
||||||
@ -115,11 +116,10 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn new_full_builder_with(starting_data: FD, form_style: FS) -> FormBuilder<FD, FS> {
|
fn new_full_builder_with(starting_data: FD, form_style: FS) -> FormBuilder<FD, FS> {
|
||||||
let (fd_get, fd_set) = create_signal(starting_data);
|
let fd = create_rw_signal(starting_data);
|
||||||
FormBuilder {
|
FormBuilder {
|
||||||
inner: FormBuilderInner::FullBuilder(FullFormBuilder {
|
inner: FormBuilderInner::FullBuilder(FullFormBuilder {
|
||||||
fd_get,
|
fd,
|
||||||
fd_set,
|
|
||||||
fs: form_style,
|
fs: form_style,
|
||||||
validations: Vec::new(),
|
validations: Vec::new(),
|
||||||
views: Vec::new(),
|
views: Vec::new(),
|
||||||
@ -145,7 +145,7 @@ impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_control<C: ControlData + Default, FDT>(
|
pub(crate) fn new_control<C: ControlData + Default, FDT: 'static>(
|
||||||
mut self,
|
mut self,
|
||||||
builder: impl Fn(ControlBuilder<FD, FS, C, FDT>) -> ControlBuilder<FD, FS, C, FDT>,
|
builder: impl Fn(ControlBuilder<FD, FS, C, FDT>) -> ControlBuilder<FD, FS, C, FDT>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@ -156,67 +156,134 @@ impl<FD: FormData, 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 full_builder = match &mut self.inner {
|
let builder = match &mut self.inner {
|
||||||
|
FormBuilderInner::FullBuilder(fb) => fb,
|
||||||
FormBuilderInner::ValidationBuilder { validations: _ } => return,
|
FormBuilderInner::ValidationBuilder { validations: _ } => return,
|
||||||
FormBuilderInner::FullBuilder(full_builder) => full_builder,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let render_data = vanity_control.build();
|
let render_data = vanity_control.build();
|
||||||
let view = VanityControlData::build_control(&full_builder.fs, render_data);
|
let view = VanityControlData::build_control(&builder.fs, render_data);
|
||||||
full_builder.views.push(view);
|
builder.views.push(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn add_control<C: ControlData, FDT>(&mut self, control: ControlBuilder<FD, FS, C, FDT>) {
|
fn add_control<C: ControlData, FDT: 'static>(
|
||||||
let full_builder = match &mut self.inner {
|
&mut self,
|
||||||
FormBuilderInner::ValidationBuilder { validations } => {
|
control: ControlBuilder<FD, FS, C, FDT>,
|
||||||
validations.push(control.validation_fn);
|
) {
|
||||||
|
let BuiltControlData {
|
||||||
|
render_data,
|
||||||
|
field_fn,
|
||||||
|
parse_fn,
|
||||||
|
unparse_fn,
|
||||||
|
validation_fn,
|
||||||
|
} = match control.build() {
|
||||||
|
Ok(c) => c,
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
FormBuilderInner::FullBuilder(full_builder) => full_builder,
|
|
||||||
};
|
};
|
||||||
let (render_data, parse_fn, validation_fn) = control.build();
|
if let Some(ref validation_fn) = validation_fn {
|
||||||
let (validation_signal, validation_signal_set) = create_signal(Ok(()));
|
builder.validations.push(validation_fn.clone());
|
||||||
let (view, control_value) =
|
}
|
||||||
ControlData::build_control(&full_builder.fs, render_data, validation_signal.into());
|
|
||||||
// TODO: add a signal that triggers on submit to refresh the validation on_submit
|
let view = Self::build_control_view(
|
||||||
let fd_setter = full_builder.fd_set;
|
builder,
|
||||||
create_effect(move |last_value| {
|
field_fn,
|
||||||
let control_value = control_value.get();
|
unparse_fn,
|
||||||
let mut validation_result = Ok(());
|
parse_fn,
|
||||||
fd_setter.update(|v| {
|
validation_fn,
|
||||||
validation_result = (parse_fn)(control_value, v).and_then(|_| (validation_fn)(v));
|
render_data,
|
||||||
});
|
);
|
||||||
// TODO: or this happened on a submit
|
|
||||||
if last_value.is_some_and(|last_value| last_value != validation_result) {
|
builder.views.push(view);
|
||||||
validation_signal_set.set(validation_result.clone());
|
|
||||||
}
|
|
||||||
validation_result
|
|
||||||
});
|
|
||||||
full_builder.views.push(view);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn build(self) -> Form<FD> {
|
fn build_control_view<C: ControlData, FDT: 'static>(
|
||||||
match self.inner {
|
builder: &FullFormBuilder<FD, FS>,
|
||||||
FormBuilderInner::FullBuilder(full_builder) => Form {
|
field_fn: Rc<dyn FieldFn<FD, FDT>>,
|
||||||
fd: full_builder.fd_get,
|
unparse_fn: Box<dyn UnparseFn<<C as ControlData>::ReturnType, FDT>>,
|
||||||
validations: full_builder.validations,
|
parse_fn: Box<dyn ParseFn<<C as ControlData>::ReturnType, FDT>>,
|
||||||
// TODO: wrap in the style's form wrapper
|
validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
||||||
view: full_builder.views.into_view(),
|
render_data: crate::controls::ControlRenderData<FS, C>,
|
||||||
},
|
) -> View {
|
||||||
FormBuilderInner::ValidationBuilder { validations } => Form {
|
let (validation_signal, validation_signal_set) = create_signal(Ok(()));
|
||||||
fd: create_signal(FD::default()).0,
|
// TODO: The on-submit triggering a validation could be done here with a create_effect
|
||||||
validations,
|
let field_fn2 = field_fn.clone();
|
||||||
view: ().into_view(),
|
let fd_getter = builder.fd.read_only();
|
||||||
},
|
let value_getter = move || {
|
||||||
}
|
// TODO: ideally, this should not be borrowed. If we get a clone when we call `.get` we should pass in the clone, not borrow this clone
|
||||||
|
let mut fd = fd_getter.get();
|
||||||
|
let field = field_fn2(&mut fd);
|
||||||
|
(unparse_fn)(field)
|
||||||
|
};
|
||||||
|
let value_getter = value_getter.into_signal();
|
||||||
|
|
||||||
|
let fd = builder.fd.clone();
|
||||||
|
let value_setter = move |value| {
|
||||||
|
// TODO: also do this on a submit somehow
|
||||||
|
let parsed = match parse_fn(&value) {
|
||||||
|
Ok(p) => p,
|
||||||
|
Err(e) => {
|
||||||
|
validation_signal_set.set(Err(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// parse succeeded, update value and validate
|
||||||
|
let field_fn = field_fn.clone(); // move
|
||||||
|
let validation_fn = validation_fn.clone(); // move
|
||||||
|
fd.update(move |fd| {
|
||||||
|
let field = field_fn(fd);
|
||||||
|
*field = parsed;
|
||||||
|
|
||||||
|
validation_fn
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|vfn| vfn(fd).err())
|
||||||
|
.map(|e| validation_signal_set.set(Err(e)));
|
||||||
|
});
|
||||||
|
};
|
||||||
|
let value_setter = Box::new(value_setter);
|
||||||
|
// TODO: add a signal that triggers on submit to refresh the validation on_submit
|
||||||
|
|
||||||
|
C::build_control(
|
||||||
|
&builder.fs,
|
||||||
|
render_data,
|
||||||
|
value_getter,
|
||||||
|
value_setter,
|
||||||
|
validation_signal.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build(self) -> Option<Form<FD>> {
|
||||||
|
let builder = match self.inner {
|
||||||
|
FormBuilderInner::FullBuilder(fb) => fb,
|
||||||
|
FormBuilderInner::ValidationBuilder { validations: _ } => return None,
|
||||||
|
};
|
||||||
|
let view = builder.fs.form_frame(builder.views.into_view());
|
||||||
|
|
||||||
|
Some(Form {
|
||||||
|
fd: builder.fd,
|
||||||
|
validations: builder.validations,
|
||||||
|
view,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn validator(self) -> Validator<FD> {
|
fn validator(self) -> Validator<FD> {
|
||||||
match self.inner {
|
let validations = match self.inner {
|
||||||
FormBuilderInner::FullBuilder(full_builder) => Validator {
|
FormBuilderInner::FullBuilder(fb) => fb.validations,
|
||||||
validations: full_builder.validations,
|
FormBuilderInner::ValidationBuilder { validations } => validations,
|
||||||
},
|
};
|
||||||
FormBuilderInner::ValidationBuilder { validations } => Validator { validations },
|
Validator { validations }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -240,13 +307,13 @@ pub trait FormData: Default + Clone + 'static {
|
|||||||
fn get_form(style: Self::Style) -> Form<Self> {
|
fn get_form(style: Self::Style) -> Form<Self> {
|
||||||
let builder = FormBuilder::new_full_builder(style);
|
let builder = FormBuilder::new_full_builder(style);
|
||||||
let builder = Self::build_form(builder);
|
let builder = Self::build_form(builder);
|
||||||
builder.build()
|
builder.build().expect("builder should be full builder")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_form_with_starting_data(self, style: Self::Style) -> Form<Self> {
|
fn get_form_with_starting_data(self, style: Self::Style) -> Form<Self> {
|
||||||
let builder = FormBuilder::new_full_builder_with(self, style);
|
let builder = FormBuilder::new_full_builder_with(self, style);
|
||||||
let builder = Self::build_form(builder);
|
let builder = Self::build_form(builder);
|
||||||
builder.build()
|
builder.build().expect("builder should be full builder")
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_validator() -> Validator<Self> {
|
fn get_validator() -> Validator<Self> {
|
||||||
|
|||||||
@ -8,29 +8,33 @@ use crate::controls::{
|
|||||||
};
|
};
|
||||||
use leptos::{Signal, View};
|
use leptos::{Signal, View};
|
||||||
|
|
||||||
pub trait FormStyle: 'static {
|
pub trait FormStyle: Default + 'static {
|
||||||
type StylingAttributes;
|
type StylingAttributes;
|
||||||
|
|
||||||
// TODO: add form frame
|
fn form_frame(&self, children: View) -> View;
|
||||||
// TODO: perhaps we don't want to send the full control type anymore.
|
|
||||||
// as the rendering shouldn't depend on parse or validate anymore.
|
|
||||||
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View;
|
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View;
|
||||||
fn text_input(
|
fn text_input(
|
||||||
&self,
|
&self,
|
||||||
control: ControlRenderData<Self, TextInputData>,
|
control: ControlRenderData<Self, TextInputData>,
|
||||||
|
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Box<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> (View, Signal<<TextInputData as ControlData>::ReturnType>);
|
) -> View;
|
||||||
fn select(
|
fn select(
|
||||||
&self,
|
&self,
|
||||||
control: ControlRenderData<Self, SelectData>,
|
control: ControlRenderData<Self, SelectData>,
|
||||||
|
value_getter: Signal<<SelectData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Box<dyn Fn(<SelectData as ControlData>::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> (View, Signal<<SelectData as ControlData>::ReturnType>);
|
) -> View;
|
||||||
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View;
|
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View;
|
||||||
fn text_area(
|
fn text_area(
|
||||||
&self,
|
&self,
|
||||||
control: ControlRenderData<Self, TextAreaData>,
|
control: ControlRenderData<Self, TextAreaData>,
|
||||||
|
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Box<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> (View, Signal<<TextAreaData as ControlData>::ReturnType>);
|
) -> View;
|
||||||
// TODO: test custom component
|
// TODO: test custom component
|
||||||
fn custom_component(&self, view: View) -> View;
|
fn custom_component(&self, view: View) -> View;
|
||||||
// TODO: add group
|
// TODO: add group
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
use super::FormStyle;
|
use super::FormStyle;
|
||||||
use crate::controls::{
|
use crate::controls::{
|
||||||
heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData,
|
heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData,
|
||||||
text_input::TextInputData, ControlRenderData,
|
text_input::TextInputData, ControlData, ControlRenderData,
|
||||||
};
|
};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
|
|
||||||
@ -9,11 +9,22 @@ pub enum TailwindGridStylingAttributes {
|
|||||||
Width(u32),
|
Width(u32),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct TailwindGridFormStyle;
|
pub struct TailwindGridFormStyle;
|
||||||
|
|
||||||
impl FormStyle for TailwindGridFormStyle {
|
impl FormStyle for TailwindGridFormStyle {
|
||||||
type StylingAttributes = TailwindGridStylingAttributes;
|
type StylingAttributes = TailwindGridStylingAttributes;
|
||||||
|
|
||||||
|
// TODO: something about an on-submit thing
|
||||||
|
fn form_frame(&self, children: View) -> View {
|
||||||
|
view! {
|
||||||
|
<form>
|
||||||
|
{children}
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
.into_view()
|
||||||
|
}
|
||||||
|
|
||||||
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View {
|
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View {
|
||||||
view! {
|
view! {
|
||||||
<h2 class="text-xl py-2 text-center font-bold text-gray-700 border-b-2 border-gray-800/60 mb-8">
|
<h2 class="text-xl py-2 text-center font-bold text-gray-700 border-b-2 border-gray-800/60 mb-8">
|
||||||
@ -26,12 +37,11 @@ impl FormStyle for TailwindGridFormStyle {
|
|||||||
fn text_input(
|
fn text_input(
|
||||||
&self,
|
&self,
|
||||||
control: ControlRenderData<Self, TextInputData>,
|
control: ControlRenderData<Self, TextInputData>,
|
||||||
|
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Box<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> (View, Signal<String>) {
|
) -> View {
|
||||||
let (read, write) = create_signal(String::new());
|
view! {
|
||||||
|
|
||||||
leptos::logging::log!("Rendering text input");
|
|
||||||
let view = view! {
|
|
||||||
<div>
|
<div>
|
||||||
{move || format!("{:?}", validation_state.get())}
|
{move || format!("{:?}", validation_state.get())}
|
||||||
<label
|
<label
|
||||||
@ -46,24 +56,23 @@ impl FormStyle for TailwindGridFormStyle {
|
|||||||
id=&control.data.name
|
id=&control.data.name
|
||||||
name=control.data.name
|
name=control.data.name
|
||||||
placeholder=control.data.placeholder
|
placeholder=control.data.placeholder
|
||||||
value=control.data.initial_text
|
prop:value=move || value_getter.get()
|
||||||
on:change=move |ev| {
|
on:change=move |ev| {
|
||||||
write.set(event_target_value(&ev));
|
value_setter(event_target_value(&ev));
|
||||||
}
|
}
|
||||||
class="block w-full bg-gray-100 border-2 border-gray-300 text-gray-700 py-2 px-4 rounded-lg leading-tight focus:outline-none focus:bg-white focus:border-sky-400"
|
class="block w-full bg-gray-100 border-2 border-gray-300 text-gray-700 py-2 px-4 rounded-lg leading-tight focus:outline-none focus:bg-white focus:border-sky-400"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}.into_view();
|
}.into_view()
|
||||||
(view, read.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn select(
|
fn select(
|
||||||
&self,
|
&self,
|
||||||
control: ControlRenderData<Self, SelectData>,
|
control: ControlRenderData<Self, SelectData>,
|
||||||
|
value_getter: Signal<<SelectData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Box<dyn Fn(<SelectData as ControlData>::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> (View, Signal<String>) {
|
) -> View {
|
||||||
let (read, write) = create_signal(String::new());
|
|
||||||
|
|
||||||
let options_view = control
|
let options_view = control
|
||||||
.data
|
.data
|
||||||
.options
|
.options
|
||||||
@ -74,7 +83,7 @@ impl FormStyle for TailwindGridFormStyle {
|
|||||||
view! {
|
view! {
|
||||||
<option
|
<option
|
||||||
value={value}
|
value={value}
|
||||||
selected=move || read.get() == *cloned_value
|
selected=move || value_getter.get() == *cloned_value
|
||||||
>
|
>
|
||||||
*value
|
*value
|
||||||
</option>
|
</option>
|
||||||
@ -82,7 +91,7 @@ impl FormStyle for TailwindGridFormStyle {
|
|||||||
})
|
})
|
||||||
.collect_view();
|
.collect_view();
|
||||||
|
|
||||||
let view = view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
{move || format!("{:?}", validation_state.get())}
|
{move || format!("{:?}", validation_state.get())}
|
||||||
<select
|
<select
|
||||||
@ -90,15 +99,13 @@ impl FormStyle for TailwindGridFormStyle {
|
|||||||
name=control.data.name
|
name=control.data.name
|
||||||
class="block w-full bg-gray-100 border-2 border-gray-300 text-gray-700 py-2 px-4 rounded-lg leading-tight focus:outline-none focus:bg-white focus:border-sky-400"
|
class="block w-full bg-gray-100 border-2 border-gray-300 text-gray-700 py-2 px-4 rounded-lg leading-tight focus:outline-none focus:bg-white focus:border-sky-400"
|
||||||
on:change=move |ev| {
|
on:change=move |ev| {
|
||||||
write.set(event_target_value(&ev));
|
value_setter(event_target_value(&ev));
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{options_view}
|
{options_view}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
}.into_view();
|
}.into_view()
|
||||||
|
|
||||||
(view, read.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View {
|
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View {
|
||||||
@ -115,11 +122,11 @@ impl FormStyle for TailwindGridFormStyle {
|
|||||||
fn text_area(
|
fn text_area(
|
||||||
&self,
|
&self,
|
||||||
control: ControlRenderData<Self, TextAreaData>,
|
control: ControlRenderData<Self, TextAreaData>,
|
||||||
|
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Box<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> (View, Signal<String>) {
|
) -> View {
|
||||||
let (read, write) = create_signal(String::new());
|
view! {
|
||||||
|
|
||||||
let view = view! {
|
|
||||||
<div>
|
<div>
|
||||||
{move || format!("{:?}", validation_state.get())}
|
{move || format!("{:?}", validation_state.get())}
|
||||||
<textarea
|
<textarea
|
||||||
@ -127,14 +134,13 @@ impl FormStyle for TailwindGridFormStyle {
|
|||||||
name=control.data.name
|
name=control.data.name
|
||||||
placeholder=control.data.placeholder
|
placeholder=control.data.placeholder
|
||||||
class="block w-full bg-gray-100 border-2 border-gray-300 text-gray-700 py-2 px-4 rounded-lg leading-tight focus:outline-none focus:bg-white focus:border-sky-400"
|
class="block w-full bg-gray-100 border-2 border-gray-300 text-gray-700 py-2 px-4 rounded-lg leading-tight focus:outline-none focus:bg-white focus:border-sky-400"
|
||||||
|
prop:value=move || value_getter.get()
|
||||||
on:change=move |ev| {
|
on:change=move |ev| {
|
||||||
write.set(event_target_value(&ev));
|
value_setter(event_target_value(&ev));
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}.into_view();
|
}.into_view()
|
||||||
|
|
||||||
(view, read.into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn custom_component(&self, view: View) -> View {
|
fn custom_component(&self, view: View) -> View {
|
||||||
|
|||||||
Reference in New Issue
Block a user