Compare commits

..
44 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
mitchell bd73591b9f merge 2024-06-12 16:48:40 -05:00
mitchell a5fd4f85ee Merge pull request 'Various improvements' (#26) from feature/separate_validation_builder into main
Reviewed-on: #26
2024-06-12 21:46:27 +00:00
mitchell 079c9bde53 lint 2024-06-12 16:46:03 -05:00
mitchell 1e2709dc8c ControlRenderData no long has boxed data 2024-06-12 16:29:35 -05:00
mitchell 64d2631140 clean up style trait 2024-06-12 16:21:09 -05:00
mitchell 115ff1abde remove fd and fs from builder 2024-06-12 16:02:43 -05:00
mitchell a39f2ce664 selects can have different values and customized button builder 2024-06-12 15:49:30 -05:00
mitchell 1dc676cc37 add spacer 2024-06-10 13:54:37 -05:00
mitchell e7fd8ec7e1 validation builder additions 2024-06-10 12:14:21 -05:00
mitchell bf04957370 add validation builder 2024-06-10 12:14:21 -05:00
mitchell 360ef63b58 small rename 2024-06-07 15:19:29 -05:00
mitchell edc9c4d371 form now has a style for the entire form 2024-06-07 14:42:47 -05:00
mitchell 31b24a23a1 impl groups 2024-06-07 14:21:14 -05:00
mitchell fb47d553e7 modify parse options 2024-06-07 12:09:41 -05:00
mitchell 5d0b4a7449 add button 2024-06-07 11:50:21 -05:00
mitchell 9b4bbfdb74 vanity controls can now have optional getters 2024-06-05 17:04:28 -05:00
mitchell 749ccc272b remove todos in favor of issues. Fmt 2024-06-05 16:32:31 -05:00
mitchell ead75f050a implemented some other controls 2024-06-05 16:15:07 -05:00
mitchell 63153d76a0 organized 2024-06-05 12:41:15 -05:00
mitchell a3311b2b63 remove vanity builder and add docs 2024-06-05 12:30:15 -05:00
mitchell 3621e5fb7b differ rendering 2024-06-05 11:46:48 -05:00
mitchell f19d53830c helper functions and docs 2024-06-05 09:57:51 -05:00
mitchell 9368b676d2 Update README.md 2024-06-04 21:31:15 +00:00
mitchell 161727ab7a working 2024-06-04 16:00:19 -05:00
mitchell fedc6d37bd Merge pull request 'Update README.md' (#4) from docs into main
Reviewed-on: #4
2024-06-04 16:17:56 +00:00
mitchell 74407d6c0a cleanup 2024-05-20 09:51:01 -05:00
mitchell fb44b5d303 Update README.md 2024-03-20 23:17:34 +00:00
26 changed files with 3031 additions and 636 deletions
+3
View File
@@ -6,4 +6,7 @@ edition = "2021"
[dependencies]
leptos = "0.6"
leptos_router = "0.6"
serde = { version = "1.0.203", features = ["derive"] }
# Leptos should pick the web_sys version for us
web-sys = "*"
+107 -1
View File
@@ -1,4 +1,110 @@
# leptos_form_tool
A tool for making forms in leptos.
A declaritive way to create forms for [leptos](https://leptos.dev/).
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
## Validations
You might find yourself asking, but why not just use components?
The biggest reason for creating leptos_form_tool is support for
validating the fields. This validation logic can get rather complex, for
instance, you likely want to preform validation on the client when the user
clicks submit to immediatly give the user feedback about any invalid input.
But you often also want to do the same validation on the server to protect
against any requests that don't come from your client or for a user that
doesn't have wasm enabled.
Additionally, you might want to change the validation of one control based
on the value of another. For example, you might want to make sure the "day"
field is in a valid range, but that range depends on what the user selects in
the "month" field. Or you might want to make sure the "confirm password" field
matches the "password" field. leptos_form_tool makes this easy, as the
validation function you provide operates on the entire form's data.
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`.
+24 -10
View File
@@ -23,6 +23,13 @@
border-bottom: 2px solid #374151;
margin-bottom: 2rem;
grid-column: span 12;
display: grid;
}
.form_group {
background-color: #0295f744;
border-radius: 25px;
padding: 20px;
}
.form_label {
@@ -40,6 +47,7 @@
.form_input {
display: block;
box-sizing: border-box;
width: 100%;
background-color: #f7fafc;
border-width: 2px;
@@ -68,7 +76,7 @@
color: #ef4444;
}
.form_submit {
.form_button {
display: block;
outline: none;
border: none;
@@ -78,36 +86,42 @@
color: #fff;
font-weight: bold;
cursor: pointer;
margin: 0 auto;
margin: 0 auto auto auto;
padding: 0.75rem 1.25rem;
font-size: 1rem;
appearance: none;
min-height: 40px;
}
.form_submit {
@extend .form_button;
@extend .col-span-full;
margin: 0 auto;
}
.form_submit:hover {
background-color: #005fb3;
}
// column widths
.w-full {
.col-span-full {
grid-column: 1 / -1;
}
.w-12 {
.col-span-12 {
grid-column: span 12 / span 12;
}
.w-6 {
.col-span-6 {
grid-column: span 6 / span 6;
}
.w-4 {
.col-span-4 {
grid-column: span 4 / span 4;
}
.w-3 {
.col-span-3 {
grid-column: span 3 / span 3;
}
.w-2 {
.col-span-2 {
grid-column: span 2 / span 2;
}
.w-1 {
.col-span-1 {
grid-column: span 1 / span 1;
}
+120
View File
@@ -0,0 +1,120 @@
use super::{BuilderCxFn, BuilderFn, ControlRenderData, ShowWhenFn};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::view;
use leptos::RwSignal;
use leptos::Show;
use leptos::Signal;
use std::rc::Rc;
use web_sys::MouseEvent;
type ButtonAction<FD> = dyn Fn(MouseEvent, RwSignal<FD>) + 'static;
/// Data used for the button control.
pub struct ButtonData<FD: FormToolData> {
pub text: String,
pub action: Option<Rc<ButtonAction<FD>>>,
}
impl<FD: FormToolData> Default for ButtonData<FD> {
fn default() -> Self {
ButtonData {
text: String::default(),
action: None,
}
}
}
impl<FD: FormToolData> Clone for ButtonData<FD> {
fn clone(&self) -> Self {
ButtonData {
text: self.text.clone(),
action: self.action.clone(),
}
}
}
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a button and adds it to the form.
pub fn button(self, builder: impl BuilderFn<ButtonBuilder<FD>>) -> Self {
let button_builder = ButtonBuilder::new();
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 {
data: control.data,
styles: control.styles,
};
let show_when = control.show_when;
let cx = self.cx.clone();
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)
};
self.render_fns.push(Box::new(render_fn));
self
}
}
/// The struct that allows you to specify the attributes of the button.
pub struct ButtonBuilder<FD: FormToolData> {
pub(crate) styles: Vec<<FD::Style as FormStyle>::StylingAttributes>,
pub(crate) data: ButtonData<FD>,
pub(crate) show_when: Option<Box<dyn ShowWhenFn<FD, FD::Context>>>,
}
impl<FD: FormToolData> ButtonBuilder<FD> {
/// Creates a new [`ButtonBuilder`].
fn new() -> Self {
ButtonBuilder {
styles: Vec::default(),
data: ButtonData::default(),
show_when: None,
}
}
/// 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
}
/// Sets the text of the button.
pub fn text(mut self, text: impl ToString) -> Self {
self.data.text = text.to_string();
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
}
}
+64
View File
@@ -0,0 +1,64 @@
use super::{BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData};
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)]
pub struct CheckboxData {
pub name: String,
pub label: Option<String>,
}
impl ControlData for CheckboxData {
type ReturnType = bool;
fn build_control<FS: FormStyle>(
fs: &FS,
control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>,
value_setter: Rc<dyn Fn(Self::ReturnType)>,
_validation_state: Signal<Result<(), String>>,
) -> View {
fs.checkbox(control, value_getter, value_setter)
}
}
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a checkbox and adds it to the form.
pub fn checkbox<FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderFn<ControlBuilder<FD, CheckboxData, FDT>>,
) -> Self {
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, 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 {
self.data.name = control_name.to_string();
self
}
/// Sets the text of the checkbox's label.
pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string());
self
}
}
+47
View File
@@ -0,0 +1,47 @@
use super::{BuilderCxFn, BuilderFn, ControlBuilder, ControlData};
use crate::{FormBuilder, FormToolData};
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a custom component and adds it to the form.
pub fn custom<CC: ControlData, FDT: Clone + PartialEq + 'static>(
mut self,
control_data: CC,
builder: impl BuilderFn<ControlBuilder<FD, CC, FDT>>,
) -> Self {
let control_builder = ControlBuilder::new(control_data);
let control = builder(control_builder);
self.add_control(control);
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>(
self,
builder: impl BuilderFn<ControlBuilder<FD, CC, FDT>>,
) -> Self {
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)
}
}
+50
View File
@@ -0,0 +1,50 @@
use std::rc::Rc;
use super::{ControlRenderData, ValidationCb};
use crate::styles::FormStyle;
use crate::{form::FormToolData, form_builder::FormBuilder};
use leptos::{CollectView, RwSignal};
impl<FD: FormToolData> FormBuilder<FD> {
/// Creates a form group.
///
/// 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);
for validation in group_builder.validations {
self.validations.push(validation);
}
let render_fn = move |fs: Rc<FD::Style>, fd: RwSignal<FD>| {
let (views, validation_cbs): (Vec<_>, Vec<_>) = group_builder
.render_fns
.into_iter()
.map(|r_fn| r_fn(fs.clone(), fd))
.unzip();
let render_data = Rc::new(ControlRenderData {
data: views.collect_view(),
styles: group_builder.styles,
});
let view = fs.group(render_data.clone());
let validation_cb = move || {
let mut success = true;
for validation in validation_cbs.iter().flatten() {
if !validation() {
success = false;
}
}
success
};
(view, Some(Box::new(validation_cb) as Box<dyn ValidationCb>))
};
self.render_fns.push(Box::new(render_fn));
self
}
}
+32 -17
View File
@@ -1,34 +1,49 @@
use leptos::View;
use super::{BuilderCxFn, BuilderFn, ControlRenderData, VanityControlBuilder, VanityControlData};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{MaybeSignal, Signal, View};
use std::rc::Rc;
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
use crate::{
form::{FormBuilder, FormData},
styles::FormStyle,
};
#[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(crate) title: String,
pub title: MaybeSignal<String>,
}
impl VanityControlData for HeadingData {
fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View {
fn build_control<FS: FormStyle>(
fs: &FS,
control: Rc<ControlRenderData<FS, Self>>,
_value_getter: Option<leptos::prelude::Signal<String>>,
) -> View {
fs.heading(control)
}
}
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
pub fn heading(
self,
builder: impl Fn(VanityControlBuilder<FS, HeadingData>) -> VanityControlBuilder<FS, HeadingData>,
) -> Self {
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a heading and adds it to the form.
pub fn heading(self, builder: impl BuilderFn<VanityControlBuilder<FD, HeadingData>>) -> Self {
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<FS: FormStyle> VanityControlBuilder<FS, HeadingData> {
impl<FD: FormToolData> VanityControlBuilder<FD, HeadingData> {
/// Sets the title of this heading.
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
}
}
+58
View File
@@ -0,0 +1,58 @@
use super::{
BuilderCxFn, BuilderFn, ControlRenderData, GetterVanityControlData, VanityControlBuilder,
VanityControlData,
};
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)]
pub struct HiddenData {
pub name: String,
}
impl VanityControlData for HiddenData {
fn build_control<FS: FormStyle>(
fs: &FS,
control: Rc<ControlRenderData<FS, Self>>,
value_getter: Option<Signal<String>>,
) -> View {
fs.hidden(control, value_getter)
}
}
impl GetterVanityControlData for HiddenData {}
impl<FD: FormToolData> FormBuilder<FD> {
/// 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,
builder: impl BuilderCxFn<VanityControlBuilder<FD, HiddenData>, FD::Context>,
) -> Self {
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
}
}
+289 -74
View File
@@ -1,154 +1,288 @@
use std::{fmt::Display, rc::Rc};
use crate::{form::FormData, styles::FormStyle};
use leptos::{Signal, View};
use crate::{form::FormToolData, styles::FormStyle};
use leptos::{RwSignal, Signal, View};
use std::{fmt::Display, rc::Rc, str::FromStr};
pub mod button;
pub mod checkbox;
pub mod custom;
pub mod group;
pub mod heading;
pub mod hidden;
pub mod output;
pub mod radio_buttons;
pub mod select;
pub mod slider;
pub mod spacer;
pub mod stepper;
pub mod submit;
pub mod text_area;
pub mod text_input;
// TODO: change to returning an Option<String>
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 ParseFn<CR, FDT>: Fn(CR) -> Result<FDT, String> + 'static {}
pub trait UnparseFn<CR, FDT>: Fn(&FDT) -> CR + 'static {}
pub trait FieldFn<FD, FDT>: Fn(&mut FD) -> &mut FDT + 'static {}
// implement the traits for all valid types
impl<FDT, T> ValidationFn<FDT> for T where T: Fn(&FDT) -> Result<(), String> + 'static {}
impl<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<FD, FDT, F> FieldFn<FD, FDT> for F where F: Fn(&mut FD) -> &mut FDT + 'static {}
pub trait VanityControlData: 'static {
fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View;
pub trait UnparseFn<CR, FDT>: Fn(FDT) -> CR + 'static {}
pub trait FieldGetter<FD, FDT>: Fn(FD) -> FDT + 'static {}
pub trait FieldSetter<FD, FDT>: Fn(&mut FD, FDT) + 'static {}
pub trait ShowWhenFn<FD: 'static, CX>: Fn(Signal<FD>, Rc<CX>) -> bool + 'static {}
pub trait RenderFn<FS, FD: 'static>:
FnOnce(Rc<FS>, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + 'static
{
}
// TODO: what if the `FS` parameter was extracted to the trait level.
// Then this would be trait object able.
// If this is trait object able, then we can store this in a list,
// and differ rendering the control until we actually need to form view.
// Which, in turn, would get rid of the Form Builder as an enum (which was
// done to avoid rendering on the server).
pub trait ControlData: 'static {
type ReturnType: Clone;
// 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<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> 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> FieldSetter<FD, FDT> for F where F: Fn(&mut FD, FDT) + 'static {}
impl<FD: 'static, CX, F> ShowWhenFn<FD, CX> for F where F: Fn(Signal<FD>, Rc<CX>) -> bool + 'static {}
impl<FS, FD: 'static, F> RenderFn<FS, FD> for F where
F: FnOnce(Rc<FS>, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + 'static
{
}
// TODO: this should also return a getter for the data
/// A trait for the data needed to render an read-only control.
pub trait VanityControlData: 'static {
/// Builds the control, returning the [`View`] that was built.
fn build_control<FS: FormStyle>(
fs: &FS,
control: ControlRenderData<FS, Self>,
control: Rc<ControlRenderData<FS, Self>>,
value_getter: Option<Signal<String>>,
) -> View;
}
pub trait GetterVanityControlData: VanityControlData {}
/// A trait for the data needed to render an interactive control.
pub trait ControlData: 'static {
/// This is the data type returned by this control.
type ReturnType: Clone;
/// Builds the control, returning the [`View`] that was built.
fn build_control<FS: FormStyle>(
fs: &FS,
control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>,
value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View;
}
pub trait ValidatedControlData: ControlData {}
/// The data needed to render a interactive control of type `C`.
pub struct ControlRenderData<FS: FormStyle + ?Sized, C: ?Sized> {
pub data: Box<C>,
pub style: Vec<FS::StylingAttributes>,
pub styles: Vec<FS::StylingAttributes>,
pub data: C,
}
pub struct VanityControlBuilder<FS: FormStyle, C: VanityControlData> {
pub(crate) style_attributes: Vec<FS::StylingAttributes>,
/// The data needed to render a read-only control of type `C`.
pub struct VanityControlBuilder<FD: FormToolData, C: VanityControlData> {
pub(crate) style_attributes: Vec<<FD::Style as FormStyle>::StylingAttributes>,
pub(crate) data: C,
pub(crate) getter: Option<Rc<dyn FieldGetter<FD, String>>>,
pub(crate) show_when: Option<Box<dyn ShowWhenFn<FD, FD::Context>>>,
}
impl<FS: FormStyle, C: VanityControlData> VanityControlBuilder<FS, C> {
pub(crate) struct BuiltVanityControlData<FD: FormToolData, C: VanityControlData> {
pub(crate) render_data: ControlRenderData<FD::Style, C>,
pub(crate) getter: Option<Rc<dyn FieldGetter<FD, String>>>,
pub(crate) show_when: Option<Box<dyn ShowWhenFn<FD, FD::Context>>>,
}
impl<FD: FormToolData, C: VanityControlData> VanityControlBuilder<FD, C> {
/// Creates a new [`VanityControlBuilder`] with the given [`VanityControlData`].
pub(crate) fn new(data: C) -> Self {
VanityControlBuilder {
data,
style_attributes: Vec::new(),
getter: None,
show_when: None,
}
}
pub(crate) fn build(self) -> ControlRenderData<FS, C> {
ControlRenderData {
data: Box::new(self.data),
style: self.style_attributes,
/// Builds the builder into the data needed to render the control.
pub(crate) fn build(self) -> BuiltVanityControlData<FD, C> {
BuiltVanityControlData {
render_data: ControlRenderData {
data: self.data,
styles: self.style_attributes,
},
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.
pub fn style(mut self, attribute: <FD::Style as FormStyle>::StylingAttributes) -> Self {
self.style_attributes.push(attribute);
self
}
}
impl<FD: FormToolData, C: GetterVanityControlData> VanityControlBuilder<FD, C> {
/// Sets the getter function.
///
/// This function can get a string from the form data to be displayed
///
/// Setting this getter field is NOT required for vanity controls like this one.
pub fn getter(mut self, getter: impl FieldGetter<FD, String>) -> Self {
self.getter = Some(Rc::new(getter));
self
}
}
/// The possibilities for errors when building a control.
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
pub enum ControlBuildError {
/// The field that this control belongs to is not specified.
MissingField,
/// The getter field was not specified.
MissingGetter,
/// The setter field was not specified.
MissingSetter,
/// The parse function was not specified.
MissingParseFn,
/// The unparse function was not specified.
MissingUnParseFn,
}
impl Display for ControlBuildError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let message = match self {
ControlBuildError::MissingField => "you must specify what field this control is for",
ControlBuildError::MissingGetter => "missing getter function",
ControlBuildError::MissingSetter => "missing setter function",
ControlBuildError::MissingParseFn => "missing parse function",
ControlBuildError::MissingUnParseFn => "missing unparse function",
};
write!(f, "{}", message)
}
}
pub(crate) struct BuiltControlData<FD: FormData, FS: FormStyle, C: ControlData, FDT> {
pub(crate) render_data: ControlRenderData<FS, C>,
pub(crate) field_fn: Rc<dyn FieldFn<FD, FDT>>,
/// The data returned fomr a control's build function.
pub(crate) struct BuiltControlData<FD: FormToolData, C: ControlData, FDT> {
pub(crate) render_data: ControlRenderData<FD::Style, C>,
pub(crate) getter: Rc<dyn FieldGetter<FD, FDT>>,
pub(crate) setter: Rc<dyn FieldSetter<FD, FDT>>,
pub(crate) parse_fn: Box<dyn ParseFn<C::ReturnType, FDT>>,
pub(crate) unparse_fn: Box<dyn UnparseFn<C::ReturnType, FDT>>,
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
pub(crate) show_when: Option<Rc<dyn ShowWhenFn<FD, FD::Context>>>,
}
pub struct ControlBuilder<FD: FormData, FS: FormStyle, C: ControlData, FDT> {
pub(crate) field_fn: Option<Rc<dyn FieldFn<FD, FDT>>>,
/// A builder for a interactive control.
pub struct ControlBuilder<FD: FormToolData, C: ControlData, FDT> {
pub(crate) getter: Option<Rc<dyn FieldGetter<FD, FDT>>>,
pub(crate) setter: Option<Rc<dyn FieldSetter<FD, 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) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
pub(crate) style_attributes: Vec<FS::StylingAttributes>,
pub(crate) data: C,
pub(crate) style_attributes: Vec<<FD::Style as FormStyle>::StylingAttributes>,
pub(crate) show_when: Option<Rc<dyn ShowWhenFn<FD, FD::Context>>>,
pub data: C,
}
impl<FD: FormData, 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`].
pub(crate) fn new(data: C) -> Self {
ControlBuilder {
data,
field_fn: None,
getter: None,
setter: None,
parse_fn: None,
unparse_fn: None,
validation_fn: None,
style_attributes: Vec::new(),
show_when: None,
}
}
pub(crate) fn build(self) -> Result<BuiltControlData<FD, FS, C, FDT>, ControlBuildError> {
// either all 3 should be specified or none of them due to the possible `field` for `field_with` calls.
let (field_fn, parse_fn, unparse_fn) = match (self.field_fn, self.parse_fn, self.unparse_fn)
{
(Some(f), Some(p), Some(u)) => (f, p, u),
_ => return Err(ControlBuildError::MissingField),
/// Builds the builder into the data needed to render the control.
///
/// This fails if a required field was not specified.
pub(crate) fn build(self) -> Result<BuiltControlData<FD, C, FDT>, ControlBuildError> {
let getter = match self.getter {
Some(getter) => getter,
None => return Err(ControlBuildError::MissingGetter),
};
let setter = match self.setter {
Some(setter) => setter,
None => return Err(ControlBuildError::MissingSetter),
};
let parse_fn = match self.parse_fn {
Some(parse_fn) => parse_fn,
None => return Err(ControlBuildError::MissingParseFn),
};
let unparse_fn = match self.unparse_fn {
Some(unparse_fn) => unparse_fn,
None => return Err(ControlBuildError::MissingUnParseFn),
};
Ok(BuiltControlData {
render_data: ControlRenderData {
data: Box::new(self.data),
style: self.style_attributes,
data: self.data,
styles: self.style_attributes,
},
field_fn,
getter,
setter,
parse_fn,
unparse_fn,
validation_fn: self.validation_fn,
show_when: self.show_when,
})
}
// TODO: add method that automatically does the parse and unparse using
// TryInto<C::ReturnValue> and TryFrom<C::ReturnVlaue>
pub fn field_with(
/// 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,
field_fn: impl FieldFn<FD, FDT>,
parse_fn: impl ParseFn<C::ReturnType, FDT>,
unparse_fn: impl UnparseFn<C::ReturnType, FDT>,
when: impl Fn(Signal<FD>, Rc<FD::Context>) -> bool + 'static,
) -> Self {
self.field_fn = Some(Rc::new(field_fn));
self.parse_fn = Some(Box::new(parse_fn));
self.unparse_fn = Some(Box::new(unparse_fn));
self.show_when = Some(Rc::new(when));
self
}
/// Overrides the field's parse functions with the ones given.
pub fn parse_fns(
/// Sets the getter function.
///
/// This function should get the field from the form data
/// for use in the form field.
///
/// Setting this getter field is required.
pub fn getter(mut self, getter: impl FieldGetter<FD, FDT>) -> Self {
self.getter = Some(Rc::new(getter));
self
}
/// Sets the setter function.
///
/// This function should get the field from the form data
/// for use in the form field.
///
/// Setting this setter field is required.
pub fn setter(mut self, setter: impl FieldSetter<FD, FDT>) -> Self {
self.setter = Some(Rc::new(setter));
self
}
/// Sets the parse functions to the ones given.
///
/// 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
/// vice versa.
pub fn parse_custom(
mut self,
parse_fn: impl ParseFn<C::ReturnType, FDT>,
unparse_fn: impl UnparseFn<C::ReturnType, FDT>,
@@ -158,7 +292,93 @@ impl<FD: FormData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C,
self
}
/// Adds a styling attribute to this control.
pub fn style(mut self, attribute: <FD::Style as FormStyle>::StylingAttributes) -> Self {
self.style_attributes.push(attribute);
self
}
}
impl<FD, C, FDT> ControlBuilder<FD, C, FDT>
where
FD: FormToolData,
C: ControlData,
FDT: TryFrom<<C as ControlData>::ReturnType>,
<FDT as TryFrom<<C as ControlData>::ReturnType>>::Error: ToString,
<C as ControlData>::ReturnType: From<FDT>,
{
/// Sets the parse functions to use the [`TryFrom`] and [`From`] traits
/// for parsing and unparsing respectively.
///
/// 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
/// vice versa.
pub fn parse_from(mut self) -> Self {
self.parse_fn = Some(Box::new(|control_return_value| {
FDT::try_from(control_return_value).map_err(|e| e.to_string())
}));
self.unparse_fn = Some(Box::new(|field| {
<C as ControlData>::ReturnType::from(field)
}));
self
}
}
impl<FD, C, FDT> ControlBuilder<FD, C, FDT>
where
FD: FormToolData,
C: ControlData<ReturnType = String>,
FDT: FromStr + ToString,
<FDT as FromStr>::Err: ToString,
{
/// Sets the parse functions to use the [`FromStr`] [`ToString`] and traits
/// for parsing and unparsing respectively. To trim the string before
/// parsing, see [`parse_trimmed`](Self::parse_trimmed)().
///
/// 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
/// vice versa.
pub fn parse_string(mut self) -> Self {
self.parse_fn = Some(Box::new(|control_return_value| {
control_return_value
.parse::<FDT>()
.map_err(|e| e.to_string())
}));
self.unparse_fn = Some(Box::new(|field| field.to_string()));
self
}
/// Sets the parse functions to use the [`FromStr`] [`ToString`] and traits
/// for parsing and unparsing respectively, similar to
/// [`parse_string`](Self::parse_string)().
/// However, this method trims the string before parsing.
///
/// 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
/// vice versa.
pub fn parse_trimmed(mut self) -> Self {
self.parse_fn = Some(Box::new(|control_return_value| {
control_return_value
.trim()
.parse::<FDT>()
.map_err(|e| e.to_string())
}));
self.unparse_fn = Some(Box::new(|field| field.to_string()));
self
}
}
impl<FD: FormToolData, C: ValidatedControlData, FDT> ControlBuilder<FD, C, FDT> {
/// Sets the validation function for this control
///
/// This allows you to check if the parsed value is a valid value.
///
/// You are given the entire [`FormToolData`] struct, but you should only
/// validate the field you are creating. You can use the other fields in
/// the struct as context.
///
/// Ex. You have a month and a day field in a form. You use the month
/// field to help ensure that the day is a valid day of that month.
pub fn validation_fn(
mut self,
validation_fn: impl Fn(&FD) -> Result<(), String> + 'static,
@@ -166,9 +386,4 @@ impl<FD: FormData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C,
self.validation_fn = Some(Rc::new(validation_fn));
self
}
pub fn style(mut self, attribute: FS::StylingAttributes) -> Self {
self.style_attributes.push(attribute);
self
}
}
+44
View File
@@ -0,0 +1,44 @@
use super::{
BuilderCxFn, BuilderFn, ControlRenderData, GetterVanityControlData, VanityControlBuilder,
VanityControlData,
};
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)]
pub struct OutputData;
impl VanityControlData for OutputData {
fn build_control<FS: FormStyle>(
fs: &FS,
control: Rc<ControlRenderData<FS, Self>>,
value_getter: Option<Signal<String>>,
) -> View {
fs.output(control, value_getter)
}
}
impl GetterVanityControlData for OutputData {}
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds an output form control and adds it to the form.
///
/// This control allows you to output some text to the user based on the
/// form data.
pub fn output(self, builder: impl BuilderFn<VanityControlBuilder<FD, OutputData>>) -> Self {
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)
}
}
+111
View File
@@ -0,0 +1,111 @@
use super::{
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
};
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)]
pub struct RadioButtonsData {
pub name: String,
pub label: Option<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 {
type ReturnType = String;
fn build_control<FS: FormStyle>(
fs: &FS,
control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>,
value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
fs.radio_buttons(control, value_getter, value_setter, validation_state)
}
}
impl ValidatedControlData for RadioButtonsData {}
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a radio buttons control and adds it to the form.
pub fn radio_buttons<FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderFn<ControlBuilder<FD, RadioButtonsData, FDT>>,
) -> Self {
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, 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 {
self.data.name = control_name.to_string();
self
}
/// Sets the label for the radio button group.
pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string());
self
}
/// Adds the option to the radio button group.
pub fn with_option(mut self, option: impl ToString) -> Self {
self.data
.options
.push((option.to_string(), option.to_string()));
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 {
for option in options {
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
}
}
+103 -21
View File
@@ -1,15 +1,21 @@
use leptos::{Signal, View};
use super::{ControlBuilder, ControlData, ControlRenderData};
use crate::{
form::{FormBuilder, FormData},
styles::FormStyle,
use super::{
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
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(crate) name: String,
pub(crate) options: Vec<String>,
pub name: String,
pub label: Option<String>,
/// The options for the select.
///
/// The first value is the string to display, the second is the value.
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 {
@@ -17,34 +23,110 @@ impl ControlData for SelectData {
fn build_control<FS: FormStyle>(
fs: &FS,
control: ControlRenderData<FS, Self>,
control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>,
value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
fs.select(control, value_getter, value_setter, validation_state)
}
}
impl ValidatedControlData for SelectData {}
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
pub fn select<FDT: 'static>(
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a select control and adds it to the form.
pub fn select<FDT: Clone + PartialEq + 'static>(
self,
builder: impl Fn(
ControlBuilder<FD, FS, SelectData, FDT>,
) -> ControlBuilder<FD, FS, SelectData, FDT>,
builder: impl BuilderFn<ControlBuilder<FD, SelectData, FDT>>,
) -> Self {
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: FormData, FS: FormStyle, FDT> ControlBuilder<FD, FS, SelectData, FDT> {
pub fn options(mut self, options: Vec<String>) -> Self {
self.data.options = options;
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 {
self.data.name = control_name.to_string();
self
}
pub fn and_option(mut self, option: impl ToString) -> Self {
self.data.options.push(option.to_string());
/// Sets the label for the select.
pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string());
self
}
/// Sets the options from the provided iterator.
///
/// This will overwrite any pervious options setting.
pub fn with_options(mut self, options: impl Iterator<Item = impl ToString>) -> Self {
let options = options.map(|v| (v.to_string(), v.to_string())).collect();
self.data.options = MaybeSignal::Static(options);
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(
mut self,
options: impl Iterator<Item = (impl ToString, impl ToString)>,
) -> Self {
let options = options
.map(|(d, v)| (d.to_string(), v.to_string()))
.collect();
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
}
}
+99
View File
@@ -0,0 +1,99 @@
use super::{BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{MaybeSignal, Signal, View};
use std::rc::Rc;
/// Data used for the slider control.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SliderData {
pub name: String,
pub label: Option<String>,
pub min: MaybeSignal<i32>,
pub max: MaybeSignal<i32>,
}
impl Default for SliderData {
fn default() -> Self {
SliderData {
name: String::new(),
label: None,
min: MaybeSignal::Static(0),
max: MaybeSignal::Static(1),
}
}
}
impl ControlData for SliderData {
type ReturnType = i32;
fn build_control<FS: FormStyle>(
fs: &FS,
control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>,
value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
fs.slider(control, value_getter, value_setter, validation_state)
}
}
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a slider (or range) control and adds it to the form.
pub fn slider<FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderFn<ControlBuilder<FD, SliderData, FDT>>,
) -> Self {
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, 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 {
self.data.name = control_name.to_string();
self
}
/// Sets the label for the slider.
pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string());
self
}
/// Sets the minimum value for the slider.
pub fn min(mut self, min: i32) -> Self {
self.data.min = MaybeSignal::Static(min);
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 {
self.data.max = MaybeSignal::Static(max);
self
}
/// Sets the maximum value for the slider to a signal.
pub fn max_signal(mut self, max: Signal<i32>) -> Self {
self.data.max = MaybeSignal::Dynamic(max);
self
}
}
+47
View File
@@ -0,0 +1,47 @@
use super::{BuilderCxFn, BuilderFn, ControlRenderData, VanityControlBuilder, VanityControlData};
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)]
pub struct SpacerData {
pub height: Option<String>,
}
impl VanityControlData for SpacerData {
fn build_control<FS: FormStyle>(
fs: &FS,
control: Rc<ControlRenderData<FS, Self>>,
_value_getter: Option<Signal<String>>,
) -> View {
fs.spacer(control)
}
}
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a spacer and adds it to the form.
pub fn spacer(self, builder: impl BuilderFn<VanityControlBuilder<FD, SpacerData>>) -> Self {
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> 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 {
self.data.height = Some(height.to_string());
self
}
}
+104
View File
@@ -0,0 +1,104 @@
use super::{
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{MaybeSignal, Signal, View};
use std::rc::Rc;
/// Data used for the stepper control.
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct StepperData {
pub name: String,
pub label: Option<String>,
pub step: Option<MaybeSignal<i32>>,
pub min: Option<MaybeSignal<i32>>,
pub max: Option<MaybeSignal<i32>>,
}
impl ControlData for StepperData {
type ReturnType = String;
fn build_control<FS: FormStyle>(
fs: &FS,
control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>,
value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
fs.stepper(control, value_getter, value_setter, validation_state)
}
}
impl ValidatedControlData for StepperData {}
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a stepper control and adds it to the form.
pub fn stepper<FDT: Clone + PartialEq + 'static>(
self,
builder: impl BuilderFn<ControlBuilder<FD, StepperData, FDT>>,
) -> Self {
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, 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 {
self.data.name = control_name.to_string();
self
}
/// Sets the label of the stepper.
pub fn labeled(mut self, label: impl ToString) -> Self {
self.data.label = Some(label.to_string());
self
}
/// Sets the step ammount.
pub fn step(mut self, step: i32) -> Self {
self.data.step = Some(MaybeSignal::Static(step));
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 {
self.data.min = Some(MaybeSignal::Static(min));
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 {
self.data.max = Some(MaybeSignal::Static(max));
self
}
/// Sets the maximum value for the slider to a signal.
pub fn max_signal(mut self, max: Signal<i32>) -> Self {
self.data.max = Some(MaybeSignal::Dynamic(max));
self
}
}
+25 -15
View File
@@ -1,32 +1,42 @@
use leptos::View;
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
use crate::{
form::{FormBuilder, FormData},
styles::FormStyle,
};
use super::{BuilderCxFn, BuilderFn, ControlRenderData, VanityControlBuilder, VanityControlData};
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)]
pub struct SubmitData {
pub(crate) text: String,
pub text: String,
}
impl VanityControlData for SubmitData {
fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View {
fn build_control<FS: FormStyle>(
fs: &FS,
control: Rc<ControlRenderData<FS, Self>>,
_value_getter: Option<Signal<String>>,
) -> View {
fs.submit(control)
}
}
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
pub fn submit(
self,
builder: impl Fn(VanityControlBuilder<FS, SubmitData>) -> VanityControlBuilder<FS, SubmitData>,
) -> Self {
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a submit button and adds it to the form.
pub fn submit(self, builder: impl BuilderFn<VanityControlBuilder<FD, SubmitData>>) -> Self {
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<FS: FormStyle> VanityControlBuilder<FS, SubmitData> {
impl<FD: FormToolData> VanityControlBuilder<FD, SubmitData> {
/// Sets the submit button's text.
pub fn text(mut self, text: impl ToString) -> Self {
self.data.text = text.to_string();
self
+42 -14
View File
@@ -1,14 +1,16 @@
use super::{ControlBuilder, ControlData, ControlRenderData};
use crate::{
form::{FormBuilder, FormData},
styles::FormStyle,
use super::{
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
};
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
use leptos::{Signal, View};
use std::rc::Rc;
/// Data used for the text area control.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
pub struct TextAreaData {
pub(crate) name: String,
pub(crate) placeholder: Option<String>,
pub name: String,
pub label: Option<String>,
pub placeholder: Option<String>,
}
impl ControlData for TextAreaData {
@@ -16,27 +18,53 @@ impl ControlData for TextAreaData {
fn build_control<FS: FormStyle>(
fs: &FS,
control: ControlRenderData<FS, Self>,
control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>,
value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
fs.text_area(control, value_getter, value_setter, validation_state)
}
}
impl ValidatedControlData for TextAreaData {}
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
pub fn text_area<FDT: 'static>(
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a text area control and adds it to the form.
pub fn text_area<FDT: Clone + PartialEq + 'static>(
self,
builder: impl Fn(
ControlBuilder<FD, FS, TextAreaData, FDT>,
) -> ControlBuilder<FD, FS, TextAreaData, FDT>,
builder: impl BuilderFn<ControlBuilder<FD, TextAreaData, FDT>>,
) -> Self {
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: FormData, 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 {
self.data.placeholder = Some(placeholder.to_string());
self
+57 -30
View File
@@ -1,18 +1,17 @@
use leptos::{Signal, View};
use super::{ControlBuilder, ControlData, ControlRenderData};
use crate::{
form::{FormBuilder, FormData},
styles::FormStyle,
use super::{
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
};
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)]
pub struct TextInputData {
pub(crate) name: String,
pub(crate) placeholder: Option<String>,
pub(crate) label: Option<String>,
pub(crate) initial_text: String,
pub(crate) input_type: &'static str,
pub name: String,
pub label: Option<String>,
pub placeholder: Option<String>,
pub input_type: &'static str,
}
impl Default for TextInputData {
@@ -21,7 +20,6 @@ impl Default for TextInputData {
name: String::new(),
placeholder: None,
label: None,
initial_text: String::new(),
input_type: "input",
}
}
@@ -32,44 +30,73 @@ impl ControlData for TextInputData {
fn build_control<FS: FormStyle>(
fs: &FS,
control: ControlRenderData<FS, Self>,
control: Rc<ControlRenderData<FS, Self>>,
value_getter: Signal<Self::ReturnType>,
value_setter: Box<dyn Fn(Self::ReturnType)>,
value_setter: Rc<dyn Fn(Self::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
fs.text_input(control, value_getter, value_setter, validation_state)
}
}
impl ValidatedControlData for TextInputData {}
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
pub fn text_input<FDT: 'static>(
impl<FD: FormToolData> FormBuilder<FD> {
/// Builds a text input control and adds it to the form.
pub fn text_input<FDT: Clone + PartialEq + 'static>(
self,
builder: impl Fn(
ControlBuilder<FD, FS, TextInputData, FDT>,
) -> ControlBuilder<FD, FS, TextInputData, FDT>,
builder: impl BuilderFn<ControlBuilder<FD, TextInputData, FDT>>,
) -> Self {
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: FormData, 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 {
self.data.name = control_name.to_string();
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 {
self.data.placeholder = Some(placeholder.to_string());
self
}
pub fn label(mut self, label: impl ToString) -> Self {
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
}
/// Sets the text input to be the "password" type.
pub fn password(mut self) -> Self {
self.data.input_type = "password";
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
}
}
+146 -267
View File
@@ -1,22 +1,25 @@
use std::rc::Rc;
use crate::{
controls::{
BuiltControlData, ControlBuilder, ControlData, FieldFn, ParseFn, UnparseFn, ValidationFn,
VanityControlBuilder, VanityControlData,
},
styles::FormStyle,
};
use crate::{controls::ValidationFn, form_builder::FormBuilder, styles::FormStyle};
use leptos::{
create_rw_signal, create_signal, IntoSignal, IntoView, RwSignal, SignalGet, SignalSet,
SignalUpdate, View,
server_fn::{client::Client, codec::PostUrl, request::ClientReq, ServerFn},
*,
};
use serde::de::DeserializeOwned;
use std::rc::Rc;
use web_sys::FormData;
pub struct Validator<FD: FormData> {
validations: Vec<Rc<dyn ValidationFn<FD>>>,
/// A type that can be used to validate the form data.
///
/// This can be useful to use the same validation logic on the front
/// end and backend without duplicating the logic.
pub struct FormValidator<FD> {
pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
}
impl<FD: FormData> Validator<FD> {
impl<FD: FormToolData> FormValidator<FD> {
/// Validates the given form data.
///
/// This runs all the validation functions for all the fields
/// in the form. The first falure to occur (if any) will be returned.
pub fn validate(&self, form_data: &FD) -> Result<(), String> {
for v in self.validations.iter() {
(*v)(form_data)?;
@@ -25,38 +28,43 @@ impl<FD: FormData> Validator<FD> {
}
}
/// A constructed form object.
/// A constructed, rendered form object.
///
/// With this, you can render the form, get the form data, or get
/// a validator for the data.
pub struct Form<FD: FormData> {
#[derive(Clone)]
pub struct Form<FD: FormToolData> {
/// The form data signal.
pub fd: RwSignal<FD>,
validations: Vec<Rc<dyn ValidationFn<FD>>>,
view: View,
/// The list of validations
pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
pub(crate) view: View,
}
impl<FD: FormData> Form<FD> {
pub fn validator(self) -> Validator<FD> {
Validator {
validations: self.validations,
impl<FD: FormToolData> Form<FD> {
/// Gets the [`FormValidator`] for this form.
pub fn validator(&self) -> FormValidator<FD> {
FormValidator {
validations: self.validations.clone(),
}
}
pub fn validate(&self, form_data: &FD) -> Result<(), String> {
for v in self.validations.iter() {
(*v)(form_data)?;
}
Ok(())
/// Validates the [`FormToolData`], returning the result.
pub fn validate(&self) -> Result<(), String> {
let validator = self.validator();
validator.validate(&self.fd.get_untracked())
}
/// Gets the view associated with this [`Form`].
pub fn view(&self) -> View {
self.view.clone()
}
pub fn to_parts(self) -> (RwSignal<FD>, Validator<FD>, View) {
/// Splits this [`Form`] into it's parts.
pub fn to_parts(self) -> (RwSignal<FD>, FormValidator<FD>, View) {
(
self.fd,
Validator {
FormValidator {
validations: self.validations,
},
self.view,
@@ -64,266 +72,137 @@ impl<FD: FormData> Form<FD> {
}
}
impl<FD: FormData> IntoView for Form<FD> {
impl<FD: FormToolData> IntoView for Form<FD> {
fn into_view(self) -> View {
self.view()
}
}
/// A version of the [`FormBuilder`] that contains all the data
/// needed for full building of a [`Form`].
struct FullFormBuilder<FD: FormData, FS: FormStyle> {
fd: RwSignal<FD>,
fs: FS,
validations: Vec<Rc<dyn ValidationFn<FD>>>,
views: Vec<View>,
}
/// The internal type for building forms
///
/// This allows us to build either the full form
/// with views, validation and data. Or we can just
/// build the validation functions.
///
/// This is useful in the context of a server that
/// cannot or should not render the form. You can
/// still get all the validation functions from the
/// form data.
enum FormBuilderInner<FD: FormData, FS: FormStyle> {
/// For building the form with views
FullBuilder(FullFormBuilder<FD, FS>),
/// For building only the validations for the form
ValidationBuilder {
validations: Vec<Rc<dyn ValidationFn<FD>>>,
},
}
pub struct FormBuilder<FD: FormData, FS: FormStyle> {
inner: FormBuilderInner<FD, FS>,
}
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
// TODO: remove the Default trait bound and bind it to this function only
fn new_full_builder(form_style: FS) -> FormBuilder<FD, FS> {
let fd = create_rw_signal(FD::default());
FormBuilder {
inner: FormBuilderInner::FullBuilder(FullFormBuilder {
fd,
fs: form_style,
validations: Vec::new(),
views: Vec::new(),
}),
}
}
fn new_full_builder_with(starting_data: FD, form_style: FS) -> FormBuilder<FD, FS> {
let fd = create_rw_signal(starting_data);
FormBuilder {
inner: FormBuilderInner::FullBuilder(FullFormBuilder {
fd,
fs: form_style,
validations: Vec::new(),
views: Vec::new(),
}),
}
}
fn new_validation_builder() -> FormBuilder<FD, FS> {
FormBuilder {
inner: FormBuilderInner::ValidationBuilder {
validations: Vec::new(),
},
}
}
pub(crate) fn new_vanity<C: VanityControlData + Default>(
mut self,
builder: impl Fn(VanityControlBuilder<FS, C>) -> VanityControlBuilder<FS, C>,
) -> Self {
let vanity_builder = VanityControlBuilder::new(C::default());
let control = builder(vanity_builder);
self.add_vanity(control);
self
}
pub(crate) fn new_control<C: ControlData + Default, FDT: 'static>(
mut self,
builder: impl Fn(ControlBuilder<FD, FS, C, FDT>) -> ControlBuilder<FD, FS, C, FDT>,
) -> Self {
let control_builder = ControlBuilder::new(C::default());
let control = builder(control_builder);
self.add_control(control);
self
}
fn add_vanity<C: VanityControlData>(&mut self, vanity_control: VanityControlBuilder<FS, C>) {
let builder = match &mut self.inner {
FormBuilderInner::FullBuilder(fb) => fb,
FormBuilderInner::ValidationBuilder { validations: _ } => return,
};
let render_data = vanity_control.build();
let view = VanityControlData::build_control(&builder.fs, render_data);
builder.views.push(view);
}
fn add_control<C: ControlData, FDT: 'static>(
&mut self,
control: ControlBuilder<FD, FS, C, FDT>,
) {
let BuiltControlData {
render_data,
field_fn,
parse_fn,
unparse_fn,
validation_fn,
} = match control.build() {
Ok(c) => c,
Err(e) => panic!("Invalid Component: {}", e),
};
let builder = match &mut self.inner {
FormBuilderInner::FullBuilder(fb) => fb,
FormBuilderInner::ValidationBuilder {
ref mut validations,
} => {
if let Some(validation_fn) = validation_fn {
validations.push(validation_fn);
}
return;
}
};
if let Some(ref validation_fn) = validation_fn {
builder.validations.push(validation_fn.clone());
}
let view = Self::build_control_view(
builder,
field_fn,
unparse_fn,
parse_fn,
validation_fn,
render_data,
);
builder.views.push(view);
}
fn build_control_view<C: ControlData, FDT: 'static>(
builder: &FullFormBuilder<FD, FS>,
field_fn: Rc<dyn FieldFn<FD, FDT>>,
unparse_fn: Box<dyn UnparseFn<<C as ControlData>::ReturnType, FDT>>,
parse_fn: Box<dyn ParseFn<<C as ControlData>::ReturnType, FDT>>,
validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
render_data: crate::controls::ControlRenderData<FS, C>,
) -> View {
let (validation_signal, validation_signal_set) = create_signal(Ok(()));
// TODO: The on-submit triggering a validation could be done here with a create_effect
let field_fn2 = field_fn.clone();
let fd_getter = builder.fd.read_only();
let value_getter = move || {
// TODO: ideally, this should not be borrowed. If we get a clone when we call `.get` we should pass in the clone, not borrow this clone
let mut fd = fd_getter.get();
let field = field_fn2(&mut fd);
(unparse_fn)(field)
};
let value_getter = value_getter.into_signal();
let fd = builder.fd.clone();
let value_setter = move |value| {
// TODO: also do this on a submit somehow
let parsed = match parse_fn(value) {
Ok(p) => p,
Err(e) => {
validation_signal_set.set(Err(e));
return;
}
};
// parse succeeded, update value and validate
let field_fn = field_fn.clone(); // move
let validation_fn = validation_fn.clone(); // move
// TODO: change this to a get then set, so that if the validation fails,
// The value is not actually updated
fd.update(move |fd| {
let field = field_fn(fd);
*field = parsed;
if let Some(ref validation_fn) = validation_fn {
let validation_result = (validation_fn)(fd);
validation_signal_set.set(validation_result);
}
});
};
let value_setter = Box::new(value_setter);
// TODO: add a signal that triggers on submit to refresh the validation on_submit
C::build_control(
&builder.fs,
render_data,
value_getter,
value_setter,
validation_signal.into(),
)
}
fn build(self) -> Option<Form<FD>> {
let builder = match self.inner {
FormBuilderInner::FullBuilder(fb) => fb,
FormBuilderInner::ValidationBuilder { validations: _ } => return None,
};
let view = builder.fs.form_frame(builder.views.into_view());
Some(Form {
fd: builder.fd,
validations: builder.validations,
view,
})
}
fn validator(self) -> Validator<FD> {
let validations = match self.inner {
FormBuilderInner::FullBuilder(fb) => fb.validations,
FormBuilderInner::ValidationBuilder { validations } => validations,
};
Validator { validations }
}
}
/// 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
/// to physically lay out a form, and how that data should be parsed and validated.
pub trait FormData: Default + Clone + 'static {
/// This trait defines a function that can be used to build all the data
/// needed to physically lay out a form, and how that data should be parsed
/// and validated.
pub trait FormToolData: Clone + 'static {
/// The style that this form uses.
type Style: FormStyle;
/// Defines how the form should be layed out and how the data should be parsed and validated.
/// The context that this form is rendered in.
///
/// 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
/// in the form, what properties those fields should have, and how that
/// 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 FormData type.
/// Constructs a [`Form`] for this [`FormToolData`] type.
///
/// The [`Form`] provides the way to render the form.
fn get_form(style: Self::Style) -> Form<Self> {
let builder = FormBuilder::new_full_builder(style);
/// This renders the form as a enhanced
/// [`ActionForm`](leptos_router::ActionForm) that sends the form data
/// directly by calling the server function.
///
/// By doing this, we avoid doing the
/// [`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);
builder.build().expect("builder should be full builder")
builder.build_form(action, self, style)
}
fn get_form_with_starting_data(self, style: Self::Style) -> Form<Self> {
let builder = FormBuilder::new_full_builder_with(self, style);
/// Constructs a [`Form`] for this [`FormToolData`] type.
///
/// This renders the form as a the leptos_router
/// [`ActionForm`](leptos_router::ActionForm)
/// component.
///
/// 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>(
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>,
{
let builder = FormBuilder::new(context);
let builder = Self::build_form(builder);
builder.build().expect("builder should be full builder")
builder.build_action_form(action, self, style)
}
fn get_validator() -> Validator<Self> {
let builder = FormBuilder::new_validation_builder();
/// 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
/// Functions from building the form. That means it can be called on the
/// Server and no rendering will be done.
///
/// However, the code to render the views are not configured out, it
/// simply doesn't run, so the view needs to compile even on the server.
fn get_validator(context: Self::Context) -> FormValidator<Self> {
let builder = FormBuilder::new(context);
let builder = Self::build_form(builder);
builder.validator()
}
/// Validates this [`FormToolData`] struct.
///
/// This is shorthand for creating a validator with
/// [`get_validator`](Self::get_validator)()
/// and then calling `validator.validate(&self, context)`.
fn validate(&self, context: Self::Context) -> Result<(), String> {
let validator = Self::get_validator(context);
validator.validate(self)
}
}
+460
View File
@@ -0,0 +1,460 @@
use crate::{
controls::{
BuilderCxFn, BuilderFn, BuiltControlData, BuiltVanityControlData, ControlBuilder,
ControlData, ControlRenderData, FieldSetter, ParseFn, RenderFn, ValidationCb, ValidationFn,
VanityControlBuilder, VanityControlData,
},
form::{Form, FormToolData, FormValidator},
styles::FormStyle,
};
use leptos::{
server_fn::{client::Client, codec::PostUrl, request::ClientReq, ServerFn},
*,
};
use leptos_router::{ActionForm, Form};
use serde::de::DeserializeOwned;
use std::rc::Rc;
use web_sys::{FormData, SubmitEvent};
/// A builder for laying out forms.
///
/// This builder allows you to specify what components should make up the form.
pub struct FormBuilder<FD: FormToolData> {
pub(crate) cx: Rc<FD::Context>,
/// The list of [`ValidationFn`]s.
pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
/// The list of functions that will render the form.
pub(crate) render_fns: Vec<Box<dyn RenderFn<FD::Style, FD>>>,
/// The list of styling attributes applied on the form level.
pub(crate) styles: Vec<<FD::Style as FormStyle>::StylingAttributes>,
}
impl<FD: FormToolData> FormBuilder<FD> {
/// Creates a new [`FormBuilder`]
pub(crate) fn new(cx: FD::Context) -> Self {
FormBuilder {
cx: Rc::new(cx),
validations: Vec::new(),
render_fns: Vec::new(),
styles: Vec::new(),
}
}
/// 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
}
/// Adds a new vanity control to the form.
pub(crate) fn new_vanity<C: VanityControlData + Default>(
mut self,
builder: impl BuilderFn<VanityControlBuilder<FD, C>>,
) -> Self {
let vanity_builder = VanityControlBuilder::new(C::default());
let control = builder(vanity_builder);
self.add_vanity(control);
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>(
mut self,
builder: impl BuilderFn<ControlBuilder<FD, C, FDT>>,
) -> Self {
let control_builder = ControlBuilder::new(C::default());
let control = builder(control_builder);
self.add_control(control);
self
}
/// Adds a new control to the form using the form's context.
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,
vanity_control: VanityControlBuilder<FD, C>,
) {
let BuiltVanityControlData {
render_data,
getter,
show_when,
} = vanity_control.build();
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 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)
};
self.render_fns.push(Box::new(render_fn));
}
/// Adds a control to the form.
pub(crate) fn add_control<C: ControlData, FDT: Clone + PartialEq + 'static>(
&mut self,
control: ControlBuilder<FD, C, FDT>,
) {
let built_control_data = match control.build() {
Ok(c) => c,
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(validation_fn) = built_control_data.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 cx = self.cx.clone();
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))
};
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>(
fd: RwSignal<FD>,
fs: Rc<FD::Style>,
control_data: BuiltControlData<FD, C, FDT>,
cx: Rc<FD::Context>,
) -> (View, Box<dyn ValidationCb>) {
let BuiltControlData {
render_data,
getter,
setter,
parse_fn,
unparse_fn,
validation_fn,
show_when,
} = control_data;
let render_data = Rc::new(render_data);
let (validation_signal, validation_signal_set) = create_signal(Ok(()));
let validation_fn_clone = validation_fn.clone();
let value_getter = move || {
let fd = fd.get();
// rerun validation if it is failing
if validation_signal.get_untracked().is_err() {
if let Some(ref validation_fn) = validation_fn_clone {
let validation_result = validation_fn(&fd);
// if validation succeeds this time, resolve the validation error
if validation_result.is_ok() {
validation_signal_set.set(Ok(()));
}
}
}
unparse_fn(getter(fd))
};
let value_getter = value_getter.into_signal();
let cloned_show_when = show_when.clone();
let cloned_cx = cx.clone();
let validation_cb = move || {
// 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 {
Some(ref v) => v,
None => return true, // No validation function so validation passes
};
let data = fd.get_untracked();
let validation_result = validation_fn(&data);
let succeeded = validation_result.is_ok();
validation_signal_set.set(validation_result);
succeeded
};
let validation_cb = Box::new(validation_cb);
let value_setter = Self::create_value_setter(
validation_cb.clone(),
validation_signal_set,
parse_fn,
setter,
fd,
);
let view = move || {
C::build_control(
&*fs,
render_data.clone(),
value_getter,
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)
}
/// Helper for creating a setter function.
fn create_value_setter<CRT: 'static, FDT: 'static>(
validation_cb: Box<dyn Fn() -> bool + 'static>,
validation_signal_set: WriteSignal<Result<(), String>>,
parse_fn: Box<dyn ParseFn<CRT, FDT>>,
setter: Rc<dyn FieldSetter<FD, FDT>>,
fd: RwSignal<FD>,
) -> Rc<dyn Fn(CRT) + 'static> {
let value_setter = move |value| {
let parsed = match parse_fn(value) {
Ok(p) => {
validation_signal_set.set(Ok(()));
p
}
Err(e) => {
validation_signal_set.set(Err(e));
return;
}
};
// parse succeeded, update value and validate
fd.update(|data| {
setter(data, parsed);
});
// run validation
(validation_cb)();
};
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>(
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>,
{
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();
}
};
let view = view! {
<ActionForm action=action on:submit=on_submit>
{elements}
</ActionForm>
};
Form {
fd,
validations: self.validations,
view,
}
}
/// 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 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();
}
};
let view = view! {
<Form action=url on:submit=on_submit>
{elements}
</Form>
};
Form {
fd,
validations: self.validations,
view,
}
}
/// Creates a [`FormValidator`] from this builder.
pub(crate) fn validator(&self) -> FormValidator<FD> {
FormValidator {
validations: self.validations.clone(),
}
}
}
+12 -1
View File
@@ -1,3 +1,14 @@
//! `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 form;
mod form;
mod form_builder;
pub mod styles;
mod validation_builder;
pub use form::{Form, FormToolData, FormValidator};
pub use form_builder::FormBuilder;
pub use validation_builder::ValidationBuilder;
+354 -94
View File
@@ -1,155 +1,415 @@
use super::FormStyle;
use crate::controls::{
heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData,
text_input::TextInputData, ControlData, ControlRenderData,
use crate::{
controls::{
button::ButtonData, checkbox::CheckboxData, heading::HeadingData, hidden::HiddenData,
output::OutputData, radio_buttons::RadioButtonsData, select::SelectData,
slider::SliderData, spacer::SpacerData, stepper::StepperData, submit::SubmitData,
text_area::TextAreaData, text_input::TextInputData, ControlData, ControlRenderData,
},
FormToolData,
};
use leptos::*;
use leptos_router::Form;
use std::rc::Rc;
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),
/// 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)]
pub struct GridFormStyle;
impl FormStyle for GridFormStyle {
type StylingAttributes = GridFormStylingAttributes;
type StylingAttributes = GFStyleAttr;
fn form_frame(&self, form: ControlRenderData<Self, View>) -> View {
view! { <div class="form_grid">{form.data}</div> }.into_view()
}
/// A common function that wraps the given view in the styles
fn custom_component(&self, styles: &[Self::StylingAttributes], inner: View) -> View {
let mut width = 12;
let mut tooltip = None;
for style in styles.iter() {
match style {
GFStyleAttr::Width(w) => width = *w,
GFStyleAttr::Tooltip(t) => tooltip = Some(t),
}
}
// TODO: something about an on-submit thing
fn form_frame(&self, children: View) -> View {
view! {
<Form action="" class="form_grid">
{children}
</Form>
<div style:grid-column=format!("span {}", width) title=tooltip>
{inner}
</div>
}
.into_view()
}
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View {
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,
control: Rc<ControlRenderData<Self, ButtonData<FD>>>,
data_signal: RwSignal<FD>,
) -> View {
let action = control.data.action.clone();
let on_click = move |ev: MouseEvent| {
if let Some(action) = action.clone() {
action(ev, data_signal)
}
};
let view = view! {
<button type="button" class="form_button" on:click=on_click>
{&control.data.text}
</button>
}
.into_view();
self.custom_component(&control.styles, view)
}
fn output(
&self,
control: Rc<ControlRenderData<Self, OutputData>>,
value_getter: Option<Signal<String>>,
) -> View {
let view = view! { <span>{move || value_getter.map(|g| g.get())}</span> }.into_view();
self.custom_component(&control.styles, view)
}
fn hidden(
&self,
control: Rc<ControlRenderData<Self, HiddenData>>,
value_getter: Option<Signal<String>>,
) -> View {
let value_getter = move || value_getter.map(|g| g.get());
view! {
<h2 class="form_heading">
{&control.data.title}
</h2>
<input
name=&control.data.name
prop:value=value_getter
style="visibility: hidden; column-span: none"
/>
}
.into_view()
}
fn text_input(
&self,
control: ControlRenderData<Self, TextInputData>,
control: Rc<ControlRenderData<Self, TextInputData>>,
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
value_setter: Box<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
value_setter: Rc<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
view! {
let view = view! {
<div>
<div>
<label for={&control.data.name} class="form_label">
<label for=&control.data.name class="form_label">
{control.data.label.as_ref()}
</label>
<span class="form_error">
{move || format!("{}", validation_state.get().err().unwrap_or_default())}
</span>
<span class="form_error">{move || validation_state.get().err()}</span>
</div>
<input
// TODO:
type=control.data.input_type
id=&control.data.name
name=control.data.name
placeholder=control.data.placeholder
prop:value=move || value_getter.get()
on:change=move |ev| {
value_setter(event_target_value(&ev));
}
name=&control.data.name
placeholder=control.data.placeholder.as_ref()
class="form_input"
class=("form_input_invalid", move || validation_state.get().is_err())
/>
</div>
}
.into_view()
}
fn select(
&self,
control: ControlRenderData<Self, SelectData>,
value_getter: Signal<<SelectData as ControlData>::ReturnType>,
value_setter: Box<dyn Fn(<SelectData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
let options_view = control
.data
.options
.into_iter()
.map(|value| {
// let value = value;
let cloned_value = value.clone();
view! {
<option
value={value}
selected=move || value_getter.get() == *cloned_value
>
*value
</option>
}
})
.collect_view();
view! {
<div>
{move || format!("{:?}", validation_state.get())}
<select
id=&control.data.name
name=control.data.name
class="form_input"
on:change=move |ev| {
prop:value=move || value_getter.get()
on:focusout=move |ev| {
value_setter(event_target_value(&ev));
}
>
{options_view}
</select>
</div>
}
.into_view()
}
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View {
view! {
<input
type="submit"
value=control.data.text
class="w-full form_submit"
/>
}
.into_view()
.into_view();
self.custom_component(&control.styles, view)
}
fn text_area(
&self,
control: ControlRenderData<Self, TextAreaData>,
control: Rc<ControlRenderData<Self, TextAreaData>>,
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
value_setter: Box<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
value_setter: Rc<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View {
view! {
let view = view! {
<div>
{move || format!("{:?}", validation_state.get())}
<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
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(
&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 {
let buttons_view = control
.data
.options
.iter()
.map(|(display, value)| {
let value_setter = value_setter.clone();
let display = display.clone();
let value = value.clone();
let value_clone = value.clone();
let value_clone2 = value.clone();
view! {
<input
type="radio"
id=&value
name=&control.data.name
value=&value
prop:checked=move || { &value_getter.get() == &value_clone }
on:input=move |ev| {
let new_value = event_target_checked(&ev);
if new_value {
value_setter(value_clone2.clone());
}
}
/>
<label for=&value>{display}</label>
<br/>
}
})
.collect_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>
<div
class="form_input"
class:form_input_invalid=move || validation_state.get().is_err()
>
{buttons_view}
</div>
}
.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(
&self,
control: Rc<ControlRenderData<Self, CheckboxData>>,
value_getter: Signal<<CheckboxData as ControlData>::ReturnType>,
value_setter: Rc<dyn Fn(<CheckboxData as ControlData>::ReturnType)>,
) -> View {
let view = view! {
<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.as_ref()}</span>
</label>
}
.into_view();
self.custom_component(&control.styles, view)
}
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 {
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="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));
}
/>
</div>
}
.into_view()
.into_view();
self.custom_component(&control.styles, view)
}
fn custom_component(&self, view: View) -> View {
view
fn slider(
&self,
control: Rc<ControlRenderData<Self, SliderData>>,
value_getter: Signal<<SliderData as ControlData>::ReturnType>,
value_setter: Rc<dyn Fn(<SliderData 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="range"
id=&control.data.name
name=&control.data.name
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:input=move |ev| {
let value = event_target_value(&ev).parse::<i32>().ok();
if let Some(value) = value {
value_setter(value);
}
}
/>
}
.into_view();
self.custom_component(&control.styles, view)
}
}
+153 -23
View File
@@ -1,40 +1,170 @@
mod grid_form;
pub use grid_form::{GridFormStyle, GridFormStylingAttributes};
use crate::controls::{
heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData,
text_input::TextInputData, ControlData, ControlRenderData,
use crate::{
controls::{
button::ButtonData, checkbox::CheckboxData, heading::HeadingData, hidden::HiddenData,
output::OutputData, radio_buttons::RadioButtonsData, select::SelectData,
slider::SliderData, spacer::SpacerData, stepper::StepperData, submit::SubmitData,
text_area::TextAreaData, text_input::TextInputData, ControlData, ControlRenderData,
},
FormToolData,
};
use leptos::{Signal, View};
use leptos::{RwSignal, Signal, View};
use std::rc::Rc;
pub trait FormStyle: Default + 'static {
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 {
/// The type of styling attributes that this [`FormStyle`] takes.
///
/// These styling attributes can be applied to any of the controls.
type StylingAttributes;
fn form_frame(&self, children: View) -> View;
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View;
/// Render any containing components for the form.
///
/// This allows you to wrap all the form components
/// in another component if neccisary.
///
/// Do NOT wrap it in an actual `form` element; any
/// wrapping should be done with `div` or similar elements.
fn form_frame(&self, form: ControlRenderData<Self, View>) -> View;
/// Wraps the view of a custom component.
///
/// The rendering of the custom component is given by the `inner` view.
/// Here the styler has a chance wrap the view with other components, or
/// applying the styling attributes.
///
/// This method does not need to be called by the custom component, but
/// the custom component may make use of this method for the
/// aforementioned reasons.
fn custom_component(&self, style: &[Self::StylingAttributes], inner: View) -> View;
/// Renders a group.
///
/// The inner view for the group's components is provided.
/// This method should wrap the group in any visual grouping elements,
/// and apply the styles.
fn group(&self, group: Rc<ControlRenderData<Self, View>>) -> View;
/// Renders a spacer.
///
/// See [`SpacerData`].
fn spacer(&self, control: Rc<ControlRenderData<Self, SpacerData>>) -> View;
/// Renders a heading for a section of the form.
fn heading(&self, control: Rc<ControlRenderData<Self, HeadingData>>) -> View;
/// Renders a submit button.
///
/// See [`SubmitData`].
fn submit(&self, control: Rc<ControlRenderData<Self, SubmitData>>) -> View;
/// Renders a button.
///
/// See [`BuuttonData`]
fn button<FD: FormToolData>(
&self,
control: Rc<ControlRenderData<Self, ButtonData<FD>>>,
data_signal: RwSignal<FD>,
) -> View;
/// Renders some output text.
///
/// See [`OutputData`].
fn output(
&self,
control: Rc<ControlRenderData<Self, OutputData>>,
value_getter: Option<Signal<String>>,
) -> View;
/// Renders a input control that should be hidden from the user.
///
/// See [`HiddenData`].
fn hidden(
&self,
control: Rc<ControlRenderData<Self, HiddenData>>,
value_getter: Option<Signal<String>>,
) -> View;
/// Renders a text input control.
///
/// See [`TextInputData`].
fn text_input(
&self,
control: ControlRenderData<Self, TextInputData>,
control: Rc<ControlRenderData<Self, TextInputData>>,
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
value_setter: Box<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
value_setter: Rc<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View;
fn select(
&self,
control: ControlRenderData<Self, SelectData>,
value_getter: Signal<<SelectData as ControlData>::ReturnType>,
value_setter: Box<dyn Fn(<SelectData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View;
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View;
/// Renders a text area control.
///
/// See [`TextAreaData`].
fn text_area(
&self,
control: ControlRenderData<Self, TextAreaData>,
control: Rc<ControlRenderData<Self, TextAreaData>>,
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
value_setter: Box<dyn Fn(<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_setter: Rc<dyn Fn(<SliderData as ControlData>::ReturnType)>,
validation_state: Signal<Result<(), String>>,
) -> View;
// TODO: test custom component
fn custom_component(&self, view: View) -> View;
// TODO: add group
}
+171
View File
@@ -0,0 +1,171 @@
use crate::{controls::ValidationFn, FormToolData};
use std::fmt::Display;
/// A function that validates a field.
///
/// This is similar to [`ValidationFn`](crate::controls::ValidationFn)
/// but takes a &str for the name of the field for improved error messages.
type ValidationBuilderFn<T> = dyn Fn(&str, &T) -> Result<(), String> + 'static;
/// A helper builder that allows you to specify a validation function
/// declaritivly
///
/// Using this builder is not required as validation functions can just be
/// closures, but for simple validation function this builder can be helpful
///
/// Validations are run in the order that they are called in the builder.
pub struct ValidationBuilder<FD: FormToolData, T: ?Sized + 'static> {
/// The name of the field, for error messages.
name: String,
/// The getter function for the field to validate.
field_fn: Box<dyn Fn(&FD) -> &T + 'static>,
/// The functions to be called when validating.
functions: Vec<Box<ValidationBuilderFn<T>>>,
}
impl<FD: FormToolData, T: ?Sized + 'static> ValidationBuilder<FD, T> {
/// Creates a new empty [`ValidationBuilder`] on the given field.
pub fn for_field(field_fn: impl Fn(&FD) -> &T + 'static) -> Self {
ValidationBuilder {
name: String::from("Field"),
field_fn: Box::new(field_fn),
functions: Vec::new(),
}
}
/// The name of the field that is being validated.
///
/// This is the name that will be used for error messages.
pub fn named(mut self, name: impl ToString) -> Self {
self.name = name.to_string();
self
}
/// Adds a custom validation function.
///
/// The function should take the value as an argument and return
/// a [`Result<(), String>`], just like any other validation function.
pub fn custom(mut self, f: impl ValidationFn<T>) -> Self {
self.functions.push(Box::new(move |_name, value| f(value)));
self
}
/// Builds the action validation function.
pub fn build(self) -> impl ValidationFn<FD> {
move |form_data| {
let value = (self.field_fn)(form_data);
for f in self.functions.iter() {
match f(self.name.as_str(), value) {
Ok(()) => {}
err => return err,
}
}
Ok(())
}
}
}
impl<FD: FormToolData> ValidationBuilder<FD, str> {
/// Requires the field to not be empty.
pub fn required(mut self) -> Self {
self.functions.push(Box::new(move |name, value| {
if value.is_empty() {
Err(format!("{} is required", name))
} else {
Ok(())
}
}));
self
}
/// Requires the field's length to be at least `min_len`.
pub fn min_len(mut self, min_len: usize) -> Self {
self.functions.push(Box::new(move |name, value| {
if value.len() < min_len {
Err(format!("{} must be >= {} characters", name, min_len))
} else {
Ok(())
}
}));
self
}
/// Requires the field's length to be less than or equal to `min_len`.
pub fn max_len(mut self, max_len: usize) -> Self {
self.functions.push(Box::new(move |name, value| {
if value.len() > max_len {
Err(format!("{} must be <= {} characters", name, max_len))
} else {
Ok(())
}
}));
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> {
/// Requires the value to be at least `min_value` according to
/// `PartialOrd`.
pub fn min_value(mut self, min_value: T) -> Self {
self.functions.push(Box::new(move |name, value| {
if value < &min_value {
Err(format!("{} mut be >= {}", name, min_value))
} else {
Ok(())
}
}));
self
}
/// Requires the value to be at most `max_value` according to
/// `PartialOrd`.
pub fn max_value(mut self, max_value: T) -> Self {
self.functions.push(Box::new(move |name, value| {
if value > &max_value {
Err(format!("{} mut be <= {}", name, max_value))
} else {
Ok(())
}
}));
self
}
}
impl<FD: FormToolData, T: PartialEq<T> + Display + 'static> ValidationBuilder<FD, T> {
/// Requires the field to be in the provided whitelist.
pub fn whitelist(mut self, whitelist: Vec<T>) -> Self {
self.functions.push(Box::new(move |name, value| {
if !whitelist.contains(value) {
Err(format!("{} cannot be {}", name, value))
} else {
Ok(())
}
}));
self
}
/// Requires the field to not be in the provided blacklist.
pub fn blacklist(mut self, blacklist: Vec<T>) -> Self {
self.functions.push(Box::new(move |name, value| {
if blacklist.contains(value) {
Err(format!("{} cannot be {}", name, value))
} else {
Ok(())
}
}));
self
}
}