Compare commits

...
17 Commits
Author SHA1 Message Date
mitchell f2397a47c5 doc 2024-06-19 14:34:35 -05:00
mitchell 3c96a15680 add signals where appropriate 2024-06-19 11:21:28 -05:00
mitchell c7c98f985f small tweaks 2024-06-19 09:39:51 -05:00
mitchell 7c4e3b162d doc 2024-06-19 09:13:08 -05:00
mitchell 5f4358277e add docs and refactor GFStyle 2024-06-18 18:30:16 -05:00
mitchell 1dc93d69f4 update docs 2024-06-18 15:42:23 -05:00
mitchell e16b7fc9cb add another way to build the form 2024-06-18 15:27:38 -05:00
mitchell 18a99ec181 add context and non-context version of the controls 2024-06-18 14:57:46 -05:00
mitchell 89375a5b0c conditional validation 2024-06-18 14:18:36 -05:00
mitchell bbde4d6331 context is now a Rc 2024-06-18 13:10:49 -05:00
mitchell da84bdbb27 Conditional Rendering WORKS! 2024-06-18 12:57:56 -05:00
mitchell 835271cd22 control flow 2024-06-18 11:12:26 -05:00
mitchell 0339a2ee96 change the privacy modifiers on the ControlData 2024-06-17 17:36:56 -05:00
mitchell 7d855e764f Merge pull request 'Add context' (#27) from feature/context into main
Reviewed-on: #27
2024-06-12 23:45:33 +00:00
mitchell fac8e515f6 remove the FS and CX generics 2024-06-12 18:44:43 -05:00
mitchell e371ff748b impl context 2024-06-12 17:36:46 -05:00
mitchell 4c6b6fa920 add context 2024-06-12 17:25:05 -05:00
24 changed files with 1787 additions and 776 deletions
+101 -55
View File
@@ -2,63 +2,109 @@
A declaritive way to create forms for [leptos](https://leptos.dev/). A declaritive way to create forms for [leptos](https://leptos.dev/).
leptos_form_tool allows you to define forms in a declaritive way, without specifying how to render each component. You define what controls and visual components the form should have, as well as how to parse and validate the data. That form definition can then be used to render a `View` for the form, or create a Validator so the client and server can both check the integrity of the data in the same way, without duplicating code. leptos_form_tool allows you to define forms in a declaritive way.
Want text box? Just call `.text_input` on the form builder. Then sepperatly,
you define a FormStyle to specify how that text box should be rendered.
This has a number of advantages:
- Sepperates the layout of the form from how it is rendered and styled
- Allows different styles to be swapped in and out quickly
The rendering of the form controls are defined seperatly from the form itself. This sepperation allows different styles to be swapped in and out, relatively easily and helps reduce code duplication. A particular style can be created by creating a type that implements the FormStyle trait. This FormStyle trait specifies functions for defining all the common form controls such as TextInput, Select, RadioButtons, etc. leptos_form_tool also support custom components if you want more controls than the just the common ones. Custom controls natrually cannot be fully styled with by the FormStyle. ## Validations
An implmentation of FormStyle might have some variables that should be defined on a per-component basis. Take, for example, a FormStyle that renders it's controls in a grid (like TWGridFormStyle). We would like to be able to define a width for how many columns in the grid that a control will take up. For this, the FormStyle also defines a type for it's attributes: You might find yourself asking, but why not just use components?
```rust
pub enum TWGridFormAttributes {
Width(u32),
}
```
With this, you add these styling attributes to components with the `style()` method.
Here is an example of how a form can be defined: The biggest reason for creating leptos_form_tool is support for
```rust validating the fields. This validation logic can get rather complex, for
// all FormData must implement Default and must be 'static instance, you likely want to preform validation on the client when the user
#[derive(Default)] clicks submit to immediatly give the user feedback about any invalid input.
// this struct should contain all the data that needs to be submitted. But you often also want to do the same validation on the server to protect
struct MyFormData { against any requests that don't come from your client or for a user that
name: String, doesn't have wasm enabled.
age: u32,
}
// now implment the FormData trait Additionally, you might want to change the validation of one control based
impl FormData for MyFormData { on the value of another. For example, you might want to make sure the "day"
// the form style must be decided now, as the available attributes depend on the FormStyle field is in a valid range, but that range depends on what the user selects in
type Style = TWGridFormStyle; the "month" field. Or you might want to make sure the "confirm password" field
fn build_form(FormBuilder fb) -> Form { matches the "password" field. leptos_form_tool makes this easy, as the
fb validation function you provide operates on the entire form's data.
.heading(|h| h.title("Tell Me About Yourself"))
.text_input(|t| {
t.label("Name")
.placeholder("Name")
.parse_fn(|name, form_data| Ok(form_data.name = name))
.validation_fn(|fd| {
if fd.name.is_empty() {
Err("Name is required")
} else if fd.name.len() >= 32 {
Err("Name must be shorter than 32 characters")
} else {
Ok(())
}
})
})
.text_input(|t| {
t.label("age")
.placeholder("age")
.parse_fn(|age, form_data| { Ok(form_data.age = age.parse().map_err(|e| e.to_string())?)}
.validation_fn(|form_data| {
if form_data.age > 99 {
Err("Please enter an age <= 99")
} else {
Ok(())
}
})
})
.submit(|s| s.text("Submit"))
}
}
``` Sometimes you might not want to show some controls, and validation for those
controls should only be done when they are visible.
lepos_form_tool takes care of all this for you.
## FormStyle
To define how to render all the components that a form might use, you define
a type that implements the `FormStyle` trait. This trait has a method for each
control that the form might need to render. Once you implement that trait you
just need to change the `Style` associated trait of your form to use that new
style.
Its actually a little more complicated than that...
To give custom styles a little more freedom to configure how to render their
controls on a per-control-basis, the style will define an associated type
(usually an enum) called `StylingAttributes`. A styling attribute can be added
to a control by calling `.style(/* style */)` on the control builder. These
styling attributes are accessable to the `FormStyle` implementation when
rendering that control.
Therefore, swaping out styles also requires swapping out all the `.style()`
calls.
## Builders
leptos_form_tool makes heavy use of the builder pattern. You will build the
form, controls and sometimes even validation functions by calling methods on
a builder that will construct the object based on the values you give it.
## Context
Sometimes, you might want to be able to use something from the form's context
to render the form. For example, you may want to use a user's token as context
and only render part of the form if they are an administrator. Or, you may
need to get the options for a certain drop-down dynamically. The form's context
is the solution to these problems.
On the form, you define the associated type `Context`. Then, when you construct
the `Form` object, you must provide the context. The context can be used in
the building of the form, and can change what is rendered. Each control
builder function has a context varient that allows you to use the context in
the building of the form.
To avoid a whole lot of headache, the context is immutable once passed into to
the form. However, you can have leptos signals in the context, as they dont
require mutable access to call get/set on the signal.
Since the context can change how the form is rendered, and what controls are
shown/hidden (thus changing what controls are validated), the context is
needed to validate the form data on the server side. If are sure that the
context doesn't change any of the validations, you don't have to make sure
the context is the same on client and server. If the context does change
how the form is validated, make sure to keep the context the same to maximize
the validation that can happen on the clients side, before the user even
submits the form.
## Custom Components
leptos_form_tool also supports custom components that can be defined in the
user space. Though less ergonomic, this keeps leptos_form_tool from putting
limits on what you can do. There are `custom_*` methods on the form builder
that allow you to add your component. Unfortunatly you cannot define methods
on the `ControlBuilder` to help build your controls data, so you must
construct the ControlData for your custom type before adding it to the form.
## Getting Started
To learn by example, see the example project.
To follow a Getting Started guide, see [`getting_started.md`].
## Contributing
To contribute, fork the repo and make a PR.
If you find a bug, feel free to open an issue.
By contributing, you agree that your changes are
subject to the license found in [`/LICENSE`].
+240
View File
@@ -0,0 +1,240 @@
# Getting Started
This guide will walk you through creating your first leptos_form_tool form.
## Form Data
Start by creating the form data.
This struct should contain all the data for the entire form.
```rust
// all FormToolData implementors must also implement Clone and be 'static
// Default and Debug are not required, but helpful
#[derive(Clone, Default, Debug)]
struct HelloWorldFormData {
first: String,
last: String,
age: u32,
sport: String,
}
```
## Defining the Form Layout
Then, to define how the for should be rendered, implement the `FormToolData`
trait. You will need to define the style that this form will use and what
context it will have.
It is required to define the style here, as the style needs to be known to
add the StylingAttributes to the controls.
The `build_form` method provides you with a `FormBuilder<Self>`. You can define
controls on the form by using the builder methods. Some controls don't accept
input from the user, they just display information. These are refered to as
`VanityControls` by leptos_form_tool. An example of a vanity control would
be a heading. Other controls do accept user input and are just refered to as
`Controls` by leptos_form_tool.
### Defining Controls
There are other builder methods defined the the ControlBuilder for certain
controls. For example, the TextInput builder has a `placeholder` method
that sets the placeholder of the text input.
When building a control, you will need to provide a getter and setter
to get the field and set the field on the form data. The getter is a function
that takes the full form data, and returns the field. The setter is a function
that takes the full form data and a value, and sets the field to that value.
Examples for these getters and setters are shown below.
A VanityControl will never need a setter, as they only display information.
Sometimes they need getters if the information they display is based on the
form data. For example, the output control can display information from the
form data, so it needs a getter.
#### Parse and Unparse Functions
Controls also need a set of parse and unparse functions. The type that the
control returns could be anything. For example, a range slider might return a
`i32`, a text input might return a `String`. leptos_form_tool needs a way to
convert the type of the field, to the type of the control and vice versa.
This is what the un/parse functions are for.
The parse and unparse functions can always be specified with the `parse_custom`
method, but there are several builder methods that create the parse functions
automatically. For example, if the field type and control type can be
converted to and from each other with the `From` trait (this is true if the
two types are the same) then you can call the `parse_from` method to
automatically create the un/parse functions using that conversion.
If the control's type is String, and the field type implements `FromStr` and
`ToString`, you can call `parse_string` to generate un/parse functions using
that trait. `parse_trimmed` does the same, but trims the string before parsing.
This should cover most use cases, but you always have the option to define
your own.
It is important to note that parsing from the control's type to the field type
IS allowed to fail. If it fails, it will be displayed just like any other
validation error. Conversion from the field type to the control type is NOT
allowed to fail; the FormData should always be able to be displayed.
#### Validation Functions
Validation functions can be defined on a field to add some extra criteria
for what counts as a valid entry. The validation function takes the entire
forms data. This allows you to use the entire state of the form to decide if
this field is valid or not. If any validation function fails, the form will
not be allowed to submit. In addition, when you build a validator, it will
collect all of the validation functions that you define on these fields.
leptos_form_tool provides a `ValidationBuilder` to help you build validation
functions, which, in some cases, might be easier than defining a closure
yourself.
```rust
impl FormToolData for HelloWorldFormData {
// The form style to use.
type Style = GridFormStyle;
// The external context needed for rendering the form.
// In this case, nothing.
type Context = ();
fn build_form(fb: FormBuilder<Self>) -> FormBuilder<Self> {
fb.heading(|h| h.title("Welcome"))
.text_input(|t| {
t.named("data[first]")
.labeled("First Name")
.getter(|fd| fd.first)
.setter(|fd, value| fd.first = value)
// trim the string before writing to the field
.parse_trimmed()
.validation_fn(
// Using the ValidationBuilder to set a required field
ValidationBuilder::for_field(|fd: &HelloWorldFormData| fd.first.as_str())
.named("First Name")
.required()
.build(),
)
// width out of 12
.style(Width(4))
// defines text that shows up when hovering over it
.style(Tooltip("Your given first name".to_string()))
})
.text_input(|t| {
t.named("data[last]")
.labeled("Last Name")
.getter(|fd| fd.last)
.setter(|fd, value| fd.last = value)
// dont trim the string, just write it to the field
.parse_from()
.validation_fn(
// using the ValidationBuilder to set a required field
ValidationBuilder::for_field(|fd: &HelloWorldFormData| fd.last.as_str())
.named("Last Name")
.required()
.build(),
)
.style(Width(8))
.style(Tooltip("Your last name".to_string()))
})
// using the _cx varient allows access to the context
// in this case, its `()` which doesnt help us that much.
.stepper_cx(|s, _cx| {
s.named("data[age]")
.labeled("Age")
.getter(|fd| fd.age)
.setter(|fd, value| fd.age = value)
// trim the string then try to parse to a `u32`
.parse_trimmed()
.validation_fn(move |fd| {
// defining a validation function with a closure
(fd.age > 13)
.then_some(())
.ok_or_else(|| String::from("Too Young"))
})
.style(Width(6))
.style(Tooltip("Your age in years".to_string()))
})
.select(|s| {
s.named("data[sport]")
.labeled("Favorite Sport")
.getter(|fd| fd.sport)
.setter(|fd, value| fd.sport = value)
.parse_from()
// set the options for the select along with their values
.with_options_valued(vec![
("Football", "football"),
("Soccer", "soccer"),
("Ice Hockey", "ice_hockey"),
("Golf", "golf"),
].into_iter())
.style(Width(6))
})
.submit(|s| s.text("Submit"))
}
}
```
Now, using the form is quite simple. You just need to provide the form data,
style, context, and where the form should point to.
```rust
let form = ExampleFormData::default().get_plain_form("/api/endpoint", GridFormStyle::default(), ());
view! {
<div>
<h1> "This is My Form!" </h1>
{form}
</div>
}
```
You can also have the form build to an `ActionForm`, this will be very
familiar if you've used an `ActionForm` before.
You might have noticed the goofy names that we put in our form above, like
"data[first]" instead of just "first". This is done to allow the form to use
the SubmitForm as an action. See
[ActionForms](https://book.leptos.dev/progressive_enhancement/action_form.html#complex-inputs)
in the leptos book for more.
```rust
#[component]
fn FormWrapper() -> impl IntoView {
let server_fn_action = create_server_action::<SubmitForm>();
let form = HelloWorldFormData::default().get_action_form(server_fn_action, GridFormStyle::default(), ());
let response = server_fn_action.value();
view! {
<div>
<h1> "This is My Form!" </h1>
// display the form
{form}
// display the result from the server
{move || response.get().map(|result| result.ok())}
</div>
}
}
#[server(SubmitForm)]
async fn submit_form(data: HelloWorldFormData) -> Result<String, ServerFnError> {
data.validate(()).map_err(ServerFnError::new)?;
Ok(format!(
"Hello {} {} ({}), You must like {}!",
data.first, data.last, data.age, data.sport
))
}
```
Lastly there is the `get_form` method. This almost identical to the ActionForm
version. In fact, if you do everything right, you wont even notice a
difference. Under the hood, this sends your FormToolData struct directly
by calling the server function, whereas the `ActionForm` version will try
to construct your type using the
[`FromFormData`](https://docs.rs/leptos_router/latest/leptos_router/trait.FromFormData.html)
trait. Using the `get_form` method instead will allow you to name the inputs
whatever you want (though you should try to name them correctly anyway to
support progressive enhancement) and it will still work. The example is the
same as above, just replace `get_action_form` with `get_form`.
+64 -18
View File
@@ -1,15 +1,18 @@
use super::ControlRenderData; use super::{BuilderCxFn, BuilderFn, ControlRenderData, ShowWhenFn};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::view;
use leptos::RwSignal; use leptos::RwSignal;
use leptos::Show;
use leptos::Signal;
use std::rc::Rc; use std::rc::Rc;
use web_sys::MouseEvent; use web_sys::MouseEvent;
type ButtonAction<FD> = dyn Fn(MouseEvent, &mut FD); type ButtonAction<FD> = dyn Fn(MouseEvent, RwSignal<FD>) + 'static;
#[derive(Clone)] /// Data used for the button control.
pub struct ButtonData<FD: FormToolData> { pub struct ButtonData<FD: FormToolData> {
pub(crate) text: String, pub text: String,
pub(crate) action: Option<Rc<ButtonAction<FD>>>, pub action: Option<Rc<ButtonAction<FD>>>,
} }
impl<FD: FormToolData> Default for ButtonData<FD> { impl<FD: FormToolData> Default for ButtonData<FD> {
fn default() -> Self { fn default() -> Self {
@@ -19,22 +22,50 @@ impl<FD: FormToolData> Default for ButtonData<FD> {
} }
} }
} }
impl<FD: FormToolData> Clone for ButtonData<FD> {
fn clone(&self) -> Self {
ButtonData {
text: self.text.clone(),
action: self.action.clone(),
}
}
}
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
pub fn button( /// Builds a button and adds it to the form.
mut self, pub fn button(self, builder: impl BuilderFn<ButtonBuilder<FD>>) -> Self {
builder: impl Fn(ButtonBuilder<FD, FS>) -> ButtonBuilder<FD, FS>,
) -> Self {
let button_builder = ButtonBuilder::new(); let button_builder = ButtonBuilder::new();
let control = builder(button_builder); let control = builder(button_builder);
self.button_helper(control)
}
/// Builds a button using the form's context and adds it to the form.
pub fn button_cx(self, builder: impl BuilderCxFn<ButtonBuilder<FD>, FD::Context>) -> Self {
let button_builder = ButtonBuilder::new();
let control = builder(button_builder, self.cx.clone());
self.button_helper(control)
}
/// The common functionality for adding a button.
fn button_helper(mut self, control: ButtonBuilder<FD>) -> Self {
let render_data = ControlRenderData { let render_data = ControlRenderData {
data: control.data, data: control.data,
styles: control.styles, styles: control.styles,
}; };
let show_when = control.show_when;
let render_fn = move |fs: &FS, fd: RwSignal<FD>| { let cx = self.cx.clone();
let view = fs.button(render_data, fd); let render_fn = move |fs: Rc<FD::Style>, fd: RwSignal<FD>| {
let render_data = Rc::new(render_data);
let view = move || fs.clone().button(render_data.clone(), fd);
let view = match show_when {
Some(when) => {
let when = move || when(fd.into(), cx.clone());
view! { <Show when=when>{view.clone()}</Show> }
}
None => view(),
};
(view, None) (view, None)
}; };
self.render_fns.push(Box::new(render_fn)); self.render_fns.push(Box::new(render_fn));
@@ -43,31 +74,46 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
} }
} }
#[derive(Clone)] /// The struct that allows you to specify the attributes of the button.
pub struct ButtonBuilder<FD: FormToolData, FS: FormStyle> { pub struct ButtonBuilder<FD: FormToolData> {
pub(crate) styles: Vec<FS::StylingAttributes>, pub(crate) styles: Vec<<FD::Style as FormStyle>::StylingAttributes>,
pub(crate) data: ButtonData<FD>, pub(crate) data: ButtonData<FD>,
pub(crate) show_when: Option<Box<dyn ShowWhenFn<FD, FD::Context>>>,
} }
impl<FD: FormToolData, FS: FormStyle> ButtonBuilder<FD, FS> { impl<FD: FormToolData> ButtonBuilder<FD> {
/// Creates a new [`ButtonBuilder`].
fn new() -> Self { fn new() -> Self {
ButtonBuilder { ButtonBuilder {
styles: Vec::default(), styles: Vec::default(),
data: ButtonData::default(), data: ButtonData::default(),
show_when: None,
} }
} }
pub fn style(mut self, style: FS::StylingAttributes) -> Self { /// Sets the function to decide when to render the control.
pub fn show_when(
mut self,
when: impl Fn(Signal<FD>, Rc<FD::Context>) -> bool + 'static,
) -> Self {
self.show_when = Some(Box::new(when));
self
}
/// Adds a styling attribute to the button.
pub fn style(mut self, style: <FD::Style as FormStyle>::StylingAttributes) -> Self {
self.styles.push(style); self.styles.push(style);
self self
} }
/// Sets the text of the button.
pub fn text(mut self, text: impl ToString) -> Self { pub fn text(mut self, text: impl ToString) -> Self {
self.data.text = text.to_string(); self.data.text = text.to_string();
self self
} }
pub fn action(mut self, action: impl Fn(MouseEvent, &mut FD) + 'static) -> Self { /// Sets the action that is preformed when the button is clicked.
pub fn action(mut self, action: impl Fn(MouseEvent, RwSignal<FD>) + 'static) -> Self {
self.data.action = Some(Rc::new(action)); self.data.action = Some(Rc::new(action));
self self
} }
+29 -12
View File
@@ -1,12 +1,13 @@
use leptos::{Signal, View}; use super::{BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData};
use super::{ControlBuilder, ControlData, ControlRenderData};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{Signal, View};
use std::rc::Rc;
/// Data used for the checkbox control.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct CheckboxData { pub struct CheckboxData {
pub(crate) name: String, pub name: String,
pub(crate) label: Option<String>, pub label: Option<String>,
} }
impl ControlData for CheckboxData { impl ControlData for CheckboxData {
@@ -14,32 +15,48 @@ impl ControlData for CheckboxData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>, value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>, value_setter: Rc<dyn Fn(Self::ReturnType)>,
_validation_state: Signal<Result<(), String>>, _validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
fs.checkbox(control, value_getter, value_setter) fs.checkbox(control, value_getter, value_setter)
} }
} }
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a checkbox and adds it to the form.
pub fn checkbox<FDT: Clone + PartialEq + 'static>( pub fn checkbox<FDT: Clone + PartialEq + 'static>(
self, self,
builder: impl Fn( builder: impl BuilderFn<ControlBuilder<FD, CheckboxData, FDT>>,
ControlBuilder<FD, FS, CheckboxData, FDT>,
) -> ControlBuilder<FD, FS, CheckboxData, FDT>,
) -> Self { ) -> Self {
self.new_control(builder) self.new_control(builder)
} }
/// Builds a checkbox using the form's context and adds it to the form.
pub fn checkbox_cx<FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderCxFn<ControlBuilder<FD, CheckboxData, FDT>, FD::Context>,
) -> Self {
self.new_control_cx(builder)
}
} }
impl<FD: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, CheckboxData, FDT> { impl<FD: FormToolData, FDT> ControlBuilder<FD, CheckboxData, FDT> {
/// Sets the name of the checkbox.
///
/// This is used for the html element's "name" attribute.
/// In forms, the name attribute is the key that the data is sent
/// with.
///
/// For checkbox controls, the value "checked" is sent or no key value
/// pair is sent.
pub fn named(mut self, control_name: impl ToString) -> Self { pub fn named(mut self, control_name: impl ToString) -> Self {
self.data.name = control_name.to_string(); self.data.name = control_name.to_string();
self self
} }
/// Sets the text of the checkbox's label.
pub fn labeled(mut self, label: impl ToString) -> Self { pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string()); self.data.label = Some(label.to_string());
self self
+30 -5
View File
@@ -1,11 +1,12 @@
use super::{ControlBuilder, ControlData}; use super::{BuilderCxFn, BuilderFn, ControlBuilder, ControlData};
use crate::{styles::FormStyle, FormBuilder, FormToolData}; use crate::{FormBuilder, FormToolData};
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a custom component and adds it to the form.
pub fn custom<CC: ControlData, FDT: Clone + PartialEq + 'static>( pub fn custom<CC: ControlData, FDT: Clone + PartialEq + 'static>(
mut self, mut self,
control_data: CC, control_data: CC,
builder: impl Fn(ControlBuilder<FD, FS, CC, FDT>) -> ControlBuilder<FD, FS, CC, FDT>, builder: impl BuilderFn<ControlBuilder<FD, CC, FDT>>,
) -> Self { ) -> Self {
let control_builder = ControlBuilder::new(control_data); let control_builder = ControlBuilder::new(control_data);
let control = builder(control_builder); let control = builder(control_builder);
@@ -13,10 +14,34 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
self self
} }
/// Builds a custom component using the form's context and adds it to the
/// form.
pub fn custom_cx<CC: ControlData, FDT: Clone + PartialEq + 'static>(
mut self,
control_data: CC,
builder: impl BuilderCxFn<ControlBuilder<FD, CC, FDT>, FD::Context>,
) -> Self {
let control_builder = ControlBuilder::new(control_data);
let control = builder(control_builder, self.cx.clone());
self.add_control(control);
self
}
/// Builds a custom component, starting with the default
/// CustomControlData, and adds it to the form.
pub fn custom_default<CC: Default + ControlData, FDT: Clone + PartialEq + 'static>( pub fn custom_default<CC: Default + ControlData, FDT: Clone + PartialEq + 'static>(
self, self,
builder: impl Fn(ControlBuilder<FD, FS, CC, FDT>) -> ControlBuilder<FD, FS, CC, FDT>, builder: impl BuilderFn<ControlBuilder<FD, CC, FDT>>,
) -> Self { ) -> Self {
self.new_control(builder) self.new_control(builder)
} }
/// Builds a custom component, starting with the default
/// CustomControlData using the form's context, and adds it to the form.
pub fn custom_default_cx<CC: Default + ControlData, FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderCxFn<ControlBuilder<FD, CC, FDT>, FD::Context>,
) -> Self {
self.new_control_cx(builder)
}
} }
+18 -8
View File
@@ -1,27 +1,37 @@
use super::ValidationCb; use std::rc::Rc;
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use super::{ControlRenderData, ValidationCb};
use crate::styles::FormStyle;
use crate::{form::FormToolData, form_builder::FormBuilder};
use leptos::{CollectView, RwSignal}; use leptos::{CollectView, RwSignal};
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
pub fn group(mut self, builder: impl Fn(FormBuilder<FD, FS>) -> FormBuilder<FD, FS>) -> Self { /// Creates a form group.
let mut group_builder = FormBuilder::new(); ///
/// This creates a subsection of the form that controls can be added to
/// like a normal form.
pub fn group(mut self, builder: impl Fn(FormBuilder<FD>) -> FormBuilder<FD>) -> Self {
let mut group_builder = FormBuilder::new_group(self.cx.clone());
group_builder = builder(group_builder); group_builder = builder(group_builder);
for validation in group_builder.validations { for validation in group_builder.validations {
self.validations.push(validation); self.validations.push(validation);
} }
let render_fn = move |fs: &FS, fd: RwSignal<FD>| { let render_fn = move |fs: Rc<FD::Style>, fd: RwSignal<FD>| {
let (views, validation_cbs): (Vec<_>, Vec<_>) = group_builder let (views, validation_cbs): (Vec<_>, Vec<_>) = group_builder
.render_fns .render_fns
.into_iter() .into_iter()
.map(|r_fn| r_fn(fs, fd)) .map(|r_fn| r_fn(fs.clone(), fd))
.unzip(); .unzip();
let view = fs.group(super::ControlRenderData { let render_data = Rc::new(ControlRenderData {
data: views.collect_view(), data: views.collect_view(),
styles: group_builder.styles, styles: group_builder.styles,
}); });
let view = fs.group(render_data.clone());
let validation_cb = move || { let validation_cb = move || {
let mut success = true; let mut success = true;
for validation in validation_cbs.iter().flatten() { for validation in validation_cbs.iter().flatten() {
+27 -14
View File
@@ -1,36 +1,49 @@
use super::{ControlRenderData, VanityControlBuilder, VanityControlData}; use super::{BuilderCxFn, BuilderFn, ControlRenderData, VanityControlBuilder, VanityControlData};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::View; use leptos::{MaybeSignal, Signal, View};
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] /// Data used for the heading control.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct HeadingData { pub struct HeadingData {
pub(crate) title: String, pub title: MaybeSignal<String>,
} }
impl VanityControlData for HeadingData { impl VanityControlData for HeadingData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
_value_getter: Option<leptos::prelude::Signal<String>>, _value_getter: Option<leptos::prelude::Signal<String>>,
) -> View { ) -> View {
fs.heading(control) fs.heading(control)
} }
} }
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
pub fn heading( /// Builds a heading and adds it to the form.
self, pub fn heading(self, builder: impl BuilderFn<VanityControlBuilder<FD, HeadingData>>) -> Self {
builder: impl Fn(
VanityControlBuilder<FD, FS, HeadingData>,
) -> VanityControlBuilder<FD, FS, HeadingData>,
) -> Self {
self.new_vanity(builder) self.new_vanity(builder)
} }
/// Builds a hehading using the form's context and adds it to the form.
pub fn heading_cx(
self,
builder: impl BuilderCxFn<VanityControlBuilder<FD, HeadingData>, FD::Context>,
) -> Self {
self.new_vanity_cx(builder)
}
} }
impl<FD: FormToolData, FS: FormStyle> VanityControlBuilder<FD, FS, HeadingData> { impl<FD: FormToolData> VanityControlBuilder<FD, HeadingData> {
/// Sets the title of this heading.
pub fn title(mut self, title: impl ToString) -> Self { pub fn title(mut self, title: impl ToString) -> Self {
self.data.title = title.to_string(); self.data.title = MaybeSignal::Static(title.to_string());
self
}
/// Sets the title of this heading to a signal.
pub fn title_signal(mut self, title: Signal<String>) -> Self {
self.data.title = MaybeSignal::Dynamic(title);
self self
} }
} }
+43 -17
View File
@@ -1,32 +1,58 @@
use leptos::{Signal, View}; use super::{
BuilderCxFn, BuilderFn, ControlRenderData, GetterVanityControlData, VanityControlBuilder,
use super::{ControlBuilder, ControlData, ControlRenderData}; VanityControlData,
};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{Signal, View};
use std::rc::Rc;
/// Data used for the hidden control.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct HiddenData; pub struct HiddenData {
pub name: String,
impl ControlData for HiddenData { }
type ReturnType = String;
impl VanityControlData for HiddenData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>, value_getter: Option<Signal<String>>,
_value_setter: Box<dyn Fn(Self::ReturnType)>,
_validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
fs.hidden(control, value_getter) fs.hidden(control, value_getter)
} }
} }
impl GetterVanityControlData for HiddenData {}
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
pub fn hidden<FDT: Clone + PartialEq + 'static>( /// Builds a hidden form control and adds it to the form.
///
/// This will be an input in the html form allowing you to insert some
/// data the you might not want the user modifying.
pub fn hidden(self, builder: impl BuilderFn<VanityControlBuilder<FD, HiddenData>>) -> Self {
self.new_vanity(builder)
}
/// Builds a hidden form control using the form's context and adds it to
/// the form.
///
/// This will be an input in the html form allowing you to insert some
/// data the you might not want the user modifying.
pub fn hidden_cx(
self, self,
builder: impl Fn( builder: impl BuilderCxFn<VanityControlBuilder<FD, HiddenData>, FD::Context>,
ControlBuilder<FD, FS, HiddenData, FDT>,
) -> ControlBuilder<FD, FS, HiddenData, FDT>,
) -> Self { ) -> Self {
self.new_control(builder) self.new_vanity_cx(builder)
}
}
impl<FD: FormToolData> VanityControlBuilder<FD, HiddenData> {
/// Sets the name of the hidden control.
///
/// This is used for the html element's "name" attribute.
/// In forms, the name attribute is the key that the data is sent
/// with.
pub fn named(mut self, control_name: impl ToString) -> Self {
self.data.name = control_name.to_string();
self
} }
} }
+67 -32
View File
@@ -18,26 +18,32 @@ pub mod submit;
pub mod text_area; pub mod text_area;
pub mod text_input; pub mod text_input;
pub trait ValidationFn<FDT>: Fn(&FDT) -> Result<(), String> + 'static {} pub trait BuilderFn<B>: Fn(B) -> B {}
pub trait BuilderCxFn<B, CX>: Fn(B, Rc<CX>) -> B {}
pub trait ValidationFn<FD: ?Sized>: Fn(&FD) -> Result<(), String> + 'static {}
pub trait ValidationCb: Fn() -> bool + 'static {} pub trait ValidationCb: Fn() -> bool + 'static {}
pub trait ParseFn<CR, FDT>: Fn(CR) -> Result<FDT, String> + 'static {} 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>: pub trait ShowWhenFn<FD: 'static, CX>: Fn(Signal<FD>, Rc<CX>) -> bool + 'static {}
FnOnce(&FS, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + 'static pub trait RenderFn<FS, FD: 'static>:
FnOnce(Rc<FS>, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + 'static
{ {
} }
// implement the traits for all valid types // implement the traits for all valid types
impl<B, T> BuilderFn<B> for T where T: Fn(B) -> B {}
impl<B, CX, T> BuilderCxFn<B, CX> for T where T: Fn(B, Rc<CX>) -> B {}
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 {}
impl<T> ValidationCb for T where T: Fn() -> bool + 'static {} impl<T> ValidationCb for T where T: Fn() -> bool + 'static {}
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 {}
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 impl<FD: 'static, CX, F> ShowWhenFn<FD, CX> for F where F: Fn(Signal<FD>, Rc<CX>) -> bool + 'static {}
F: FnOnce(&FS, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + 'static impl<FS, FD: 'static, F> RenderFn<FS, FD> for F where
F: FnOnce(Rc<FS>, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + 'static
{ {
} }
@@ -46,7 +52,7 @@ pub trait VanityControlData: 'static {
/// Builds the control, returning the [`View`] that was built. /// 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: Rc<ControlRenderData<FS, Self>>,
value_getter: Option<Signal<String>>, value_getter: Option<Signal<String>>,
) -> View; ) -> View;
} }
@@ -60,9 +66,9 @@ pub trait ControlData: 'static {
/// Builds the control, returning the [`View`] that was built. /// 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: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>, value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>, value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>, validation_state: Signal<Result<(), String>>,
) -> View; ) -> View;
} }
@@ -75,46 +81,61 @@ pub struct ControlRenderData<FS: FormStyle + ?Sized, C: ?Sized> {
} }
/// The data needed to render a read-only control of type `C`. /// The data needed to render a read-only control of type `C`.
pub struct VanityControlBuilder<FD: FormToolData, FS: FormStyle, C: VanityControlData> { pub struct VanityControlBuilder<FD: FormToolData, C: VanityControlData> {
pub(crate) style_attributes: Vec<FS::StylingAttributes>, pub(crate) style_attributes: Vec<<FD::Style as FormStyle>::StylingAttributes>,
pub(crate) data: C, pub(crate) data: C,
pub(crate) getter: Option<Rc<dyn FieldGetter<FD, String>>>, pub(crate) getter: Option<Rc<dyn FieldGetter<FD, String>>>,
pub(crate) show_when: Option<Box<dyn ShowWhenFn<FD, FD::Context>>>,
} }
pub(crate) struct BuiltVanityControlData<FD: FormToolData, FS: FormStyle, C: VanityControlData> { pub(crate) struct BuiltVanityControlData<FD: FormToolData, C: VanityControlData> {
pub(crate) render_data: ControlRenderData<FS, C>, pub(crate) render_data: ControlRenderData<FD::Style, C>,
pub(crate) getter: Option<Rc<dyn FieldGetter<FD, String>>>, pub(crate) getter: Option<Rc<dyn FieldGetter<FD, String>>>,
pub(crate) show_when: Option<Box<dyn ShowWhenFn<FD, FD::Context>>>,
} }
impl<FD: FormToolData, FS: FormStyle, C: VanityControlData> VanityControlBuilder<FD, FS, C> { impl<FD: FormToolData, C: VanityControlData> VanityControlBuilder<FD, C> {
/// Creates a new [`VanityControlBuilder`] with the given [`VanityControlData`]. /// 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,
style_attributes: Vec::new(), style_attributes: Vec::new(),
getter: None, getter: None,
show_when: None,
} }
} }
/// Builds the builder into the data needed to render the control. /// Builds the builder into the data needed to render the control.
pub(crate) fn build(self) -> BuiltVanityControlData<FD, FS, C> { pub(crate) fn build(self) -> BuiltVanityControlData<FD, C> {
BuiltVanityControlData { BuiltVanityControlData {
render_data: ControlRenderData { render_data: ControlRenderData {
data: self.data, data: self.data,
styles: self.style_attributes, styles: self.style_attributes,
}, },
getter: self.getter, getter: self.getter,
show_when: self.show_when,
} }
} }
/// Sets the function to decide when to render the control.
///
/// Validations for components that are not shown DO NOT run.
pub fn show_when(
mut self,
when: impl Fn(Signal<FD>, Rc<FD::Context>) -> bool + 'static,
) -> Self {
self.show_when = Some(Box::new(when));
self
}
/// Adds a styling attribute to this control. /// Adds a styling attribute to this control.
pub fn style(mut self, attribute: FS::StylingAttributes) -> Self { pub fn style(mut self, attribute: <FD::Style as FormStyle>::StylingAttributes) -> Self {
self.style_attributes.push(attribute); self.style_attributes.push(attribute);
self self
} }
} }
impl<FD: FormToolData, FS: FormStyle, C: GetterVanityControlData> VanityControlBuilder<FD, FS, C> { impl<FD: FormToolData, C: GetterVanityControlData> VanityControlBuilder<FD, C> {
/// Sets the getter function. /// Sets the getter function.
/// ///
/// This function can get a string from the form data to be displayed /// This function can get a string from the form data to be displayed
@@ -151,27 +172,29 @@ impl Display for ControlBuildError {
} }
/// The data returned fomr a control's build function. /// 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, C: ControlData, FDT> {
pub(crate) render_data: ControlRenderData<FS, C>, pub(crate) render_data: ControlRenderData<FD::Style, C>,
pub(crate) getter: Rc<dyn FieldGetter<FD, FDT>>, pub(crate) getter: Rc<dyn FieldGetter<FD, FDT>>,
pub(crate) setter: Rc<dyn FieldSetter<FD, FDT>>, pub(crate) setter: Rc<dyn FieldSetter<FD, FDT>>,
pub(crate) parse_fn: Box<dyn ParseFn<C::ReturnType, FDT>>, pub(crate) parse_fn: Box<dyn ParseFn<C::ReturnType, FDT>>,
pub(crate) unparse_fn: Box<dyn UnparseFn<C::ReturnType, FDT>>, pub(crate) unparse_fn: Box<dyn UnparseFn<C::ReturnType, FDT>>,
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>, pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
pub(crate) show_when: Option<Rc<dyn ShowWhenFn<FD, FD::Context>>>,
} }
/// A builder for a interactive control. /// A builder for a interactive control.
pub struct ControlBuilder<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> { pub struct ControlBuilder<FD: FormToolData, 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>>>,
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 UnparseFn<C::ReturnType, FDT>>>, pub(crate) unparse_fn: Option<Box<dyn UnparseFn<C::ReturnType, FDT>>>,
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>, pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
pub(crate) style_attributes: Vec<FS::StylingAttributes>, pub(crate) style_attributes: Vec<<FD::Style as FormStyle>::StylingAttributes>,
pub(crate) show_when: Option<Rc<dyn ShowWhenFn<FD, FD::Context>>>,
pub data: C, pub data: C,
} }
impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C, FDT> { impl<FD: FormToolData, C: ControlData, FDT> ControlBuilder<FD, C, FDT> {
/// Creates a new [`ControlBuilder`] with the given [`ControlData`]. /// Creates a new [`ControlBuilder`] with the given [`ControlData`].
pub(crate) fn new(data: C) -> Self { pub(crate) fn new(data: C) -> Self {
ControlBuilder { ControlBuilder {
@@ -182,13 +205,14 @@ impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS
unparse_fn: None, unparse_fn: None,
validation_fn: None, validation_fn: None,
style_attributes: Vec::new(), style_attributes: Vec::new(),
show_when: None,
} }
} }
/// Builds the builder into the data needed to render the control. /// Builds the builder into the data needed to render the control.
/// ///
/// This fails if a required field was not specified. /// 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, C, FDT>, ControlBuildError> {
let getter = match self.getter { let getter = match self.getter {
Some(getter) => getter, Some(getter) => getter,
None => return Err(ControlBuildError::MissingGetter), None => return Err(ControlBuildError::MissingGetter),
@@ -216,9 +240,21 @@ impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS
parse_fn, parse_fn,
unparse_fn, unparse_fn,
validation_fn: self.validation_fn, validation_fn: self.validation_fn,
show_when: self.show_when,
}) })
} }
/// Sets the function to decide when to render the control.
///
/// Validations for components that are not shown DO NOT run.
pub fn show_when(
mut self,
when: impl Fn(Signal<FD>, Rc<FD::Context>) -> bool + 'static,
) -> Self {
self.show_when = Some(Rc::new(when));
self
}
/// Sets the getter function. /// Sets the getter function.
/// ///
/// This function should get the field from the form data /// This function should get the field from the form data
@@ -241,7 +277,7 @@ impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS
self self
} }
/// Sets the parse functions to the ones given /// Sets the parse functions to the ones given.
/// ///
/// The parse and unparse functions define how to turn what the user /// The parse and unparse functions define how to turn what the user
/// types in the form into what is stored in the form data struct and /// types in the form into what is stored in the form data struct and
@@ -257,16 +293,15 @@ impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS
} }
/// Adds a styling attribute to this control. /// Adds a styling attribute to this control.
pub fn style(mut self, attribute: FS::StylingAttributes) -> Self { pub fn style(mut self, attribute: <FD::Style as FormStyle>::StylingAttributes) -> Self {
self.style_attributes.push(attribute); self.style_attributes.push(attribute);
self self
} }
} }
impl<FD, FS, C, FDT> ControlBuilder<FD, FS, C, FDT> impl<FD, C, FDT> ControlBuilder<FD, C, FDT>
where where
FD: FormToolData, FD: FormToolData,
FS: FormStyle,
C: ControlData, C: ControlData,
FDT: TryFrom<<C as ControlData>::ReturnType>, FDT: TryFrom<<C as ControlData>::ReturnType>,
<FDT as TryFrom<<C as ControlData>::ReturnType>>::Error: ToString, <FDT as TryFrom<<C as ControlData>::ReturnType>>::Error: ToString,
@@ -289,17 +324,16 @@ where
} }
} }
impl<FD, FS, C, FDT> ControlBuilder<FD, FS, C, FDT> impl<FD, C, FDT> ControlBuilder<FD, C, FDT>
where where
FD: FormToolData, FD: FormToolData,
FS: FormStyle,
C: ControlData<ReturnType = String>, C: ControlData<ReturnType = String>,
FDT: FromStr + ToString, FDT: FromStr + ToString,
<FDT as FromStr>::Err: ToString, <FDT as FromStr>::Err: ToString,
{ {
/// Sets the parse functions to use the [`FromStr`] [`ToString`] and traits /// Sets the parse functions to use the [`FromStr`] [`ToString`] and traits
/// for parsing and unparsing respectively. To trim the string before parsing, /// for parsing and unparsing respectively. To trim the string before
/// see [`parse_trimed_string`]. /// parsing, see [`parse_trimmed`](Self::parse_trimmed)().
/// ///
/// The parse and unparse functions define how to turn what the user /// The parse and unparse functions define how to turn what the user
/// types in the form into what is stored in the form data struct and /// types in the form into what is stored in the form data struct and
@@ -315,7 +349,8 @@ where
} }
/// Sets the parse functions to use the [`FromStr`] [`ToString`] and traits /// Sets the parse functions to use the [`FromStr`] [`ToString`] and traits
/// for parsing and unparsing respectively, similar to [`parse_string`]. /// for parsing and unparsing respectively, similar to
/// [`parse_string`](Self::parse_string)().
/// However, this method trims the string before parsing. /// However, this method trims the string before parsing.
/// ///
/// The parse and unparse functions define how to turn what the user /// The parse and unparse functions define how to turn what the user
@@ -333,7 +368,7 @@ where
} }
} }
impl<FD: FormToolData, FS: FormStyle, C: ValidatedControlData, FDT> ControlBuilder<FD, FS, C, FDT> { impl<FD: FormToolData, C: ValidatedControlData, FDT> ControlBuilder<FD, C, FDT> {
/// Sets the validation function for this control /// Sets the validation function for this control
/// ///
/// This allows you to check if the parsed value is a valid value. /// This allows you to check if the parsed value is a valid value.
+26 -11
View File
@@ -1,15 +1,19 @@
use leptos::{Signal, View}; use super::{
BuilderCxFn, BuilderFn, ControlRenderData, GetterVanityControlData, VanityControlBuilder,
use super::{ControlRenderData, GetterVanityControlData, VanityControlBuilder, VanityControlData}; VanityControlData,
};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{Signal, View};
use std::rc::Rc;
/// Data used for the output control.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct OutputData; pub struct OutputData;
impl VanityControlData for OutputData { impl VanityControlData for OutputData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
value_getter: Option<Signal<String>>, value_getter: Option<Signal<String>>,
) -> View { ) -> View {
fs.output(control, value_getter) fs.output(control, value_getter)
@@ -17,13 +21,24 @@ impl VanityControlData for OutputData {
} }
impl GetterVanityControlData for OutputData {} impl GetterVanityControlData for OutputData {}
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
pub fn output( /// Builds an output form control and adds it to the form.
self, ///
builder: impl Fn( /// This control allows you to output some text to the user based on the
VanityControlBuilder<FD, FS, OutputData>, /// form data.
) -> VanityControlBuilder<FD, FS, OutputData>, pub fn output(self, builder: impl BuilderFn<VanityControlBuilder<FD, OutputData>>) -> Self {
) -> Self {
self.new_vanity(builder) self.new_vanity(builder)
} }
/// Builds an output form control using the form's context and adds it to
/// the form.
///
/// This control allows you to output some text to the user based on the
/// form data and form context.
pub fn output_cx(
self,
builder: impl BuilderCxFn<VanityControlBuilder<FD, OutputData>, FD::Context>,
) -> Self {
self.new_vanity_cx(builder)
}
} }
+65 -15
View File
@@ -1,13 +1,19 @@
use leptos::{Signal, View}; use super::{
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData}; };
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{Signal, View};
use std::rc::Rc;
/// Data used for the radio buttons control.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct RadioButtonsData { pub struct RadioButtonsData {
pub(crate) name: String, pub name: String,
pub(crate) label: Option<String>, pub label: Option<String>,
pub(crate) options: Vec<String>, /// The options for the select.
///
/// The first value is the string to display, the second is the value.
pub options: Vec<(String, String)>,
} }
impl ControlData for RadioButtonsData { impl ControlData for RadioButtonsData {
@@ -15,9 +21,9 @@ impl ControlData for RadioButtonsData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>, value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>, value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>, validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
fs.radio_buttons(control, value_getter, value_setter, validation_state) fs.radio_buttons(control, value_getter, value_setter, validation_state)
@@ -25,36 +31,80 @@ impl ControlData for RadioButtonsData {
} }
impl ValidatedControlData for RadioButtonsData {} impl ValidatedControlData for RadioButtonsData {}
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a radio buttons control and adds it to the form.
pub fn radio_buttons<FDT: Clone + PartialEq + 'static>( pub fn radio_buttons<FDT: Clone + PartialEq + 'static>(
self, self,
builder: impl Fn( builder: impl BuilderFn<ControlBuilder<FD, RadioButtonsData, FDT>>,
ControlBuilder<FD, FS, RadioButtonsData, FDT>,
) -> ControlBuilder<FD, FS, RadioButtonsData, FDT>,
) -> Self { ) -> Self {
self.new_control(builder) self.new_control(builder)
} }
/// Builds a radio buttons control using the form's context and adds it to
/// the form.
pub fn radio_buttons_cx<FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderCxFn<ControlBuilder<FD, RadioButtonsData, FDT>, FD::Context>,
) -> Self {
self.new_control_cx(builder)
}
} }
impl<FD: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, RadioButtonsData, FDT> { impl<FD: FormToolData, FDT> ControlBuilder<FD, RadioButtonsData, FDT> {
/// Sets the name of the radio button inputs.
///
/// This is used for the html element's "name" attribute.
/// In forms, the name attribute is the key that the data is sent
/// with.
pub fn named(mut self, control_name: impl ToString) -> Self { pub fn named(mut self, control_name: impl ToString) -> Self {
self.data.name = control_name.to_string(); self.data.name = control_name.to_string();
self self
} }
/// Sets the label for the radio button group.
pub fn labeled(mut self, label: impl ToString) -> Self { pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string()); self.data.label = Some(label.to_string());
self self
} }
/// Adds the option to the radio button group.
pub fn with_option(mut self, option: impl ToString) -> Self { pub fn with_option(mut self, option: impl ToString) -> Self {
self.data.options.push(option.to_string()); self.data
.options
.push((option.to_string(), option.to_string()));
self self
} }
/// Adds the option to the radio button group, specifying a different
/// value than what is displayed.
pub fn with_option_valued(mut self, display: impl ToString, value: impl ToString) -> Self {
self.data
.options
.push((display.to_string(), value.to_string()));
self
}
/// Adds all the options in the provided iterator to the radio button
/// group.
pub fn with_options(mut self, options: impl Iterator<Item = impl ToString>) -> Self { pub fn with_options(mut self, options: impl Iterator<Item = impl ToString>) -> Self {
for option in options { for option in options {
self.data.options.push(option.to_string()); self.data
.options
.push((option.to_string(), option.to_string()));
}
self
}
/// Adds all the (display_string, value) pairs in the provided iterator
/// to the radio button group.
pub fn with_options_valued(
mut self,
options: impl Iterator<Item = (impl ToString, impl ToString)>,
) -> Self {
for option in options {
self.data
.options
.push((option.0.to_string(), option.1.to_string()));
} }
self self
} }
+83 -37
View File
@@ -1,15 +1,21 @@
use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData}; use super::{
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{Signal, View}; use leptos::{IntoSignal, MaybeSignal, Signal, SignalGet, View};
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] /// Data used for the select control.
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct SelectData { pub struct SelectData {
pub(crate) name: String, pub name: String,
pub(crate) label: Option<String>, pub label: Option<String>,
/// The options for the select. /// The options for the select.
/// ///
/// The first value is the string to display, the second is the value. /// The first value is the string to display, the second is the value.
pub(crate) options: Vec<(String, String)>, pub options: MaybeSignal<Vec<(String, String)>>,
/// The display text for the blank option, if there is one.
pub blank_option: Option<String>,
} }
impl ControlData for SelectData { impl ControlData for SelectData {
@@ -17,9 +23,9 @@ impl ControlData for SelectData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>, value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>, value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>, validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
fs.select(control, value_getter, value_setter, validation_state) fs.select(control, value_getter, value_setter, validation_state)
@@ -27,60 +33,100 @@ impl ControlData for SelectData {
} }
impl ValidatedControlData for SelectData {} impl ValidatedControlData for SelectData {}
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a select control and adds it to the form.
pub fn select<FDT: Clone + PartialEq + 'static>( pub fn select<FDT: Clone + PartialEq + 'static>(
self, self,
builder: impl Fn( builder: impl BuilderFn<ControlBuilder<FD, SelectData, FDT>>,
ControlBuilder<FD, FS, SelectData, FDT>,
) -> ControlBuilder<FD, FS, SelectData, FDT>,
) -> Self { ) -> Self {
self.new_control(builder) self.new_control(builder)
} }
/// Builds a select control using the form's context and adds it to the
/// form.
pub fn select_cx<FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderCxFn<ControlBuilder<FD, SelectData, FDT>, FD::Context>,
) -> Self {
self.new_control_cx(builder)
}
} }
impl<FD: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, SelectData, FDT> { impl<FD: FormToolData, FDT> ControlBuilder<FD, SelectData, FDT> {
/// Sets the name of the radio button inputs.
///
/// This is used for the html element's "name" attribute.
/// In forms, the name attribute is the key that the data is sent
/// with.
pub fn named(mut self, control_name: impl ToString) -> Self { pub fn named(mut self, control_name: impl ToString) -> Self {
self.data.name = control_name.to_string(); self.data.name = control_name.to_string();
self self
} }
/// Sets the label for the select.
pub fn labeled(mut self, label: impl ToString) -> Self { pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string()); self.data.label = Some(label.to_string());
self self
} }
pub fn with_option(mut self, option: impl ToString) -> Self { /// Sets the options from the provided iterator.
self.data ///
.options /// This will overwrite any pervious options setting.
.push((option.to_string(), option.to_string()));
self
}
pub fn with_option_valued(mut self, display: impl ToString, value: impl ToString) -> Self {
self.data
.options
.push((display.to_string(), value.to_string()));
self
}
pub fn with_options(mut self, options: impl Iterator<Item = impl ToString>) -> Self { pub fn with_options(mut self, options: impl Iterator<Item = impl ToString>) -> Self {
for option in options { let options = options.map(|v| (v.to_string(), v.to_string())).collect();
self.data self.data.options = MaybeSignal::Static(options);
.options
.push((option.to_string(), option.to_string()));
}
self self
} }
/// Sets the options to the (display_string, value) pairs from the
/// provided iterator.
///
/// This will overwrite any pervious options setting.
pub fn with_options_valued( pub fn with_options_valued(
mut self, mut self,
options: impl Iterator<Item = (impl ToString, impl ToString)>, options: impl Iterator<Item = (impl ToString, impl ToString)>,
) -> Self { ) -> Self {
for option in options { let options = options
self.data .map(|(d, v)| (d.to_string(), v.to_string()))
.options .collect();
.push((option.0.to_string(), option.1.to_string())); self.data.options = MaybeSignal::Static(options);
} self
}
/// Sets the options from the provided signal.
///
/// This will overwrite any pervious options setting.
pub fn with_options_signal(mut self, options: Signal<Vec<String>>) -> Self {
let options = move || {
options
.get()
.into_iter()
.map(|v| (v.clone(), v))
.collect::<Vec<_>>()
};
self.data.options = MaybeSignal::Dynamic(options.into_signal());
self
}
/// Sets the options to the (display_string, value) pairs from the
/// provided signal.
///
/// This will overwrite any pervious options setting.
pub fn with_options_valued_signal(mut self, options: Signal<Vec<(String, String)>>) -> Self {
self.data.options = MaybeSignal::Dynamic(options);
self
}
/// Adds a blank option as the first option for the select.
pub fn with_blank_option(mut self) -> Self {
self.data.blank_option = Some(String::new());
self
}
/// Adds a blank option as the first option for the select,
/// but sets the display string to the given value.
pub fn with_blank_option_displayed(mut self, display: impl ToString) -> Self {
self.data.blank_option = Some(display.to_string());
self self
} }
} }
+45 -24
View File
@@ -1,16 +1,15 @@
use std::ops::RangeInclusive; use super::{BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData};
use leptos::{Signal, View};
use super::{ControlBuilder, ControlData, ControlRenderData};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{MaybeSignal, Signal, View};
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] /// Data used for the slider control.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SliderData { pub struct SliderData {
pub(crate) name: String, pub name: String,
pub(crate) label: Option<String>, pub label: Option<String>,
pub(crate) min: i32, pub min: MaybeSignal<i32>,
pub(crate) max: i32, pub max: MaybeSignal<i32>,
} }
impl Default for SliderData { impl Default for SliderData {
@@ -18,8 +17,8 @@ impl Default for SliderData {
SliderData { SliderData {
name: String::new(), name: String::new(),
label: None, label: None,
min: 0, min: MaybeSignal::Static(0),
max: 1, max: MaybeSignal::Static(1),
} }
} }
} }
@@ -29,50 +28,72 @@ impl ControlData for SliderData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>, value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>, value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>, validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
fs.slider(control, value_getter, value_setter, validation_state) fs.slider(control, value_getter, value_setter, validation_state)
} }
} }
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a slider (or range) control and adds it to the form.
pub fn slider<FDT: Clone + PartialEq + 'static>( pub fn slider<FDT: Clone + PartialEq + 'static>(
self, self,
builder: impl Fn( builder: impl BuilderFn<ControlBuilder<FD, SliderData, FDT>>,
ControlBuilder<FD, FS, SliderData, FDT>,
) -> ControlBuilder<FD, FS, SliderData, FDT>,
) -> Self { ) -> Self {
self.new_control(builder) self.new_control(builder)
} }
/// Bulids a slider (or range) control using the form's context and adds
/// it to the form.
pub fn slider_cx<FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderCxFn<ControlBuilder<FD, SliderData, FDT>, FD::Context>,
) -> Self {
self.new_control_cx(builder)
}
} }
impl<FD: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, SliderData, FDT> { impl<FD: FormToolData, FDT> ControlBuilder<FD, SliderData, FDT> {
/// Sets the name of the slider.
///
/// This is used for the html element's "name" attribute.
/// In forms, the name attribute is the key that the data is sent
/// with.
pub fn named(mut self, control_name: impl ToString) -> Self { pub fn named(mut self, control_name: impl ToString) -> Self {
self.data.name = control_name.to_string(); self.data.name = control_name.to_string();
self self
} }
/// Sets the label for the slider.
pub fn labeled(mut self, label: impl ToString) -> Self { pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string()); self.data.label = Some(label.to_string());
self self
} }
/// Sets the minimum value for the slider.
pub fn min(mut self, min: i32) -> Self { pub fn min(mut self, min: i32) -> Self {
self.data.min = min; self.data.min = MaybeSignal::Static(min);
self self
} }
/// Sets the minimum value for the slider to a signal.
pub fn min_signal(mut self, min: Signal<i32>) -> Self {
self.data.min = MaybeSignal::Dynamic(min);
self
}
/// Sets the maximum value for the slider.
pub fn max(mut self, max: i32) -> Self { pub fn max(mut self, max: i32) -> Self {
self.data.max = max; self.data.max = MaybeSignal::Static(max);
self self
} }
pub fn range(mut self, range: RangeInclusive<i32>) -> Self { /// Sets the maximum value for the slider to a signal.
self.data.min = *range.start(); pub fn max_signal(mut self, max: Signal<i32>) -> Self {
self.data.max = *range.end(); self.data.max = MaybeSignal::Dynamic(max);
self self
} }
} }
+23 -13
View File
@@ -1,35 +1,45 @@
use leptos::{prelude::Signal, View}; use super::{BuilderCxFn, BuilderFn, ControlRenderData, VanityControlBuilder, VanityControlData};
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{prelude::Signal, View};
use std::rc::Rc;
/// Data used for the spacer control.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SpacerData { pub struct SpacerData {
pub(crate) height: Option<String>, pub height: Option<String>,
} }
impl VanityControlData for SpacerData { impl VanityControlData for SpacerData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
_value_getter: Option<Signal<String>>, _value_getter: Option<Signal<String>>,
) -> View { ) -> View {
fs.spacer(control) fs.spacer(control)
} }
} }
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
pub fn spacer( /// Builds a spacer and adds it to the form.
self, pub fn spacer(self, builder: impl BuilderFn<VanityControlBuilder<FD, SpacerData>>) -> Self {
builder: impl Fn(
VanityControlBuilder<FD, FS, SpacerData>,
) -> VanityControlBuilder<FD, FS, SpacerData>,
) -> Self {
self.new_vanity(builder) self.new_vanity(builder)
} }
/// Builds a spacer using the form's context and adds it to the form.
pub fn spacer_cx(
self,
builder: impl BuilderCxFn<VanityControlBuilder<FD, SpacerData>, FD::Context>,
) -> Self {
self.new_vanity_cx(builder)
}
} }
impl<FD: FormToolData, FS: FormStyle> VanityControlBuilder<FD, FS, SpacerData> { impl<FD: FormToolData> VanityControlBuilder<FD, SpacerData> {
/// Sets the height of the spacer.
///
/// This is a string to allow different units like "10px" or "1.25em".
///
/// This may or may not be respected based on the Style implementation.
pub fn height(mut self, height: impl ToString) -> Self { pub fn height(mut self, height: impl ToString) -> Self {
self.data.height = Some(height.to_string()); self.data.height = Some(height.to_string());
self self
+54 -24
View File
@@ -1,17 +1,18 @@
use std::ops::RangeInclusive; use super::{
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
use leptos::{Signal, View}; };
use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{MaybeSignal, Signal, View};
use std::rc::Rc;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] /// Data used for the stepper control.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct StepperData { pub struct StepperData {
pub(crate) name: String, pub name: String,
pub(crate) label: Option<String>, pub label: Option<String>,
pub(crate) step: Option<i32>, pub step: Option<MaybeSignal<i32>>,
pub(crate) min: Option<i32>, pub min: Option<MaybeSignal<i32>>,
pub(crate) max: Option<i32>, pub max: Option<MaybeSignal<i32>>,
} }
impl ControlData for StepperData { impl ControlData for StepperData {
@@ -19,9 +20,9 @@ impl ControlData for StepperData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>, value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>, value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>, validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
fs.stepper(control, value_getter, value_setter, validation_state) fs.stepper(control, value_getter, value_setter, validation_state)
@@ -29,46 +30,75 @@ impl ControlData for StepperData {
} }
impl ValidatedControlData for StepperData {} impl ValidatedControlData for StepperData {}
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a stepper control and adds it to the form.
pub fn stepper<FDT: Clone + PartialEq + 'static>( pub fn stepper<FDT: Clone + PartialEq + 'static>(
self, self,
builder: impl Fn( builder: impl BuilderFn<ControlBuilder<FD, StepperData, FDT>>,
ControlBuilder<FD, FS, StepperData, FDT>,
) -> ControlBuilder<FD, FS, StepperData, FDT>,
) -> Self { ) -> Self {
self.new_control(builder) self.new_control(builder)
} }
/// Builds a new stepper control using the form's context and adds it to
/// the form.
pub fn stepper_cx<FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderCxFn<ControlBuilder<FD, StepperData, FDT>, FD::Context>,
) -> Self {
self.new_control_cx(builder)
}
} }
impl<FD: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, StepperData, FDT> { impl<FD: FormToolData, FDT> ControlBuilder<FD, StepperData, FDT> {
/// Sets the name of the stepper control.
///
/// This is used for the html element's "name" attribute.
/// In forms, the name attribute is the key that the data is sent
/// with.
pub fn named(mut self, control_name: impl ToString) -> Self { pub fn named(mut self, control_name: impl ToString) -> Self {
self.data.name = control_name.to_string(); self.data.name = control_name.to_string();
self self
} }
/// Sets the label of the stepper.
pub fn labeled(mut self, label: impl ToString) -> Self { pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string()); self.data.label = Some(label.to_string());
self self
} }
/// Sets the step ammount.
pub fn step(mut self, step: i32) -> Self { pub fn step(mut self, step: i32) -> Self {
self.data.step = Some(step); self.data.step = Some(MaybeSignal::Static(step));
self self
} }
/// Sets the step ammount.
pub fn step_signal(mut self, step: Signal<i32>) -> Self {
self.data.step = Some(MaybeSignal::Dynamic(step));
self
}
/// Sets the minimum value for the slider.
pub fn min(mut self, min: i32) -> Self { pub fn min(mut self, min: i32) -> Self {
self.data.min = Some(min); self.data.min = Some(MaybeSignal::Static(min));
self self
} }
/// Sets the minimum value for the slider to a signal.
pub fn min_signal(mut self, min: Signal<i32>) -> Self {
self.data.min = Some(MaybeSignal::Dynamic(min));
self
}
/// Sets the maximum value for the slider.
pub fn max(mut self, max: i32) -> Self { pub fn max(mut self, max: i32) -> Self {
self.data.max = Some(max); self.data.max = Some(MaybeSignal::Static(max));
self self
} }
pub fn range(mut self, range: RangeInclusive<i32>) -> Self { /// Sets the maximum value for the slider to a signal.
self.data.min = Some(*range.start()); pub fn max_signal(mut self, max: Signal<i32>) -> Self {
self.data.max = Some(*range.end()); self.data.max = Some(MaybeSignal::Dynamic(max));
self self
} }
} }
+20 -13
View File
@@ -1,35 +1,42 @@
use leptos::{prelude::Signal, View}; use super::{BuilderCxFn, BuilderFn, ControlRenderData, VanityControlBuilder, VanityControlData};
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{prelude::Signal, View};
use std::rc::Rc;
/// Data used for the submit button control.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct SubmitData { pub struct SubmitData {
pub(crate) text: String, pub text: String,
} }
impl VanityControlData for SubmitData { impl VanityControlData for SubmitData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
_value_getter: Option<Signal<String>>, _value_getter: Option<Signal<String>>,
) -> View { ) -> View {
fs.submit(control) fs.submit(control)
} }
} }
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
pub fn submit( /// Builds a submit button and adds it to the form.
self, pub fn submit(self, builder: impl BuilderFn<VanityControlBuilder<FD, SubmitData>>) -> Self {
builder: impl Fn(
VanityControlBuilder<FD, FS, SubmitData>,
) -> VanityControlBuilder<FD, FS, SubmitData>,
) -> Self {
self.new_vanity(builder) self.new_vanity(builder)
} }
/// Builds a submit button using the form's context and adds it to the
/// form.
pub fn submit_cx(
self,
builder: impl BuilderCxFn<VanityControlBuilder<FD, SubmitData>, FD::Context>,
) -> Self {
self.new_vanity_cx(builder)
}
} }
impl<FD: FormToolData, FS: FormStyle> VanityControlBuilder<FD, FS, SubmitData> { impl<FD: FormToolData> VanityControlBuilder<FD, SubmitData> {
/// Sets the submit button's text.
pub fn text(mut self, text: impl ToString) -> Self { pub fn text(mut self, text: impl ToString) -> Self {
self.data.text = text.to_string(); self.data.text = text.to_string();
self self
+40 -10
View File
@@ -1,11 +1,16 @@
use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData}; use super::{
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{Signal, View}; use leptos::{Signal, View};
use std::rc::Rc;
/// Data used for the text area control.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct TextAreaData { pub struct TextAreaData {
pub(crate) name: String, pub name: String,
pub(crate) placeholder: Option<String>, pub label: Option<String>,
pub placeholder: Option<String>,
} }
impl ControlData for TextAreaData { impl ControlData for TextAreaData {
@@ -13,9 +18,9 @@ impl ControlData for TextAreaData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>, value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>, value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>, validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
fs.text_area(control, value_getter, value_setter, validation_state) fs.text_area(control, value_getter, value_setter, validation_state)
@@ -23,18 +28,43 @@ impl ControlData for TextAreaData {
} }
impl ValidatedControlData for TextAreaData {} impl ValidatedControlData for TextAreaData {}
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a text area control and adds it to the form.
pub fn text_area<FDT: Clone + PartialEq + 'static>( pub fn text_area<FDT: Clone + PartialEq + 'static>(
self, self,
builder: impl Fn( builder: impl BuilderFn<ControlBuilder<FD, TextAreaData, FDT>>,
ControlBuilder<FD, FS, TextAreaData, FDT>,
) -> ControlBuilder<FD, FS, TextAreaData, FDT>,
) -> Self { ) -> Self {
self.new_control(builder) self.new_control(builder)
} }
/// Builds a text area control using the forms context and adds it to the
/// form.
pub fn text_area_cx<FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderCxFn<ControlBuilder<FD, TextAreaData, FDT>, FD::Context>,
) -> Self {
self.new_control_cx(builder)
}
} }
impl<FD: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, TextAreaData, FDT> { impl<FD: FormToolData, FDT> ControlBuilder<FD, TextAreaData, FDT> {
/// Sets the name of the text area.
///
/// This is used for the html element's "name" attribute.
/// In forms, the name attribute is the key that the data is sent
/// with.
pub fn named(mut self, control_name: impl ToString) -> Self {
self.data.name = control_name.to_string();
self
}
/// Sets the label for the text area.
pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string());
self
}
/// Sets the placeholder for the text area.
pub fn placeholder(mut self, placeholder: impl ToString) -> Self { pub fn placeholder(mut self, placeholder: impl ToString) -> Self {
self.data.placeholder = Some(placeholder.to_string()); self.data.placeholder = Some(placeholder.to_string());
self self
+50 -26
View File
@@ -1,15 +1,17 @@
use leptos::{Signal, View}; use super::{
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData}; };
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle}; use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{Signal, View};
use std::rc::Rc;
/// Data used for the text input control.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TextInputData { pub struct TextInputData {
pub(crate) name: String, pub name: String,
pub(crate) placeholder: Option<String>, pub label: Option<String>,
pub(crate) label: Option<String>, pub placeholder: Option<String>,
pub(crate) initial_text: String, pub input_type: &'static str,
pub(crate) input_type: &'static str,
} }
impl Default for TextInputData { impl Default for TextInputData {
@@ -18,7 +20,6 @@ impl Default for TextInputData {
name: String::new(), name: String::new(),
placeholder: None, placeholder: None,
label: None, label: None,
initial_text: String::new(),
input_type: "input", input_type: "input",
} }
} }
@@ -29,9 +30,9 @@ impl ControlData for TextInputData {
fn build_control<FS: FormStyle>( fn build_control<FS: FormStyle>(
fs: &FS, fs: &FS,
control: ControlRenderData<FS, Self>, control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>, value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>, value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>, validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
fs.text_input(control, value_getter, value_setter, validation_state) fs.text_input(control, value_getter, value_setter, validation_state)
@@ -39,40 +40,63 @@ impl ControlData for TextInputData {
} }
impl ValidatedControlData for TextInputData {} impl ValidatedControlData for TextInputData {}
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a text input control and adds it to the form.
pub fn text_input<FDT: Clone + PartialEq + 'static>( pub fn text_input<FDT: Clone + PartialEq + 'static>(
self, self,
builder: impl Fn( builder: impl BuilderFn<ControlBuilder<FD, TextInputData, FDT>>,
ControlBuilder<FD, FS, TextInputData, FDT>,
) -> ControlBuilder<FD, FS, TextInputData, FDT>,
) -> Self { ) -> Self {
self.new_control(builder) self.new_control(builder)
} }
/// Builds a text input control using the form's context and adds it to
/// the form.
pub fn text_input_cx<FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderCxFn<ControlBuilder<FD, TextInputData, FDT>, FD::Context>,
) -> Self {
self.new_control_cx(builder)
}
} }
impl<FD: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, TextInputData, FDT> { impl<FD: FormToolData, FDT> ControlBuilder<FD, TextInputData, FDT> {
/// Sets the name of the text input.
///
/// This is used for the html element's "name" attribute.
/// In forms, the name attribute is the key that the data is sent
/// with.
pub fn named(mut self, control_name: impl ToString) -> Self { pub fn named(mut self, control_name: impl ToString) -> Self {
self.data.name = control_name.to_string(); self.data.name = control_name.to_string();
self self
} }
/// Sets the label for the text input.
pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string());
self
}
/// Sets the placeholder for the text input.
pub fn placeholder(mut self, placeholder: impl ToString) -> Self { pub fn placeholder(mut self, placeholder: impl ToString) -> Self {
self.data.placeholder = Some(placeholder.to_string()); self.data.placeholder = Some(placeholder.to_string());
self self
} }
pub fn label(mut self, label: impl ToString) -> Self { /// Sets the text input to be the "password" type.
self.data.label = Some(label.to_string());
self
}
pub fn initial_text(mut self, text: impl ToString) -> Self {
self.data.initial_text = text.to_string();
self
}
pub fn password(mut self) -> Self { pub fn password(mut self) -> Self {
self.data.input_type = "password"; self.data.input_type = "password";
self self
} }
/// Sets the text input to be the "date" type.
pub fn date(mut self) -> Self {
self.data.input_type = "date";
self
}
/// Sets the text input to be the specified type.
pub fn input_type(mut self, input_type: &'static str) -> Self {
self.data.input_type = input_type;
self
}
} }
+89 -29
View File
@@ -28,27 +28,31 @@ impl<FD: FormToolData> FormValidator<FD> {
} }
} }
/// A constructed form object. /// A constructed, rendered form object.
/// ///
/// 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.
#[derive(Clone)]
pub struct Form<FD: FormToolData> { pub struct Form<FD: FormToolData> {
/// The form data signal.
pub fd: RwSignal<FD>, pub fd: RwSignal<FD>,
/// The list of validations
pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>, pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
pub(crate) view: View, pub(crate) view: View,
} }
impl<FD: FormToolData> Form<FD> { impl<FD: FormToolData> Form<FD> {
/// Gets the [`Validator`] for this form. /// Gets the [`FormValidator`] for this form.
pub fn validator(self) -> FormValidator<FD> { pub fn validator(&self) -> FormValidator<FD> {
FormValidator { FormValidator {
validations: self.validations, validations: self.validations.clone(),
} }
} }
/// Validates the [`ToolFormData`], returning the result /// Validates the [`FormToolData`], returning the result.
pub fn validate(&self) -> Result<(), String> { pub fn validate(&self) -> Result<(), String> {
self.fd.get_untracked().validate() let validator = self.validator();
validator.validate(&self.fd.get_untracked())
} }
/// Gets the view associated with this [`Form`]. /// Gets the view associated with this [`Form`].
@@ -76,54 +80,109 @@ impl<FD: FormToolData> IntoView for Form<FD> {
/// 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
/// to physically lay out a form, and how that data should be parsed and validated. /// needed to physically lay out a form, and how that data should be parsed
/// and validated.
pub trait FormToolData: Clone + 'static { pub trait FormToolData: Clone + 'static {
/// The style that this form uses.
type Style: FormStyle; type Style: FormStyle;
/// The context that this form is rendered in.
/// Defines how the form should be layed out and how the data should be parsed and validated.
/// ///
/// To construct a [`From`] object, use one of the [`get_form`()] methods. /// This will need to be provided when building a form or a validator.
/// Therefore, you will need to be able to replicate this context
/// on the client for rendering and the server for validating.
type Context: 'static;
/// Defines how the form should be laid out and how the data should be
/// parsed and validated.
///
/// To construct a [`From`] object, use one of the `get_form` methods.
/// ///
/// Uses the given form builder to specify what fields should be present /// Uses the given form builder to specify what fields should be present
/// in the form, what properties those fields should have, and how that /// in the form, what properties those fields should have, and how that
/// data should be parsed and checked. /// data should be parsed and checked.
fn build_form(fb: FormBuilder<Self, Self::Style>) -> FormBuilder<Self, Self::Style>; fn build_form(fb: FormBuilder<Self>) -> FormBuilder<Self>;
/// Constructs a [`Form`] for this [`FormToolData`] type. /// Constructs a [`Form`] for this [`FormToolData`] type.
/// ///
/// This renders the form as a the leptos_router /// This renders the form as a enhanced
/// [`Form`](leptos_router::Form) /// [`ActionForm`](leptos_router::ActionForm) that sends the form data
/// component. Call [`get_action_form`]\() to get the /// directly by calling the server function.
/// [`ActionForm`](leptos_router::ActionForm) version. ///
fn get_form(self, action: impl ToString, style: Self::Style) -> Form<Self> { /// By doing this, we avoid doing the
let builder = FormBuilder::new(); /// [`FromFormData`](leptos_router::FromFormData)
/// conversion. However, to support
/// [Progressive Enhancement](https://book.leptos.dev/progressive_enhancement/index.html),
/// you should name the form elements to work with a plain ActionForm
/// anyway. If progresssive enhancement is not important to you, you may
/// freely use this version.
///
/// For the other ways to construct a [`Form`], see:
/// - [`get_action_form`](Self::get_action_form)
/// - [`get_plain_form`](Self::get_plain_form)
fn get_form<ServFn>(
self,
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
style: Self::Style,
context: Self::Context,
) -> Form<Self>
where
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
From<FormData>,
ServFn: From<Self>,
{
let builder = FormBuilder::new(context);
let builder = Self::build_form(builder); let builder = Self::build_form(builder);
builder.build_plain_form(action.to_string(), self, style) builder.build_form(action, self, style)
} }
/// Constructs a [`Form`] for this [`FormToolData`] type. /// Constructs a [`Form`] for this [`FormToolData`] type.
/// ///
/// This renders the form as a the leptos_router /// This renders the form as a the leptos_router
/// [`ActionForm`](leptos_router::ActionForm) /// [`ActionForm`](leptos_router::ActionForm)
/// component. Call [`get_form`]\() to get the plain /// component.
/// [`Form`](leptos_router::Form) version. ///
/// For the other ways to construct a [`Form`], see:
/// - [`get_form`](Self::get_form)
/// - [`get_action_form`](Self::get_action_form)
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>>>,
style: Self::Style, style: Self::Style,
context: Self::Context,
) -> Form<Self> ) -> Form<Self>
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 = FormBuilder::new(); let builder = FormBuilder::new(context);
let builder = Self::build_form(builder); let builder = Self::build_form(builder);
builder.build_action_form(action, self, style) builder.build_action_form(action, self, style)
} }
/// Gets a [`Validator`] for this [`ToolFormData`]. /// Constructs a [`Form`] for this [`FormToolData`] type.
///
/// This renders the form as a the leptos_router
/// [`Form`](leptos_router::Form)
/// component.
///
/// For the other ways to construct a [`Form`], see:
/// - [`get_form`](Self::get_form)
/// - [`get_action_form`](Self::get_action_form)
fn get_plain_form(
self,
action: impl ToString,
style: Self::Style,
context: Self::Context,
) -> Form<Self> {
let builder = FormBuilder::new(context);
let builder = Self::build_form(builder);
builder.build_plain_form(action.to_string(), self, style)
}
/// Gets a [`FormValidator`] for this [`FormToolData`].
/// ///
/// This doesn't render the view, but just collects all the validation /// 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 /// Functions from building the form. That means it can be called on the
@@ -131,18 +190,19 @@ pub trait FormToolData: Clone + 'static {
/// ///
/// However, the code to render the views are not configured out, it /// 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. /// simply doesn't run, so the view needs to compile even on the server.
fn get_validator() -> FormValidator<Self> { fn get_validator(context: Self::Context) -> FormValidator<Self> {
let builder = FormBuilder::new(); let builder = FormBuilder::new(context);
let builder = Self::build_form(builder); let builder = Self::build_form(builder);
builder.validator() builder.validator()
} }
/// Validates this [`FormToolData`] struct. /// Validates this [`FormToolData`] struct.
/// ///
/// This is shorthand for creating a validator with [`get_validator`]\() /// This is shorthand for creating a validator with
/// and then calling `validator.validate(&self)`. /// [`get_validator`](Self::get_validator)()
fn validate(&self) -> Result<(), String> { /// and then calling `validator.validate(&self, context)`.
let validator = Self::get_validator(); fn validate(&self, context: Self::Context) -> Result<(), String> {
let validator = Self::get_validator(context);
validator.validate(self) validator.validate(self)
} }
} }
+208 -44
View File
@@ -1,8 +1,8 @@
use crate::{ use crate::{
controls::{ controls::{
BuiltControlData, BuiltVanityControlData, ControlBuilder, ControlData, ControlRenderData, BuilderCxFn, BuilderFn, BuiltControlData, BuiltVanityControlData, ControlBuilder,
FieldSetter, ParseFn, RenderFn, ValidationCb, ValidationFn, VanityControlBuilder, ControlData, ControlRenderData, FieldSetter, ParseFn, RenderFn, ValidationCb, ValidationFn,
VanityControlData, VanityControlBuilder, VanityControlData,
}, },
form::{Form, FormToolData, FormValidator}, form::{Form, FormToolData, FormValidator},
styles::FormStyle, styles::FormStyle,
@@ -19,33 +19,48 @@ use web_sys::{FormData, SubmitEvent};
/// A builder for laying out forms. /// A builder for laying out forms.
/// ///
/// This builder allows you to specify what components 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> {
pub(crate) cx: Rc<FD::Context>,
/// The list of [`ValidationFn`]s. /// The list of [`ValidationFn`]s.
pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>, pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
/// The list of functions that will render the form. /// The list of functions that will render the form.
pub(crate) render_fns: Vec<Box<dyn RenderFn<FS, FD>>>, pub(crate) render_fns: Vec<Box<dyn RenderFn<FD::Style, FD>>>,
/// The list of styling attributes applied on the form level /// The list of styling attributes applied on the form level.
pub(crate) styles: Vec<FS::StylingAttributes>, pub(crate) styles: Vec<<FD::Style as FormStyle>::StylingAttributes>,
} }
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> { impl<FD: FormToolData> FormBuilder<FD> {
/// Creates a new [`FormBuilder`] /// Creates a new [`FormBuilder`]
pub(crate) fn new() -> FormBuilder<FD, FS> { pub(crate) fn new(cx: FD::Context) -> Self {
FormBuilder { FormBuilder {
cx: Rc::new(cx),
validations: Vec::new(), validations: Vec::new(),
render_fns: Vec::new(), render_fns: Vec::new(),
styles: Vec::new(), styles: Vec::new(),
} }
} }
pub fn style(mut self, style: FS::StylingAttributes) -> Self { /// Creates a new [`FormBuilder`] with the given Rc'ed context, for
//// building a form group.
pub(crate) fn new_group(cx: Rc<FD::Context>) -> Self {
FormBuilder {
cx,
validations: Vec::new(),
render_fns: Vec::new(),
styles: Vec::new(),
}
}
/// Adds a styling attribute to the entire form.
pub fn style(mut self, style: <FD::Style as FormStyle>::StylingAttributes) -> Self {
self.styles.push(style); self.styles.push(style);
self self
} }
/// Adds a new vanity control to the form.
pub(crate) fn new_vanity<C: VanityControlData + Default>( pub(crate) fn new_vanity<C: VanityControlData + Default>(
mut self, mut self,
builder: impl Fn(VanityControlBuilder<FD, FS, C>) -> VanityControlBuilder<FD, FS, C>, builder: impl BuilderFn<VanityControlBuilder<FD, C>>,
) -> Self { ) -> Self {
let vanity_builder = VanityControlBuilder::new(C::default()); let vanity_builder = VanityControlBuilder::new(C::default());
let control = builder(vanity_builder); let control = builder(vanity_builder);
@@ -53,9 +68,21 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
self self
} }
/// Adds a new vanity control to the form using the form's context.
pub(crate) fn new_vanity_cx<C: VanityControlData + Default>(
mut self,
builder: impl BuilderCxFn<VanityControlBuilder<FD, C>, FD::Context>,
) -> Self {
let vanity_builder = VanityControlBuilder::new(C::default());
let control = builder(vanity_builder, self.cx.clone());
self.add_vanity(control);
self
}
/// Adds a new control to the form using the form's context.
pub(crate) fn new_control<C: ControlData + Default, FDT: Clone + PartialEq + 'static>( pub(crate) fn new_control<C: ControlData + Default, FDT: Clone + PartialEq + 'static>(
mut self, mut self,
builder: impl Fn(ControlBuilder<FD, FS, C, FDT>) -> ControlBuilder<FD, FS, C, FDT>, builder: impl BuilderFn<ControlBuilder<FD, C, FDT>>,
) -> Self { ) -> Self {
let control_builder = ControlBuilder::new(C::default()); let control_builder = ControlBuilder::new(C::default());
let control = builder(control_builder); let control = builder(control_builder);
@@ -63,51 +90,99 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
self self
} }
// TODO: test this from a user context. A user adding a custom defined component. /// Adds a new control to the form using the form's context.
pub fn add_vanity<C: VanityControlData>( pub(crate) fn new_control_cx<C: ControlData + Default, FDT: Clone + PartialEq + 'static>(
mut self,
builder: impl BuilderCxFn<ControlBuilder<FD, C, FDT>, FD::Context>,
) -> Self {
let control_builder = ControlBuilder::new(C::default());
let control = builder(control_builder, self.cx.clone());
self.add_control(control);
self
}
/// Adds a vanity control to the form.
pub(crate) fn add_vanity<C: VanityControlData>(
&mut self, &mut self,
vanity_control: VanityControlBuilder<FD, FS, C>, vanity_control: VanityControlBuilder<FD, C>,
) { ) {
let BuiltVanityControlData { let BuiltVanityControlData {
render_data, render_data,
getter, getter,
show_when,
} = vanity_control.build(); } = vanity_control.build();
let render_fn = move |fs: &FS, fd: RwSignal<FD>| { let cx = self.cx.clone();
let render_fn = move |fs: Rc<FD::Style>, fd: RwSignal<FD>| {
let render_data = Rc::new(render_data);
let value_getter = getter.map(|getter| (move || getter(fd.get())).into_signal()); let value_getter = getter.map(|getter| (move || getter(fd.get())).into_signal());
let view = VanityControlData::build_control(fs, render_data, value_getter); let view =
move || VanityControlData::build_control(&*fs, render_data.clone(), value_getter);
let view = match show_when {
Some(when) => {
let when = move || when(fd.into(), cx.clone());
view! { <Show when=when>{view.clone()}</Show> }
}
None => view(),
};
(view, None) (view, None)
}; };
self.render_fns.push(Box::new(render_fn)); self.render_fns.push(Box::new(render_fn));
} }
// TODO: test this from a user context. A user adding a custom defined component. /// Adds a control to the form.
pub fn add_control<C: ControlData, FDT: Clone + PartialEq + 'static>( pub(crate) fn add_control<C: ControlData, FDT: Clone + PartialEq + 'static>(
&mut self, &mut self,
control: ControlBuilder<FD, FS, C, FDT>, control: ControlBuilder<FD, C, FDT>,
) { ) {
let built_control_data = match control.build() { let built_control_data = match control.build() {
Ok(c) => c, Ok(c) => c,
Err(e) => panic!("Invalid Component: {}", e), Err(e) => {
let item_name = std::any::type_name::<C>()
.rsplit("::")
.next()
.expect("split to have at least 1 element");
panic!("Invalid Component ({}): {}", item_name, e)
}
}; };
if let Some(ref validation_fn) = built_control_data.validation_fn { if let Some(validation_fn) = built_control_data.validation_fn.clone() {
self.validations.push(validation_fn.clone()); let validation_fn = if let Some(show_when) = built_control_data.show_when.clone() {
// we want the validation function to always succeed for hidden components
// thus, we need to modify the validation function
let cx = self.cx.clone();
let new_validation_fn = move |fd: &FD| {
let (fd_signal, _) = create_signal(fd.clone());
if !show_when(fd_signal.into(), cx.clone()) {
return Ok(());
}
validation_fn(fd)
};
Rc::new(new_validation_fn)
} else {
validation_fn
};
self.validations.push(validation_fn);
} }
let render_fn = move |fs: &FS, fd: RwSignal<FD>| { let cx = self.cx.clone();
let (view, cb) = Self::build_control_view(fd, fs, built_control_data); let render_fn = move |fs: Rc<FD::Style>, fd: RwSignal<FD>| {
let (view, cb) = Self::build_control_view(fd, fs, built_control_data, cx);
(view, Some(cb)) (view, Some(cb))
}; };
self.render_fns.push(Box::new(render_fn)); self.render_fns.push(Box::new(render_fn));
} }
/// Helper for building all the functions and everything needed to render
/// the view.
fn build_control_view<C: ControlData, FDT: 'static>( fn build_control_view<C: ControlData, FDT: 'static>(
fd: RwSignal<FD>, fd: RwSignal<FD>,
fs: &FS, fs: Rc<FD::Style>,
control_data: BuiltControlData<FD, FS, C, FDT>, control_data: BuiltControlData<FD, C, FDT>,
cx: Rc<FD::Context>,
) -> (View, Box<dyn ValidationCb>) { ) -> (View, Box<dyn ValidationCb>) {
let BuiltControlData { let BuiltControlData {
render_data, render_data,
@@ -116,8 +191,10 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
parse_fn, parse_fn,
unparse_fn, unparse_fn,
validation_fn, validation_fn,
show_when,
} = control_data; } = control_data;
let render_data = Rc::new(render_data);
let (validation_signal, validation_signal_set) = create_signal(Ok(())); let (validation_signal, validation_signal_set) = create_signal(Ok(()));
let validation_fn_clone = validation_fn.clone(); let validation_fn_clone = validation_fn.clone();
let value_getter = move || { let value_getter = move || {
@@ -137,14 +214,29 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
}; };
let value_getter = value_getter.into_signal(); let value_getter = value_getter.into_signal();
let cloned_show_when = show_when.clone();
let cloned_cx = cx.clone();
let validation_cb = move || { let validation_cb = move || {
let validation_fn = validation_fn.as_ref(); // first check if the validation signal is an error so that we
// can fail on parsing issues too
if let Some(Err(_)) = validation_signal.try_get_untracked() {
return false;
}
// validation for non-visible fields always succeeds
if let Some(ref show_when) = cloned_show_when {
if !show_when(fd.into(), cloned_cx.clone()) {
return true;
}
}
// run the validation function on the value now
let validation_fn = match validation_fn { let validation_fn = match validation_fn {
Some(v) => v, Some(ref v) => v,
None => return true, // No validation function, so validation passes None => return true, // No validation function so validation passes
}; };
let data = fd.get(); let data = fd.get_untracked();
let validation_result = validation_fn(&data); let validation_result = validation_fn(&data);
let succeeded = validation_result.is_ok(); let succeeded = validation_result.is_ok();
validation_signal_set.set(validation_result); validation_signal_set.set(validation_result);
@@ -160,23 +252,33 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
fd, fd,
); );
let view = C::build_control( let view = move || {
fs, C::build_control(
render_data, &*fs,
value_getter, render_data.clone(),
value_setter, value_getter,
validation_signal.into(), value_setter.clone(),
); validation_signal.into(),
)
};
let view = match show_when {
Some(when) => {
let when = move || when(fd.into(), cx.clone());
view! { <Show when=when>{view.clone()}</Show> }
}
None => view(),
};
(view, validation_cb) (view, validation_cb)
} }
/// Helper for creating a setter function.
fn create_value_setter<CRT: 'static, FDT: 'static>( fn create_value_setter<CRT: 'static, FDT: 'static>(
validation_cb: Box<dyn Fn() -> bool + 'static>, validation_cb: Box<dyn Fn() -> bool + 'static>,
validation_signal_set: WriteSignal<Result<(), String>>, validation_signal_set: WriteSignal<Result<(), String>>,
parse_fn: Box<dyn ParseFn<CRT, FDT>>, parse_fn: Box<dyn ParseFn<CRT, FDT>>,
setter: Rc<dyn FieldSetter<FD, FDT>>, setter: Rc<dyn FieldSetter<FD, FDT>>,
fd: RwSignal<FD>, fd: RwSignal<FD>,
) -> Box<dyn Fn(CRT) + 'static> { ) -> Rc<dyn Fn(CRT) + 'static> {
let value_setter = move |value| { let value_setter = move |value| {
let parsed = match parse_fn(value) { let parsed = match parse_fn(value) {
Ok(p) => { Ok(p) => {
@@ -197,14 +299,72 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
// run validation // run validation
(validation_cb)(); (validation_cb)();
}; };
Box::new(value_setter) Rc::new(value_setter)
} }
/// Builds the direct send version of the form.
pub(crate) fn build_form<ServFn>(
self,
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
fd: FD,
fs: FD::Style,
) -> Form<FD>
where
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
From<FormData>,
ServFn: From<FD>,
{
let fd = create_rw_signal(fd);
let fs = Rc::new(fs);
let (views, validation_cbs): (Vec<_>, Vec<_>) = self
.render_fns
.into_iter()
.map(|r_fn| r_fn(fs.clone(), fd))
.unzip();
let elements = fs.form_frame(ControlRenderData {
data: views.into_view(),
styles: self.styles,
});
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();
return;
}
ev.prevent_default();
let server_fn = ServFn::from(fd.get_untracked());
action.dispatch(server_fn);
};
let view = view! {
<ActionForm action=action on:submit=on_submit>
{elements}
</ActionForm>
};
Form {
fd,
validations: self.validations,
view,
}
}
/// Builds the action form version of the form.
pub(crate) fn build_action_form<ServFn>( pub(crate) 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>>>,
fd: FD, fd: FD,
fs: FS, fs: FD::Style,
) -> Form<FD> ) -> Form<FD>
where where
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static, ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
@@ -212,11 +372,12 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
From<FormData>, From<FormData>,
{ {
let fd = create_rw_signal(fd); let fd = create_rw_signal(fd);
let fs = Rc::new(fs);
let (views, validation_cbs): (Vec<_>, Vec<_>) = self let (views, validation_cbs): (Vec<_>, Vec<_>) = self
.render_fns .render_fns
.into_iter() .into_iter()
.map(|r_fn| r_fn(&fs, fd)) .map(|r_fn| r_fn(fs.clone(), fd))
.unzip(); .unzip();
let elements = fs.form_frame(ControlRenderData { let elements = fs.form_frame(ControlRenderData {
@@ -249,13 +410,15 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
} }
} }
pub(crate) fn build_plain_form(self, url: String, fd: FD, fs: FS) -> Form<FD> { /// builds the plain form version of the form.
pub(crate) fn build_plain_form(self, url: String, fd: FD, fs: FD::Style) -> Form<FD> {
let fd = create_rw_signal(fd); let fd = create_rw_signal(fd);
let fs = Rc::new(fs);
let (views, validation_cbs): (Vec<_>, Vec<_>) = self let (views, validation_cbs): (Vec<_>, Vec<_>) = self
.render_fns .render_fns
.into_iter() .into_iter()
.map(|r_fn| r_fn(&fs, fd)) .map(|r_fn| r_fn(fs.clone(), fd))
.unzip(); .unzip();
let elements = fs.form_frame(ControlRenderData { let elements = fs.form_frame(ControlRenderData {
@@ -288,6 +451,7 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
} }
} }
/// Creates a [`FormValidator`] from this builder.
pub(crate) fn validator(&self) -> FormValidator<FD> { pub(crate) fn validator(&self) -> FormValidator<FD> {
FormValidator { FormValidator {
validations: self.validations.clone(), validations: self.validations.clone(),
+5
View File
@@ -1,3 +1,8 @@
//! `leptos_form_tool` offers a declaritve way to create forms for
//! [leptos](https://leptos.dev/).
//!
//! To learn more, see the
//! [README.md](https://github.com/MitchellMarinoDev/leptos_form_tool/src/branch/main/README.md)
pub mod controls; pub mod controls;
mod form; mod form;
mod form_builder; mod form_builder;
+312 -309
View File
@@ -2,7 +2,8 @@ use super::FormStyle;
use crate::{ use crate::{
controls::{ controls::{
button::ButtonData, checkbox::CheckboxData, heading::HeadingData, hidden::HiddenData, button::ButtonData, checkbox::CheckboxData, heading::HeadingData, hidden::HiddenData,
output::OutputData, select::SelectData, spacer::SpacerData, submit::SubmitData, output::OutputData, radio_buttons::RadioButtonsData, select::SelectData,
slider::SliderData, spacer::SpacerData, stepper::StepperData, submit::SubmitData,
text_area::TextAreaData, text_input::TextInputData, ControlData, ControlRenderData, text_area::TextAreaData, text_input::TextInputData, ControlData, ControlRenderData,
}, },
FormToolData, FormToolData,
@@ -11,402 +12,404 @@ use leptos::*;
use std::rc::Rc; use std::rc::Rc;
use web_sys::MouseEvent; use web_sys::MouseEvent;
pub enum GridFormStylingAttributes { /// Styling attributes for the [`GridFormStyle`].
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub enum GFStyleAttr {
/// Set the width of the control out of 12.
/// Defaults to 12/12 (full width).
Width(u32), Width(u32),
/// Adds a tooltip to the control.
/// This sets the html title attribute, which shows the text when the
/// user hovers their mouse over the control for a couple seconds.
Tooltip(String),
} }
/// A complete useable example for defining a form style.
///
/// This can be used directly in by your form, or you can copy `grid_form.rs`
/// into your project and make any neccesary change. You will also want to
/// copy `grid_form.scss` from the git repo and put that in the `styles`
/// directory for your leptos project to get all the styling.
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
pub struct GridFormStyle; pub struct GridFormStyle;
impl FormStyle for GridFormStyle { impl FormStyle for GridFormStyle {
type StylingAttributes = GridFormStylingAttributes; type StylingAttributes = GFStyleAttr;
fn form_frame(&self, form: ControlRenderData<Self, View>) -> View { fn form_frame(&self, form: ControlRenderData<Self, View>) -> View {
view! { <div class="form_grid">{form.data}</div> }.into_view() view! { <div class="form_grid">{form.data}</div> }.into_view()
} }
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View { /// A common function that wraps the given view in the styles
view! { <h2 class="form_heading">{&control.data.title}</h2> }.into_view() fn custom_component(&self, styles: &[Self::StylingAttributes], inner: View) -> View {
} let mut width = 12;
let mut tooltip = None;
fn text_input( for style in styles.iter() {
&self,
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>>,
) -> View {
// TODO: extract this to a common thing
let mut width = 1;
for style in control.styles {
match style { match style {
GridFormStylingAttributes::Width(w) => width = w, GFStyleAttr::Width(w) => width = *w,
GFStyleAttr::Tooltip(t) => tooltip = Some(t),
} }
} }
view! { view! {
<div style:grid-column=format!("span {}", width)> <div style:grid-column=format!("span {}", width) title=tooltip>
<div> {inner}
<label for=&control.data.name class="form_label">
{control.data.label.as_ref()}
</label>
<span class="form_error">{move || validation_state.get().err()}</span>
</div>
<input
type=control.data.input_type
id=&control.data.name
name=control.data.name
placeholder=control.data.placeholder
prop:value=move || value_getter.get()
on:focusout=move |ev| {
value_setter(event_target_value(&ev));
}
class="form_input"
class=("form_input_invalid", move || validation_state.get().is_err())
/>
</div> </div>
} }
.into_view() .into_view()
} }
fn select( fn group(&self, group: Rc<ControlRenderData<Self, View>>) -> View {
let view = view! { <div class="form_group form_grid">{&group.data}</div> }.into_view();
self.custom_component(&group.styles, view)
}
fn spacer(&self, control: Rc<ControlRenderData<Self, SpacerData>>) -> View {
self.custom_component(
&control.styles,
view! { <div style:height=control.data.height.as_ref()></div> }.into_view(),
)
}
fn heading(&self, control: Rc<ControlRenderData<Self, HeadingData>>) -> View {
self.custom_component(
&control.styles,
view! { <h2 class="form_heading">{control.data.title.clone()}</h2> }.into_view(),
)
}
fn submit(&self, control: Rc<ControlRenderData<Self, SubmitData>>) -> View {
self.custom_component(
&control.styles,
view! { <input type="submit" value=&control.data.text class="form_submit"/> }
.into_view(),
)
}
fn button<FD: FormToolData>(
&self, &self,
control: ControlRenderData<Self, SelectData>, control: Rc<ControlRenderData<Self, ButtonData<FD>>>,
value_getter: Signal<<SelectData as ControlData>::ReturnType>, data_signal: RwSignal<FD>,
value_setter: Box<dyn Fn(<SelectData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
let options_view = control let action = control.data.action.clone();
.data let on_click = move |ev: MouseEvent| {
.options if let Some(action) = action.clone() {
.into_iter() action(ev, data_signal)
.map(|(display, value)| { }
view! { };
<option value=value.clone() selected=move || { value_getter.get() == *value }>
{display.clone()}
</option>
}
})
.collect_view();
view! { let view = view! {
<div> <button type="button" class="form_button" on:click=on_click>
<span>{control.data.label}</span> {&control.data.text}
{move || validation_state.get().err()} </button>
<select
id=&control.data.name
name=control.data.name
class="form_input"
on:input=move |ev| {
value_setter(event_target_value(&ev));
}
>
{options_view}
</select>
</div>
} }
.into_view() .into_view();
self.custom_component(&control.styles, view)
} }
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View { fn output(
view! { <input type="submit" value=control.data.text class="form_submit"/> }.into_view()
}
fn text_area(
&self, &self,
control: ControlRenderData<Self, TextAreaData>, control: Rc<ControlRenderData<Self, OutputData>>,
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>, value_getter: Option<Signal<String>>,
value_setter: Box<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
view! { let view = view! { <span>{move || value_getter.map(|g| g.get())}</span> }.into_view();
<div> self.custom_component(&control.styles, view)
{move || format!("{:?}", validation_state.get())}
<textarea
id=&control.data.name
name=control.data.name
placeholder=control.data.placeholder
class="form_input"
prop:value=move || value_getter.get()
on:change=move |ev| {
value_setter(event_target_value(&ev));
}
>
</textarea>
</div>
}
.into_view()
}
fn custom_component(&self, view: View) -> View {
view
} }
fn hidden( fn hidden(
&self, &self,
_control: ControlRenderData<Self, HiddenData>, control: Rc<ControlRenderData<Self, HiddenData>>,
value_getter: Signal<String>, value_getter: Option<Signal<String>>,
) -> View { ) -> View {
view! { <input prop:value=value_getter style="visibility: hidden"/> }.into_view() let value_getter = move || value_getter.map(|g| g.get());
view! {
<input
name=&control.data.name
prop:value=value_getter
style="visibility: hidden; column-span: none"
/>
}
.into_view()
}
fn text_input(
&self,
control: Rc<ControlRenderData<Self, TextInputData>>,
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
value_setter: Rc<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
let view = view! {
<div>
<label for=&control.data.name class="form_label">
{control.data.label.as_ref()}
</label>
<span class="form_error">{move || validation_state.get().err()}</span>
</div>
<input
type=control.data.input_type
id=&control.data.name
name=&control.data.name
placeholder=control.data.placeholder.as_ref()
class="form_input"
class=("form_input_invalid", move || validation_state.get().is_err())
prop:value=move || value_getter.get()
on:focusout=move |ev| {
value_setter(event_target_value(&ev));
}
/>
}
.into_view();
self.custom_component(&control.styles, view)
}
fn text_area(
&self,
control: Rc<ControlRenderData<Self, TextAreaData>>,
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
value_setter: Rc<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
let view = view! {
<div>
<label for=&control.data.name class="form_label">
{control.data.label.as_ref()}
</label>
<span class="form_error">{move || validation_state.get().err()}</span>
</div>
<textarea
id=&control.data.name
name=&control.data.name
placeholder=control.data.placeholder.as_ref()
prop:value=move || value_getter.get()
class="form_input"
class=("form_input_invalid", move || validation_state.get().is_err())
on:focusout=move |ev| {
value_setter(event_target_value(&ev));
}
></textarea>
}
.into_view();
self.custom_component(&control.styles, view)
} }
fn radio_buttons( fn radio_buttons(
&self, &self,
control: ControlRenderData<Self, crate::controls::radio_buttons::RadioButtonsData>, control: Rc<ControlRenderData<Self, RadioButtonsData>>,
value_getter: Signal< value_getter: Signal<<RadioButtonsData as ControlData>::ReturnType>,
<crate::controls::radio_buttons::RadioButtonsData as ControlData>::ReturnType, value_setter: Rc<dyn Fn(<RadioButtonsData as ControlData>::ReturnType)>,
>,
value_setter: Box<
dyn Fn(<crate::controls::radio_buttons::RadioButtonsData as ControlData>::ReturnType),
>,
validation_state: Signal<Result<(), String>>, validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
let mut width = 1;
for style in control.styles {
match style {
GridFormStylingAttributes::Width(w) => width = w,
}
}
let value_setter = Rc::new(value_setter);
let buttons_view = control let buttons_view = control
.data .data
.options .options
.into_iter() .iter()
.map(|o| { .map(|(display, value)| {
let value_setter = value_setter.clone(); let value_setter = value_setter.clone();
let o_clone1 = o.clone(); let display = display.clone();
let o_clone2 = o.clone(); let value = value.clone();
let value_clone = value.clone();
let value_clone2 = value.clone();
view! { view! {
<input <input
type="radio" type="radio"
id=o.clone() id=&value
_str
name=&control.data.name name=&control.data.name
value=o.clone() value=&value
prop:checked=move || { value_getter.get() == o_clone1 } prop:checked=move || { &value_getter.get() == &value_clone }
on:input=move |ev| { on:input=move |ev| {
let new_value = event_target_checked(&ev); let new_value = event_target_checked(&ev);
if new_value { if new_value {
value_setter(o_clone2.clone()); value_setter(value_clone2.clone());
} }
} }
/> />
<label for=&o>{&o}</label> <label for=&value>{display}</label>
<br/> <br/>
} }
}) })
.collect_view(); .collect_view();
view! { let view = view! {
<div style:grid-column=format!("span {}", width)> <div>
<div> <label for=&control.data.name class="form_label">
<label for=&control.data.name class="form_label"> {control.data.label.as_ref()}
{control.data.label.as_ref()} </label>
</label> <span class="form_error">{move || validation_state.get().err()}</span>
<span class="form_error">{move || validation_state.get().err()}</span> </div>
</div> <div
<div class="form_input"
class="form_input" class:form_input_invalid=move || validation_state.get().is_err()
class:form_input_invalid=move || validation_state.get().is_err() >
> {buttons_view}
{buttons_view}
</div>
</div> </div>
} }
.into_view() .into_view();
self.custom_component(&control.styles, view)
}
fn select(
&self,
control: Rc<ControlRenderData<Self, SelectData>>,
value_getter: Signal<<SelectData as ControlData>::ReturnType>,
value_setter: Rc<dyn Fn(<SelectData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
let control_clone = control.clone();
let options_view = move || {
control_clone
.data
.options
.get()
.iter()
.map(|(display, value)| {
let display = display.clone();
let value = value.clone();
view! {
<option value=value.clone() selected=move || { value_getter.get() == *value }>
{display}
</option>
}
})
.collect_view()
};
let blank_option_view = control.data.blank_option.as_ref().map(|display| {
view! {
<option value="" selected=move || { value_getter.get().as_str() == "" }>
{display}
</option>
}
});
let view = view! {
<div>
<label for=&control.data.name class="form_label">
{control.data.label.as_ref()}
</label>
<span class="form_error">{move || validation_state.get().err()}</span>
</div>
<select
id=&control.data.name
name=&control.data.name
class="form_input"
class=("form_input_invalid", move || validation_state.get().is_err())
on:input=move |ev| {
value_setter(event_target_value(&ev));
}
>
{blank_option_view}
{options_view}
</select>
}
.into_view();
self.custom_component(&control.styles, view)
} }
fn checkbox( fn checkbox(
&self, &self,
control: ControlRenderData<Self, CheckboxData>, control: Rc<ControlRenderData<Self, CheckboxData>>,
value_getter: Signal<<CheckboxData as ControlData>::ReturnType>, value_getter: Signal<<CheckboxData as ControlData>::ReturnType>,
value_setter: Box<dyn Fn(<CheckboxData as ControlData>::ReturnType)>, value_setter: Rc<dyn Fn(<CheckboxData as ControlData>::ReturnType)>,
) -> View { ) -> View {
let mut width = 1; let view = view! {
for style in control.styles { <label for=&control.data.name class="form_label">
match style { {control.data.label.as_ref()}
GridFormStylingAttributes::Width(w) => width = w, </label>
} <label class="form_input" for=&control.data.name>
} <input
type="checkbox"
id=&control.data.name
name=&control.data.name
prop:checked=value_getter
on:input=move |ev| {
let new_value = event_target_checked(&ev);
value_setter(new_value);
}
/>
view! { <span>{control.data.label.as_ref()}</span>
<div style:grid-column=format!("span {}", width)> </label>
<label for=&control.data.name class="form_label">
{control.data.label.as_ref()}
</label>
<label class="form_input" for=&control.data.name>
<input
type="checkbox"
id=&control.data.name
name=&control.data.name
prop:checked=value_getter
on:input=move |ev| {
let new_value = event_target_checked(&ev);
value_setter(new_value);
}
/>
<span>{control.data.label}</span>
</label>
</div>
} }
.into_view() .into_view();
self.custom_component(&control.styles, view)
} }
fn stepper( fn stepper(
&self, &self,
control: ControlRenderData<Self, crate::controls::stepper::StepperData>, control: Rc<ControlRenderData<Self, StepperData>>,
value_getter: Signal<<crate::controls::stepper::StepperData as ControlData>::ReturnType>, value_getter: Signal<<StepperData as ControlData>::ReturnType>,
value_setter: Box< value_setter: Rc<dyn Fn(<StepperData as ControlData>::ReturnType)>,
dyn Fn(<crate::controls::stepper::StepperData as ControlData>::ReturnType),
>,
validation_state: Signal<Result<(), String>>, validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
let mut width = 1; let view = view! {
for style in control.styles { <div>
match style { <label for=&control.data.name class="form_label">
GridFormStylingAttributes::Width(w) => width = w, {control.data.label.as_ref()}
} </label>
} <span class="form_error">{move || validation_state.get().err()}</span>
view! {
<div style:grid-column=format!("span {}", width)>
<div>
<label for=&control.data.name class="form_label">
{control.data.label.as_ref()}
</label>
<span class="form_error">{move || validation_state.get().err()}</span>
</div>
<input
type="number"
id=&control.data.name
name=&control.data.name
step=control.data.step
prop:value=move || value_getter.get()
on:change=move |ev| {
value_setter(event_target_value(&ev));
}
class="form_input"
/>
</div> </div>
<input
type="number"
id=&control.data.name
name=&control.data.name
step=control.data.step
min=control.data.min
max=control.data.max
class="form_input"
class=("form_input_invalid", move || validation_state.get().is_err())
prop:value=move || value_getter.get()
on:change=move |ev| {
value_setter(event_target_value(&ev));
}
/>
} }
.into_view() .into_view();
}
fn output( self.custom_component(&control.styles, view)
&self,
_control: ControlRenderData<Self, OutputData>,
value_getter: Option<Signal<String>>,
) -> View {
view! { <div>{move || value_getter.map(|g| g.get())}</div> }.into_view()
} }
fn slider( fn slider(
&self, &self,
control: ControlRenderData<Self, crate::controls::slider::SliderData>, control: Rc<ControlRenderData<Self, SliderData>>,
value_getter: Signal<<crate::controls::slider::SliderData as ControlData>::ReturnType>, value_getter: Signal<<SliderData as ControlData>::ReturnType>,
value_setter: Box<dyn Fn(<crate::controls::slider::SliderData as ControlData>::ReturnType)>, value_setter: Rc<dyn Fn(<SliderData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>, validation_state: Signal<Result<(), String>>,
) -> View { ) -> View {
let mut width = 1; let view = view! {
for style in control.styles { <div>
match style { <label for=&control.data.name class="form_label">
GridFormStylingAttributes::Width(w) => width = w, {control.data.label.as_ref()}
} </label>
} <span class="form_error">{move || validation_state.get().err()}</span>
</div>
view! { <input
<div style:grid-column=format!("span {}", width)> type="range"
<div> id=&control.data.name
<label for=&control.data.name class="form_label"> name=&control.data.name
{control.data.label.as_ref()} min=control.data.min
</label> max=control.data.max
<span class="form_error">{move || validation_state.get().err()}</span> class="form_input"
</div> class=("form_input_invalid", move || validation_state.get().is_err())
<input prop:value=move || value_getter.get()
type="range" on:input=move |ev| {
id=&control.data.name let value = event_target_value(&ev).parse::<i32>().ok();
name=&control.data.name if let Some(value) = value {
min=control.data.min value_setter(value);
max=control.data.max
prop:value=move || value_getter.get()
on:input=move |ev| {
let value = event_target_value(&ev).parse::<i32>().ok();
if let Some(value) = value {
value_setter(value);
}
} }
}
class="form_input" />
/>
</div>
} }
.into_view() .into_view();
}
fn button<FD: FormToolData>( self.custom_component(&control.styles, view)
&self,
control: ControlRenderData<Self, ButtonData<FD>>,
data_signal: RwSignal<FD>,
) -> View {
let mut width = 1;
for style in control.styles {
match style {
GridFormStylingAttributes::Width(w) => width = w,
}
}
let action = control.data.action.clone();
let on_click = move |ev: MouseEvent| {
if let Some(action) = action.clone() {
data_signal.update(|fd| action(ev, fd));
}
};
view! {
<button
type="button"
class="form_button"
on:click=on_click
style:grid-column=format!("span {}", width)
>
{control.data.text}
</button>
}
.into_view()
}
fn group(&self, group: ControlRenderData<Self, View>) -> View {
let mut width = 12;
for style in group.styles {
match style {
GridFormStylingAttributes::Width(w) => width = w,
}
}
view! {
<div class="form_group form_grid" style:grid-column=format!("span {}", width)>
{group.data}
</div>
}
.into_view()
}
fn spacer(&self, control: ControlRenderData<Self, SpacerData>) -> View {
let mut width = 12;
for style in control.styles {
match style {
GridFormStylingAttributes::Width(w) => width = w,
}
}
view! { <div style:grid-column=format!("span {}", width) style:height=control.data.height></div> }
.into_view()
} }
} }
+132 -57
View File
@@ -10,10 +10,19 @@ use crate::{
FormToolData, FormToolData,
}; };
use leptos::{RwSignal, Signal, View}; use leptos::{RwSignal, Signal, View};
use std::rc::Rc;
pub use grid_form::{GridFormStyle, GridFormStylingAttributes}; pub use grid_form::{GFStyleAttr, GridFormStyle};
/// Defines a way to style a form.
///
/// Provides methods for rendering all the controls.
/// This provider is in charge of figuring out what html elements should be
/// rendered and how they should be styled.
pub trait FormStyle: 'static { pub trait FormStyle: 'static {
/// The type of styling attributes that this [`FormStyle`] takes.
///
/// These styling attributes can be applied to any of the controls.
type StylingAttributes; type StylingAttributes;
/// Render any containing components for the form. /// Render any containing components for the form.
@@ -24,72 +33,138 @@ pub trait FormStyle: 'static {
/// Do NOT wrap it in an actual `form` element; any /// Do NOT wrap it in an actual `form` element; any
/// wrapping should be done with `div` or similar elements. /// wrapping should be done with `div` or similar elements.
fn form_frame(&self, form: ControlRenderData<Self, View>) -> View; fn form_frame(&self, form: ControlRenderData<Self, View>) -> View;
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View;
fn hidden( /// Wraps the view of a custom component.
&self, ///
control: ControlRenderData<Self, HiddenData>, /// The rendering of the custom component is given by the `inner` view.
value_getter: Signal<String>, /// Here the styler has a chance wrap the view with other components, or
) -> View; /// applying the styling attributes.
fn text_input( ///
&self, /// This method does not need to be called by the custom component, but
control: ControlRenderData<Self, TextInputData>, /// the custom component may make use of this method for the
value_getter: Signal<<TextInputData as ControlData>::ReturnType>, /// aforementioned reasons.
value_setter: Box<dyn Fn(<TextInputData as ControlData>::ReturnType)>, fn custom_component(&self, style: &[Self::StylingAttributes], inner: View) -> View;
validation_state: Signal<Result<(), String>>,
) -> View; /// Renders a group.
fn text_area( ///
&self, /// The inner view for the group's components is provided.
control: ControlRenderData<Self, TextAreaData>, /// This method should wrap the group in any visual grouping elements,
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>, /// and apply the styles.
value_setter: Box<dyn Fn(<TextAreaData as ControlData>::ReturnType)>, fn group(&self, group: Rc<ControlRenderData<Self, View>>) -> View;
validation_state: Signal<Result<(), String>>,
) -> View; /// Renders a spacer.
fn radio_buttons( ///
&self, /// See [`SpacerData`].
control: ControlRenderData<Self, RadioButtonsData>, fn spacer(&self, control: Rc<ControlRenderData<Self, SpacerData>>) -> View;
value_getter: Signal<<RadioButtonsData as ControlData>::ReturnType>,
value_setter: Box<dyn Fn(<RadioButtonsData as ControlData>::ReturnType)>, /// Renders a heading for a section of the form.
validation_state: Signal<Result<(), String>>, fn heading(&self, control: Rc<ControlRenderData<Self, HeadingData>>) -> View;
) -> View;
fn select( /// Renders a submit button.
&self, ///
control: ControlRenderData<Self, SelectData>, /// See [`SubmitData`].
value_getter: Signal<<SelectData as ControlData>::ReturnType>, fn submit(&self, control: Rc<ControlRenderData<Self, SubmitData>>) -> View;
value_setter: Box<dyn Fn(<SelectData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>, /// Renders a button.
) -> View; ///
/// See [`BuuttonData`]
fn button<FD: FormToolData>( fn button<FD: FormToolData>(
&self, &self,
control: ControlRenderData<Self, ButtonData<FD>>, control: Rc<ControlRenderData<Self, ButtonData<FD>>>,
data_signal: RwSignal<FD>, data_signal: RwSignal<FD>,
) -> View; ) -> View;
fn checkbox(
&self, /// Renders some output text.
control: ControlRenderData<Self, CheckboxData>, ///
value_getter: Signal<<CheckboxData as ControlData>::ReturnType>, /// See [`OutputData`].
value_setter: Box<dyn Fn(<CheckboxData as ControlData>::ReturnType)>,
) -> View;
fn stepper(
&self,
control: ControlRenderData<Self, StepperData>,
value_getter: Signal<<StepperData as ControlData>::ReturnType>,
value_setter: Box<dyn Fn(<StepperData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View;
fn output( fn output(
&self, &self,
control: ControlRenderData<Self, OutputData>, control: Rc<ControlRenderData<Self, OutputData>>,
value_getter: Option<Signal<String>>, value_getter: Option<Signal<String>>,
) -> View; ) -> View;
fn slider(
/// Renders a input control that should be hidden from the user.
///
/// See [`HiddenData`].
fn hidden(
&self, &self,
control: ControlRenderData<Self, SliderData>, control: Rc<ControlRenderData<Self, HiddenData>>,
value_getter: Option<Signal<String>>,
) -> View;
/// Renders a text input control.
///
/// See [`TextInputData`].
fn text_input(
&self,
control: Rc<ControlRenderData<Self, TextInputData>>,
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
value_setter: Rc<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View;
/// Renders a text area control.
///
/// See [`TextAreaData`].
fn text_area(
&self,
control: Rc<ControlRenderData<Self, TextAreaData>>,
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
value_setter: Rc<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View;
/// Renders a group of radio buttons.
///
/// See [`RadioButtonsData`].
fn radio_buttons(
&self,
control: Rc<ControlRenderData<Self, RadioButtonsData>>,
value_getter: Signal<<RadioButtonsData as ControlData>::ReturnType>,
value_setter: Rc<dyn Fn(<RadioButtonsData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View;
/// Renders a select (or dropdown) control.
///
/// See [`SelectData`].
fn select(
&self,
control: Rc<ControlRenderData<Self, SelectData>>,
value_getter: Signal<<SelectData as ControlData>::ReturnType>,
value_setter: Rc<dyn Fn(<SelectData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View;
/// Renders a checkbox control.
///
/// See [`CheckboxData`].
fn checkbox(
&self,
control: Rc<ControlRenderData<Self, CheckboxData>>,
value_getter: Signal<<CheckboxData as ControlData>::ReturnType>,
value_setter: Rc<dyn Fn(<CheckboxData as ControlData>::ReturnType)>,
) -> View;
/// Renders a stepper control.
///
/// See [`StepperData`].
fn stepper(
&self,
control: Rc<ControlRenderData<Self, StepperData>>,
value_getter: Signal<<StepperData as ControlData>::ReturnType>,
value_setter: Rc<dyn Fn(<StepperData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View;
/// Renders a slider control.
///
/// See [`SliderData`].
fn slider(
&self,
control: Rc<ControlRenderData<Self, SliderData>>,
value_getter: Signal<<SliderData as ControlData>::ReturnType>, value_getter: Signal<<SliderData as ControlData>::ReturnType>,
value_setter: Box<dyn Fn(<SliderData as ControlData>::ReturnType)>, value_setter: Rc<dyn Fn(<SliderData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>, validation_state: Signal<Result<(), String>>,
) -> View; ) -> View;
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View;
fn custom_component(&self, view: View) -> View;
fn group(&self, group: ControlRenderData<Self, View>) -> View;
fn spacer(&self, control: ControlRenderData<Self, SpacerData>) -> View;
} }
+16 -3
View File
@@ -14,7 +14,7 @@ type ValidationBuilderFn<T> = dyn Fn(&str, &T) -> Result<(), String> + 'static;
/// closures, but for simple validation function this builder can be helpful /// closures, but for simple validation function this builder can be helpful
/// ///
/// Validations are run in the order that they are called in the builder. /// Validations are run in the order that they are called in the builder.
pub struct ValidationBuilder<FD: FormToolData, T: 'static> { pub struct ValidationBuilder<FD: FormToolData, T: ?Sized + 'static> {
/// The name of the field, for error messages. /// The name of the field, for error messages.
name: String, name: String,
/// The getter function for the field to validate. /// The getter function for the field to validate.
@@ -23,7 +23,7 @@ pub struct ValidationBuilder<FD: FormToolData, T: 'static> {
functions: Vec<Box<ValidationBuilderFn<T>>>, functions: Vec<Box<ValidationBuilderFn<T>>>,
} }
impl<FD: FormToolData, T: 'static> ValidationBuilder<FD, T> { impl<FD: FormToolData, T: ?Sized + 'static> ValidationBuilder<FD, T> {
/// Creates a new empty [`ValidationBuilder`] on the given field. /// Creates a new empty [`ValidationBuilder`] on the given field.
pub fn for_field(field_fn: impl Fn(&FD) -> &T + 'static) -> Self { pub fn for_field(field_fn: impl Fn(&FD) -> &T + 'static) -> Self {
ValidationBuilder { ValidationBuilder {
@@ -65,7 +65,7 @@ impl<FD: FormToolData, T: 'static> ValidationBuilder<FD, T> {
} }
} }
impl<FD: FormToolData> ValidationBuilder<FD, String> { impl<FD: FormToolData> ValidationBuilder<FD, str> {
/// Requires the field to not be empty. /// Requires the field to not be empty.
pub fn required(mut self) -> Self { pub fn required(mut self) -> Self {
self.functions.push(Box::new(move |name, value| { self.functions.push(Box::new(move |name, value| {
@@ -101,6 +101,19 @@ impl<FD: FormToolData> ValidationBuilder<FD, String> {
})); }));
self self
} }
/// Requires the field to contain `pattern`.
pub fn contains(mut self, pattern: impl ToString) -> Self {
let pattern = pattern.to_string();
self.functions.push(Box::new(move |name, value| {
if !value.contains(&pattern) {
Err(format!("{} must contain {}", name, &pattern))
} else {
Ok(())
}
}));
self
}
} }
impl<FD: FormToolData, T: PartialOrd<T> + Display + 'static> ValidationBuilder<FD, T> { impl<FD: FormToolData, T: PartialOrd<T> + Display + 'static> ValidationBuilder<FD, T> {