Archived
generated from mitchell/rust_template
Compare commits
24
Commits
1dc676cc37
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f2397a47c5 | ||
|
|
3c96a15680 | ||
|
|
c7c98f985f | ||
|
|
7c4e3b162d | ||
|
|
5f4358277e | ||
|
|
1dc93d69f4 | ||
|
|
e16b7fc9cb | ||
|
|
18a99ec181 | ||
|
|
89375a5b0c | ||
|
|
bbde4d6331 | ||
|
|
da84bdbb27 | ||
|
|
835271cd22 | ||
|
|
0339a2ee96 | ||
|
|
7d855e764f | ||
|
|
fac8e515f6 | ||
|
|
e371ff748b | ||
|
|
4c6b6fa920 | ||
|
|
bd73591b9f | ||
|
|
a5fd4f85ee | ||
|
|
079c9bde53 | ||
|
|
1e2709dc8c | ||
|
|
64d2631140 | ||
|
|
115ff1abde | ||
|
|
a39f2ce664 |
@@ -2,63 +2,109 @@
|
||||
|
||||
A declaritive way to create forms for [leptos](https://leptos.dev/).
|
||||
|
||||
leptos_form_tool allows you to define forms in a declaritive way, without specifying how to render each component. You define what controls and visual components the form should have, as well as how to parse and validate the data. That form definition can then be used to render a `View` for the form, or create a Validator so the client and server can both check the integrity of the data in the same way, without duplicating code.
|
||||
leptos_form_tool allows you to define forms in a declaritive way.
|
||||
Want text box? Just call `.text_input` on the form builder. Then sepperatly,
|
||||
you define a FormStyle to specify how that text box should be rendered.
|
||||
This has a number of advantages:
|
||||
- Sepperates the layout of the form from how it is rendered and styled
|
||||
- Allows different styles to be swapped in and out quickly
|
||||
|
||||
The rendering of the form controls are defined seperatly from the form itself. This sepperation allows different styles to be swapped in and out, relatively easily and helps reduce code duplication. A particular style can be created by creating a type that implements the FormStyle trait. This FormStyle trait specifies functions for defining all the common form controls such as TextInput, Select, RadioButtons, etc. leptos_form_tool also support custom components if you want more controls than the just the common ones. Custom controls natrually cannot be fully styled with by the FormStyle.
|
||||
## Validations
|
||||
|
||||
An implmentation of FormStyle might have some variables that should be defined on a per-component basis. Take, for example, a FormStyle that renders it's controls in a grid (like TWGridFormStyle). We would like to be able to define a width for how many columns in the grid that a control will take up. For this, the FormStyle also defines a type for it's attributes:
|
||||
```rust
|
||||
pub enum TWGridFormAttributes {
|
||||
Width(u32),
|
||||
}
|
||||
```
|
||||
With this, you add these styling attributes to components with the `style()` method.
|
||||
You might find yourself asking, but why not just use components?
|
||||
|
||||
Here is an example of how a form can be defined:
|
||||
```rust
|
||||
// all FormData must implement Default and must be 'static
|
||||
#[derive(Default)]
|
||||
// this struct should contain all the data that needs to be submitted.
|
||||
struct MyFormData {
|
||||
name: String,
|
||||
age: u32,
|
||||
}
|
||||
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.
|
||||
|
||||
// now implment the FormData trait
|
||||
impl FormData for MyFormData {
|
||||
// the form style must be decided now, as the available attributes depend on the FormStyle
|
||||
type Style = TWGridFormStyle;
|
||||
fn build_form(FormBuilder fb) -> Form {
|
||||
fb
|
||||
.heading(|h| h.title("Tell Me About Yourself"))
|
||||
.text_input(|t| {
|
||||
t.label("Name")
|
||||
.placeholder("Name")
|
||||
.parse_fn(|name, form_data| Ok(form_data.name = name))
|
||||
.validation_fn(|fd| {
|
||||
if fd.name.is_empty() {
|
||||
Err("Name is required")
|
||||
} else if fd.name.len() >= 32 {
|
||||
Err("Name must be shorter than 32 characters")
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
})
|
||||
.text_input(|t| {
|
||||
t.label("age")
|
||||
.placeholder("age")
|
||||
.parse_fn(|age, form_data| { Ok(form_data.age = age.parse().map_err(|e| e.to_string())?)}
|
||||
.validation_fn(|form_data| {
|
||||
if form_data.age > 99 {
|
||||
Err("Please enter an age <= 99")
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
})
|
||||
})
|
||||
.submit(|s| s.text("Submit"))
|
||||
}
|
||||
}
|
||||
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`].
|
||||
|
||||
@@ -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`.
|
||||
+97
-32
@@ -1,54 +1,119 @@
|
||||
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
|
||||
use super::{BuilderCxFn, BuilderFn, ControlRenderData, ShowWhenFn};
|
||||
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||
use leptos::{RwSignal, Signal, View};
|
||||
use leptos::view;
|
||||
use leptos::RwSignal;
|
||||
use leptos::Show;
|
||||
use leptos::Signal;
|
||||
use std::rc::Rc;
|
||||
use web_sys::MouseEvent;
|
||||
|
||||
#[derive(Clone)]
|
||||
type ButtonAction<FD> = dyn Fn(MouseEvent, RwSignal<FD>) + 'static;
|
||||
|
||||
/// Data used for the button control.
|
||||
pub struct ButtonData<FD: FormToolData> {
|
||||
pub(crate) text: String,
|
||||
pub(crate) action: Option<Rc<dyn Fn(MouseEvent, &mut FD)>>,
|
||||
|
||||
// this will need to be set before calling the build method
|
||||
pub(crate) fd_signal: RwSignal<FD>,
|
||||
pub text: String,
|
||||
pub action: Option<Rc<ButtonAction<FD>>>,
|
||||
}
|
||||
|
||||
impl<FD: FormToolData> VanityControlData for ButtonData<FD> {
|
||||
fn build_control<FS: FormStyle>(
|
||||
fs: &FS,
|
||||
control: ControlRenderData<FS, Self>,
|
||||
_value_getter: Option<Signal<String>>,
|
||||
) -> View {
|
||||
fs.button(control)
|
||||
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, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
pub fn button(
|
||||
mut self,
|
||||
builder: impl Fn(
|
||||
VanityControlBuilder<FD, FS, ButtonData<FD>>,
|
||||
) -> VanityControlBuilder<FD, FS, ButtonData<FD>>,
|
||||
) -> Self {
|
||||
let data = ButtonData {
|
||||
text: String::default(),
|
||||
action: None,
|
||||
fd_signal: self.fd,
|
||||
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 vanity_builder = VanityControlBuilder::new(data);
|
||||
let control = builder(vanity_builder);
|
||||
self.add_vanity(control);
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> VanityControlBuilder<FD, FS, ButtonData<FD>> {
|
||||
/// 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
|
||||
}
|
||||
|
||||
pub fn action(mut self, action: impl Fn(MouseEvent, &mut FD) + 'static) -> Self {
|
||||
/// Sets the action that is preformed when the button is clicked.
|
||||
pub fn action(mut self, action: impl Fn(MouseEvent, RwSignal<FD>) + 'static) -> Self {
|
||||
self.data.action = Some(Rc::new(action));
|
||||
self
|
||||
}
|
||||
|
||||
+29
-12
@@ -1,12 +1,13 @@
|
||||
use leptos::{Signal, View};
|
||||
|
||||
use super::{ControlBuilder, ControlData, ControlRenderData};
|
||||
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(crate) name: String,
|
||||
pub(crate) label: Option<String>,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
impl ControlData for CheckboxData {
|
||||
@@ -14,32 +15,48 @@ impl ControlData for CheckboxData {
|
||||
|
||||
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.checkbox(control, value_getter, value_setter)
|
||||
}
|
||||
}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
impl<FD: FormToolData> FormBuilder<FD> {
|
||||
/// Builds a checkbox and adds it to the form.
|
||||
pub fn checkbox<FDT: Clone + PartialEq + 'static>(
|
||||
self,
|
||||
builder: impl Fn(
|
||||
ControlBuilder<FD, FS, CheckboxData, FDT>,
|
||||
) -> ControlBuilder<FD, FS, CheckboxData, FDT>,
|
||||
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, FS: FormStyle, FDT> ControlBuilder<FD, FS, CheckboxData, FDT> {
|
||||
impl<FD: FormToolData, FDT> ControlBuilder<FD, CheckboxData, FDT> {
|
||||
/// Sets the name of the checkbox.
|
||||
///
|
||||
/// This is used for the html element's "name" attribute.
|
||||
/// In forms, the name attribute is the key that the data is sent
|
||||
/// with.
|
||||
///
|
||||
/// For checkbox controls, the value "checked" is sent or no key value
|
||||
/// pair is sent.
|
||||
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||
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
|
||||
|
||||
+30
-5
@@ -1,11 +1,12 @@
|
||||
use super::{ControlBuilder, ControlData};
|
||||
use crate::{styles::FormStyle, FormBuilder, FormToolData};
|
||||
use super::{BuilderCxFn, BuilderFn, ControlBuilder, ControlData};
|
||||
use crate::{FormBuilder, FormToolData};
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
impl<FD: FormToolData> FormBuilder<FD> {
|
||||
/// Builds a custom component and adds it to the form.
|
||||
pub fn custom<CC: ControlData, FDT: Clone + PartialEq + 'static>(
|
||||
mut self,
|
||||
control_data: CC,
|
||||
builder: impl Fn(ControlBuilder<FD, FS, CC, FDT>) -> ControlBuilder<FD, FS, CC, FDT>,
|
||||
builder: impl BuilderFn<ControlBuilder<FD, CC, FDT>>,
|
||||
) -> Self {
|
||||
let control_builder = ControlBuilder::new(control_data);
|
||||
let control = builder(control_builder);
|
||||
@@ -13,10 +14,34 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
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 Fn(ControlBuilder<FD, FS, CC, FDT>) -> ControlBuilder<FD, FS, CC, FDT>,
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
+21
-9
@@ -1,25 +1,37 @@
|
||||
use super::ValidationCb;
|
||||
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||
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, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
pub fn group(mut self, builder: impl Fn(FormBuilder<FD, FS>) -> FormBuilder<FD, FS>) -> Self {
|
||||
let mut group_builder = FormBuilder::new_group(self.fd, self.fs);
|
||||
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);
|
||||
self.fs = group_builder.fs; // take the style back
|
||||
|
||||
for validation in group_builder.validations {
|
||||
self.validations.push(validation);
|
||||
}
|
||||
|
||||
let render_fn = move |fs: &FS, fd: RwSignal<FD>| {
|
||||
let render_fn = move |fs: Rc<FD::Style>, fd: RwSignal<FD>| {
|
||||
let (views, validation_cbs): (Vec<_>, Vec<_>) = group_builder
|
||||
.render_fns
|
||||
.into_iter()
|
||||
.map(|r_fn| r_fn(&fs, fd))
|
||||
.map(|r_fn| r_fn(fs.clone(), fd))
|
||||
.unzip();
|
||||
|
||||
let view = fs.group(views.collect_view(), group_builder.styles);
|
||||
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() {
|
||||
|
||||
+27
-15
@@ -1,37 +1,49 @@
|
||||
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
|
||||
use super::{BuilderCxFn, BuilderFn, ControlRenderData, VanityControlBuilder, VanityControlData};
|
||||
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||
use leptos::View;
|
||||
use leptos::{MaybeSignal, Signal, View};
|
||||
use std::rc::Rc;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
/// Data used for the heading control.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct HeadingData {
|
||||
pub(crate) title: String,
|
||||
pub title: MaybeSignal<String>,
|
||||
}
|
||||
|
||||
impl VanityControlData for HeadingData {
|
||||
fn build_control<FS: FormStyle>(
|
||||
fs: &FS,
|
||||
control: ControlRenderData<FS, Self>,
|
||||
control: Rc<ControlRenderData<FS, Self>>,
|
||||
_value_getter: Option<leptos::prelude::Signal<String>>,
|
||||
) -> View {
|
||||
fs.heading(control)
|
||||
}
|
||||
}
|
||||
// TODO: impl GetterVanityControl
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
pub fn heading(
|
||||
self,
|
||||
builder: impl Fn(
|
||||
VanityControlBuilder<FD, FS, HeadingData>,
|
||||
) -> VanityControlBuilder<FD, 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<FD: FormToolData, FS: FormStyle> VanityControlBuilder<FD, FS, HeadingData> {
|
||||
impl<FD: FormToolData> VanityControlBuilder<FD, HeadingData> {
|
||||
/// Sets the title of this heading.
|
||||
pub fn title(mut self, title: impl ToString) -> Self {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+43
-17
@@ -1,32 +1,58 @@
|
||||
use leptos::{Signal, View};
|
||||
|
||||
use super::{ControlBuilder, ControlData, ControlRenderData};
|
||||
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;
|
||||
|
||||
impl ControlData for HiddenData {
|
||||
type ReturnType = String;
|
||||
pub struct HiddenData {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl VanityControlData for HiddenData {
|
||||
fn build_control<FS: FormStyle>(
|
||||
fs: &FS,
|
||||
control: ControlRenderData<FS, Self>,
|
||||
value_getter: Signal<Self::ReturnType>,
|
||||
_value_setter: Box<dyn Fn(Self::ReturnType)>,
|
||||
_validation_state: Signal<Result<(), String>>,
|
||||
control: Rc<ControlRenderData<FS, Self>>,
|
||||
value_getter: Option<Signal<String>>,
|
||||
) -> View {
|
||||
fs.hidden(control, value_getter)
|
||||
}
|
||||
}
|
||||
impl GetterVanityControlData for HiddenData {}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
pub fn hidden<FDT: Clone + PartialEq + 'static>(
|
||||
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 Fn(
|
||||
ControlBuilder<FD, FS, HiddenData, FDT>,
|
||||
) -> ControlBuilder<FD, FS, HiddenData, FDT>,
|
||||
builder: impl BuilderCxFn<VanityControlBuilder<FD, HiddenData>, FD::Context>,
|
||||
) -> Self {
|
||||
self.new_control(builder)
|
||||
self.new_vanity_cx(builder)
|
||||
}
|
||||
}
|
||||
|
||||
impl<FD: FormToolData> VanityControlBuilder<FD, HiddenData> {
|
||||
/// Sets the name of the hidden control.
|
||||
///
|
||||
/// This is used for the html element's "name" attribute.
|
||||
/// In forms, the name attribute is the key that the data is sent
|
||||
/// with.
|
||||
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||
self.data.name = control_name.to_string();
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
+73
-38
@@ -18,26 +18,32 @@ pub mod submit;
|
||||
pub mod text_area;
|
||||
pub mod text_input;
|
||||
|
||||
pub trait ValidationFn<FDT>: Fn(&FDT) -> Result<(), String> + 'static {}
|
||||
pub trait BuilderFn<B>: Fn(B) -> B {}
|
||||
pub trait BuilderCxFn<B, CX>: Fn(B, Rc<CX>) -> B {}
|
||||
pub trait ValidationFn<FD: ?Sized>: Fn(&FD) -> Result<(), String> + 'static {}
|
||||
pub trait ValidationCb: Fn() -> bool + 'static {}
|
||||
pub trait ParseFn<CR, FDT>: Fn(CR) -> Result<FDT, String> + 'static {}
|
||||
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 RenderFn<FS, FD>:
|
||||
FnOnce(&FS, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + '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
|
||||
{
|
||||
}
|
||||
|
||||
// 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<FS, FD, F> RenderFn<FS, FD> for F where
|
||||
F: FnOnce(&FS, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + '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
|
||||
{
|
||||
}
|
||||
|
||||
@@ -46,7 +52,7 @@ 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;
|
||||
}
|
||||
@@ -60,9 +66,9 @@ pub trait ControlData: '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: Signal<Self::ReturnType>,
|
||||
value_setter: Box<dyn Fn(Self::ReturnType)>,
|
||||
value_setter: Rc<dyn Fn(Self::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View;
|
||||
}
|
||||
@@ -70,51 +76,66 @@ 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,
|
||||
}
|
||||
|
||||
/// The data needed to render a read-only control of type `C`.
|
||||
pub struct VanityControlBuilder<FD: FormToolData, FS: FormStyle, C: VanityControlData> {
|
||||
pub(crate) style_attributes: Vec<FS::StylingAttributes>,
|
||||
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>>>,
|
||||
}
|
||||
|
||||
pub(crate) struct BuiltVanityControlData<FD: FormToolData, FS: FormStyle, C: VanityControlData> {
|
||||
pub(crate) render_data: ControlRenderData<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, FS: FormStyle, C: VanityControlData> VanityControlBuilder<FD, FS, C> {
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the builder into the data needed to render the control.
|
||||
pub(crate) fn build(self) -> BuiltVanityControlData<FD, FS, C> {
|
||||
pub(crate) fn build(self) -> BuiltVanityControlData<FD, C> {
|
||||
BuiltVanityControlData {
|
||||
render_data: ControlRenderData {
|
||||
data: Box::new(self.data),
|
||||
style: self.style_attributes,
|
||||
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: FS::StylingAttributes) -> Self {
|
||||
pub fn style(mut self, attribute: <FD::Style as FormStyle>::StylingAttributes) -> Self {
|
||||
self.style_attributes.push(attribute);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle, C: GetterVanityControlData> VanityControlBuilder<FD, FS, C> {
|
||||
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
|
||||
@@ -151,27 +172,29 @@ impl Display for ControlBuildError {
|
||||
}
|
||||
|
||||
/// The data returned fomr a control's build function.
|
||||
pub(crate) struct BuiltControlData<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> {
|
||||
pub(crate) render_data: ControlRenderData<FS, C>,
|
||||
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>>>,
|
||||
}
|
||||
|
||||
/// A builder for a interactive control.
|
||||
pub struct ControlBuilder<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> {
|
||||
pub struct ControlBuilder<FD: FormToolData, C: ControlData, FDT> {
|
||||
pub(crate) getter: Option<Rc<dyn FieldGetter<FD, FDT>>>,
|
||||
pub(crate) 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) style_attributes: Vec<<FD::Style as FormStyle>::StylingAttributes>,
|
||||
pub(crate) show_when: Option<Rc<dyn ShowWhenFn<FD, FD::Context>>>,
|
||||
pub data: C,
|
||||
}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C, FDT> {
|
||||
impl<FD: FormToolData, C: ControlData, FDT> ControlBuilder<FD, C, FDT> {
|
||||
/// Creates a new [`ControlBuilder`] with the given [`ControlData`].
|
||||
pub(crate) fn new(data: C) -> Self {
|
||||
ControlBuilder {
|
||||
@@ -182,13 +205,14 @@ impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS
|
||||
unparse_fn: None,
|
||||
validation_fn: None,
|
||||
style_attributes: Vec::new(),
|
||||
show_when: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the builder into the data needed to render the control.
|
||||
///
|
||||
/// This fails if a required field was not specified.
|
||||
pub(crate) fn build(self) -> Result<BuiltControlData<FD, FS, C, FDT>, ControlBuildError> {
|
||||
pub(crate) fn build(self) -> Result<BuiltControlData<FD, C, FDT>, ControlBuildError> {
|
||||
let getter = match self.getter {
|
||||
Some(getter) => getter,
|
||||
None => return Err(ControlBuildError::MissingGetter),
|
||||
@@ -208,17 +232,29 @@ impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS
|
||||
|
||||
Ok(BuiltControlData {
|
||||
render_data: ControlRenderData {
|
||||
data: Box::new(self.data),
|
||||
style: self.style_attributes,
|
||||
data: self.data,
|
||||
styles: self.style_attributes,
|
||||
},
|
||||
getter,
|
||||
setter,
|
||||
parse_fn,
|
||||
unparse_fn,
|
||||
validation_fn: self.validation_fn,
|
||||
show_when: self.show_when,
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the function to decide when to render the control.
|
||||
///
|
||||
/// Validations for components that are not shown DO NOT run.
|
||||
pub fn show_when(
|
||||
mut self,
|
||||
when: impl Fn(Signal<FD>, Rc<FD::Context>) -> bool + 'static,
|
||||
) -> Self {
|
||||
self.show_when = Some(Rc::new(when));
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the getter function.
|
||||
///
|
||||
/// This function should get the field from the form data
|
||||
@@ -241,7 +277,7 @@ impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the parse functions to the ones given
|
||||
/// Sets the parse functions to the ones given.
|
||||
///
|
||||
/// The parse and unparse functions define how to turn what the user
|
||||
/// types in the form into what is stored in the form data struct and
|
||||
@@ -257,16 +293,15 @@ impl<FD: FormToolData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS
|
||||
}
|
||||
|
||||
/// Adds a styling attribute to this control.
|
||||
pub fn style(mut self, attribute: FS::StylingAttributes) -> Self {
|
||||
pub fn style(mut self, attribute: <FD::Style as FormStyle>::StylingAttributes) -> Self {
|
||||
self.style_attributes.push(attribute);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<FD, FS, C, FDT> ControlBuilder<FD, FS, C, FDT>
|
||||
impl<FD, C, FDT> ControlBuilder<FD, C, FDT>
|
||||
where
|
||||
FD: FormToolData,
|
||||
FS: FormStyle,
|
||||
C: ControlData,
|
||||
FDT: TryFrom<<C as ControlData>::ReturnType>,
|
||||
<FDT as TryFrom<<C as ControlData>::ReturnType>>::Error: ToString,
|
||||
@@ -289,17 +324,16 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<FD, FS, C, FDT> ControlBuilder<FD, FS, C, FDT>
|
||||
impl<FD, C, FDT> ControlBuilder<FD, C, FDT>
|
||||
where
|
||||
FD: FormToolData,
|
||||
FS: FormStyle,
|
||||
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_trimed_string`].
|
||||
/// 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
|
||||
@@ -315,7 +349,8 @@ where
|
||||
}
|
||||
|
||||
/// Sets the parse functions to use the [`FromStr`] [`ToString`] and traits
|
||||
/// for parsing and unparsing respectively, similar to [`parse_string`].
|
||||
/// for parsing and unparsing respectively, similar to
|
||||
/// [`parse_string`](Self::parse_string)().
|
||||
/// However, this method trims the string before parsing.
|
||||
///
|
||||
/// The parse and unparse functions define how to turn what the user
|
||||
@@ -333,7 +368,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle, C: ValidatedControlData, FDT> ControlBuilder<FD, FS, C, FDT> {
|
||||
impl<FD: FormToolData, C: ValidatedControlData, FDT> ControlBuilder<FD, C, FDT> {
|
||||
/// Sets the validation function for this control
|
||||
///
|
||||
/// This allows you to check if the parsed value is a valid value.
|
||||
|
||||
+26
-11
@@ -1,15 +1,19 @@
|
||||
use leptos::{Signal, View};
|
||||
|
||||
use super::{ControlRenderData, GetterVanityControlData, VanityControlBuilder, VanityControlData};
|
||||
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: ControlRenderData<FS, Self>,
|
||||
control: Rc<ControlRenderData<FS, Self>>,
|
||||
value_getter: Option<Signal<String>>,
|
||||
) -> View {
|
||||
fs.output(control, value_getter)
|
||||
@@ -17,13 +21,24 @@ impl VanityControlData for OutputData {
|
||||
}
|
||||
impl GetterVanityControlData for OutputData {}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
pub fn output(
|
||||
self,
|
||||
builder: impl Fn(
|
||||
VanityControlBuilder<FD, FS, OutputData>,
|
||||
) -> VanityControlBuilder<FD, FS, OutputData>,
|
||||
) -> Self {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
use leptos::{Signal, View};
|
||||
|
||||
use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData};
|
||||
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(crate) name: String,
|
||||
pub(crate) label: Option<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: Vec<(String, String)>,
|
||||
}
|
||||
|
||||
impl ControlData for RadioButtonsData {
|
||||
@@ -15,9 +21,9 @@ impl ControlData for RadioButtonsData {
|
||||
|
||||
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.radio_buttons(control, value_getter, value_setter, validation_state)
|
||||
@@ -25,36 +31,80 @@ impl ControlData for RadioButtonsData {
|
||||
}
|
||||
impl ValidatedControlData for RadioButtonsData {}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
impl<FD: FormToolData> FormBuilder<FD> {
|
||||
/// Builds a radio buttons control and adds it to the form.
|
||||
pub fn radio_buttons<FDT: Clone + PartialEq + 'static>(
|
||||
self,
|
||||
builder: impl Fn(
|
||||
ControlBuilder<FD, FS, RadioButtonsData, FDT>,
|
||||
) -> ControlBuilder<FD, FS, RadioButtonsData, FDT>,
|
||||
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, FS: FormStyle, FDT> ControlBuilder<FD, FS, RadioButtonsData, FDT> {
|
||||
impl<FD: FormToolData, FDT> ControlBuilder<FD, RadioButtonsData, FDT> {
|
||||
/// Sets the name of the radio button inputs.
|
||||
///
|
||||
/// This is used for the html element's "name" attribute.
|
||||
/// In forms, the name attribute is the key that the data is sent
|
||||
/// with.
|
||||
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||
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());
|
||||
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());
|
||||
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
|
||||
}
|
||||
|
||||
+91
-22
@@ -1,15 +1,21 @@
|
||||
use leptos::{Signal, View};
|
||||
|
||||
use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData};
|
||||
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;
|
||||
|
||||
// TODO: have an option to have a display string and a value string in the options field
|
||||
|
||||
#[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) label: Option<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,9 +23,9 @@ 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)
|
||||
@@ -27,37 +33,100 @@ impl ControlData for SelectData {
|
||||
}
|
||||
impl ValidatedControlData for SelectData {}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
impl<FD: FormToolData> FormBuilder<FD> {
|
||||
/// Builds a select control and adds it to the form.
|
||||
pub fn select<FDT: Clone + PartialEq + 'static>(
|
||||
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: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, SelectData, FDT> {
|
||||
impl<FD: FormToolData, FDT> ControlBuilder<FD, SelectData, FDT> {
|
||||
/// Sets the name of the radio button inputs.
|
||||
///
|
||||
/// This is used for the html element's "name" attribute.
|
||||
/// In forms, the name attribute is the key that the data is sent
|
||||
/// with.
|
||||
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||
self.data.name = control_name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the label for the select.
|
||||
pub fn labeled(mut self, label: impl ToString) -> Self {
|
||||
self.data.label = Some(label.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_option(mut self, option: impl ToString) -> Self {
|
||||
self.data.options.push(option.to_string());
|
||||
/// 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
|
||||
}
|
||||
|
||||
pub fn with_options(mut self, options: impl Iterator<Item = impl ToString>) -> Self {
|
||||
for option in options {
|
||||
self.data.options.push(option.to_string());
|
||||
}
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
+45
-24
@@ -1,16 +1,15 @@
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use leptos::{Signal, View};
|
||||
|
||||
use super::{ControlBuilder, ControlData, ControlRenderData};
|
||||
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;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||
/// Data used for the slider control.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct SliderData {
|
||||
pub(crate) name: String,
|
||||
pub(crate) label: Option<String>,
|
||||
pub(crate) min: i32,
|
||||
pub(crate) max: i32,
|
||||
pub name: String,
|
||||
pub label: Option<String>,
|
||||
pub min: MaybeSignal<i32>,
|
||||
pub max: MaybeSignal<i32>,
|
||||
}
|
||||
|
||||
impl Default for SliderData {
|
||||
@@ -18,8 +17,8 @@ impl Default for SliderData {
|
||||
SliderData {
|
||||
name: String::new(),
|
||||
label: None,
|
||||
min: 0,
|
||||
max: 1,
|
||||
min: MaybeSignal::Static(0),
|
||||
max: MaybeSignal::Static(1),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,50 +28,72 @@ impl ControlData for SliderData {
|
||||
|
||||
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.slider(control, value_getter, value_setter, validation_state)
|
||||
}
|
||||
}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
impl<FD: FormToolData> FormBuilder<FD> {
|
||||
/// Builds a slider (or range) control and adds it to the form.
|
||||
pub fn slider<FDT: Clone + PartialEq + 'static>(
|
||||
self,
|
||||
builder: impl Fn(
|
||||
ControlBuilder<FD, FS, SliderData, FDT>,
|
||||
) -> ControlBuilder<FD, FS, SliderData, FDT>,
|
||||
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, FS: FormStyle, FDT> ControlBuilder<FD, FS, SliderData, FDT> {
|
||||
impl<FD: FormToolData, FDT> ControlBuilder<FD, SliderData, FDT> {
|
||||
/// Sets the name of the slider.
|
||||
///
|
||||
/// This is used for the html element's "name" attribute.
|
||||
/// In forms, the name attribute is the key that the data is sent
|
||||
/// with.
|
||||
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||
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 = min;
|
||||
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 = max;
|
||||
self.data.max = MaybeSignal::Static(max);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn range(mut self, range: RangeInclusive<i32>) -> Self {
|
||||
self.data.min = *range.start();
|
||||
self.data.max = *range.end();
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
+23
-13
@@ -1,35 +1,45 @@
|
||||
use leptos::{prelude::Signal, View};
|
||||
|
||||
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
|
||||
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(crate) height: Option<String>,
|
||||
pub height: Option<String>,
|
||||
}
|
||||
|
||||
impl VanityControlData for SpacerData {
|
||||
fn build_control<FS: FormStyle>(
|
||||
fs: &FS,
|
||||
control: ControlRenderData<FS, Self>,
|
||||
control: Rc<ControlRenderData<FS, Self>>,
|
||||
_value_getter: Option<Signal<String>>,
|
||||
) -> View {
|
||||
fs.spacer(control)
|
||||
}
|
||||
}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
pub fn spacer(
|
||||
self,
|
||||
builder: impl Fn(
|
||||
VanityControlBuilder<FD, FS, SpacerData>,
|
||||
) -> VanityControlBuilder<FD, FS, SpacerData>,
|
||||
) -> Self {
|
||||
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, FS: FormStyle> VanityControlBuilder<FD, FS, SpacerData> {
|
||||
impl<FD: FormToolData> VanityControlBuilder<FD, SpacerData> {
|
||||
/// Sets the height of the spacer.
|
||||
///
|
||||
/// This is a string to allow different units like "10px" or "1.25em".
|
||||
///
|
||||
/// This may or may not be respected based on the Style implementation.
|
||||
pub fn height(mut self, height: impl ToString) -> Self {
|
||||
self.data.height = Some(height.to_string());
|
||||
self
|
||||
|
||||
+54
-24
@@ -1,17 +1,18 @@
|
||||
use std::ops::RangeInclusive;
|
||||
|
||||
use leptos::{Signal, View};
|
||||
|
||||
use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData};
|
||||
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;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||
/// Data used for the stepper control.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||
pub struct StepperData {
|
||||
pub(crate) name: String,
|
||||
pub(crate) label: Option<String>,
|
||||
pub(crate) step: Option<i32>,
|
||||
pub(crate) min: Option<i32>,
|
||||
pub(crate) max: Option<i32>,
|
||||
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 {
|
||||
@@ -19,9 +20,9 @@ impl ControlData for StepperData {
|
||||
|
||||
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.stepper(control, value_getter, value_setter, validation_state)
|
||||
@@ -29,46 +30,75 @@ impl ControlData for StepperData {
|
||||
}
|
||||
impl ValidatedControlData for StepperData {}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
impl<FD: FormToolData> FormBuilder<FD> {
|
||||
/// Builds a stepper control and adds it to the form.
|
||||
pub fn stepper<FDT: Clone + PartialEq + 'static>(
|
||||
self,
|
||||
builder: impl Fn(
|
||||
ControlBuilder<FD, FS, StepperData, FDT>,
|
||||
) -> ControlBuilder<FD, FS, StepperData, FDT>,
|
||||
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, FS: FormStyle, FDT> ControlBuilder<FD, FS, StepperData, FDT> {
|
||||
impl<FD: FormToolData, FDT> ControlBuilder<FD, StepperData, FDT> {
|
||||
/// Sets the name of the stepper control.
|
||||
///
|
||||
/// This is used for the html element's "name" attribute.
|
||||
/// In forms, the name attribute is the key that the data is sent
|
||||
/// with.
|
||||
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||
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(step);
|
||||
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(min);
|
||||
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(max);
|
||||
self.data.max = Some(MaybeSignal::Static(max));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn range(mut self, range: RangeInclusive<i32>) -> Self {
|
||||
self.data.min = Some(*range.start());
|
||||
self.data.max = Some(*range.end());
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
+20
-13
@@ -1,35 +1,42 @@
|
||||
use leptos::{prelude::Signal, View};
|
||||
|
||||
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
|
||||
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>,
|
||||
control: Rc<ControlRenderData<FS, Self>>,
|
||||
_value_getter: Option<Signal<String>>,
|
||||
) -> View {
|
||||
fs.submit(control)
|
||||
}
|
||||
}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
pub fn submit(
|
||||
self,
|
||||
builder: impl Fn(
|
||||
VanityControlBuilder<FD, FS, SubmitData>,
|
||||
) -> VanityControlBuilder<FD, 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<FD: FormToolData, FS: FormStyle> VanityControlBuilder<FD, FS, SubmitData> {
|
||||
impl<FD: FormToolData> VanityControlBuilder<FD, SubmitData> {
|
||||
/// Sets the submit button's text.
|
||||
pub fn text(mut self, text: impl ToString) -> Self {
|
||||
self.data.text = text.to_string();
|
||||
self
|
||||
|
||||
+40
-10
@@ -1,11 +1,16 @@
|
||||
use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData};
|
||||
use super::{
|
||||
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
|
||||
};
|
||||
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||
use 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 {
|
||||
@@ -13,9 +18,9 @@ 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)
|
||||
@@ -23,18 +28,43 @@ impl ControlData for TextAreaData {
|
||||
}
|
||||
impl ValidatedControlData for TextAreaData {}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
impl<FD: FormToolData> FormBuilder<FD> {
|
||||
/// Builds a text area control and adds it to the form.
|
||||
pub fn text_area<FDT: Clone + PartialEq + 'static>(
|
||||
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: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, TextAreaData, FDT> {
|
||||
impl<FD: FormToolData, FDT> ControlBuilder<FD, TextAreaData, FDT> {
|
||||
/// Sets the name of the text area.
|
||||
///
|
||||
/// This is used for the html element's "name" attribute.
|
||||
/// In forms, the name attribute is the key that the data is sent
|
||||
/// with.
|
||||
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||
self.data.name = control_name.to_string();
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the label for the text area.
|
||||
pub fn labeled(mut self, label: impl ToString) -> Self {
|
||||
self.data.label = Some(label.to_string());
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the placeholder for the text area.
|
||||
pub fn placeholder(mut self, placeholder: impl ToString) -> Self {
|
||||
self.data.placeholder = Some(placeholder.to_string());
|
||||
self
|
||||
|
||||
+50
-26
@@ -1,15 +1,17 @@
|
||||
use leptos::{Signal, View};
|
||||
|
||||
use super::{ControlBuilder, ControlData, ControlRenderData, ValidatedControlData};
|
||||
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 {
|
||||
@@ -18,7 +20,6 @@ impl Default for TextInputData {
|
||||
name: String::new(),
|
||||
placeholder: None,
|
||||
label: None,
|
||||
initial_text: String::new(),
|
||||
input_type: "input",
|
||||
}
|
||||
}
|
||||
@@ -29,9 +30,9 @@ 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)
|
||||
@@ -39,40 +40,63 @@ impl ControlData for TextInputData {
|
||||
}
|
||||
impl ValidatedControlData for TextInputData {}
|
||||
|
||||
impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
impl<FD: FormToolData> FormBuilder<FD> {
|
||||
/// Builds a text input control and adds it to the form.
|
||||
pub fn text_input<FDT: Clone + PartialEq + 'static>(
|
||||
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: FormToolData, FS: FormStyle, FDT> ControlBuilder<FD, FS, TextInputData, FDT> {
|
||||
impl<FD: FormToolData, FDT> ControlBuilder<FD, TextInputData, FDT> {
|
||||
/// Sets the name of the text input.
|
||||
///
|
||||
/// This is used for the html element's "name" attribute.
|
||||
/// In forms, the name attribute is the key that the data is sent
|
||||
/// with.
|
||||
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
+91
-31
@@ -28,27 +28,31 @@ impl<FD: FormToolData> FormValidator<FD> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A constructed form object.
|
||||
/// A constructed, rendered form object.
|
||||
///
|
||||
/// With this, you can render the form, get the form data, or get
|
||||
/// a validator for the data.
|
||||
#[derive(Clone)]
|
||||
pub struct Form<FD: FormToolData> {
|
||||
/// The form data signal.
|
||||
pub fd: RwSignal<FD>,
|
||||
/// The list of validations
|
||||
pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
||||
pub(crate) view: View,
|
||||
}
|
||||
|
||||
impl<FD: FormToolData> Form<FD> {
|
||||
/// Gets the [`Validator`] for this form.
|
||||
pub fn validator(self) -> FormValidator<FD> {
|
||||
/// Gets the [`FormValidator`] for this form.
|
||||
pub fn validator(&self) -> FormValidator<FD> {
|
||||
FormValidator {
|
||||
validations: self.validations,
|
||||
validations: self.validations.clone(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Validates the [`ToolFormData`], returning the result
|
||||
/// Validates the [`FormToolData`], returning the result.
|
||||
pub fn validate(&self) -> Result<(), String> {
|
||||
self.fd.get_untracked().validate()
|
||||
let validator = self.validator();
|
||||
validator.validate(&self.fd.get_untracked())
|
||||
}
|
||||
|
||||
/// Gets the view associated with this [`Form`].
|
||||
@@ -76,54 +80,109 @@ impl<FD: FormToolData> IntoView for Form<FD> {
|
||||
|
||||
/// A trait allowing a form to be built around its containing data.
|
||||
///
|
||||
/// 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: 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 [`FormToolData`] type.
|
||||
///
|
||||
/// This renders the form as a the leptos_router
|
||||
/// [`Form`](leptos_router::Form)
|
||||
/// component. Call [`get_action_form`]\() to get the
|
||||
/// [`ActionForm`](leptos_router::ActionForm) version.
|
||||
fn get_form(self, action: impl ToString, style: Self::Style) -> Form<Self> {
|
||||
let builder = FormBuilder::new(self, 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_plain_form(action.to_string())
|
||||
builder.build_form(action, self, style)
|
||||
}
|
||||
|
||||
/// Constructs a [`Form`] for this [`FormToolData`] type.
|
||||
///
|
||||
/// This renders the form as a the leptos_router
|
||||
/// [`ActionForm`](leptos_router::ActionForm)
|
||||
/// component. Call [`get_form`]\() to get the plain
|
||||
/// [`Form`](leptos_router::Form) version.
|
||||
/// 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(self, style);
|
||||
let builder = FormBuilder::new(context);
|
||||
let builder = Self::build_form(builder);
|
||||
builder.build_action_form(action)
|
||||
builder.build_action_form(action, self, style)
|
||||
}
|
||||
|
||||
/// Gets a [`Validator`] for this [`ToolFormData`].
|
||||
/// Constructs a [`Form`] for this [`FormToolData`] type.
|
||||
///
|
||||
/// This renders the form as a the leptos_router
|
||||
/// [`Form`](leptos_router::Form)
|
||||
/// component.
|
||||
///
|
||||
/// For the other ways to construct a [`Form`], see:
|
||||
/// - [`get_form`](Self::get_form)
|
||||
/// - [`get_action_form`](Self::get_action_form)
|
||||
fn get_plain_form(
|
||||
self,
|
||||
action: impl ToString,
|
||||
style: Self::Style,
|
||||
context: Self::Context,
|
||||
) -> Form<Self> {
|
||||
let builder = FormBuilder::new(context);
|
||||
let builder = Self::build_form(builder);
|
||||
builder.build_plain_form(action.to_string(), self, style)
|
||||
}
|
||||
|
||||
/// Gets a [`FormValidator`] for this [`FormToolData`].
|
||||
///
|
||||
/// This doesn't render the view, but just collects all the validation
|
||||
/// Functions from building the form. That means it can be called on the
|
||||
@@ -131,18 +190,19 @@ pub trait FormToolData: Default + Clone + 'static {
|
||||
///
|
||||
/// 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() -> FormValidator<Self> {
|
||||
let builder = FormBuilder::new(Self::default(), Self::Style::default());
|
||||
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`]\()
|
||||
/// and then calling `validator.validate(&self)`.
|
||||
fn validate(&self) -> Result<(), String> {
|
||||
let validator = Self::get_validator();
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
+231
-84
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
controls::{
|
||||
BuiltControlData, BuiltVanityControlData, ControlBuilder, ControlData, FieldGetter,
|
||||
FieldSetter, ParseFn, RenderFn, UnparseFn, ValidationCb, ValidationFn,
|
||||
BuilderCxFn, BuilderFn, BuiltControlData, BuiltVanityControlData, ControlBuilder,
|
||||
ControlData, ControlRenderData, FieldSetter, ParseFn, RenderFn, ValidationCb, ValidationFn,
|
||||
VanityControlBuilder, VanityControlData,
|
||||
},
|
||||
form::{Form, FormToolData, FormValidator},
|
||||
@@ -19,50 +19,48 @@ use web_sys::{FormData, SubmitEvent};
|
||||
/// A builder for laying out forms.
|
||||
///
|
||||
/// This builder allows you to specify what components should make up the form.
|
||||
pub struct FormBuilder<FD: FormToolData, FS: FormStyle> {
|
||||
/// The [`ToolFormData`] signal.
|
||||
pub(crate) fd: RwSignal<FD>,
|
||||
/// The [`FormStyle`].
|
||||
pub(crate) fs: FS,
|
||||
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<FS, FD>>>,
|
||||
/// The list of styling attributes applied on the form level
|
||||
pub(crate) styles: Vec<FS::StylingAttributes>,
|
||||
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, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
impl<FD: FormToolData> FormBuilder<FD> {
|
||||
/// Creates a new [`FormBuilder`]
|
||||
pub(crate) fn new(starting_data: FD, form_style: FS) -> FormBuilder<FD, FS> {
|
||||
let fd = create_rw_signal(starting_data);
|
||||
pub(crate) fn new(cx: FD::Context) -> Self {
|
||||
FormBuilder {
|
||||
fd,
|
||||
fs: form_style,
|
||||
cx: Rc::new(cx),
|
||||
validations: Vec::new(),
|
||||
render_fns: Vec::new(),
|
||||
styles: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn new_group(fd: RwSignal<FD>, fs: FS) -> FormBuilder<FD, FS> {
|
||||
/// 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 {
|
||||
fd,
|
||||
fs,
|
||||
cx,
|
||||
validations: Vec::new(),
|
||||
render_fns: Vec::new(),
|
||||
styles: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(mut self, style: FS::StylingAttributes) -> Self {
|
||||
/// 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 Fn(VanityControlBuilder<FD, FS, C>) -> VanityControlBuilder<FD, FS, C>,
|
||||
builder: impl BuilderFn<VanityControlBuilder<FD, C>>,
|
||||
) -> Self {
|
||||
let vanity_builder = VanityControlBuilder::new(C::default());
|
||||
let control = builder(vanity_builder);
|
||||
@@ -70,9 +68,21 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
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 Fn(ControlBuilder<FD, FS, C, FDT>) -> ControlBuilder<FD, FS, C, FDT>,
|
||||
builder: impl BuilderFn<ControlBuilder<FD, C, FDT>>,
|
||||
) -> Self {
|
||||
let control_builder = ControlBuilder::new(C::default());
|
||||
let control = builder(control_builder);
|
||||
@@ -80,30 +90,100 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
self
|
||||
}
|
||||
|
||||
// TODO: test this from a user context. A user adding a custom defined component.
|
||||
pub fn add_vanity<C: VanityControlData>(
|
||||
/// 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, FS, C>,
|
||||
vanity_control: VanityControlBuilder<FD, C>,
|
||||
) {
|
||||
let BuiltVanityControlData {
|
||||
render_data,
|
||||
getter,
|
||||
show_when,
|
||||
} = vanity_control.build();
|
||||
|
||||
let render_fn = move |fs: &FS, fd: RwSignal<FD>| {
|
||||
let cx = self.cx.clone();
|
||||
let render_fn = move |fs: Rc<FD::Style>, fd: RwSignal<FD>| {
|
||||
let render_data = Rc::new(render_data);
|
||||
let value_getter = getter.map(|getter| (move || getter(fd.get())).into_signal());
|
||||
let view = VanityControlData::build_control(fs, render_data, value_getter);
|
||||
let view =
|
||||
move || VanityControlData::build_control(&*fs, render_data.clone(), value_getter);
|
||||
let view = match show_when {
|
||||
Some(when) => {
|
||||
let when = move || when(fd.into(), cx.clone());
|
||||
view! { <Show when=when>{view.clone()}</Show> }
|
||||
}
|
||||
None => view(),
|
||||
};
|
||||
(view, None)
|
||||
};
|
||||
|
||||
self.render_fns.push(Box::new(render_fn));
|
||||
}
|
||||
|
||||
// TODO: test this from a user context. A user adding a custom defined component.
|
||||
pub fn add_control<C: ControlData, FDT: Clone + PartialEq + 'static>(
|
||||
/// Adds a control to the form.
|
||||
pub(crate) fn add_control<C: ControlData, FDT: Clone + PartialEq + 'static>(
|
||||
&mut self,
|
||||
control: ControlBuilder<FD, FS, C, FDT>,
|
||||
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,
|
||||
@@ -111,42 +191,10 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
parse_fn,
|
||||
unparse_fn,
|
||||
validation_fn,
|
||||
} = match control.build() {
|
||||
Ok(c) => c,
|
||||
Err(e) => panic!("Invalid Component: {}", e),
|
||||
};
|
||||
show_when,
|
||||
} = control_data;
|
||||
|
||||
if let Some(ref validation_fn) = validation_fn {
|
||||
self.validations.push(validation_fn.clone());
|
||||
}
|
||||
|
||||
let render_fn = move |fs: &FS, fd: RwSignal<FD>| {
|
||||
let (view, cb) = Self::build_control_view(
|
||||
fd,
|
||||
fs,
|
||||
getter,
|
||||
setter,
|
||||
unparse_fn,
|
||||
parse_fn,
|
||||
validation_fn,
|
||||
render_data,
|
||||
);
|
||||
(view, Some(cb))
|
||||
};
|
||||
|
||||
self.render_fns.push(Box::new(render_fn));
|
||||
}
|
||||
|
||||
fn build_control_view<C: ControlData, FDT: 'static>(
|
||||
fd: RwSignal<FD>,
|
||||
fs: &FS,
|
||||
getter: Rc<dyn FieldGetter<FD, FDT>>,
|
||||
setter: Rc<dyn FieldSetter<FD, FDT>>,
|
||||
unparse_fn: Box<dyn UnparseFn<<C as ControlData>::ReturnType, FDT>>,
|
||||
parse_fn: Box<dyn ParseFn<<C as ControlData>::ReturnType, FDT>>,
|
||||
validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
||||
render_data: crate::controls::ControlRenderData<FS, C>,
|
||||
) -> (View, Box<dyn ValidationCb>) {
|
||||
let 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 || {
|
||||
@@ -166,14 +214,29 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
};
|
||||
let value_getter = value_getter.into_signal();
|
||||
|
||||
let cloned_show_when = show_when.clone();
|
||||
let cloned_cx = cx.clone();
|
||||
let validation_cb = move || {
|
||||
let validation_fn = validation_fn.as_ref();
|
||||
// first check if the validation signal is an error so that we
|
||||
// can fail on parsing issues too
|
||||
if let Some(Err(_)) = validation_signal.try_get_untracked() {
|
||||
return false;
|
||||
}
|
||||
|
||||
// validation for non-visible fields always succeeds
|
||||
if let Some(ref show_when) = cloned_show_when {
|
||||
if !show_when(fd.into(), cloned_cx.clone()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// run the validation function on the value now
|
||||
let validation_fn = match validation_fn {
|
||||
Some(v) => v,
|
||||
None => return true, // No validation function, so validation passes
|
||||
Some(ref v) => v,
|
||||
None => return true, // No validation function so validation passes
|
||||
};
|
||||
|
||||
let data = fd.get();
|
||||
let data = fd.get_untracked();
|
||||
let validation_result = validation_fn(&data);
|
||||
let succeeded = validation_result.is_ok();
|
||||
validation_signal_set.set(validation_result);
|
||||
@@ -189,23 +252,33 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
fd,
|
||||
);
|
||||
|
||||
let view = C::build_control(
|
||||
fs,
|
||||
render_data,
|
||||
value_getter,
|
||||
value_setter,
|
||||
validation_signal.into(),
|
||||
);
|
||||
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>,
|
||||
) -> Box<dyn Fn(CRT) + 'static> {
|
||||
) -> Rc<dyn Fn(CRT) + 'static> {
|
||||
let value_setter = move |value| {
|
||||
let parsed = match parse_fn(value) {
|
||||
Ok(p) => {
|
||||
@@ -226,25 +299,91 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
// run validation
|
||||
(validation_cb)();
|
||||
};
|
||||
Box::new(value_setter)
|
||||
Rc::new(value_setter)
|
||||
}
|
||||
|
||||
/// Builds the direct send version of the form.
|
||||
pub(crate) fn build_form<ServFn>(
|
||||
self,
|
||||
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
|
||||
fd: FD,
|
||||
fs: FD::Style,
|
||||
) -> Form<FD>
|
||||
where
|
||||
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
|
||||
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
|
||||
From<FormData>,
|
||||
ServFn: From<FD>,
|
||||
{
|
||||
let fd = create_rw_signal(fd);
|
||||
let fs = Rc::new(fs);
|
||||
|
||||
let (views, validation_cbs): (Vec<_>, Vec<_>) = self
|
||||
.render_fns
|
||||
.into_iter()
|
||||
.map(|r_fn| r_fn(fs.clone(), fd))
|
||||
.unzip();
|
||||
|
||||
let elements = fs.form_frame(ControlRenderData {
|
||||
data: views.into_view(),
|
||||
styles: self.styles,
|
||||
});
|
||||
|
||||
let on_submit = move |ev: SubmitEvent| {
|
||||
let mut failed = false;
|
||||
for validation in validation_cbs.iter().flatten() {
|
||||
if !validation() {
|
||||
failed = true;
|
||||
}
|
||||
}
|
||||
if failed {
|
||||
ev.prevent_default();
|
||||
return;
|
||||
}
|
||||
|
||||
ev.prevent_default();
|
||||
let server_fn = ServFn::from(fd.get_untracked());
|
||||
action.dispatch(server_fn);
|
||||
};
|
||||
|
||||
let view = view! {
|
||||
<ActionForm action=action on:submit=on_submit>
|
||||
{elements}
|
||||
</ActionForm>
|
||||
};
|
||||
|
||||
Form {
|
||||
fd,
|
||||
validations: self.validations,
|
||||
view,
|
||||
}
|
||||
}
|
||||
|
||||
/// Builds the action form version of the form.
|
||||
pub(crate) fn build_action_form<ServFn>(
|
||||
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(&self.fs, self.fd))
|
||||
.map(|r_fn| r_fn(fs.clone(), fd))
|
||||
.unzip();
|
||||
|
||||
let elements = self.fs.form_frame(views.into_view(), self.styles);
|
||||
let elements = fs.form_frame(ControlRenderData {
|
||||
data: views.into_view(),
|
||||
styles: self.styles,
|
||||
});
|
||||
|
||||
let on_submit = move |ev: SubmitEvent| {
|
||||
let mut failed = false;
|
||||
@@ -265,20 +404,27 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
};
|
||||
|
||||
Form {
|
||||
fd: self.fd,
|
||||
fd,
|
||||
validations: self.validations,
|
||||
view,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn build_plain_form(self, url: String) -> Form<FD> {
|
||||
/// builds the plain form version of the form.
|
||||
pub(crate) fn build_plain_form(self, url: String, fd: FD, fs: FD::Style) -> Form<FD> {
|
||||
let fd = create_rw_signal(fd);
|
||||
let fs = Rc::new(fs);
|
||||
|
||||
let (views, validation_cbs): (Vec<_>, Vec<_>) = self
|
||||
.render_fns
|
||||
.into_iter()
|
||||
.map(|r_fn| r_fn(&self.fs, self.fd))
|
||||
.map(|r_fn| r_fn(fs.clone(), fd))
|
||||
.unzip();
|
||||
|
||||
let elements = self.fs.form_frame(views.into_view(), self.styles);
|
||||
let elements = fs.form_frame(ControlRenderData {
|
||||
data: views.into_view(),
|
||||
styles: self.styles,
|
||||
});
|
||||
|
||||
let on_submit = move |ev: SubmitEvent| {
|
||||
let mut failed = false;
|
||||
@@ -299,12 +445,13 @@ impl<FD: FormToolData, FS: FormStyle> FormBuilder<FD, FS> {
|
||||
};
|
||||
|
||||
Form {
|
||||
fd: self.fd,
|
||||
fd,
|
||||
validations: self.validations,
|
||||
view,
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`FormValidator`] from this builder.
|
||||
pub(crate) fn validator(&self) -> FormValidator<FD> {
|
||||
FormValidator {
|
||||
validations: self.validations.clone(),
|
||||
|
||||
@@ -1,3 +1,8 @@
|
||||
//! `leptos_form_tool` offers a declaritve way to create forms for
|
||||
//! [leptos](https://leptos.dev/).
|
||||
//!
|
||||
//! To learn more, see the
|
||||
//! [README.md](https://github.com/MitchellMarinoDev/leptos_form_tool/src/branch/main/README.md)
|
||||
pub mod controls;
|
||||
mod form;
|
||||
mod form_builder;
|
||||
|
||||
+381
-378
@@ -1,412 +1,415 @@
|
||||
use super::FormStyle;
|
||||
use crate::controls::{
|
||||
checkbox::CheckboxData, heading::HeadingData, hidden::HiddenData, output::OutputData,
|
||||
select::SelectData, spacer::SpacerData, 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 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, children: View, _styles: Vec<Self::StylingAttributes>) -> View {
|
||||
view! { <div class="form_grid">{children}</div> }.into_view()
|
||||
fn form_frame(&self, form: ControlRenderData<Self, View>) -> View {
|
||||
view! { <div class="form_grid">{form.data}</div> }.into_view()
|
||||
}
|
||||
|
||||
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View {
|
||||
view! { <h2 class="form_heading">{&control.data.title}</h2> }.into_view()
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&self,
|
||||
control: ControlRenderData<Self, TextInputData>,
|
||||
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
|
||||
value_setter: Box<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View {
|
||||
// TODO: extract this to a common thing
|
||||
let mut width = 1;
|
||||
for style in control.style {
|
||||
match style {
|
||||
GridFormStylingAttributes::Width(w) => width = w,
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
<div style:grid-column=format!("span {}", width)>
|
||||
<div>
|
||||
<label for=&control.data.name class="form_label">
|
||||
{control.data.label.as_ref()}
|
||||
</label>
|
||||
<span class="form_error">{move || validation_state.get().err()}</span>
|
||||
</div>
|
||||
<input
|
||||
type=control.data.input_type
|
||||
id=&control.data.name
|
||||
name=control.data.name
|
||||
placeholder=control.data.placeholder
|
||||
prop:value=move || value_getter.get()
|
||||
on:focusout=move |ev| {
|
||||
value_setter(event_target_value(&ev));
|
||||
}
|
||||
|
||||
class="form_input"
|
||||
class=("form_input_invalid", move || validation_state.get().is_err())
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
.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 cloned_value = value.clone();
|
||||
view! {
|
||||
<option
|
||||
value=value.clone()
|
||||
selected=move || {value_getter.get() == *cloned_value}
|
||||
>
|
||||
{value.clone()}
|
||||
</option>
|
||||
}
|
||||
})
|
||||
.collect_view();
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<span>{control.data.label}</span>
|
||||
{move || validation_state.get().err()}
|
||||
<select
|
||||
id=&control.data.name
|
||||
name=control.data.name
|
||||
class="form_input"
|
||||
on:input=move |ev| {
|
||||
value_setter(event_target_value(&ev));
|
||||
}
|
||||
>
|
||||
|
||||
{options_view}
|
||||
</select>
|
||||
</div>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
|
||||
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View {
|
||||
view! {
|
||||
<input type="submit" value=control.data.text class="form_submit"/>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
|
||||
fn text_area(
|
||||
&self,
|
||||
control: ControlRenderData<Self, TextAreaData>,
|
||||
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
|
||||
value_setter: Box<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View {
|
||||
view! {
|
||||
<div>
|
||||
{move || format!("{:?}", validation_state.get())}
|
||||
<textarea
|
||||
id=&control.data.name
|
||||
name=control.data.name
|
||||
placeholder=control.data.placeholder
|
||||
class="form_input"
|
||||
prop:value=move || value_getter.get()
|
||||
on:change=move |ev| {
|
||||
value_setter(event_target_value(&ev));
|
||||
}
|
||||
></textarea>
|
||||
|
||||
</div>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
|
||||
fn custom_component(&self, view: View) -> View {
|
||||
view
|
||||
}
|
||||
|
||||
fn hidden(
|
||||
&self,
|
||||
_control: ControlRenderData<Self, HiddenData>,
|
||||
value_getter: Signal<String>,
|
||||
) -> View {
|
||||
view! { <input prop:value=value_getter style="visibility: hidden"/> }.into_view()
|
||||
}
|
||||
|
||||
fn radio_buttons(
|
||||
&self,
|
||||
control: ControlRenderData<Self, crate::controls::radio_buttons::RadioButtonsData>,
|
||||
value_getter: Signal<
|
||||
<crate::controls::radio_buttons::RadioButtonsData as ControlData>::ReturnType,
|
||||
>,
|
||||
value_setter: Box<
|
||||
dyn Fn(<crate::controls::radio_buttons::RadioButtonsData as ControlData>::ReturnType),
|
||||
>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View {
|
||||
let mut width = 1;
|
||||
for style in control.style {
|
||||
match style {
|
||||
GridFormStylingAttributes::Width(w) => width = w,
|
||||
}
|
||||
}
|
||||
|
||||
let value_setter = Rc::new(value_setter);
|
||||
let buttons_view = control
|
||||
.data
|
||||
.options
|
||||
.into_iter()
|
||||
.map(|o| {
|
||||
let value_setter = value_setter.clone();
|
||||
let o_clone1 = o.clone();
|
||||
let o_clone2 = o.clone();
|
||||
view! {
|
||||
<input
|
||||
type="radio"
|
||||
id=o.clone()
|
||||
_str
|
||||
name=&control.data.name
|
||||
value=o.clone()
|
||||
prop:checked=move || { value_getter.get() == o_clone1 }
|
||||
on:input=move |ev| {
|
||||
let new_value = event_target_checked(&ev);
|
||||
if new_value {
|
||||
value_setter(o_clone2.clone());
|
||||
}
|
||||
}
|
||||
/>
|
||||
<label for=&o>{&o}</label>
|
||||
<br/>
|
||||
}
|
||||
})
|
||||
.collect_view();
|
||||
|
||||
view! {
|
||||
<div style:grid-column=format!("span {}", width)>
|
||||
<div>
|
||||
<label for=&control.data.name class="form_label">
|
||||
{control.data.label.as_ref()}
|
||||
</label>
|
||||
<span class="form_error">{move || validation_state.get().err()}</span>
|
||||
</div>
|
||||
<div
|
||||
class="form_input"
|
||||
class:form_input_invalid=move || validation_state.get().is_err()
|
||||
>
|
||||
{buttons_view}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
|
||||
fn checkbox(
|
||||
&self,
|
||||
control: ControlRenderData<Self, CheckboxData>,
|
||||
value_getter: Signal<<CheckboxData as ControlData>::ReturnType>,
|
||||
value_setter: Box<dyn Fn(<CheckboxData as ControlData>::ReturnType)>,
|
||||
) -> View {
|
||||
let mut width = 1;
|
||||
for style in control.style {
|
||||
match style {
|
||||
GridFormStylingAttributes::Width(w) => width = w,
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
<div style:grid-column=format!("span {}", width)>
|
||||
<label for=&control.data.name class="form_label">
|
||||
{control.data.label.as_ref()}
|
||||
</label>
|
||||
<label class="form_input" for=&control.data.name>
|
||||
<input
|
||||
type="checkbox"
|
||||
id=&control.data.name
|
||||
name=&control.data.name
|
||||
prop:checked=value_getter
|
||||
on:input=move |ev| {
|
||||
let new_value = event_target_checked(&ev);
|
||||
value_setter(new_value);
|
||||
}
|
||||
/>
|
||||
|
||||
<span>{control.data.label}</span>
|
||||
</label>
|
||||
</div>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
|
||||
fn stepper(
|
||||
&self,
|
||||
control: ControlRenderData<Self, crate::controls::stepper::StepperData>,
|
||||
value_getter: Signal<<crate::controls::stepper::StepperData as ControlData>::ReturnType>,
|
||||
value_setter: Box<
|
||||
dyn Fn(<crate::controls::stepper::StepperData as ControlData>::ReturnType),
|
||||
>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View {
|
||||
let mut width = 1;
|
||||
for style in control.style {
|
||||
match style {
|
||||
GridFormStylingAttributes::Width(w) => width = w,
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
<div style:grid-column=format!("span {}", width)>
|
||||
<div>
|
||||
<label for=&control.data.name class="form_label">
|
||||
{control.data.label.as_ref()}
|
||||
</label>
|
||||
<span class="form_error">{move || validation_state.get().err()}</span>
|
||||
</div>
|
||||
<input
|
||||
type="number"
|
||||
id=&control.data.name
|
||||
name=&control.data.name
|
||||
step=control.data.step
|
||||
prop:value=move || value_getter.get()
|
||||
on:change=move |ev| {
|
||||
value_setter(event_target_value(&ev));
|
||||
}
|
||||
|
||||
class="form_input"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
|
||||
fn output(
|
||||
&self,
|
||||
_control: ControlRenderData<Self, OutputData>,
|
||||
value_getter: Option<Signal<String>>,
|
||||
) -> View {
|
||||
view! { <div>{move || value_getter.map(|g| g.get())}</div> }.into_view()
|
||||
}
|
||||
|
||||
fn slider(
|
||||
&self,
|
||||
control: ControlRenderData<Self, crate::controls::slider::SliderData>,
|
||||
value_getter: Signal<<crate::controls::slider::SliderData as ControlData>::ReturnType>,
|
||||
value_setter: Box<dyn Fn(<crate::controls::slider::SliderData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View {
|
||||
let mut width = 1;
|
||||
for style in control.style {
|
||||
match style {
|
||||
GridFormStylingAttributes::Width(w) => width = w,
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
<div style:grid-column=format!("span {}", width)>
|
||||
<div>
|
||||
<label for=&control.data.name class="form_label">
|
||||
{control.data.label.as_ref()}
|
||||
</label>
|
||||
<span class="form_error">{move || validation_state.get().err()}</span>
|
||||
</div>
|
||||
<input
|
||||
type="range"
|
||||
id=&control.data.name
|
||||
name=&control.data.name
|
||||
min=control.data.min
|
||||
max=control.data.max
|
||||
prop:value=move || value_getter.get()
|
||||
on:input=move |ev| {
|
||||
let value = event_target_value(&ev).parse::<i32>().ok();
|
||||
if let Some(value) = value {
|
||||
value_setter(value);
|
||||
}
|
||||
}
|
||||
|
||||
class="form_input"
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
|
||||
fn button<FD: crate::FormToolData>(
|
||||
&self,
|
||||
control: ControlRenderData<Self, crate::controls::button::ButtonData<FD>>,
|
||||
) -> View {
|
||||
let mut width = 1;
|
||||
for style in control.style {
|
||||
match style {
|
||||
GridFormStylingAttributes::Width(w) => width = w,
|
||||
}
|
||||
}
|
||||
|
||||
let action = control.data.action.clone();
|
||||
let signal = control.data.fd_signal.clone();
|
||||
let on_click = move |ev: MouseEvent| {
|
||||
if let Some(action) = action.clone() {
|
||||
signal.update(|fd| action(ev, fd));
|
||||
}
|
||||
};
|
||||
|
||||
view! {
|
||||
<button type="button" class="form_button" on:click=on_click style:grid-column=format!("span {}", width)>
|
||||
{control.data.text}
|
||||
</button>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
|
||||
// TODO: change this and form frame to use ControlRenderData
|
||||
fn group(&self, inner: View, styles: Vec<Self::StylingAttributes>) -> 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;
|
||||
for style in styles {
|
||||
let mut tooltip = None;
|
||||
for style in styles.iter() {
|
||||
match style {
|
||||
GridFormStylingAttributes::Width(w) => width = w,
|
||||
GFStyleAttr::Width(w) => width = *w,
|
||||
GFStyleAttr::Tooltip(t) => tooltip = Some(t),
|
||||
}
|
||||
}
|
||||
|
||||
view! {
|
||||
<div class="form_group form_grid" style:grid-column=format!("span {}", width)>
|
||||
<div style:grid-column=format!("span {}", width) title=tooltip>
|
||||
{inner}
|
||||
</div>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
|
||||
fn spacer(&self, control: ControlRenderData<Self, SpacerData>) -> View {
|
||||
let mut width = 12;
|
||||
for style in control.style {
|
||||
match style {
|
||||
GridFormStylingAttributes::Width(w) => width = w,
|
||||
}
|
||||
}
|
||||
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! {
|
||||
<div style:grid-column=format!("span {}", width) style:height=control.data.height/>
|
||||
<input
|
||||
name=&control.data.name
|
||||
prop:value=value_getter
|
||||
style="visibility: hidden; column-span: none"
|
||||
/>
|
||||
}
|
||||
.into_view()
|
||||
}
|
||||
|
||||
fn text_input(
|
||||
&self,
|
||||
control: Rc<ControlRenderData<Self, TextInputData>>,
|
||||
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
|
||||
value_setter: Rc<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View {
|
||||
let view = view! {
|
||||
<div>
|
||||
<label for=&control.data.name class="form_label">
|
||||
{control.data.label.as_ref()}
|
||||
</label>
|
||||
<span class="form_error">{move || validation_state.get().err()}</span>
|
||||
</div>
|
||||
<input
|
||||
type=control.data.input_type
|
||||
id=&control.data.name
|
||||
name=&control.data.name
|
||||
placeholder=control.data.placeholder.as_ref()
|
||||
class="form_input"
|
||||
class=("form_input_invalid", move || validation_state.get().is_err())
|
||||
prop:value=move || value_getter.get()
|
||||
on:focusout=move |ev| {
|
||||
value_setter(event_target_value(&ev));
|
||||
}
|
||||
/>
|
||||
}
|
||||
.into_view();
|
||||
|
||||
self.custom_component(&control.styles, view)
|
||||
}
|
||||
|
||||
fn text_area(
|
||||
&self,
|
||||
control: Rc<ControlRenderData<Self, TextAreaData>>,
|
||||
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
|
||||
value_setter: Rc<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View {
|
||||
let view = view! {
|
||||
<div>
|
||||
<label for=&control.data.name class="form_label">
|
||||
{control.data.label.as_ref()}
|
||||
</label>
|
||||
<span class="form_error">{move || validation_state.get().err()}</span>
|
||||
</div>
|
||||
<textarea
|
||||
id=&control.data.name
|
||||
name=&control.data.name
|
||||
placeholder=control.data.placeholder.as_ref()
|
||||
prop:value=move || value_getter.get()
|
||||
class="form_input"
|
||||
class=("form_input_invalid", move || validation_state.get().is_err())
|
||||
on:focusout=move |ev| {
|
||||
value_setter(event_target_value(&ev));
|
||||
}
|
||||
></textarea>
|
||||
}
|
||||
.into_view();
|
||||
|
||||
self.custom_component(&control.styles, view)
|
||||
}
|
||||
|
||||
fn radio_buttons(
|
||||
&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));
|
||||
}
|
||||
/>
|
||||
}
|
||||
.into_view();
|
||||
|
||||
self.custom_component(&control.styles, 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)
|
||||
}
|
||||
}
|
||||
|
||||
+137
-59
@@ -9,11 +9,20 @@ use crate::{
|
||||
},
|
||||
FormToolData,
|
||||
};
|
||||
use leptos::{Signal, View};
|
||||
use leptos::{RwSignal, Signal, View};
|
||||
use std::rc::Rc;
|
||||
|
||||
pub use grid_form::{GridFormStyle, GridFormStylingAttributes};
|
||||
pub use grid_form::{GFStyleAttr, GridFormStyle};
|
||||
|
||||
pub trait FormStyle: Default + 'static {
|
||||
/// 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;
|
||||
|
||||
/// Render any containing components for the form.
|
||||
@@ -23,70 +32,139 @@ pub trait FormStyle: Default + 'static {
|
||||
///
|
||||
/// Do NOT wrap it in an actual `form` element; any
|
||||
/// wrapping should be done with `div` or similar elements.
|
||||
fn form_frame(&self, children: View, style: Vec<Self::StylingAttributes>) -> View;
|
||||
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View;
|
||||
fn hidden(
|
||||
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: ControlRenderData<Self, HiddenData>,
|
||||
value_getter: Signal<String>,
|
||||
) -> View;
|
||||
fn text_input(
|
||||
&self,
|
||||
control: ControlRenderData<Self, TextInputData>,
|
||||
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
|
||||
value_setter: Box<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View;
|
||||
fn text_area(
|
||||
&self,
|
||||
control: ControlRenderData<Self, TextAreaData>,
|
||||
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
|
||||
value_setter: Box<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View;
|
||||
fn radio_buttons(
|
||||
&self,
|
||||
control: ControlRenderData<Self, RadioButtonsData>,
|
||||
value_getter: Signal<<RadioButtonsData as ControlData>::ReturnType>,
|
||||
value_setter: Box<dyn Fn(<RadioButtonsData 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 button<FD: FormToolData>(&self, control: ControlRenderData<Self, ButtonData<FD>>) -> View;
|
||||
fn checkbox(
|
||||
&self,
|
||||
control: ControlRenderData<Self, CheckboxData>,
|
||||
value_getter: Signal<<CheckboxData as ControlData>::ReturnType>,
|
||||
value_setter: Box<dyn Fn(<CheckboxData as ControlData>::ReturnType)>,
|
||||
) -> View;
|
||||
fn stepper(
|
||||
&self,
|
||||
control: ControlRenderData<Self, StepperData>,
|
||||
value_getter: Signal<<StepperData as ControlData>::ReturnType>,
|
||||
value_setter: Box<dyn Fn(<StepperData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
control: Rc<ControlRenderData<Self, ButtonData<FD>>>,
|
||||
data_signal: RwSignal<FD>,
|
||||
) -> View;
|
||||
|
||||
/// Renders some output text.
|
||||
///
|
||||
/// See [`OutputData`].
|
||||
fn output(
|
||||
&self,
|
||||
control: ControlRenderData<Self, OutputData>,
|
||||
control: Rc<ControlRenderData<Self, OutputData>>,
|
||||
value_getter: Option<Signal<String>>,
|
||||
) -> View;
|
||||
fn slider(
|
||||
|
||||
/// Renders a input control that should be hidden from the user.
|
||||
///
|
||||
/// See [`HiddenData`].
|
||||
fn hidden(
|
||||
&self,
|
||||
control: ControlRenderData<Self, SliderData>,
|
||||
control: Rc<ControlRenderData<Self, HiddenData>>,
|
||||
value_getter: Option<Signal<String>>,
|
||||
) -> View;
|
||||
|
||||
/// Renders a text input control.
|
||||
///
|
||||
/// See [`TextInputData`].
|
||||
fn text_input(
|
||||
&self,
|
||||
control: Rc<ControlRenderData<Self, TextInputData>>,
|
||||
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
|
||||
value_setter: Rc<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View;
|
||||
|
||||
/// Renders a text area control.
|
||||
///
|
||||
/// See [`TextAreaData`].
|
||||
fn text_area(
|
||||
&self,
|
||||
control: Rc<ControlRenderData<Self, TextAreaData>>,
|
||||
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
|
||||
value_setter: Rc<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View;
|
||||
|
||||
/// Renders a group of radio buttons.
|
||||
///
|
||||
/// See [`RadioButtonsData`].
|
||||
fn radio_buttons(
|
||||
&self,
|
||||
control: Rc<ControlRenderData<Self, RadioButtonsData>>,
|
||||
value_getter: Signal<<RadioButtonsData as ControlData>::ReturnType>,
|
||||
value_setter: Rc<dyn Fn(<RadioButtonsData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View;
|
||||
|
||||
/// Renders a select (or dropdown) control.
|
||||
///
|
||||
/// See [`SelectData`].
|
||||
fn select(
|
||||
&self,
|
||||
control: Rc<ControlRenderData<Self, SelectData>>,
|
||||
value_getter: Signal<<SelectData as ControlData>::ReturnType>,
|
||||
value_setter: Rc<dyn Fn(<SelectData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View;
|
||||
|
||||
/// Renders a checkbox control.
|
||||
///
|
||||
/// See [`CheckboxData`].
|
||||
fn checkbox(
|
||||
&self,
|
||||
control: Rc<ControlRenderData<Self, CheckboxData>>,
|
||||
value_getter: Signal<<CheckboxData as ControlData>::ReturnType>,
|
||||
value_setter: Rc<dyn Fn(<CheckboxData as ControlData>::ReturnType)>,
|
||||
) -> View;
|
||||
|
||||
/// Renders a stepper control.
|
||||
///
|
||||
/// See [`StepperData`].
|
||||
fn stepper(
|
||||
&self,
|
||||
control: Rc<ControlRenderData<Self, StepperData>>,
|
||||
value_getter: Signal<<StepperData as ControlData>::ReturnType>,
|
||||
value_setter: Rc<dyn Fn(<StepperData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View;
|
||||
|
||||
/// Renders a slider control.
|
||||
///
|
||||
/// See [`SliderData`].
|
||||
fn slider(
|
||||
&self,
|
||||
control: Rc<ControlRenderData<Self, SliderData>>,
|
||||
value_getter: Signal<<SliderData as ControlData>::ReturnType>,
|
||||
value_setter: Box<dyn Fn(<SliderData as ControlData>::ReturnType)>,
|
||||
value_setter: Rc<dyn Fn(<SliderData as ControlData>::ReturnType)>,
|
||||
validation_state: Signal<Result<(), String>>,
|
||||
) -> View;
|
||||
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View;
|
||||
// TODO: test custom component
|
||||
fn custom_component(&self, view: View) -> View;
|
||||
fn group(&self, inner: View, style: Vec<Self::StylingAttributes>) -> View;
|
||||
fn spacer(&self, control: ControlRenderData<Self, SpacerData>) -> View;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
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
|
||||
///
|
||||
@@ -8,16 +14,16 @@ use std::fmt::Display;
|
||||
/// 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: 'static> {
|
||||
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<dyn Fn(&str, &T) -> Result<(), String> + 'static>>,
|
||||
functions: Vec<Box<ValidationBuilderFn<T>>>,
|
||||
}
|
||||
|
||||
impl<FD: FormToolData, T: 'static> ValidationBuilder<FD, T> {
|
||||
impl<FD: FormToolData, T: ?Sized + 'static> ValidationBuilder<FD, T> {
|
||||
/// Creates a new empty [`ValidationBuilder`] on the given field.
|
||||
pub fn for_field(field_fn: impl Fn(&FD) -> &T + 'static) -> Self {
|
||||
ValidationBuilder {
|
||||
@@ -59,7 +65,7 @@ impl<FD: FormToolData, T: 'static> ValidationBuilder<FD, T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<FD: FormToolData> ValidationBuilder<FD, String> {
|
||||
impl<FD: FormToolData> ValidationBuilder<FD, str> {
|
||||
/// Requires the field to not be empty.
|
||||
pub fn required(mut self) -> Self {
|
||||
self.functions.push(Box::new(move |name, value| {
|
||||
@@ -95,6 +101,19 @@ impl<FD: FormToolData> ValidationBuilder<FD, String> {
|
||||
}));
|
||||
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> {
|
||||
|
||||
Reference in New Issue
Block a user