Archived
generated from mitchell/rust_template
Compare commits
44
Commits
68d6b857ed
...
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 | ||
|
|
1dc676cc37 | ||
|
|
e7fd8ec7e1 | ||
|
|
bf04957370 | ||
|
|
360ef63b58 | ||
|
|
edc9c4d371 | ||
|
|
31b24a23a1 | ||
|
|
fb47d553e7 | ||
|
|
5d0b4a7449 | ||
|
|
9b4bbfdb74 | ||
|
|
749ccc272b | ||
|
|
ead75f050a | ||
|
|
63153d76a0 | ||
|
|
a3311b2b63 | ||
|
|
3621e5fb7b | ||
|
|
f19d53830c | ||
|
|
9368b676d2 | ||
|
|
161727ab7a | ||
|
|
fedc6d37bd | ||
|
|
74407d6c0a | ||
|
|
fb44b5d303 |
@@ -6,4 +6,7 @@ edition = "2021"
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
leptos = "0.6"
|
leptos = "0.6"
|
||||||
leptos_router = "0.6"
|
leptos_router = "0.6"
|
||||||
|
serde = { version = "1.0.203", features = ["derive"] }
|
||||||
|
# Leptos should pick the web_sys version for us
|
||||||
|
web-sys = "*"
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,110 @@
|
|||||||
# leptos_form_tool
|
# leptos_form_tool
|
||||||
|
|
||||||
A tool for making forms in leptos.
|
A declaritive way to create forms for [leptos](https://leptos.dev/).
|
||||||
|
|
||||||
|
leptos_form_tool allows you to define forms in a declaritive way.
|
||||||
|
Want text box? Just call `.text_input` on the form builder. Then sepperatly,
|
||||||
|
you define a FormStyle to specify how that text box should be rendered.
|
||||||
|
This has a number of advantages:
|
||||||
|
- Sepperates the layout of the form from how it is rendered and styled
|
||||||
|
- Allows different styles to be swapped in and out quickly
|
||||||
|
|
||||||
|
## Validations
|
||||||
|
|
||||||
|
You might find yourself asking, but why not just use components?
|
||||||
|
|
||||||
|
The biggest reason for creating leptos_form_tool is support for
|
||||||
|
validating the fields. This validation logic can get rather complex, for
|
||||||
|
instance, you likely want to preform validation on the client when the user
|
||||||
|
clicks submit to immediatly give the user feedback about any invalid input.
|
||||||
|
But you often also want to do the same validation on the server to protect
|
||||||
|
against any requests that don't come from your client or for a user that
|
||||||
|
doesn't have wasm enabled.
|
||||||
|
|
||||||
|
Additionally, you might want to change the validation of one control based
|
||||||
|
on the value of another. For example, you might want to make sure the "day"
|
||||||
|
field is in a valid range, but that range depends on what the user selects in
|
||||||
|
the "month" field. Or you might want to make sure the "confirm password" field
|
||||||
|
matches the "password" field. leptos_form_tool makes this easy, as the
|
||||||
|
validation function you provide operates on the entire form's data.
|
||||||
|
|
||||||
|
Sometimes you might not want to show some controls, and validation for those
|
||||||
|
controls should only be done when they are visible.
|
||||||
|
|
||||||
|
lepos_form_tool takes care of all this for you.
|
||||||
|
|
||||||
|
## FormStyle
|
||||||
|
|
||||||
|
To define how to render all the components that a form might use, you define
|
||||||
|
a type that implements the `FormStyle` trait. This trait has a method for each
|
||||||
|
control that the form might need to render. Once you implement that trait you
|
||||||
|
just need to change the `Style` associated trait of your form to use that new
|
||||||
|
style.
|
||||||
|
|
||||||
|
Its actually a little more complicated than that...
|
||||||
|
|
||||||
|
To give custom styles a little more freedom to configure how to render their
|
||||||
|
controls on a per-control-basis, the style will define an associated type
|
||||||
|
(usually an enum) called `StylingAttributes`. A styling attribute can be added
|
||||||
|
to a control by calling `.style(/* style */)` on the control builder. These
|
||||||
|
styling attributes are accessable to the `FormStyle` implementation when
|
||||||
|
rendering that control.
|
||||||
|
|
||||||
|
Therefore, swaping out styles also requires swapping out all the `.style()`
|
||||||
|
calls.
|
||||||
|
|
||||||
|
## Builders
|
||||||
|
|
||||||
|
leptos_form_tool makes heavy use of the builder pattern. You will build the
|
||||||
|
form, controls and sometimes even validation functions by calling methods on
|
||||||
|
a builder that will construct the object based on the values you give it.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Sometimes, you might want to be able to use something from the form's context
|
||||||
|
to render the form. For example, you may want to use a user's token as context
|
||||||
|
and only render part of the form if they are an administrator. Or, you may
|
||||||
|
need to get the options for a certain drop-down dynamically. The form's context
|
||||||
|
is the solution to these problems.
|
||||||
|
|
||||||
|
On the form, you define the associated type `Context`. Then, when you construct
|
||||||
|
the `Form` object, you must provide the context. The context can be used in
|
||||||
|
the building of the form, and can change what is rendered. Each control
|
||||||
|
builder function has a context varient that allows you to use the context in
|
||||||
|
the building of the form.
|
||||||
|
|
||||||
|
To avoid a whole lot of headache, the context is immutable once passed into to
|
||||||
|
the form. However, you can have leptos signals in the context, as they dont
|
||||||
|
require mutable access to call get/set on the signal.
|
||||||
|
|
||||||
|
Since the context can change how the form is rendered, and what controls are
|
||||||
|
shown/hidden (thus changing what controls are validated), the context is
|
||||||
|
needed to validate the form data on the server side. If are sure that the
|
||||||
|
context doesn't change any of the validations, you don't have to make sure
|
||||||
|
the context is the same on client and server. If the context does change
|
||||||
|
how the form is validated, make sure to keep the context the same to maximize
|
||||||
|
the validation that can happen on the clients side, before the user even
|
||||||
|
submits the form.
|
||||||
|
|
||||||
|
## Custom Components
|
||||||
|
|
||||||
|
leptos_form_tool also supports custom components that can be defined in the
|
||||||
|
user space. Though less ergonomic, this keeps leptos_form_tool from putting
|
||||||
|
limits on what you can do. There are `custom_*` methods on the form builder
|
||||||
|
that allow you to add your component. Unfortunatly you cannot define methods
|
||||||
|
on the `ControlBuilder` to help build your controls data, so you must
|
||||||
|
construct the ControlData for your custom type before adding it to the form.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
To learn by example, see the example project.
|
||||||
|
|
||||||
|
To follow a Getting Started guide, see [`getting_started.md`].
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
To contribute, fork the repo and make a PR.
|
||||||
|
If you find a bug, feel free to open an issue.
|
||||||
|
|
||||||
|
By contributing, you agree that your changes are
|
||||||
|
subject to the license found in [`/LICENSE`].
|
||||||
|
|||||||
@@ -0,0 +1,240 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
||||||
|
This guide will walk you through creating your first leptos_form_tool form.
|
||||||
|
|
||||||
|
## Form Data
|
||||||
|
|
||||||
|
Start by creating the form data.
|
||||||
|
|
||||||
|
This struct should contain all the data for the entire form.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// all FormToolData implementors must also implement Clone and be 'static
|
||||||
|
// Default and Debug are not required, but helpful
|
||||||
|
#[derive(Clone, Default, Debug)]
|
||||||
|
struct HelloWorldFormData {
|
||||||
|
first: String,
|
||||||
|
last: String,
|
||||||
|
age: u32,
|
||||||
|
sport: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Defining the Form Layout
|
||||||
|
|
||||||
|
Then, to define how the for should be rendered, implement the `FormToolData`
|
||||||
|
trait. You will need to define the style that this form will use and what
|
||||||
|
context it will have.
|
||||||
|
|
||||||
|
It is required to define the style here, as the style needs to be known to
|
||||||
|
add the StylingAttributes to the controls.
|
||||||
|
|
||||||
|
The `build_form` method provides you with a `FormBuilder<Self>`. You can define
|
||||||
|
controls on the form by using the builder methods. Some controls don't accept
|
||||||
|
input from the user, they just display information. These are refered to as
|
||||||
|
`VanityControls` by leptos_form_tool. An example of a vanity control would
|
||||||
|
be a heading. Other controls do accept user input and are just refered to as
|
||||||
|
`Controls` by leptos_form_tool.
|
||||||
|
|
||||||
|
### Defining Controls
|
||||||
|
|
||||||
|
There are other builder methods defined the the ControlBuilder for certain
|
||||||
|
controls. For example, the TextInput builder has a `placeholder` method
|
||||||
|
that sets the placeholder of the text input.
|
||||||
|
|
||||||
|
When building a control, you will need to provide a getter and setter
|
||||||
|
to get the field and set the field on the form data. The getter is a function
|
||||||
|
that takes the full form data, and returns the field. The setter is a function
|
||||||
|
that takes the full form data and a value, and sets the field to that value.
|
||||||
|
Examples for these getters and setters are shown below.
|
||||||
|
|
||||||
|
A VanityControl will never need a setter, as they only display information.
|
||||||
|
Sometimes they need getters if the information they display is based on the
|
||||||
|
form data. For example, the output control can display information from the
|
||||||
|
form data, so it needs a getter.
|
||||||
|
|
||||||
|
#### Parse and Unparse Functions
|
||||||
|
|
||||||
|
Controls also need a set of parse and unparse functions. The type that the
|
||||||
|
control returns could be anything. For example, a range slider might return a
|
||||||
|
`i32`, a text input might return a `String`. leptos_form_tool needs a way to
|
||||||
|
convert the type of the field, to the type of the control and vice versa.
|
||||||
|
This is what the un/parse functions are for.
|
||||||
|
|
||||||
|
The parse and unparse functions can always be specified with the `parse_custom`
|
||||||
|
method, but there are several builder methods that create the parse functions
|
||||||
|
automatically. For example, if the field type and control type can be
|
||||||
|
converted to and from each other with the `From` trait (this is true if the
|
||||||
|
two types are the same) then you can call the `parse_from` method to
|
||||||
|
automatically create the un/parse functions using that conversion.
|
||||||
|
If the control's type is String, and the field type implements `FromStr` and
|
||||||
|
`ToString`, you can call `parse_string` to generate un/parse functions using
|
||||||
|
that trait. `parse_trimmed` does the same, but trims the string before parsing.
|
||||||
|
This should cover most use cases, but you always have the option to define
|
||||||
|
your own.
|
||||||
|
|
||||||
|
It is important to note that parsing from the control's type to the field type
|
||||||
|
IS allowed to fail. If it fails, it will be displayed just like any other
|
||||||
|
validation error. Conversion from the field type to the control type is NOT
|
||||||
|
allowed to fail; the FormData should always be able to be displayed.
|
||||||
|
|
||||||
|
#### Validation Functions
|
||||||
|
|
||||||
|
Validation functions can be defined on a field to add some extra criteria
|
||||||
|
for what counts as a valid entry. The validation function takes the entire
|
||||||
|
forms data. This allows you to use the entire state of the form to decide if
|
||||||
|
this field is valid or not. If any validation function fails, the form will
|
||||||
|
not be allowed to submit. In addition, when you build a validator, it will
|
||||||
|
collect all of the validation functions that you define on these fields.
|
||||||
|
|
||||||
|
leptos_form_tool provides a `ValidationBuilder` to help you build validation
|
||||||
|
functions, which, in some cases, might be easier than defining a closure
|
||||||
|
yourself.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl FormToolData for HelloWorldFormData {
|
||||||
|
// The form style to use.
|
||||||
|
type Style = GridFormStyle;
|
||||||
|
// The external context needed for rendering the form.
|
||||||
|
// In this case, nothing.
|
||||||
|
type Context = ();
|
||||||
|
|
||||||
|
fn build_form(fb: FormBuilder<Self>) -> FormBuilder<Self> {
|
||||||
|
fb.heading(|h| h.title("Welcome"))
|
||||||
|
.text_input(|t| {
|
||||||
|
t.named("data[first]")
|
||||||
|
.labeled("First Name")
|
||||||
|
.getter(|fd| fd.first)
|
||||||
|
.setter(|fd, value| fd.first = value)
|
||||||
|
// trim the string before writing to the field
|
||||||
|
.parse_trimmed()
|
||||||
|
.validation_fn(
|
||||||
|
// Using the ValidationBuilder to set a required field
|
||||||
|
ValidationBuilder::for_field(|fd: &HelloWorldFormData| fd.first.as_str())
|
||||||
|
.named("First Name")
|
||||||
|
.required()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
// width out of 12
|
||||||
|
.style(Width(4))
|
||||||
|
// defines text that shows up when hovering over it
|
||||||
|
.style(Tooltip("Your given first name".to_string()))
|
||||||
|
})
|
||||||
|
.text_input(|t| {
|
||||||
|
t.named("data[last]")
|
||||||
|
.labeled("Last Name")
|
||||||
|
.getter(|fd| fd.last)
|
||||||
|
.setter(|fd, value| fd.last = value)
|
||||||
|
// dont trim the string, just write it to the field
|
||||||
|
.parse_from()
|
||||||
|
.validation_fn(
|
||||||
|
// using the ValidationBuilder to set a required field
|
||||||
|
ValidationBuilder::for_field(|fd: &HelloWorldFormData| fd.last.as_str())
|
||||||
|
.named("Last Name")
|
||||||
|
.required()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.style(Width(8))
|
||||||
|
.style(Tooltip("Your last name".to_string()))
|
||||||
|
})
|
||||||
|
// using the _cx varient allows access to the context
|
||||||
|
// in this case, its `()` which doesnt help us that much.
|
||||||
|
.stepper_cx(|s, _cx| {
|
||||||
|
s.named("data[age]")
|
||||||
|
.labeled("Age")
|
||||||
|
.getter(|fd| fd.age)
|
||||||
|
.setter(|fd, value| fd.age = value)
|
||||||
|
// trim the string then try to parse to a `u32`
|
||||||
|
.parse_trimmed()
|
||||||
|
.validation_fn(move |fd| {
|
||||||
|
// defining a validation function with a closure
|
||||||
|
(fd.age > 13)
|
||||||
|
.then_some(())
|
||||||
|
.ok_or_else(|| String::from("Too Young"))
|
||||||
|
})
|
||||||
|
.style(Width(6))
|
||||||
|
.style(Tooltip("Your age in years".to_string()))
|
||||||
|
})
|
||||||
|
.select(|s| {
|
||||||
|
s.named("data[sport]")
|
||||||
|
.labeled("Favorite Sport")
|
||||||
|
.getter(|fd| fd.sport)
|
||||||
|
.setter(|fd, value| fd.sport = value)
|
||||||
|
.parse_from()
|
||||||
|
// set the options for the select along with their values
|
||||||
|
.with_options_valued(vec![
|
||||||
|
("Football", "football"),
|
||||||
|
("Soccer", "soccer"),
|
||||||
|
("Ice Hockey", "ice_hockey"),
|
||||||
|
("Golf", "golf"),
|
||||||
|
].into_iter())
|
||||||
|
.style(Width(6))
|
||||||
|
})
|
||||||
|
.submit(|s| s.text("Submit"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, using the form is quite simple. You just need to provide the form data,
|
||||||
|
style, context, and where the form should point to.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let form = ExampleFormData::default().get_plain_form("/api/endpoint", GridFormStyle::default(), ());
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<h1> "This is My Form!" </h1>
|
||||||
|
{form}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also have the form build to an `ActionForm`, this will be very
|
||||||
|
familiar if you've used an `ActionForm` before.
|
||||||
|
|
||||||
|
You might have noticed the goofy names that we put in our form above, like
|
||||||
|
"data[first]" instead of just "first". This is done to allow the form to use
|
||||||
|
the SubmitForm as an action. See
|
||||||
|
[ActionForms](https://book.leptos.dev/progressive_enhancement/action_form.html#complex-inputs)
|
||||||
|
in the leptos book for more.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn FormWrapper() -> impl IntoView {
|
||||||
|
let server_fn_action = create_server_action::<SubmitForm>();
|
||||||
|
|
||||||
|
let form = HelloWorldFormData::default().get_action_form(server_fn_action, GridFormStyle::default(), ());
|
||||||
|
let response = server_fn_action.value();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<h1> "This is My Form!" </h1>
|
||||||
|
// display the form
|
||||||
|
{form}
|
||||||
|
// display the result from the server
|
||||||
|
{move || response.get().map(|result| result.ok())}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(SubmitForm)]
|
||||||
|
async fn submit_form(data: HelloWorldFormData) -> Result<String, ServerFnError> {
|
||||||
|
data.validate(()).map_err(ServerFnError::new)?;
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"Hello {} {} ({}), You must like {}!",
|
||||||
|
data.first, data.last, data.age, data.sport
|
||||||
|
))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly there is the `get_form` method. This almost identical to the ActionForm
|
||||||
|
version. In fact, if you do everything right, you wont even notice a
|
||||||
|
difference. Under the hood, this sends your FormToolData struct directly
|
||||||
|
by calling the server function, whereas the `ActionForm` version will try
|
||||||
|
to construct your type using the
|
||||||
|
[`FromFormData`](https://docs.rs/leptos_router/latest/leptos_router/trait.FromFormData.html)
|
||||||
|
trait. Using the `get_form` method instead will allow you to name the inputs
|
||||||
|
whatever you want (though you should try to name them correctly anyway to
|
||||||
|
support progressive enhancement) and it will still work. The example is the
|
||||||
|
same as above, just replace `get_action_form` with `get_form`.
|
||||||
+24
-10
@@ -23,6 +23,13 @@
|
|||||||
border-bottom: 2px solid #374151;
|
border-bottom: 2px solid #374151;
|
||||||
margin-bottom: 2rem;
|
margin-bottom: 2rem;
|
||||||
grid-column: span 12;
|
grid-column: span 12;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form_group {
|
||||||
|
background-color: #0295f744;
|
||||||
|
border-radius: 25px;
|
||||||
|
padding: 20px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form_label {
|
.form_label {
|
||||||
@@ -40,6 +47,7 @@
|
|||||||
|
|
||||||
.form_input {
|
.form_input {
|
||||||
display: block;
|
display: block;
|
||||||
|
box-sizing: border-box;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: #f7fafc;
|
background-color: #f7fafc;
|
||||||
border-width: 2px;
|
border-width: 2px;
|
||||||
@@ -68,7 +76,7 @@
|
|||||||
color: #ef4444;
|
color: #ef4444;
|
||||||
}
|
}
|
||||||
|
|
||||||
.form_submit {
|
.form_button {
|
||||||
display: block;
|
display: block;
|
||||||
outline: none;
|
outline: none;
|
||||||
border: none;
|
border: none;
|
||||||
@@ -78,36 +86,42 @@
|
|||||||
color: #fff;
|
color: #fff;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
margin: 0 auto;
|
margin: 0 auto auto auto;
|
||||||
padding: 0.75rem 1.25rem;
|
padding: 0.75rem 1.25rem;
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
min-height: 40px;
|
min-height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.form_submit {
|
||||||
|
@extend .form_button;
|
||||||
|
@extend .col-span-full;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
|
||||||
.form_submit:hover {
|
.form_submit:hover {
|
||||||
background-color: #005fb3;
|
background-color: #005fb3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// column widths
|
// column widths
|
||||||
.w-full {
|
.col-span-full {
|
||||||
grid-column: 1 / -1;
|
grid-column: 1 / -1;
|
||||||
}
|
}
|
||||||
.w-12 {
|
.col-span-12 {
|
||||||
grid-column: span 12 / span 12;
|
grid-column: span 12 / span 12;
|
||||||
}
|
}
|
||||||
.w-6 {
|
.col-span-6 {
|
||||||
grid-column: span 6 / span 6;
|
grid-column: span 6 / span 6;
|
||||||
}
|
}
|
||||||
.w-4 {
|
.col-span-4 {
|
||||||
grid-column: span 4 / span 4;
|
grid-column: span 4 / span 4;
|
||||||
}
|
}
|
||||||
.w-3 {
|
.col-span-3 {
|
||||||
grid-column: span 3 / span 3;
|
grid-column: span 3 / span 3;
|
||||||
}
|
}
|
||||||
.w-2 {
|
.col-span-2 {
|
||||||
grid-column: span 2 / span 2;
|
grid-column: span 2 / span 2;
|
||||||
}
|
}
|
||||||
.w-1 {
|
.col-span-1 {
|
||||||
grid-column: span 1 / span 1;
|
grid-column: span 1 / span 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,120 @@
|
|||||||
|
use super::{BuilderCxFn, BuilderFn, ControlRenderData, ShowWhenFn};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
use leptos::view;
|
||||||
|
use leptos::RwSignal;
|
||||||
|
use leptos::Show;
|
||||||
|
use leptos::Signal;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use web_sys::MouseEvent;
|
||||||
|
|
||||||
|
type ButtonAction<FD> = dyn Fn(MouseEvent, RwSignal<FD>) + 'static;
|
||||||
|
|
||||||
|
/// Data used for the button control.
|
||||||
|
pub struct ButtonData<FD: FormToolData> {
|
||||||
|
pub text: String,
|
||||||
|
pub action: Option<Rc<ButtonAction<FD>>>,
|
||||||
|
}
|
||||||
|
impl<FD: FormToolData> Default for ButtonData<FD> {
|
||||||
|
fn default() -> Self {
|
||||||
|
ButtonData {
|
||||||
|
text: String::default(),
|
||||||
|
action: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl<FD: FormToolData> Clone for ButtonData<FD> {
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
ButtonData {
|
||||||
|
text: self.text.clone(),
|
||||||
|
action: self.action.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
|
/// Builds a button and adds it to the form.
|
||||||
|
pub fn button(self, builder: impl BuilderFn<ButtonBuilder<FD>>) -> Self {
|
||||||
|
let button_builder = ButtonBuilder::new();
|
||||||
|
let control = builder(button_builder);
|
||||||
|
self.button_helper(control)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a button using the form's context and adds it to the form.
|
||||||
|
pub fn button_cx(self, builder: impl BuilderCxFn<ButtonBuilder<FD>, FD::Context>) -> Self {
|
||||||
|
let button_builder = ButtonBuilder::new();
|
||||||
|
let control = builder(button_builder, self.cx.clone());
|
||||||
|
self.button_helper(control)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The common functionality for adding a button.
|
||||||
|
fn button_helper(mut self, control: ButtonBuilder<FD>) -> Self {
|
||||||
|
let render_data = ControlRenderData {
|
||||||
|
data: control.data,
|
||||||
|
styles: control.styles,
|
||||||
|
};
|
||||||
|
let show_when = control.show_when;
|
||||||
|
|
||||||
|
let cx = self.cx.clone();
|
||||||
|
let render_fn = move |fs: Rc<FD::Style>, fd: RwSignal<FD>| {
|
||||||
|
let render_data = Rc::new(render_data);
|
||||||
|
|
||||||
|
let view = move || fs.clone().button(render_data.clone(), fd);
|
||||||
|
let view = match show_when {
|
||||||
|
Some(when) => {
|
||||||
|
let when = move || when(fd.into(), cx.clone());
|
||||||
|
view! { <Show when=when>{view.clone()}</Show> }
|
||||||
|
}
|
||||||
|
None => view(),
|
||||||
|
};
|
||||||
|
(view, None)
|
||||||
|
};
|
||||||
|
self.render_fns.push(Box::new(render_fn));
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The struct that allows you to specify the attributes of the button.
|
||||||
|
pub struct ButtonBuilder<FD: FormToolData> {
|
||||||
|
pub(crate) styles: Vec<<FD::Style as FormStyle>::StylingAttributes>,
|
||||||
|
pub(crate) data: ButtonData<FD>,
|
||||||
|
pub(crate) show_when: Option<Box<dyn ShowWhenFn<FD, FD::Context>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> ButtonBuilder<FD> {
|
||||||
|
/// Creates a new [`ButtonBuilder`].
|
||||||
|
fn new() -> Self {
|
||||||
|
ButtonBuilder {
|
||||||
|
styles: Vec::default(),
|
||||||
|
data: ButtonData::default(),
|
||||||
|
show_when: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the function to decide when to render the control.
|
||||||
|
pub fn show_when(
|
||||||
|
mut self,
|
||||||
|
when: impl Fn(Signal<FD>, Rc<FD::Context>) -> bool + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.show_when = Some(Box::new(when));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a styling attribute to the button.
|
||||||
|
pub fn style(mut self, style: <FD::Style as FormStyle>::StylingAttributes) -> Self {
|
||||||
|
self.styles.push(style);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text of the button.
|
||||||
|
pub fn text(mut self, text: impl ToString) -> Self {
|
||||||
|
self.data.text = text.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the action that is preformed when the button is clicked.
|
||||||
|
pub fn action(mut self, action: impl Fn(MouseEvent, RwSignal<FD>) + 'static) -> Self {
|
||||||
|
self.data.action = Some(Rc::new(action));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
use super::{BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
use leptos::{Signal, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Data used for the checkbox control.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
|
pub struct CheckboxData {
|
||||||
|
pub name: String,
|
||||||
|
pub label: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControlData for CheckboxData {
|
||||||
|
type ReturnType = bool;
|
||||||
|
|
||||||
|
fn build_control<FS: FormStyle>(
|
||||||
|
fs: &FS,
|
||||||
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
|
value_getter: Signal<Self::ReturnType>,
|
||||||
|
value_setter: Rc<dyn Fn(Self::ReturnType)>,
|
||||||
|
_validation_state: Signal<Result<(), String>>,
|
||||||
|
) -> View {
|
||||||
|
fs.checkbox(control, value_getter, value_setter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
|
/// Builds a checkbox and adds it to the form.
|
||||||
|
pub fn checkbox<FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderFn<ControlBuilder<FD, CheckboxData, FDT>>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a checkbox using the form's context and adds it to the form.
|
||||||
|
pub fn checkbox_cx<FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<ControlBuilder<FD, CheckboxData, FDT>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control_cx(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData, FDT> ControlBuilder<FD, CheckboxData, FDT> {
|
||||||
|
/// Sets the name of the checkbox.
|
||||||
|
///
|
||||||
|
/// This is used for the html element's "name" attribute.
|
||||||
|
/// In forms, the name attribute is the key that the data is sent
|
||||||
|
/// with.
|
||||||
|
///
|
||||||
|
/// For checkbox controls, the value "checked" is sent or no key value
|
||||||
|
/// pair is sent.
|
||||||
|
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||||
|
self.data.name = control_name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text of the checkbox's label.
|
||||||
|
pub fn labeled(mut self, label: impl ToString) -> Self {
|
||||||
|
self.data.label = Some(label.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use super::{BuilderCxFn, BuilderFn, ControlBuilder, ControlData};
|
||||||
|
use crate::{FormBuilder, FormToolData};
|
||||||
|
|
||||||
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
|
/// Builds a custom component and adds it to the form.
|
||||||
|
pub fn custom<CC: ControlData, FDT: Clone + PartialEq + 'static>(
|
||||||
|
mut self,
|
||||||
|
control_data: CC,
|
||||||
|
builder: impl BuilderFn<ControlBuilder<FD, CC, FDT>>,
|
||||||
|
) -> Self {
|
||||||
|
let control_builder = ControlBuilder::new(control_data);
|
||||||
|
let control = builder(control_builder);
|
||||||
|
self.add_control(control);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a custom component using the form's context and adds it to the
|
||||||
|
/// form.
|
||||||
|
pub fn custom_cx<CC: ControlData, FDT: Clone + PartialEq + 'static>(
|
||||||
|
mut self,
|
||||||
|
control_data: CC,
|
||||||
|
builder: impl BuilderCxFn<ControlBuilder<FD, CC, FDT>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
let control_builder = ControlBuilder::new(control_data);
|
||||||
|
let control = builder(control_builder, self.cx.clone());
|
||||||
|
self.add_control(control);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a custom component, starting with the default
|
||||||
|
/// CustomControlData, and adds it to the form.
|
||||||
|
pub fn custom_default<CC: Default + ControlData, FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderFn<ControlBuilder<FD, CC, FDT>>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a custom component, starting with the default
|
||||||
|
/// CustomControlData using the form's context, and adds it to the form.
|
||||||
|
pub fn custom_default_cx<CC: Default + ControlData, FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<ControlBuilder<FD, CC, FDT>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control_cx(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use super::{ControlRenderData, ValidationCb};
|
||||||
|
use crate::styles::FormStyle;
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder};
|
||||||
|
use leptos::{CollectView, RwSignal};
|
||||||
|
|
||||||
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
|
/// Creates a form group.
|
||||||
|
///
|
||||||
|
/// This creates a subsection of the form that controls can be added to
|
||||||
|
/// like a normal form.
|
||||||
|
pub fn group(mut self, builder: impl Fn(FormBuilder<FD>) -> FormBuilder<FD>) -> Self {
|
||||||
|
let mut group_builder = FormBuilder::new_group(self.cx.clone());
|
||||||
|
group_builder = builder(group_builder);
|
||||||
|
|
||||||
|
for validation in group_builder.validations {
|
||||||
|
self.validations.push(validation);
|
||||||
|
}
|
||||||
|
|
||||||
|
let render_fn = move |fs: Rc<FD::Style>, fd: RwSignal<FD>| {
|
||||||
|
let (views, validation_cbs): (Vec<_>, Vec<_>) = group_builder
|
||||||
|
.render_fns
|
||||||
|
.into_iter()
|
||||||
|
.map(|r_fn| r_fn(fs.clone(), fd))
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
let render_data = Rc::new(ControlRenderData {
|
||||||
|
data: views.collect_view(),
|
||||||
|
styles: group_builder.styles,
|
||||||
|
});
|
||||||
|
|
||||||
|
let view = fs.group(render_data.clone());
|
||||||
|
|
||||||
|
let validation_cb = move || {
|
||||||
|
let mut success = true;
|
||||||
|
for validation in validation_cbs.iter().flatten() {
|
||||||
|
if !validation() {
|
||||||
|
success = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
success
|
||||||
|
};
|
||||||
|
(view, Some(Box::new(validation_cb) as Box<dyn ValidationCb>))
|
||||||
|
};
|
||||||
|
|
||||||
|
self.render_fns.push(Box::new(render_fn));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
+32
-17
@@ -1,34 +1,49 @@
|
|||||||
use leptos::View;
|
use super::{BuilderCxFn, BuilderFn, ControlRenderData, VanityControlBuilder, VanityControlData};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
use leptos::{MaybeSignal, Signal, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
|
/// Data used for the heading control.
|
||||||
use crate::{
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
form::{FormBuilder, FormData},
|
|
||||||
styles::FormStyle,
|
|
||||||
};
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
|
||||||
pub struct HeadingData {
|
pub struct HeadingData {
|
||||||
pub(crate) title: String,
|
pub title: MaybeSignal<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VanityControlData for HeadingData {
|
impl VanityControlData for HeadingData {
|
||||||
fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View {
|
fn build_control<FS: FormStyle>(
|
||||||
|
fs: &FS,
|
||||||
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
|
_value_getter: Option<leptos::prelude::Signal<String>>,
|
||||||
|
) -> View {
|
||||||
fs.heading(control)
|
fs.heading(control)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
pub fn heading(
|
/// Builds a heading and adds it to the form.
|
||||||
self,
|
pub fn heading(self, builder: impl BuilderFn<VanityControlBuilder<FD, HeadingData>>) -> Self {
|
||||||
builder: impl Fn(VanityControlBuilder<FS, HeadingData>) -> VanityControlBuilder<FS, HeadingData>,
|
|
||||||
) -> Self {
|
|
||||||
self.new_vanity(builder)
|
self.new_vanity(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a hehading using the form's context and adds it to the form.
|
||||||
|
pub fn heading_cx(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<VanityControlBuilder<FD, HeadingData>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_vanity_cx(builder)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FS: FormStyle> VanityControlBuilder<FS, HeadingData> {
|
impl<FD: FormToolData> VanityControlBuilder<FD, HeadingData> {
|
||||||
|
/// Sets the title of this heading.
|
||||||
pub fn title(mut self, title: impl ToString) -> Self {
|
pub fn title(mut self, title: impl ToString) -> Self {
|
||||||
self.data.title = title.to_string();
|
self.data.title = MaybeSignal::Static(title.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the title of this heading to a signal.
|
||||||
|
pub fn title_signal(mut self, title: Signal<String>) -> Self {
|
||||||
|
self.data.title = MaybeSignal::Dynamic(title);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
use super::{
|
||||||
|
BuilderCxFn, BuilderFn, ControlRenderData, GetterVanityControlData, VanityControlBuilder,
|
||||||
|
VanityControlData,
|
||||||
|
};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
use leptos::{Signal, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Data used for the hidden control.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
|
pub struct HiddenData {
|
||||||
|
pub name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VanityControlData for HiddenData {
|
||||||
|
fn build_control<FS: FormStyle>(
|
||||||
|
fs: &FS,
|
||||||
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
|
value_getter: Option<Signal<String>>,
|
||||||
|
) -> View {
|
||||||
|
fs.hidden(control, value_getter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl GetterVanityControlData for HiddenData {}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
|
/// Builds a hidden form control and adds it to the form.
|
||||||
|
///
|
||||||
|
/// This will be an input in the html form allowing you to insert some
|
||||||
|
/// data the you might not want the user modifying.
|
||||||
|
pub fn hidden(self, builder: impl BuilderFn<VanityControlBuilder<FD, HiddenData>>) -> Self {
|
||||||
|
self.new_vanity(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a hidden form control using the form's context and adds it to
|
||||||
|
/// the form.
|
||||||
|
///
|
||||||
|
/// This will be an input in the html form allowing you to insert some
|
||||||
|
/// data the you might not want the user modifying.
|
||||||
|
pub fn hidden_cx(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<VanityControlBuilder<FD, HiddenData>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_vanity_cx(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> VanityControlBuilder<FD, HiddenData> {
|
||||||
|
/// Sets the name of the hidden control.
|
||||||
|
///
|
||||||
|
/// This is used for the html element's "name" attribute.
|
||||||
|
/// In forms, the name attribute is the key that the data is sent
|
||||||
|
/// with.
|
||||||
|
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||||
|
self.data.name = control_name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
+290
-75
@@ -1,154 +1,288 @@
|
|||||||
use std::{fmt::Display, rc::Rc};
|
use crate::{form::FormToolData, styles::FormStyle};
|
||||||
|
use leptos::{RwSignal, Signal, View};
|
||||||
use crate::{form::FormData, styles::FormStyle};
|
use std::{fmt::Display, rc::Rc, str::FromStr};
|
||||||
use leptos::{Signal, View};
|
|
||||||
|
|
||||||
|
pub mod button;
|
||||||
|
pub mod checkbox;
|
||||||
|
pub mod custom;
|
||||||
|
pub mod group;
|
||||||
pub mod heading;
|
pub mod heading;
|
||||||
|
pub mod hidden;
|
||||||
|
pub mod output;
|
||||||
|
pub mod radio_buttons;
|
||||||
pub mod select;
|
pub mod select;
|
||||||
|
pub mod slider;
|
||||||
|
pub mod spacer;
|
||||||
|
pub mod stepper;
|
||||||
pub mod submit;
|
pub mod submit;
|
||||||
pub mod text_area;
|
pub mod text_area;
|
||||||
pub mod text_input;
|
pub mod text_input;
|
||||||
|
|
||||||
// TODO: change to returning an Option<String>
|
pub trait BuilderFn<B>: Fn(B) -> B {}
|
||||||
pub trait ValidationFn<FDT>: Fn(&FDT) -> Result<(), String> + 'static {}
|
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 ParseFn<CR, FDT>: Fn(CR) -> Result<FDT, String> + 'static {}
|
||||||
pub trait UnparseFn<CR, FDT>: Fn(&FDT) -> CR + 'static {}
|
pub trait UnparseFn<CR, FDT>: Fn(FDT) -> CR + 'static {}
|
||||||
pub trait FieldFn<FD, FDT>: Fn(&mut FD) -> &mut FDT + 'static {}
|
pub trait FieldGetter<FD, FDT>: Fn(FD) -> FDT + 'static {}
|
||||||
|
pub trait FieldSetter<FD, FDT>: Fn(&mut FD, FDT) + 'static {}
|
||||||
// implement the traits for all valid types
|
pub trait ShowWhenFn<FD: 'static, CX>: Fn(Signal<FD>, Rc<CX>) -> bool + 'static {}
|
||||||
impl<FDT, T> ValidationFn<FDT> for T where T: Fn(&FDT) -> Result<(), String> + 'static {}
|
pub trait RenderFn<FS, FD: 'static>:
|
||||||
impl<CR, FDT, F> ParseFn<CR, FDT> for F where F: Fn(CR) -> Result<FDT, String> + 'static {}
|
FnOnce(Rc<FS>, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + 'static
|
||||||
impl<CR, FDT, F> UnparseFn<CR, FDT> for F where F: Fn(&FDT) -> CR + 'static {}
|
{
|
||||||
impl<FD, FDT, F> FieldFn<FD, FDT> for F where F: Fn(&mut FD) -> &mut FDT + 'static {}
|
|
||||||
|
|
||||||
pub trait VanityControlData: 'static {
|
|
||||||
fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: what if the `FS` parameter was extracted to the trait level.
|
// implement the traits for all valid types
|
||||||
// Then this would be trait object able.
|
impl<B, T> BuilderFn<B> for T where T: Fn(B) -> B {}
|
||||||
// If this is trait object able, then we can store this in a list,
|
impl<B, CX, T> BuilderCxFn<B, CX> for T where T: Fn(B, Rc<CX>) -> B {}
|
||||||
// and differ rendering the control until we actually need to form view.
|
impl<FDT, T> ValidationFn<FDT> for T where T: Fn(&FDT) -> Result<(), String> + 'static {}
|
||||||
// Which, in turn, would get rid of the Form Builder as an enum (which was
|
impl<T> ValidationCb for T where T: Fn() -> bool + 'static {}
|
||||||
// done to avoid rendering on the server).
|
impl<CR, FDT, F> ParseFn<CR, FDT> for F where F: Fn(CR) -> Result<FDT, String> + 'static {}
|
||||||
pub trait ControlData: 'static {
|
impl<CR, FDT, F> UnparseFn<CR, FDT> for F where F: Fn(FDT) -> CR + 'static {}
|
||||||
type ReturnType: Clone;
|
impl<FD, FDT, F> FieldGetter<FD, FDT> for F where F: Fn(FD) -> FDT + 'static {}
|
||||||
|
impl<FD, FDT, F> FieldSetter<FD, FDT> for F where F: Fn(&mut FD, FDT) + 'static {}
|
||||||
|
impl<FD: 'static, CX, F> ShowWhenFn<FD, CX> for F where F: Fn(Signal<FD>, Rc<CX>) -> bool + 'static {}
|
||||||
|
impl<FS, FD: 'static, F> RenderFn<FS, FD> for F where
|
||||||
|
F: FnOnce(Rc<FS>, RwSignal<FD>) -> (View, Option<Box<dyn ValidationCb>>) + 'static
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: this should also return a getter for the data
|
/// A trait for the data needed to render an read-only control.
|
||||||
|
pub trait VanityControlData: 'static {
|
||||||
|
/// Builds the control, returning the [`View`] that was built.
|
||||||
fn build_control<FS: FormStyle>(
|
fn build_control<FS: FormStyle>(
|
||||||
fs: &FS,
|
fs: &FS,
|
||||||
control: ControlRenderData<FS, Self>,
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
|
value_getter: Option<Signal<String>>,
|
||||||
|
) -> View;
|
||||||
|
}
|
||||||
|
pub trait GetterVanityControlData: VanityControlData {}
|
||||||
|
|
||||||
|
/// A trait for the data needed to render an interactive control.
|
||||||
|
pub trait ControlData: 'static {
|
||||||
|
/// This is the data type returned by this control.
|
||||||
|
type ReturnType: Clone;
|
||||||
|
|
||||||
|
/// Builds the control, returning the [`View`] that was built.
|
||||||
|
fn build_control<FS: FormStyle>(
|
||||||
|
fs: &FS,
|
||||||
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
value_getter: Signal<Self::ReturnType>,
|
value_getter: Signal<Self::ReturnType>,
|
||||||
value_setter: Box<dyn Fn(Self::ReturnType)>,
|
value_setter: Rc<dyn Fn(Self::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> View;
|
) -> View;
|
||||||
}
|
}
|
||||||
|
pub trait ValidatedControlData: ControlData {}
|
||||||
|
|
||||||
|
/// The data needed to render a interactive control of type `C`.
|
||||||
pub struct ControlRenderData<FS: FormStyle + ?Sized, C: ?Sized> {
|
pub struct ControlRenderData<FS: FormStyle + ?Sized, C: ?Sized> {
|
||||||
pub data: Box<C>,
|
pub styles: Vec<FS::StylingAttributes>,
|
||||||
pub style: Vec<FS::StylingAttributes>,
|
pub data: C,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct VanityControlBuilder<FS: FormStyle, C: VanityControlData> {
|
/// The data needed to render a read-only control of type `C`.
|
||||||
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) data: C,
|
||||||
|
pub(crate) getter: Option<Rc<dyn FieldGetter<FD, String>>>,
|
||||||
|
pub(crate) show_when: Option<Box<dyn ShowWhenFn<FD, FD::Context>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FS: FormStyle, C: VanityControlData> VanityControlBuilder<FS, C> {
|
pub(crate) struct BuiltVanityControlData<FD: FormToolData, C: VanityControlData> {
|
||||||
|
pub(crate) render_data: ControlRenderData<FD::Style, C>,
|
||||||
|
pub(crate) getter: Option<Rc<dyn FieldGetter<FD, String>>>,
|
||||||
|
pub(crate) show_when: Option<Box<dyn ShowWhenFn<FD, FD::Context>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData, C: VanityControlData> VanityControlBuilder<FD, C> {
|
||||||
|
/// Creates a new [`VanityControlBuilder`] with the given [`VanityControlData`].
|
||||||
pub(crate) fn new(data: C) -> Self {
|
pub(crate) fn new(data: C) -> Self {
|
||||||
VanityControlBuilder {
|
VanityControlBuilder {
|
||||||
data,
|
data,
|
||||||
style_attributes: Vec::new(),
|
style_attributes: Vec::new(),
|
||||||
|
getter: None,
|
||||||
|
show_when: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build(self) -> ControlRenderData<FS, C> {
|
/// Builds the builder into the data needed to render the control.
|
||||||
ControlRenderData {
|
pub(crate) fn build(self) -> BuiltVanityControlData<FD, C> {
|
||||||
data: Box::new(self.data),
|
BuiltVanityControlData {
|
||||||
style: self.style_attributes,
|
render_data: ControlRenderData {
|
||||||
}
|
data: self.data,
|
||||||
|
styles: self.style_attributes,
|
||||||
|
},
|
||||||
|
getter: self.getter,
|
||||||
|
show_when: self.show_when,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the function to decide when to render the control.
|
||||||
|
///
|
||||||
|
/// Validations for components that are not shown DO NOT run.
|
||||||
|
pub fn show_when(
|
||||||
|
mut self,
|
||||||
|
when: impl Fn(Signal<FD>, Rc<FD::Context>) -> bool + 'static,
|
||||||
|
) -> Self {
|
||||||
|
self.show_when = Some(Box::new(when));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a styling attribute to this control.
|
||||||
|
pub fn style(mut self, attribute: <FD::Style as FormStyle>::StylingAttributes) -> Self {
|
||||||
|
self.style_attributes.push(attribute);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData, C: GetterVanityControlData> VanityControlBuilder<FD, C> {
|
||||||
|
/// Sets the getter function.
|
||||||
|
///
|
||||||
|
/// This function can get a string from the form data to be displayed
|
||||||
|
///
|
||||||
|
/// Setting this getter field is NOT required for vanity controls like this one.
|
||||||
|
pub fn getter(mut self, getter: impl FieldGetter<FD, String>) -> Self {
|
||||||
|
self.getter = Some(Rc::new(getter));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The possibilities for errors when building a control.
|
||||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||||
pub enum ControlBuildError {
|
pub enum ControlBuildError {
|
||||||
/// The field that this control belongs to is not specified.
|
/// The getter field was not specified.
|
||||||
MissingField,
|
MissingGetter,
|
||||||
|
/// The setter field was not specified.
|
||||||
|
MissingSetter,
|
||||||
|
/// The parse function was not specified.
|
||||||
|
MissingParseFn,
|
||||||
|
/// The unparse function was not specified.
|
||||||
|
MissingUnParseFn,
|
||||||
}
|
}
|
||||||
impl Display for ControlBuildError {
|
impl Display for ControlBuildError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let message = match self {
|
let message = match self {
|
||||||
ControlBuildError::MissingField => "you must specify what field this control is for",
|
ControlBuildError::MissingGetter => "missing getter function",
|
||||||
|
ControlBuildError::MissingSetter => "missing setter function",
|
||||||
|
ControlBuildError::MissingParseFn => "missing parse function",
|
||||||
|
ControlBuildError::MissingUnParseFn => "missing unparse function",
|
||||||
};
|
};
|
||||||
write!(f, "{}", message)
|
write!(f, "{}", message)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) struct BuiltControlData<FD: FormData, FS: FormStyle, C: ControlData, FDT> {
|
/// The data returned fomr a control's build function.
|
||||||
pub(crate) render_data: ControlRenderData<FS, C>,
|
pub(crate) struct BuiltControlData<FD: FormToolData, C: ControlData, FDT> {
|
||||||
pub(crate) field_fn: Rc<dyn FieldFn<FD, 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) parse_fn: Box<dyn ParseFn<C::ReturnType, FDT>>,
|
||||||
pub(crate) unparse_fn: Box<dyn UnparseFn<C::ReturnType, FDT>>,
|
pub(crate) unparse_fn: Box<dyn UnparseFn<C::ReturnType, FDT>>,
|
||||||
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
||||||
|
pub(crate) show_when: Option<Rc<dyn ShowWhenFn<FD, FD::Context>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct ControlBuilder<FD: FormData, FS: FormStyle, C: ControlData, FDT> {
|
/// A builder for a interactive control.
|
||||||
pub(crate) field_fn: Option<Rc<dyn FieldFn<FD, 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) parse_fn: Option<Box<dyn ParseFn<C::ReturnType, FDT>>>,
|
||||||
pub(crate) unparse_fn: Option<Box<dyn UnparseFn<C::ReturnType, FDT>>>,
|
pub(crate) unparse_fn: Option<Box<dyn UnparseFn<C::ReturnType, FDT>>>,
|
||||||
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
pub(crate) validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
||||||
pub(crate) style_attributes: Vec<FS::StylingAttributes>,
|
pub(crate) style_attributes: Vec<<FD::Style as FormStyle>::StylingAttributes>,
|
||||||
pub(crate) data: C,
|
pub(crate) show_when: Option<Rc<dyn ShowWhenFn<FD, FD::Context>>>,
|
||||||
|
pub data: C,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C, FDT> {
|
impl<FD: FormToolData, C: ControlData, FDT> ControlBuilder<FD, C, FDT> {
|
||||||
|
/// Creates a new [`ControlBuilder`] with the given [`ControlData`].
|
||||||
pub(crate) fn new(data: C) -> Self {
|
pub(crate) fn new(data: C) -> Self {
|
||||||
ControlBuilder {
|
ControlBuilder {
|
||||||
data,
|
data,
|
||||||
field_fn: None,
|
getter: None,
|
||||||
|
setter: None,
|
||||||
parse_fn: None,
|
parse_fn: None,
|
||||||
unparse_fn: None,
|
unparse_fn: None,
|
||||||
validation_fn: None,
|
validation_fn: None,
|
||||||
style_attributes: Vec::new(),
|
style_attributes: Vec::new(),
|
||||||
|
show_when: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn build(self) -> Result<BuiltControlData<FD, FS, C, FDT>, ControlBuildError> {
|
/// Builds the builder into the data needed to render the control.
|
||||||
// either all 3 should be specified or none of them due to the possible `field` for `field_with` calls.
|
///
|
||||||
let (field_fn, parse_fn, unparse_fn) = match (self.field_fn, self.parse_fn, self.unparse_fn)
|
/// This fails if a required field was not specified.
|
||||||
{
|
pub(crate) fn build(self) -> Result<BuiltControlData<FD, C, FDT>, ControlBuildError> {
|
||||||
(Some(f), Some(p), Some(u)) => (f, p, u),
|
let getter = match self.getter {
|
||||||
_ => return Err(ControlBuildError::MissingField),
|
Some(getter) => getter,
|
||||||
|
None => return Err(ControlBuildError::MissingGetter),
|
||||||
|
};
|
||||||
|
let setter = match self.setter {
|
||||||
|
Some(setter) => setter,
|
||||||
|
None => return Err(ControlBuildError::MissingSetter),
|
||||||
|
};
|
||||||
|
let parse_fn = match self.parse_fn {
|
||||||
|
Some(parse_fn) => parse_fn,
|
||||||
|
None => return Err(ControlBuildError::MissingParseFn),
|
||||||
|
};
|
||||||
|
let unparse_fn = match self.unparse_fn {
|
||||||
|
Some(unparse_fn) => unparse_fn,
|
||||||
|
None => return Err(ControlBuildError::MissingUnParseFn),
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(BuiltControlData {
|
Ok(BuiltControlData {
|
||||||
render_data: ControlRenderData {
|
render_data: ControlRenderData {
|
||||||
data: Box::new(self.data),
|
data: self.data,
|
||||||
style: self.style_attributes,
|
styles: self.style_attributes,
|
||||||
},
|
},
|
||||||
field_fn,
|
getter,
|
||||||
|
setter,
|
||||||
parse_fn,
|
parse_fn,
|
||||||
unparse_fn,
|
unparse_fn,
|
||||||
validation_fn: self.validation_fn,
|
validation_fn: self.validation_fn,
|
||||||
|
show_when: self.show_when,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: add method that automatically does the parse and unparse using
|
/// Sets the function to decide when to render the control.
|
||||||
// TryInto<C::ReturnValue> and TryFrom<C::ReturnVlaue>
|
///
|
||||||
pub fn field_with(
|
/// Validations for components that are not shown DO NOT run.
|
||||||
|
pub fn show_when(
|
||||||
mut self,
|
mut self,
|
||||||
field_fn: impl FieldFn<FD, FDT>,
|
when: impl Fn(Signal<FD>, Rc<FD::Context>) -> bool + 'static,
|
||||||
parse_fn: impl ParseFn<C::ReturnType, FDT>,
|
|
||||||
unparse_fn: impl UnparseFn<C::ReturnType, FDT>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.field_fn = Some(Rc::new(field_fn));
|
self.show_when = Some(Rc::new(when));
|
||||||
self.parse_fn = Some(Box::new(parse_fn));
|
|
||||||
self.unparse_fn = Some(Box::new(unparse_fn));
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Overrides the field's parse functions with the ones given.
|
/// Sets the getter function.
|
||||||
pub fn parse_fns(
|
///
|
||||||
|
/// This function should get the field from the form data
|
||||||
|
/// for use in the form field.
|
||||||
|
///
|
||||||
|
/// Setting this getter field is required.
|
||||||
|
pub fn getter(mut self, getter: impl FieldGetter<FD, FDT>) -> Self {
|
||||||
|
self.getter = Some(Rc::new(getter));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the setter function.
|
||||||
|
///
|
||||||
|
/// This function should get the field from the form data
|
||||||
|
/// for use in the form field.
|
||||||
|
///
|
||||||
|
/// Setting this setter field is required.
|
||||||
|
pub fn setter(mut self, setter: impl FieldSetter<FD, FDT>) -> Self {
|
||||||
|
self.setter = Some(Rc::new(setter));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the parse functions to the ones given.
|
||||||
|
///
|
||||||
|
/// The parse and unparse functions define how to turn what the user
|
||||||
|
/// types in the form into what is stored in the form data struct and
|
||||||
|
/// vice versa.
|
||||||
|
pub fn parse_custom(
|
||||||
mut self,
|
mut self,
|
||||||
parse_fn: impl ParseFn<C::ReturnType, FDT>,
|
parse_fn: impl ParseFn<C::ReturnType, FDT>,
|
||||||
unparse_fn: impl UnparseFn<C::ReturnType, FDT>,
|
unparse_fn: impl UnparseFn<C::ReturnType, FDT>,
|
||||||
@@ -158,7 +292,93 @@ impl<FD: FormData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C,
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Adds a styling attribute to this control.
|
||||||
|
pub fn style(mut self, attribute: <FD::Style as FormStyle>::StylingAttributes) -> Self {
|
||||||
|
self.style_attributes.push(attribute);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD, C, FDT> ControlBuilder<FD, C, FDT>
|
||||||
|
where
|
||||||
|
FD: FormToolData,
|
||||||
|
C: ControlData,
|
||||||
|
FDT: TryFrom<<C as ControlData>::ReturnType>,
|
||||||
|
<FDT as TryFrom<<C as ControlData>::ReturnType>>::Error: ToString,
|
||||||
|
<C as ControlData>::ReturnType: From<FDT>,
|
||||||
|
{
|
||||||
|
/// Sets the parse functions to use the [`TryFrom`] and [`From`] traits
|
||||||
|
/// for parsing and unparsing respectively.
|
||||||
|
///
|
||||||
|
/// The parse and unparse functions define how to turn what the user
|
||||||
|
/// types in the form into what is stored in the form data struct and
|
||||||
|
/// vice versa.
|
||||||
|
pub fn parse_from(mut self) -> Self {
|
||||||
|
self.parse_fn = Some(Box::new(|control_return_value| {
|
||||||
|
FDT::try_from(control_return_value).map_err(|e| e.to_string())
|
||||||
|
}));
|
||||||
|
self.unparse_fn = Some(Box::new(|field| {
|
||||||
|
<C as ControlData>::ReturnType::from(field)
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD, C, FDT> ControlBuilder<FD, C, FDT>
|
||||||
|
where
|
||||||
|
FD: FormToolData,
|
||||||
|
C: ControlData<ReturnType = String>,
|
||||||
|
FDT: FromStr + ToString,
|
||||||
|
<FDT as FromStr>::Err: ToString,
|
||||||
|
{
|
||||||
|
/// Sets the parse functions to use the [`FromStr`] [`ToString`] and traits
|
||||||
|
/// for parsing and unparsing respectively. To trim the string before
|
||||||
|
/// parsing, see [`parse_trimmed`](Self::parse_trimmed)().
|
||||||
|
///
|
||||||
|
/// The parse and unparse functions define how to turn what the user
|
||||||
|
/// types in the form into what is stored in the form data struct and
|
||||||
|
/// vice versa.
|
||||||
|
pub fn parse_string(mut self) -> Self {
|
||||||
|
self.parse_fn = Some(Box::new(|control_return_value| {
|
||||||
|
control_return_value
|
||||||
|
.parse::<FDT>()
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}));
|
||||||
|
self.unparse_fn = Some(Box::new(|field| field.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the parse functions to use the [`FromStr`] [`ToString`] and traits
|
||||||
|
/// for parsing and unparsing respectively, similar to
|
||||||
|
/// [`parse_string`](Self::parse_string)().
|
||||||
|
/// However, this method trims the string before parsing.
|
||||||
|
///
|
||||||
|
/// The parse and unparse functions define how to turn what the user
|
||||||
|
/// types in the form into what is stored in the form data struct and
|
||||||
|
/// vice versa.
|
||||||
|
pub fn parse_trimmed(mut self) -> Self {
|
||||||
|
self.parse_fn = Some(Box::new(|control_return_value| {
|
||||||
|
control_return_value
|
||||||
|
.trim()
|
||||||
|
.parse::<FDT>()
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
}));
|
||||||
|
self.unparse_fn = Some(Box::new(|field| field.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData, C: ValidatedControlData, FDT> ControlBuilder<FD, C, FDT> {
|
||||||
/// Sets the validation function for this control
|
/// Sets the validation function for this control
|
||||||
|
///
|
||||||
|
/// This allows you to check if the parsed value is a valid value.
|
||||||
|
///
|
||||||
|
/// You are given the entire [`FormToolData`] struct, but you should only
|
||||||
|
/// validate the field you are creating. You can use the other fields in
|
||||||
|
/// the struct as context.
|
||||||
|
///
|
||||||
|
/// Ex. You have a month and a day field in a form. You use the month
|
||||||
|
/// field to help ensure that the day is a valid day of that month.
|
||||||
pub fn validation_fn(
|
pub fn validation_fn(
|
||||||
mut self,
|
mut self,
|
||||||
validation_fn: impl Fn(&FD) -> Result<(), String> + 'static,
|
validation_fn: impl Fn(&FD) -> Result<(), String> + 'static,
|
||||||
@@ -166,9 +386,4 @@ impl<FD: FormData, FS: FormStyle, C: ControlData, FDT> ControlBuilder<FD, FS, C,
|
|||||||
self.validation_fn = Some(Rc::new(validation_fn));
|
self.validation_fn = Some(Rc::new(validation_fn));
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn style(mut self, attribute: FS::StylingAttributes) -> Self {
|
|
||||||
self.style_attributes.push(attribute);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
use super::{
|
||||||
|
BuilderCxFn, BuilderFn, ControlRenderData, GetterVanityControlData, VanityControlBuilder,
|
||||||
|
VanityControlData,
|
||||||
|
};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
use leptos::{Signal, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Data used for the output control.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
|
pub struct OutputData;
|
||||||
|
|
||||||
|
impl VanityControlData for OutputData {
|
||||||
|
fn build_control<FS: FormStyle>(
|
||||||
|
fs: &FS,
|
||||||
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
|
value_getter: Option<Signal<String>>,
|
||||||
|
) -> View {
|
||||||
|
fs.output(control, value_getter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl GetterVanityControlData for OutputData {}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
|
/// Builds an output form control and adds it to the form.
|
||||||
|
///
|
||||||
|
/// This control allows you to output some text to the user based on the
|
||||||
|
/// form data.
|
||||||
|
pub fn output(self, builder: impl BuilderFn<VanityControlBuilder<FD, OutputData>>) -> Self {
|
||||||
|
self.new_vanity(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds an output form control using the form's context and adds it to
|
||||||
|
/// the form.
|
||||||
|
///
|
||||||
|
/// This control allows you to output some text to the user based on the
|
||||||
|
/// form data and form context.
|
||||||
|
pub fn output_cx(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<VanityControlBuilder<FD, OutputData>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_vanity_cx(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,111 @@
|
|||||||
|
use super::{
|
||||||
|
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
|
||||||
|
};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
use leptos::{Signal, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Data used for the radio buttons control.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
|
pub struct RadioButtonsData {
|
||||||
|
pub name: String,
|
||||||
|
pub label: Option<String>,
|
||||||
|
/// The options for the select.
|
||||||
|
///
|
||||||
|
/// The first value is the string to display, the second is the value.
|
||||||
|
pub options: Vec<(String, String)>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControlData for RadioButtonsData {
|
||||||
|
type ReturnType = String;
|
||||||
|
|
||||||
|
fn build_control<FS: FormStyle>(
|
||||||
|
fs: &FS,
|
||||||
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
|
value_getter: Signal<Self::ReturnType>,
|
||||||
|
value_setter: Rc<dyn Fn(Self::ReturnType)>,
|
||||||
|
validation_state: Signal<Result<(), String>>,
|
||||||
|
) -> View {
|
||||||
|
fs.radio_buttons(control, value_getter, value_setter, validation_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ValidatedControlData for RadioButtonsData {}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
|
/// Builds a radio buttons control and adds it to the form.
|
||||||
|
pub fn radio_buttons<FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderFn<ControlBuilder<FD, RadioButtonsData, FDT>>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a radio buttons control using the form's context and adds it to
|
||||||
|
/// the form.
|
||||||
|
pub fn radio_buttons_cx<FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<ControlBuilder<FD, RadioButtonsData, FDT>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control_cx(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData, FDT> ControlBuilder<FD, RadioButtonsData, FDT> {
|
||||||
|
/// Sets the name of the radio button inputs.
|
||||||
|
///
|
||||||
|
/// This is used for the html element's "name" attribute.
|
||||||
|
/// In forms, the name attribute is the key that the data is sent
|
||||||
|
/// with.
|
||||||
|
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||||
|
self.data.name = control_name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the label for the radio button group.
|
||||||
|
pub fn labeled(mut self, label: impl ToString) -> Self {
|
||||||
|
self.data.label = Some(label.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the option to the radio button group.
|
||||||
|
pub fn with_option(mut self, option: impl ToString) -> Self {
|
||||||
|
self.data
|
||||||
|
.options
|
||||||
|
.push((option.to_string(), option.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds the option to the radio button group, specifying a different
|
||||||
|
/// value than what is displayed.
|
||||||
|
pub fn with_option_valued(mut self, display: impl ToString, value: impl ToString) -> Self {
|
||||||
|
self.data
|
||||||
|
.options
|
||||||
|
.push((display.to_string(), value.to_string()));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds all the options in the provided iterator to the radio button
|
||||||
|
/// group.
|
||||||
|
pub fn with_options(mut self, options: impl Iterator<Item = impl ToString>) -> Self {
|
||||||
|
for option in options {
|
||||||
|
self.data
|
||||||
|
.options
|
||||||
|
.push((option.to_string(), option.to_string()));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds all the (display_string, value) pairs in the provided iterator
|
||||||
|
/// to the radio button group.
|
||||||
|
pub fn with_options_valued(
|
||||||
|
mut self,
|
||||||
|
options: impl Iterator<Item = (impl ToString, impl ToString)>,
|
||||||
|
) -> Self {
|
||||||
|
for option in options {
|
||||||
|
self.data
|
||||||
|
.options
|
||||||
|
.push((option.0.to_string(), option.1.to_string()));
|
||||||
|
}
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
+103
-21
@@ -1,15 +1,21 @@
|
|||||||
use leptos::{Signal, View};
|
use super::{
|
||||||
|
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
|
||||||
use super::{ControlBuilder, ControlData, ControlRenderData};
|
|
||||||
use crate::{
|
|
||||||
form::{FormBuilder, FormData},
|
|
||||||
styles::FormStyle,
|
|
||||||
};
|
};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
use leptos::{IntoSignal, MaybeSignal, Signal, SignalGet, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
/// Data used for the select control.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, Default)]
|
||||||
pub struct SelectData {
|
pub struct SelectData {
|
||||||
pub(crate) name: String,
|
pub name: String,
|
||||||
pub(crate) options: Vec<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 {
|
impl ControlData for SelectData {
|
||||||
@@ -17,34 +23,110 @@ impl ControlData for SelectData {
|
|||||||
|
|
||||||
fn build_control<FS: FormStyle>(
|
fn build_control<FS: FormStyle>(
|
||||||
fs: &FS,
|
fs: &FS,
|
||||||
control: ControlRenderData<FS, Self>,
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
value_getter: Signal<Self::ReturnType>,
|
value_getter: Signal<Self::ReturnType>,
|
||||||
value_setter: Box<dyn Fn(Self::ReturnType)>,
|
value_setter: Rc<dyn Fn(Self::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> View {
|
) -> View {
|
||||||
fs.select(control, value_getter, value_setter, validation_state)
|
fs.select(control, value_getter, value_setter, validation_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl ValidatedControlData for SelectData {}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
pub fn select<FDT: 'static>(
|
/// Builds a select control and adds it to the form.
|
||||||
|
pub fn select<FDT: Clone + PartialEq + 'static>(
|
||||||
self,
|
self,
|
||||||
builder: impl Fn(
|
builder: impl BuilderFn<ControlBuilder<FD, SelectData, FDT>>,
|
||||||
ControlBuilder<FD, FS, SelectData, FDT>,
|
|
||||||
) -> ControlBuilder<FD, FS, SelectData, FDT>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.new_control(builder)
|
self.new_control(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a select control using the form's context and adds it to the
|
||||||
|
/// form.
|
||||||
|
pub fn select_cx<FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<ControlBuilder<FD, SelectData, FDT>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control_cx(builder)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle, FDT> ControlBuilder<FD, FS, SelectData, FDT> {
|
impl<FD: FormToolData, FDT> ControlBuilder<FD, SelectData, FDT> {
|
||||||
pub fn options(mut self, options: Vec<String>) -> Self {
|
/// Sets the name of the radio button inputs.
|
||||||
self.data.options = options;
|
///
|
||||||
|
/// 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
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn and_option(mut self, option: impl ToString) -> Self {
|
/// Sets the label for the select.
|
||||||
self.data.options.push(option.to_string());
|
pub fn labeled(mut self, label: impl ToString) -> Self {
|
||||||
|
self.data.label = Some(label.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the options from the provided iterator.
|
||||||
|
///
|
||||||
|
/// This will overwrite any pervious options setting.
|
||||||
|
pub fn with_options(mut self, options: impl Iterator<Item = impl ToString>) -> Self {
|
||||||
|
let options = options.map(|v| (v.to_string(), v.to_string())).collect();
|
||||||
|
self.data.options = MaybeSignal::Static(options);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the options to the (display_string, value) pairs from the
|
||||||
|
/// provided iterator.
|
||||||
|
///
|
||||||
|
/// This will overwrite any pervious options setting.
|
||||||
|
pub fn with_options_valued(
|
||||||
|
mut self,
|
||||||
|
options: impl Iterator<Item = (impl ToString, impl ToString)>,
|
||||||
|
) -> Self {
|
||||||
|
let options = options
|
||||||
|
.map(|(d, v)| (d.to_string(), v.to_string()))
|
||||||
|
.collect();
|
||||||
|
self.data.options = MaybeSignal::Static(options);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the options from the provided signal.
|
||||||
|
///
|
||||||
|
/// This will overwrite any pervious options setting.
|
||||||
|
pub fn with_options_signal(mut self, options: Signal<Vec<String>>) -> Self {
|
||||||
|
let options = move || {
|
||||||
|
options
|
||||||
|
.get()
|
||||||
|
.into_iter()
|
||||||
|
.map(|v| (v.clone(), v))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
self.data.options = MaybeSignal::Dynamic(options.into_signal());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the options to the (display_string, value) pairs from the
|
||||||
|
/// provided signal.
|
||||||
|
///
|
||||||
|
/// This will overwrite any pervious options setting.
|
||||||
|
pub fn with_options_valued_signal(mut self, options: Signal<Vec<(String, String)>>) -> Self {
|
||||||
|
self.data.options = MaybeSignal::Dynamic(options);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a blank option as the first option for the select.
|
||||||
|
pub fn with_blank_option(mut self) -> Self {
|
||||||
|
self.data.blank_option = Some(String::new());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a blank option as the first option for the select,
|
||||||
|
/// but sets the display string to the given value.
|
||||||
|
pub fn with_blank_option_displayed(mut self, display: impl ToString) -> Self {
|
||||||
|
self.data.blank_option = Some(display.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
use super::{BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
use leptos::{MaybeSignal, Signal, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Data used for the slider control.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
|
pub struct SliderData {
|
||||||
|
pub name: String,
|
||||||
|
pub label: Option<String>,
|
||||||
|
pub min: MaybeSignal<i32>,
|
||||||
|
pub max: MaybeSignal<i32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for SliderData {
|
||||||
|
fn default() -> Self {
|
||||||
|
SliderData {
|
||||||
|
name: String::new(),
|
||||||
|
label: None,
|
||||||
|
min: MaybeSignal::Static(0),
|
||||||
|
max: MaybeSignal::Static(1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControlData for SliderData {
|
||||||
|
type ReturnType = i32;
|
||||||
|
|
||||||
|
fn build_control<FS: FormStyle>(
|
||||||
|
fs: &FS,
|
||||||
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
|
value_getter: Signal<Self::ReturnType>,
|
||||||
|
value_setter: Rc<dyn Fn(Self::ReturnType)>,
|
||||||
|
validation_state: Signal<Result<(), String>>,
|
||||||
|
) -> View {
|
||||||
|
fs.slider(control, value_getter, value_setter, validation_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
|
/// Builds a slider (or range) control and adds it to the form.
|
||||||
|
pub fn slider<FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderFn<ControlBuilder<FD, SliderData, FDT>>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bulids a slider (or range) control using the form's context and adds
|
||||||
|
/// it to the form.
|
||||||
|
pub fn slider_cx<FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<ControlBuilder<FD, SliderData, FDT>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control_cx(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData, FDT> ControlBuilder<FD, SliderData, FDT> {
|
||||||
|
/// Sets the name of the slider.
|
||||||
|
///
|
||||||
|
/// This is used for the html element's "name" attribute.
|
||||||
|
/// In forms, the name attribute is the key that the data is sent
|
||||||
|
/// with.
|
||||||
|
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||||
|
self.data.name = control_name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the label for the slider.
|
||||||
|
pub fn labeled(mut self, label: impl ToString) -> Self {
|
||||||
|
self.data.label = Some(label.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the minimum value for the slider.
|
||||||
|
pub fn min(mut self, min: i32) -> Self {
|
||||||
|
self.data.min = MaybeSignal::Static(min);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the minimum value for the slider to a signal.
|
||||||
|
pub fn min_signal(mut self, min: Signal<i32>) -> Self {
|
||||||
|
self.data.min = MaybeSignal::Dynamic(min);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum value for the slider.
|
||||||
|
pub fn max(mut self, max: i32) -> Self {
|
||||||
|
self.data.max = MaybeSignal::Static(max);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum value for the slider to a signal.
|
||||||
|
pub fn max_signal(mut self, max: Signal<i32>) -> Self {
|
||||||
|
self.data.max = MaybeSignal::Dynamic(max);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
use super::{BuilderCxFn, BuilderFn, ControlRenderData, VanityControlBuilder, VanityControlData};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
use leptos::{prelude::Signal, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Data used for the spacer control.
|
||||||
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
|
pub struct SpacerData {
|
||||||
|
pub height: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl VanityControlData for SpacerData {
|
||||||
|
fn build_control<FS: FormStyle>(
|
||||||
|
fs: &FS,
|
||||||
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
|
_value_getter: Option<Signal<String>>,
|
||||||
|
) -> View {
|
||||||
|
fs.spacer(control)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
|
/// Builds a spacer and adds it to the form.
|
||||||
|
pub fn spacer(self, builder: impl BuilderFn<VanityControlBuilder<FD, SpacerData>>) -> Self {
|
||||||
|
self.new_vanity(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a spacer using the form's context and adds it to the form.
|
||||||
|
pub fn spacer_cx(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<VanityControlBuilder<FD, SpacerData>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_vanity_cx(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> VanityControlBuilder<FD, SpacerData> {
|
||||||
|
/// Sets the height of the spacer.
|
||||||
|
///
|
||||||
|
/// This is a string to allow different units like "10px" or "1.25em".
|
||||||
|
///
|
||||||
|
/// This may or may not be respected based on the Style implementation.
|
||||||
|
pub fn height(mut self, height: impl ToString) -> Self {
|
||||||
|
self.data.height = Some(height.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
use super::{
|
||||||
|
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
|
||||||
|
};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
use leptos::{MaybeSignal, Signal, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Data used for the stepper control.
|
||||||
|
#[derive(Debug, Default, Clone, PartialEq, Eq)]
|
||||||
|
pub struct StepperData {
|
||||||
|
pub name: String,
|
||||||
|
pub label: Option<String>,
|
||||||
|
pub step: Option<MaybeSignal<i32>>,
|
||||||
|
pub min: Option<MaybeSignal<i32>>,
|
||||||
|
pub max: Option<MaybeSignal<i32>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ControlData for StepperData {
|
||||||
|
type ReturnType = String;
|
||||||
|
|
||||||
|
fn build_control<FS: FormStyle>(
|
||||||
|
fs: &FS,
|
||||||
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
|
value_getter: Signal<Self::ReturnType>,
|
||||||
|
value_setter: Rc<dyn Fn(Self::ReturnType)>,
|
||||||
|
validation_state: Signal<Result<(), String>>,
|
||||||
|
) -> View {
|
||||||
|
fs.stepper(control, value_getter, value_setter, validation_state)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl ValidatedControlData for StepperData {}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
|
/// Builds a stepper control and adds it to the form.
|
||||||
|
pub fn stepper<FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderFn<ControlBuilder<FD, StepperData, FDT>>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds a new stepper control using the form's context and adds it to
|
||||||
|
/// the form.
|
||||||
|
pub fn stepper_cx<FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<ControlBuilder<FD, StepperData, FDT>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control_cx(builder)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData, FDT> ControlBuilder<FD, StepperData, FDT> {
|
||||||
|
/// Sets the name of the stepper control.
|
||||||
|
///
|
||||||
|
/// This is used for the html element's "name" attribute.
|
||||||
|
/// In forms, the name attribute is the key that the data is sent
|
||||||
|
/// with.
|
||||||
|
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||||
|
self.data.name = control_name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the label of the stepper.
|
||||||
|
pub fn labeled(mut self, label: impl ToString) -> Self {
|
||||||
|
self.data.label = Some(label.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the step ammount.
|
||||||
|
pub fn step(mut self, step: i32) -> Self {
|
||||||
|
self.data.step = Some(MaybeSignal::Static(step));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the step ammount.
|
||||||
|
pub fn step_signal(mut self, step: Signal<i32>) -> Self {
|
||||||
|
self.data.step = Some(MaybeSignal::Dynamic(step));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the minimum value for the slider.
|
||||||
|
pub fn min(mut self, min: i32) -> Self {
|
||||||
|
self.data.min = Some(MaybeSignal::Static(min));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the minimum value for the slider to a signal.
|
||||||
|
pub fn min_signal(mut self, min: Signal<i32>) -> Self {
|
||||||
|
self.data.min = Some(MaybeSignal::Dynamic(min));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum value for the slider.
|
||||||
|
pub fn max(mut self, max: i32) -> Self {
|
||||||
|
self.data.max = Some(MaybeSignal::Static(max));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the maximum value for the slider to a signal.
|
||||||
|
pub fn max_signal(mut self, max: Signal<i32>) -> Self {
|
||||||
|
self.data.max = Some(MaybeSignal::Dynamic(max));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
+25
-15
@@ -1,32 +1,42 @@
|
|||||||
use leptos::View;
|
use super::{BuilderCxFn, BuilderFn, ControlRenderData, VanityControlBuilder, VanityControlData};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
use super::{ControlRenderData, VanityControlBuilder, VanityControlData};
|
use leptos::{prelude::Signal, View};
|
||||||
use crate::{
|
use std::rc::Rc;
|
||||||
form::{FormBuilder, FormData},
|
|
||||||
styles::FormStyle,
|
|
||||||
};
|
|
||||||
|
|
||||||
|
/// Data used for the submit button control.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
pub struct SubmitData {
|
pub struct SubmitData {
|
||||||
pub(crate) text: String,
|
pub text: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl VanityControlData for SubmitData {
|
impl VanityControlData for SubmitData {
|
||||||
fn build_control<FS: FormStyle>(fs: &FS, control: ControlRenderData<FS, Self>) -> View {
|
fn build_control<FS: FormStyle>(
|
||||||
|
fs: &FS,
|
||||||
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
|
_value_getter: Option<Signal<String>>,
|
||||||
|
) -> View {
|
||||||
fs.submit(control)
|
fs.submit(control)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
pub fn submit(
|
/// Builds a submit button and adds it to the form.
|
||||||
self,
|
pub fn submit(self, builder: impl BuilderFn<VanityControlBuilder<FD, SubmitData>>) -> Self {
|
||||||
builder: impl Fn(VanityControlBuilder<FS, SubmitData>) -> VanityControlBuilder<FS, SubmitData>,
|
|
||||||
) -> Self {
|
|
||||||
self.new_vanity(builder)
|
self.new_vanity(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a submit button using the form's context and adds it to the
|
||||||
|
/// form.
|
||||||
|
pub fn submit_cx(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<VanityControlBuilder<FD, SubmitData>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_vanity_cx(builder)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FS: FormStyle> VanityControlBuilder<FS, SubmitData> {
|
impl<FD: FormToolData> VanityControlBuilder<FD, SubmitData> {
|
||||||
|
/// Sets the submit button's text.
|
||||||
pub fn text(mut self, text: impl ToString) -> Self {
|
pub fn text(mut self, text: impl ToString) -> Self {
|
||||||
self.data.text = text.to_string();
|
self.data.text = text.to_string();
|
||||||
self
|
self
|
||||||
|
|||||||
+42
-14
@@ -1,14 +1,16 @@
|
|||||||
use super::{ControlBuilder, ControlData, ControlRenderData};
|
use super::{
|
||||||
use crate::{
|
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
|
||||||
form::{FormBuilder, FormData},
|
|
||||||
styles::FormStyle,
|
|
||||||
};
|
};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
use leptos::{Signal, View};
|
use leptos::{Signal, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Data used for the text area control.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
|
||||||
pub struct TextAreaData {
|
pub struct TextAreaData {
|
||||||
pub(crate) name: String,
|
pub name: String,
|
||||||
pub(crate) placeholder: Option<String>,
|
pub label: Option<String>,
|
||||||
|
pub placeholder: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ControlData for TextAreaData {
|
impl ControlData for TextAreaData {
|
||||||
@@ -16,27 +18,53 @@ impl ControlData for TextAreaData {
|
|||||||
|
|
||||||
fn build_control<FS: FormStyle>(
|
fn build_control<FS: FormStyle>(
|
||||||
fs: &FS,
|
fs: &FS,
|
||||||
control: ControlRenderData<FS, Self>,
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
value_getter: Signal<Self::ReturnType>,
|
value_getter: Signal<Self::ReturnType>,
|
||||||
value_setter: Box<dyn Fn(Self::ReturnType)>,
|
value_setter: Rc<dyn Fn(Self::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> View {
|
) -> View {
|
||||||
fs.text_area(control, value_getter, value_setter, validation_state)
|
fs.text_area(control, value_getter, value_setter, validation_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl ValidatedControlData for TextAreaData {}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
pub fn text_area<FDT: 'static>(
|
/// Builds a text area control and adds it to the form.
|
||||||
|
pub fn text_area<FDT: Clone + PartialEq + 'static>(
|
||||||
self,
|
self,
|
||||||
builder: impl Fn(
|
builder: impl BuilderFn<ControlBuilder<FD, TextAreaData, FDT>>,
|
||||||
ControlBuilder<FD, FS, TextAreaData, FDT>,
|
|
||||||
) -> ControlBuilder<FD, FS, TextAreaData, FDT>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.new_control(builder)
|
self.new_control(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a text area control using the forms context and adds it to the
|
||||||
|
/// form.
|
||||||
|
pub fn text_area_cx<FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<ControlBuilder<FD, TextAreaData, FDT>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control_cx(builder)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle, FDT> ControlBuilder<FD, FS, TextAreaData, FDT> {
|
impl<FD: FormToolData, FDT> ControlBuilder<FD, TextAreaData, FDT> {
|
||||||
|
/// Sets the name of the text area.
|
||||||
|
///
|
||||||
|
/// This is used for the html element's "name" attribute.
|
||||||
|
/// In forms, the name attribute is the key that the data is sent
|
||||||
|
/// with.
|
||||||
|
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||||
|
self.data.name = control_name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the label for the text area.
|
||||||
|
pub fn labeled(mut self, label: impl ToString) -> Self {
|
||||||
|
self.data.label = Some(label.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the placeholder for the text area.
|
||||||
pub fn placeholder(mut self, placeholder: impl ToString) -> Self {
|
pub fn placeholder(mut self, placeholder: impl ToString) -> Self {
|
||||||
self.data.placeholder = Some(placeholder.to_string());
|
self.data.placeholder = Some(placeholder.to_string());
|
||||||
self
|
self
|
||||||
|
|||||||
+57
-30
@@ -1,18 +1,17 @@
|
|||||||
use leptos::{Signal, View};
|
use super::{
|
||||||
|
BuilderCxFn, BuilderFn, ControlBuilder, ControlData, ControlRenderData, ValidatedControlData,
|
||||||
use super::{ControlBuilder, ControlData, ControlRenderData};
|
|
||||||
use crate::{
|
|
||||||
form::{FormBuilder, FormData},
|
|
||||||
styles::FormStyle,
|
|
||||||
};
|
};
|
||||||
|
use crate::{form::FormToolData, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
use leptos::{Signal, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
/// Data used for the text input control.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
|
||||||
pub struct TextInputData {
|
pub struct TextInputData {
|
||||||
pub(crate) name: String,
|
pub name: String,
|
||||||
pub(crate) placeholder: Option<String>,
|
pub label: Option<String>,
|
||||||
pub(crate) label: Option<String>,
|
pub placeholder: Option<String>,
|
||||||
pub(crate) initial_text: String,
|
pub input_type: &'static str,
|
||||||
pub(crate) input_type: &'static str,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for TextInputData {
|
impl Default for TextInputData {
|
||||||
@@ -21,7 +20,6 @@ impl Default for TextInputData {
|
|||||||
name: String::new(),
|
name: String::new(),
|
||||||
placeholder: None,
|
placeholder: None,
|
||||||
label: None,
|
label: None,
|
||||||
initial_text: String::new(),
|
|
||||||
input_type: "input",
|
input_type: "input",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,44 +30,73 @@ impl ControlData for TextInputData {
|
|||||||
|
|
||||||
fn build_control<FS: FormStyle>(
|
fn build_control<FS: FormStyle>(
|
||||||
fs: &FS,
|
fs: &FS,
|
||||||
control: ControlRenderData<FS, Self>,
|
control: Rc<ControlRenderData<FS, Self>>,
|
||||||
value_getter: Signal<Self::ReturnType>,
|
value_getter: Signal<Self::ReturnType>,
|
||||||
value_setter: Box<dyn Fn(Self::ReturnType)>,
|
value_setter: Rc<dyn Fn(Self::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> View {
|
) -> View {
|
||||||
fs.text_input(control, value_getter, value_setter, validation_state)
|
fs.text_input(control, value_getter, value_setter, validation_state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
impl ValidatedControlData for TextInputData {}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
pub fn text_input<FDT: 'static>(
|
/// Builds a text input control and adds it to the form.
|
||||||
|
pub fn text_input<FDT: Clone + PartialEq + 'static>(
|
||||||
self,
|
self,
|
||||||
builder: impl Fn(
|
builder: impl BuilderFn<ControlBuilder<FD, TextInputData, FDT>>,
|
||||||
ControlBuilder<FD, FS, TextInputData, FDT>,
|
|
||||||
) -> ControlBuilder<FD, FS, TextInputData, FDT>,
|
|
||||||
) -> Self {
|
) -> Self {
|
||||||
self.new_control(builder)
|
self.new_control(builder)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds a text input control using the form's context and adds it to
|
||||||
|
/// the form.
|
||||||
|
pub fn text_input_cx<FDT: Clone + PartialEq + 'static>(
|
||||||
|
self,
|
||||||
|
builder: impl BuilderCxFn<ControlBuilder<FD, TextInputData, FDT>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
self.new_control_cx(builder)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle, FDT> ControlBuilder<FD, FS, TextInputData, FDT> {
|
impl<FD: FormToolData, FDT> ControlBuilder<FD, TextInputData, FDT> {
|
||||||
|
/// Sets the name of the text input.
|
||||||
|
///
|
||||||
|
/// This is used for the html element's "name" attribute.
|
||||||
|
/// In forms, the name attribute is the key that the data is sent
|
||||||
|
/// with.
|
||||||
|
pub fn named(mut self, control_name: impl ToString) -> Self {
|
||||||
|
self.data.name = control_name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the label for the text input.
|
||||||
|
pub fn labeled(mut self, label: impl ToString) -> Self {
|
||||||
|
self.data.label = Some(label.to_string());
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the placeholder for the text input.
|
||||||
pub fn placeholder(mut self, placeholder: impl ToString) -> Self {
|
pub fn placeholder(mut self, placeholder: impl ToString) -> Self {
|
||||||
self.data.placeholder = Some(placeholder.to_string());
|
self.data.placeholder = Some(placeholder.to_string());
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn label(mut self, label: impl ToString) -> Self {
|
/// Sets the text input to be the "password" type.
|
||||||
self.data.label = Some(label.to_string());
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn initial_text(mut self, text: impl ToString) -> Self {
|
|
||||||
self.data.initial_text = text.to_string();
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn password(mut self) -> Self {
|
pub fn password(mut self) -> Self {
|
||||||
self.data.input_type = "password";
|
self.data.input_type = "password";
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the text input to be the "date" type.
|
||||||
|
pub fn date(mut self) -> Self {
|
||||||
|
self.data.input_type = "date";
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sets the text input to be the specified type.
|
||||||
|
pub fn input_type(mut self, input_type: &'static str) -> Self {
|
||||||
|
self.data.input_type = input_type;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+146
-267
@@ -1,22 +1,25 @@
|
|||||||
use std::rc::Rc;
|
use crate::{controls::ValidationFn, form_builder::FormBuilder, styles::FormStyle};
|
||||||
|
|
||||||
use crate::{
|
|
||||||
controls::{
|
|
||||||
BuiltControlData, ControlBuilder, ControlData, FieldFn, ParseFn, UnparseFn, ValidationFn,
|
|
||||||
VanityControlBuilder, VanityControlData,
|
|
||||||
},
|
|
||||||
styles::FormStyle,
|
|
||||||
};
|
|
||||||
use leptos::{
|
use leptos::{
|
||||||
create_rw_signal, create_signal, IntoSignal, IntoView, RwSignal, SignalGet, SignalSet,
|
server_fn::{client::Client, codec::PostUrl, request::ClientReq, ServerFn},
|
||||||
SignalUpdate, View,
|
*,
|
||||||
};
|
};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use web_sys::FormData;
|
||||||
|
|
||||||
pub struct Validator<FD: FormData> {
|
/// A type that can be used to validate the form data.
|
||||||
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
///
|
||||||
|
/// This can be useful to use the same validation logic on the front
|
||||||
|
/// end and backend without duplicating the logic.
|
||||||
|
pub struct FormValidator<FD> {
|
||||||
|
pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData> Validator<FD> {
|
impl<FD: FormToolData> FormValidator<FD> {
|
||||||
|
/// Validates the given form data.
|
||||||
|
///
|
||||||
|
/// This runs all the validation functions for all the fields
|
||||||
|
/// in the form. The first falure to occur (if any) will be returned.
|
||||||
pub fn validate(&self, form_data: &FD) -> Result<(), String> {
|
pub fn validate(&self, form_data: &FD) -> Result<(), String> {
|
||||||
for v in self.validations.iter() {
|
for v in self.validations.iter() {
|
||||||
(*v)(form_data)?;
|
(*v)(form_data)?;
|
||||||
@@ -25,38 +28,43 @@ impl<FD: FormData> Validator<FD> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A constructed form object.
|
/// A constructed, rendered form object.
|
||||||
///
|
///
|
||||||
/// With this, you can render the form, get the form data, or get
|
/// With this, you can render the form, get the form data, or get
|
||||||
/// a validator for the data.
|
/// a validator for the data.
|
||||||
pub struct Form<FD: FormData> {
|
#[derive(Clone)]
|
||||||
|
pub struct Form<FD: FormToolData> {
|
||||||
|
/// The form data signal.
|
||||||
pub fd: RwSignal<FD>,
|
pub fd: RwSignal<FD>,
|
||||||
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
/// The list of validations
|
||||||
view: View,
|
pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
||||||
|
pub(crate) view: View,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData> Form<FD> {
|
impl<FD: FormToolData> Form<FD> {
|
||||||
pub fn validator(self) -> Validator<FD> {
|
/// Gets the [`FormValidator`] for this form.
|
||||||
Validator {
|
pub fn validator(&self) -> FormValidator<FD> {
|
||||||
validations: self.validations,
|
FormValidator {
|
||||||
|
validations: self.validations.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn validate(&self, form_data: &FD) -> Result<(), String> {
|
/// Validates the [`FormToolData`], returning the result.
|
||||||
for v in self.validations.iter() {
|
pub fn validate(&self) -> Result<(), String> {
|
||||||
(*v)(form_data)?;
|
let validator = self.validator();
|
||||||
}
|
validator.validate(&self.fd.get_untracked())
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Gets the view associated with this [`Form`].
|
||||||
pub fn view(&self) -> View {
|
pub fn view(&self) -> View {
|
||||||
self.view.clone()
|
self.view.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn to_parts(self) -> (RwSignal<FD>, Validator<FD>, View) {
|
/// Splits this [`Form`] into it's parts.
|
||||||
|
pub fn to_parts(self) -> (RwSignal<FD>, FormValidator<FD>, View) {
|
||||||
(
|
(
|
||||||
self.fd,
|
self.fd,
|
||||||
Validator {
|
FormValidator {
|
||||||
validations: self.validations,
|
validations: self.validations,
|
||||||
},
|
},
|
||||||
self.view,
|
self.view,
|
||||||
@@ -64,266 +72,137 @@ impl<FD: FormData> Form<FD> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<FD: FormData> IntoView for Form<FD> {
|
impl<FD: FormToolData> IntoView for Form<FD> {
|
||||||
fn into_view(self) -> View {
|
fn into_view(self) -> View {
|
||||||
self.view()
|
self.view()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A version of the [`FormBuilder`] that contains all the data
|
|
||||||
/// needed for full building of a [`Form`].
|
|
||||||
struct FullFormBuilder<FD: FormData, FS: FormStyle> {
|
|
||||||
fd: RwSignal<FD>,
|
|
||||||
fs: FS,
|
|
||||||
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
|
||||||
views: Vec<View>,
|
|
||||||
}
|
|
||||||
/// The internal type for building forms
|
|
||||||
///
|
|
||||||
/// This allows us to build either the full form
|
|
||||||
/// with views, validation and data. Or we can just
|
|
||||||
/// build the validation functions.
|
|
||||||
///
|
|
||||||
/// This is useful in the context of a server that
|
|
||||||
/// cannot or should not render the form. You can
|
|
||||||
/// still get all the validation functions from the
|
|
||||||
/// form data.
|
|
||||||
enum FormBuilderInner<FD: FormData, FS: FormStyle> {
|
|
||||||
/// For building the form with views
|
|
||||||
FullBuilder(FullFormBuilder<FD, FS>),
|
|
||||||
/// For building only the validations for the form
|
|
||||||
ValidationBuilder {
|
|
||||||
validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct FormBuilder<FD: FormData, FS: FormStyle> {
|
|
||||||
inner: FormBuilderInner<FD, FS>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<FD: FormData, FS: FormStyle> FormBuilder<FD, FS> {
|
|
||||||
// TODO: remove the Default trait bound and bind it to this function only
|
|
||||||
fn new_full_builder(form_style: FS) -> FormBuilder<FD, FS> {
|
|
||||||
let fd = create_rw_signal(FD::default());
|
|
||||||
FormBuilder {
|
|
||||||
inner: FormBuilderInner::FullBuilder(FullFormBuilder {
|
|
||||||
fd,
|
|
||||||
fs: form_style,
|
|
||||||
validations: Vec::new(),
|
|
||||||
views: Vec::new(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_full_builder_with(starting_data: FD, form_style: FS) -> FormBuilder<FD, FS> {
|
|
||||||
let fd = create_rw_signal(starting_data);
|
|
||||||
FormBuilder {
|
|
||||||
inner: FormBuilderInner::FullBuilder(FullFormBuilder {
|
|
||||||
fd,
|
|
||||||
fs: form_style,
|
|
||||||
validations: Vec::new(),
|
|
||||||
views: Vec::new(),
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn new_validation_builder() -> FormBuilder<FD, FS> {
|
|
||||||
FormBuilder {
|
|
||||||
inner: FormBuilderInner::ValidationBuilder {
|
|
||||||
validations: Vec::new(),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn new_vanity<C: VanityControlData + Default>(
|
|
||||||
mut self,
|
|
||||||
builder: impl Fn(VanityControlBuilder<FS, C>) -> VanityControlBuilder<FS, C>,
|
|
||||||
) -> Self {
|
|
||||||
let vanity_builder = VanityControlBuilder::new(C::default());
|
|
||||||
let control = builder(vanity_builder);
|
|
||||||
self.add_vanity(control);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn new_control<C: ControlData + Default, FDT: 'static>(
|
|
||||||
mut self,
|
|
||||||
builder: impl Fn(ControlBuilder<FD, FS, C, FDT>) -> ControlBuilder<FD, FS, C, FDT>,
|
|
||||||
) -> Self {
|
|
||||||
let control_builder = ControlBuilder::new(C::default());
|
|
||||||
let control = builder(control_builder);
|
|
||||||
self.add_control(control);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_vanity<C: VanityControlData>(&mut self, vanity_control: VanityControlBuilder<FS, C>) {
|
|
||||||
let builder = match &mut self.inner {
|
|
||||||
FormBuilderInner::FullBuilder(fb) => fb,
|
|
||||||
FormBuilderInner::ValidationBuilder { validations: _ } => return,
|
|
||||||
};
|
|
||||||
|
|
||||||
let render_data = vanity_control.build();
|
|
||||||
let view = VanityControlData::build_control(&builder.fs, render_data);
|
|
||||||
builder.views.push(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_control<C: ControlData, FDT: 'static>(
|
|
||||||
&mut self,
|
|
||||||
control: ControlBuilder<FD, FS, C, FDT>,
|
|
||||||
) {
|
|
||||||
let BuiltControlData {
|
|
||||||
render_data,
|
|
||||||
field_fn,
|
|
||||||
parse_fn,
|
|
||||||
unparse_fn,
|
|
||||||
validation_fn,
|
|
||||||
} = match control.build() {
|
|
||||||
Ok(c) => c,
|
|
||||||
Err(e) => panic!("Invalid Component: {}", e),
|
|
||||||
};
|
|
||||||
|
|
||||||
let builder = match &mut self.inner {
|
|
||||||
FormBuilderInner::FullBuilder(fb) => fb,
|
|
||||||
FormBuilderInner::ValidationBuilder {
|
|
||||||
ref mut validations,
|
|
||||||
} => {
|
|
||||||
if let Some(validation_fn) = validation_fn {
|
|
||||||
validations.push(validation_fn);
|
|
||||||
}
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if let Some(ref validation_fn) = validation_fn {
|
|
||||||
builder.validations.push(validation_fn.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let view = Self::build_control_view(
|
|
||||||
builder,
|
|
||||||
field_fn,
|
|
||||||
unparse_fn,
|
|
||||||
parse_fn,
|
|
||||||
validation_fn,
|
|
||||||
render_data,
|
|
||||||
);
|
|
||||||
|
|
||||||
builder.views.push(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_control_view<C: ControlData, FDT: 'static>(
|
|
||||||
builder: &FullFormBuilder<FD, FS>,
|
|
||||||
field_fn: Rc<dyn FieldFn<FD, FDT>>,
|
|
||||||
unparse_fn: Box<dyn UnparseFn<<C as ControlData>::ReturnType, FDT>>,
|
|
||||||
parse_fn: Box<dyn ParseFn<<C as ControlData>::ReturnType, FDT>>,
|
|
||||||
validation_fn: Option<Rc<dyn ValidationFn<FD>>>,
|
|
||||||
render_data: crate::controls::ControlRenderData<FS, C>,
|
|
||||||
) -> View {
|
|
||||||
let (validation_signal, validation_signal_set) = create_signal(Ok(()));
|
|
||||||
// TODO: The on-submit triggering a validation could be done here with a create_effect
|
|
||||||
let field_fn2 = field_fn.clone();
|
|
||||||
let fd_getter = builder.fd.read_only();
|
|
||||||
let value_getter = move || {
|
|
||||||
// TODO: ideally, this should not be borrowed. If we get a clone when we call `.get` we should pass in the clone, not borrow this clone
|
|
||||||
let mut fd = fd_getter.get();
|
|
||||||
let field = field_fn2(&mut fd);
|
|
||||||
(unparse_fn)(field)
|
|
||||||
};
|
|
||||||
let value_getter = value_getter.into_signal();
|
|
||||||
|
|
||||||
let fd = builder.fd.clone();
|
|
||||||
let value_setter = move |value| {
|
|
||||||
// TODO: also do this on a submit somehow
|
|
||||||
let parsed = match parse_fn(value) {
|
|
||||||
Ok(p) => p,
|
|
||||||
Err(e) => {
|
|
||||||
validation_signal_set.set(Err(e));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// parse succeeded, update value and validate
|
|
||||||
let field_fn = field_fn.clone(); // move
|
|
||||||
let validation_fn = validation_fn.clone(); // move
|
|
||||||
|
|
||||||
// TODO: change this to a get then set, so that if the validation fails,
|
|
||||||
// The value is not actually updated
|
|
||||||
fd.update(move |fd| {
|
|
||||||
let field = field_fn(fd);
|
|
||||||
*field = parsed;
|
|
||||||
|
|
||||||
if let Some(ref validation_fn) = validation_fn {
|
|
||||||
let validation_result = (validation_fn)(fd);
|
|
||||||
validation_signal_set.set(validation_result);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
let value_setter = Box::new(value_setter);
|
|
||||||
// TODO: add a signal that triggers on submit to refresh the validation on_submit
|
|
||||||
|
|
||||||
C::build_control(
|
|
||||||
&builder.fs,
|
|
||||||
render_data,
|
|
||||||
value_getter,
|
|
||||||
value_setter,
|
|
||||||
validation_signal.into(),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build(self) -> Option<Form<FD>> {
|
|
||||||
let builder = match self.inner {
|
|
||||||
FormBuilderInner::FullBuilder(fb) => fb,
|
|
||||||
FormBuilderInner::ValidationBuilder { validations: _ } => return None,
|
|
||||||
};
|
|
||||||
let view = builder.fs.form_frame(builder.views.into_view());
|
|
||||||
|
|
||||||
Some(Form {
|
|
||||||
fd: builder.fd,
|
|
||||||
validations: builder.validations,
|
|
||||||
view,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn validator(self) -> Validator<FD> {
|
|
||||||
let validations = match self.inner {
|
|
||||||
FormBuilderInner::FullBuilder(fb) => fb.validations,
|
|
||||||
FormBuilderInner::ValidationBuilder { validations } => validations,
|
|
||||||
};
|
|
||||||
Validator { validations }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A trait allowing a form to be built around its containing data.
|
/// A trait allowing a form to be built around its containing data.
|
||||||
///
|
///
|
||||||
/// This trait defines a function that can be used to build all the data needed
|
/// This trait defines a function that can be used to build all the data
|
||||||
/// to physically lay out a form, and how that data should be parsed and validated.
|
/// needed to physically lay out a form, and how that data should be parsed
|
||||||
pub trait FormData: Default + Clone + 'static {
|
/// and validated.
|
||||||
|
pub trait FormToolData: Clone + 'static {
|
||||||
|
/// The style that this form uses.
|
||||||
type Style: FormStyle;
|
type Style: FormStyle;
|
||||||
|
/// The context that this form is rendered in.
|
||||||
/// Defines how the form should be layed out and how the data should be parsed and validated.
|
|
||||||
///
|
///
|
||||||
/// To construct a [`From`] object, use one of the [`get_form`()] methods.
|
/// This will need to be provided when building a form or a validator.
|
||||||
|
/// Therefore, you will need to be able to replicate this context
|
||||||
|
/// on the client for rendering and the server for validating.
|
||||||
|
type Context: 'static;
|
||||||
|
|
||||||
|
/// Defines how the form should be laid out and how the data should be
|
||||||
|
/// parsed and validated.
|
||||||
|
///
|
||||||
|
/// To construct a [`From`] object, use one of the `get_form` methods.
|
||||||
///
|
///
|
||||||
/// Uses the given form builder to specify what fields should be present
|
/// Uses the given form builder to specify what fields should be present
|
||||||
/// in the form, what properties those fields should have, and how that
|
/// in the form, what properties those fields should have, and how that
|
||||||
/// data should be parsed and checked.
|
/// data should be parsed and checked.
|
||||||
fn build_form(fb: FormBuilder<Self, Self::Style>) -> FormBuilder<Self, Self::Style>;
|
fn build_form(fb: FormBuilder<Self>) -> FormBuilder<Self>;
|
||||||
|
|
||||||
/// Constructs a [`Form`] for this FormData type.
|
/// Constructs a [`Form`] for this [`FormToolData`] type.
|
||||||
///
|
///
|
||||||
/// The [`Form`] provides the way to render the form.
|
/// This renders the form as a enhanced
|
||||||
fn get_form(style: Self::Style) -> Form<Self> {
|
/// [`ActionForm`](leptos_router::ActionForm) that sends the form data
|
||||||
let builder = FormBuilder::new_full_builder(style);
|
/// 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);
|
let builder = Self::build_form(builder);
|
||||||
builder.build().expect("builder should be full builder")
|
builder.build_form(action, self, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_form_with_starting_data(self, style: Self::Style) -> Form<Self> {
|
/// Constructs a [`Form`] for this [`FormToolData`] type.
|
||||||
let builder = FormBuilder::new_full_builder_with(self, style);
|
///
|
||||||
|
/// This renders the form as a the leptos_router
|
||||||
|
/// [`ActionForm`](leptos_router::ActionForm)
|
||||||
|
/// component.
|
||||||
|
///
|
||||||
|
/// For the other ways to construct a [`Form`], see:
|
||||||
|
/// - [`get_form`](Self::get_form)
|
||||||
|
/// - [`get_action_form`](Self::get_action_form)
|
||||||
|
fn get_action_form<ServFn>(
|
||||||
|
self,
|
||||||
|
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
|
||||||
|
style: Self::Style,
|
||||||
|
context: Self::Context,
|
||||||
|
) -> Form<Self>
|
||||||
|
where
|
||||||
|
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
|
||||||
|
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
|
||||||
|
From<FormData>,
|
||||||
|
{
|
||||||
|
let builder = FormBuilder::new(context);
|
||||||
let builder = Self::build_form(builder);
|
let builder = Self::build_form(builder);
|
||||||
builder.build().expect("builder should be full builder")
|
builder.build_action_form(action, self, style)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_validator() -> Validator<Self> {
|
/// Constructs a [`Form`] for this [`FormToolData`] type.
|
||||||
let builder = FormBuilder::new_validation_builder();
|
///
|
||||||
|
/// This renders the form as a the leptos_router
|
||||||
|
/// [`Form`](leptos_router::Form)
|
||||||
|
/// component.
|
||||||
|
///
|
||||||
|
/// For the other ways to construct a [`Form`], see:
|
||||||
|
/// - [`get_form`](Self::get_form)
|
||||||
|
/// - [`get_action_form`](Self::get_action_form)
|
||||||
|
fn get_plain_form(
|
||||||
|
self,
|
||||||
|
action: impl ToString,
|
||||||
|
style: Self::Style,
|
||||||
|
context: Self::Context,
|
||||||
|
) -> Form<Self> {
|
||||||
|
let builder = FormBuilder::new(context);
|
||||||
|
let builder = Self::build_form(builder);
|
||||||
|
builder.build_plain_form(action.to_string(), self, style)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets a [`FormValidator`] for this [`FormToolData`].
|
||||||
|
///
|
||||||
|
/// This doesn't render the view, but just collects all the validation
|
||||||
|
/// Functions from building the form. That means it can be called on the
|
||||||
|
/// Server and no rendering will be done.
|
||||||
|
///
|
||||||
|
/// However, the code to render the views are not configured out, it
|
||||||
|
/// simply doesn't run, so the view needs to compile even on the server.
|
||||||
|
fn get_validator(context: Self::Context) -> FormValidator<Self> {
|
||||||
|
let builder = FormBuilder::new(context);
|
||||||
let builder = Self::build_form(builder);
|
let builder = Self::build_form(builder);
|
||||||
builder.validator()
|
builder.validator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Validates this [`FormToolData`] struct.
|
||||||
|
///
|
||||||
|
/// This is shorthand for creating a validator with
|
||||||
|
/// [`get_validator`](Self::get_validator)()
|
||||||
|
/// and then calling `validator.validate(&self, context)`.
|
||||||
|
fn validate(&self, context: Self::Context) -> Result<(), String> {
|
||||||
|
let validator = Self::get_validator(context);
|
||||||
|
validator.validate(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,460 @@
|
|||||||
|
use crate::{
|
||||||
|
controls::{
|
||||||
|
BuilderCxFn, BuilderFn, BuiltControlData, BuiltVanityControlData, ControlBuilder,
|
||||||
|
ControlData, ControlRenderData, FieldSetter, ParseFn, RenderFn, ValidationCb, ValidationFn,
|
||||||
|
VanityControlBuilder, VanityControlData,
|
||||||
|
},
|
||||||
|
form::{Form, FormToolData, FormValidator},
|
||||||
|
styles::FormStyle,
|
||||||
|
};
|
||||||
|
use leptos::{
|
||||||
|
server_fn::{client::Client, codec::PostUrl, request::ClientReq, ServerFn},
|
||||||
|
*,
|
||||||
|
};
|
||||||
|
use leptos_router::{ActionForm, Form};
|
||||||
|
use serde::de::DeserializeOwned;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use web_sys::{FormData, SubmitEvent};
|
||||||
|
|
||||||
|
/// A builder for laying out forms.
|
||||||
|
///
|
||||||
|
/// This builder allows you to specify what components should make up the form.
|
||||||
|
pub struct FormBuilder<FD: FormToolData> {
|
||||||
|
pub(crate) cx: Rc<FD::Context>,
|
||||||
|
/// The list of [`ValidationFn`]s.
|
||||||
|
pub(crate) validations: Vec<Rc<dyn ValidationFn<FD>>>,
|
||||||
|
/// The list of functions that will render the form.
|
||||||
|
pub(crate) render_fns: Vec<Box<dyn RenderFn<FD::Style, FD>>>,
|
||||||
|
/// The list of styling attributes applied on the form level.
|
||||||
|
pub(crate) styles: Vec<<FD::Style as FormStyle>::StylingAttributes>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> FormBuilder<FD> {
|
||||||
|
/// Creates a new [`FormBuilder`]
|
||||||
|
pub(crate) fn new(cx: FD::Context) -> Self {
|
||||||
|
FormBuilder {
|
||||||
|
cx: Rc::new(cx),
|
||||||
|
validations: Vec::new(),
|
||||||
|
render_fns: Vec::new(),
|
||||||
|
styles: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new [`FormBuilder`] with the given Rc'ed context, for
|
||||||
|
//// building a form group.
|
||||||
|
pub(crate) fn new_group(cx: Rc<FD::Context>) -> Self {
|
||||||
|
FormBuilder {
|
||||||
|
cx,
|
||||||
|
validations: Vec::new(),
|
||||||
|
render_fns: Vec::new(),
|
||||||
|
styles: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a styling attribute to the entire form.
|
||||||
|
pub fn style(mut self, style: <FD::Style as FormStyle>::StylingAttributes) -> Self {
|
||||||
|
self.styles.push(style);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new vanity control to the form.
|
||||||
|
pub(crate) fn new_vanity<C: VanityControlData + Default>(
|
||||||
|
mut self,
|
||||||
|
builder: impl BuilderFn<VanityControlBuilder<FD, C>>,
|
||||||
|
) -> Self {
|
||||||
|
let vanity_builder = VanityControlBuilder::new(C::default());
|
||||||
|
let control = builder(vanity_builder);
|
||||||
|
self.add_vanity(control);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new vanity control to the form using the form's context.
|
||||||
|
pub(crate) fn new_vanity_cx<C: VanityControlData + Default>(
|
||||||
|
mut self,
|
||||||
|
builder: impl BuilderCxFn<VanityControlBuilder<FD, C>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
let vanity_builder = VanityControlBuilder::new(C::default());
|
||||||
|
let control = builder(vanity_builder, self.cx.clone());
|
||||||
|
self.add_vanity(control);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new control to the form using the form's context.
|
||||||
|
pub(crate) fn new_control<C: ControlData + Default, FDT: Clone + PartialEq + 'static>(
|
||||||
|
mut self,
|
||||||
|
builder: impl BuilderFn<ControlBuilder<FD, C, FDT>>,
|
||||||
|
) -> Self {
|
||||||
|
let control_builder = ControlBuilder::new(C::default());
|
||||||
|
let control = builder(control_builder);
|
||||||
|
self.add_control(control);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a new control to the form using the form's context.
|
||||||
|
pub(crate) fn new_control_cx<C: ControlData + Default, FDT: Clone + PartialEq + 'static>(
|
||||||
|
mut self,
|
||||||
|
builder: impl BuilderCxFn<ControlBuilder<FD, C, FDT>, FD::Context>,
|
||||||
|
) -> Self {
|
||||||
|
let control_builder = ControlBuilder::new(C::default());
|
||||||
|
let control = builder(control_builder, self.cx.clone());
|
||||||
|
self.add_control(control);
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a vanity control to the form.
|
||||||
|
pub(crate) fn add_vanity<C: VanityControlData>(
|
||||||
|
&mut self,
|
||||||
|
vanity_control: VanityControlBuilder<FD, C>,
|
||||||
|
) {
|
||||||
|
let BuiltVanityControlData {
|
||||||
|
render_data,
|
||||||
|
getter,
|
||||||
|
show_when,
|
||||||
|
} = vanity_control.build();
|
||||||
|
|
||||||
|
let cx = self.cx.clone();
|
||||||
|
let render_fn = move |fs: Rc<FD::Style>, fd: RwSignal<FD>| {
|
||||||
|
let render_data = Rc::new(render_data);
|
||||||
|
let value_getter = getter.map(|getter| (move || getter(fd.get())).into_signal());
|
||||||
|
let view =
|
||||||
|
move || VanityControlData::build_control(&*fs, render_data.clone(), value_getter);
|
||||||
|
let view = match show_when {
|
||||||
|
Some(when) => {
|
||||||
|
let when = move || when(fd.into(), cx.clone());
|
||||||
|
view! { <Show when=when>{view.clone()}</Show> }
|
||||||
|
}
|
||||||
|
None => view(),
|
||||||
|
};
|
||||||
|
(view, None)
|
||||||
|
};
|
||||||
|
|
||||||
|
self.render_fns.push(Box::new(render_fn));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a control to the form.
|
||||||
|
pub(crate) fn add_control<C: ControlData, FDT: Clone + PartialEq + 'static>(
|
||||||
|
&mut self,
|
||||||
|
control: ControlBuilder<FD, C, FDT>,
|
||||||
|
) {
|
||||||
|
let built_control_data = match control.build() {
|
||||||
|
Ok(c) => c,
|
||||||
|
Err(e) => {
|
||||||
|
let item_name = std::any::type_name::<C>()
|
||||||
|
.rsplit("::")
|
||||||
|
.next()
|
||||||
|
.expect("split to have at least 1 element");
|
||||||
|
panic!("Invalid Component ({}): {}", item_name, e)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(validation_fn) = built_control_data.validation_fn.clone() {
|
||||||
|
let validation_fn = if let Some(show_when) = built_control_data.show_when.clone() {
|
||||||
|
// we want the validation function to always succeed for hidden components
|
||||||
|
// thus, we need to modify the validation function
|
||||||
|
let cx = self.cx.clone();
|
||||||
|
let new_validation_fn = move |fd: &FD| {
|
||||||
|
let (fd_signal, _) = create_signal(fd.clone());
|
||||||
|
if !show_when(fd_signal.into(), cx.clone()) {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
validation_fn(fd)
|
||||||
|
};
|
||||||
|
Rc::new(new_validation_fn)
|
||||||
|
} else {
|
||||||
|
validation_fn
|
||||||
|
};
|
||||||
|
|
||||||
|
self.validations.push(validation_fn);
|
||||||
|
}
|
||||||
|
|
||||||
|
let cx = self.cx.clone();
|
||||||
|
let render_fn = move |fs: Rc<FD::Style>, fd: RwSignal<FD>| {
|
||||||
|
let (view, cb) = Self::build_control_view(fd, fs, built_control_data, cx);
|
||||||
|
(view, Some(cb))
|
||||||
|
};
|
||||||
|
|
||||||
|
self.render_fns.push(Box::new(render_fn));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper for building all the functions and everything needed to render
|
||||||
|
/// the view.
|
||||||
|
fn build_control_view<C: ControlData, FDT: 'static>(
|
||||||
|
fd: RwSignal<FD>,
|
||||||
|
fs: Rc<FD::Style>,
|
||||||
|
control_data: BuiltControlData<FD, C, FDT>,
|
||||||
|
cx: Rc<FD::Context>,
|
||||||
|
) -> (View, Box<dyn ValidationCb>) {
|
||||||
|
let BuiltControlData {
|
||||||
|
render_data,
|
||||||
|
getter,
|
||||||
|
setter,
|
||||||
|
parse_fn,
|
||||||
|
unparse_fn,
|
||||||
|
validation_fn,
|
||||||
|
show_when,
|
||||||
|
} = control_data;
|
||||||
|
|
||||||
|
let render_data = Rc::new(render_data);
|
||||||
|
let (validation_signal, validation_signal_set) = create_signal(Ok(()));
|
||||||
|
let validation_fn_clone = validation_fn.clone();
|
||||||
|
let value_getter = move || {
|
||||||
|
let fd = fd.get();
|
||||||
|
|
||||||
|
// rerun validation if it is failing
|
||||||
|
if validation_signal.get_untracked().is_err() {
|
||||||
|
if let Some(ref validation_fn) = validation_fn_clone {
|
||||||
|
let validation_result = validation_fn(&fd);
|
||||||
|
// if validation succeeds this time, resolve the validation error
|
||||||
|
if validation_result.is_ok() {
|
||||||
|
validation_signal_set.set(Ok(()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
unparse_fn(getter(fd))
|
||||||
|
};
|
||||||
|
let value_getter = value_getter.into_signal();
|
||||||
|
|
||||||
|
let cloned_show_when = show_when.clone();
|
||||||
|
let cloned_cx = cx.clone();
|
||||||
|
let validation_cb = move || {
|
||||||
|
// first check if the validation signal is an error so that we
|
||||||
|
// can fail on parsing issues too
|
||||||
|
if let Some(Err(_)) = validation_signal.try_get_untracked() {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// validation for non-visible fields always succeeds
|
||||||
|
if let Some(ref show_when) = cloned_show_when {
|
||||||
|
if !show_when(fd.into(), cloned_cx.clone()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// run the validation function on the value now
|
||||||
|
let validation_fn = match validation_fn {
|
||||||
|
Some(ref v) => v,
|
||||||
|
None => return true, // No validation function so validation passes
|
||||||
|
};
|
||||||
|
|
||||||
|
let data = fd.get_untracked();
|
||||||
|
let validation_result = validation_fn(&data);
|
||||||
|
let succeeded = validation_result.is_ok();
|
||||||
|
validation_signal_set.set(validation_result);
|
||||||
|
succeeded
|
||||||
|
};
|
||||||
|
let validation_cb = Box::new(validation_cb);
|
||||||
|
|
||||||
|
let value_setter = Self::create_value_setter(
|
||||||
|
validation_cb.clone(),
|
||||||
|
validation_signal_set,
|
||||||
|
parse_fn,
|
||||||
|
setter,
|
||||||
|
fd,
|
||||||
|
);
|
||||||
|
|
||||||
|
let view = move || {
|
||||||
|
C::build_control(
|
||||||
|
&*fs,
|
||||||
|
render_data.clone(),
|
||||||
|
value_getter,
|
||||||
|
value_setter.clone(),
|
||||||
|
validation_signal.into(),
|
||||||
|
)
|
||||||
|
};
|
||||||
|
let view = match show_when {
|
||||||
|
Some(when) => {
|
||||||
|
let when = move || when(fd.into(), cx.clone());
|
||||||
|
view! { <Show when=when>{view.clone()}</Show> }
|
||||||
|
}
|
||||||
|
None => view(),
|
||||||
|
};
|
||||||
|
(view, validation_cb)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Helper for creating a setter function.
|
||||||
|
fn create_value_setter<CRT: 'static, FDT: 'static>(
|
||||||
|
validation_cb: Box<dyn Fn() -> bool + 'static>,
|
||||||
|
validation_signal_set: WriteSignal<Result<(), String>>,
|
||||||
|
parse_fn: Box<dyn ParseFn<CRT, FDT>>,
|
||||||
|
setter: Rc<dyn FieldSetter<FD, FDT>>,
|
||||||
|
fd: RwSignal<FD>,
|
||||||
|
) -> Rc<dyn Fn(CRT) + 'static> {
|
||||||
|
let value_setter = move |value| {
|
||||||
|
let parsed = match parse_fn(value) {
|
||||||
|
Ok(p) => {
|
||||||
|
validation_signal_set.set(Ok(()));
|
||||||
|
p
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
validation_signal_set.set(Err(e));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// parse succeeded, update value and validate
|
||||||
|
fd.update(|data| {
|
||||||
|
setter(data, parsed);
|
||||||
|
});
|
||||||
|
|
||||||
|
// run validation
|
||||||
|
(validation_cb)();
|
||||||
|
};
|
||||||
|
Rc::new(value_setter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the direct send version of the form.
|
||||||
|
pub(crate) fn build_form<ServFn>(
|
||||||
|
self,
|
||||||
|
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
|
||||||
|
fd: FD,
|
||||||
|
fs: FD::Style,
|
||||||
|
) -> Form<FD>
|
||||||
|
where
|
||||||
|
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
|
||||||
|
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
|
||||||
|
From<FormData>,
|
||||||
|
ServFn: From<FD>,
|
||||||
|
{
|
||||||
|
let fd = create_rw_signal(fd);
|
||||||
|
let fs = Rc::new(fs);
|
||||||
|
|
||||||
|
let (views, validation_cbs): (Vec<_>, Vec<_>) = self
|
||||||
|
.render_fns
|
||||||
|
.into_iter()
|
||||||
|
.map(|r_fn| r_fn(fs.clone(), fd))
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
let elements = fs.form_frame(ControlRenderData {
|
||||||
|
data: views.into_view(),
|
||||||
|
styles: self.styles,
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_submit = move |ev: SubmitEvent| {
|
||||||
|
let mut failed = false;
|
||||||
|
for validation in validation_cbs.iter().flatten() {
|
||||||
|
if !validation() {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failed {
|
||||||
|
ev.prevent_default();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ev.prevent_default();
|
||||||
|
let server_fn = ServFn::from(fd.get_untracked());
|
||||||
|
action.dispatch(server_fn);
|
||||||
|
};
|
||||||
|
|
||||||
|
let view = view! {
|
||||||
|
<ActionForm action=action on:submit=on_submit>
|
||||||
|
{elements}
|
||||||
|
</ActionForm>
|
||||||
|
};
|
||||||
|
|
||||||
|
Form {
|
||||||
|
fd,
|
||||||
|
validations: self.validations,
|
||||||
|
view,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the action form version of the form.
|
||||||
|
pub(crate) fn build_action_form<ServFn>(
|
||||||
|
self,
|
||||||
|
action: Action<ServFn, Result<ServFn::Output, ServerFnError<ServFn::Error>>>,
|
||||||
|
fd: FD,
|
||||||
|
fs: FD::Style,
|
||||||
|
) -> Form<FD>
|
||||||
|
where
|
||||||
|
ServFn: DeserializeOwned + ServerFn<InputEncoding = PostUrl> + 'static,
|
||||||
|
<<ServFn::Client as Client<ServFn::Error>>::Request as ClientReq<ServFn::Error>>::FormData:
|
||||||
|
From<FormData>,
|
||||||
|
{
|
||||||
|
let fd = create_rw_signal(fd);
|
||||||
|
let fs = Rc::new(fs);
|
||||||
|
|
||||||
|
let (views, validation_cbs): (Vec<_>, Vec<_>) = self
|
||||||
|
.render_fns
|
||||||
|
.into_iter()
|
||||||
|
.map(|r_fn| r_fn(fs.clone(), fd))
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
let elements = fs.form_frame(ControlRenderData {
|
||||||
|
data: views.into_view(),
|
||||||
|
styles: self.styles,
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_submit = move |ev: SubmitEvent| {
|
||||||
|
let mut failed = false;
|
||||||
|
for validation in validation_cbs.iter().flatten() {
|
||||||
|
if !validation() {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failed {
|
||||||
|
ev.prevent_default();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let view = view! {
|
||||||
|
<ActionForm action=action on:submit=on_submit>
|
||||||
|
{elements}
|
||||||
|
</ActionForm>
|
||||||
|
};
|
||||||
|
|
||||||
|
Form {
|
||||||
|
fd,
|
||||||
|
validations: self.validations,
|
||||||
|
view,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// builds the plain form version of the form.
|
||||||
|
pub(crate) fn build_plain_form(self, url: String, fd: FD, fs: FD::Style) -> Form<FD> {
|
||||||
|
let fd = create_rw_signal(fd);
|
||||||
|
let fs = Rc::new(fs);
|
||||||
|
|
||||||
|
let (views, validation_cbs): (Vec<_>, Vec<_>) = self
|
||||||
|
.render_fns
|
||||||
|
.into_iter()
|
||||||
|
.map(|r_fn| r_fn(fs.clone(), fd))
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
let elements = fs.form_frame(ControlRenderData {
|
||||||
|
data: views.into_view(),
|
||||||
|
styles: self.styles,
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_submit = move |ev: SubmitEvent| {
|
||||||
|
let mut failed = false;
|
||||||
|
for validation in validation_cbs.iter().flatten() {
|
||||||
|
if !validation() {
|
||||||
|
failed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if failed {
|
||||||
|
ev.prevent_default();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let view = view! {
|
||||||
|
<Form action=url on:submit=on_submit>
|
||||||
|
{elements}
|
||||||
|
</Form>
|
||||||
|
};
|
||||||
|
|
||||||
|
Form {
|
||||||
|
fd,
|
||||||
|
validations: self.validations,
|
||||||
|
view,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a [`FormValidator`] from this builder.
|
||||||
|
pub(crate) fn validator(&self) -> FormValidator<FD> {
|
||||||
|
FormValidator {
|
||||||
|
validations: self.validations.clone(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+12
-1
@@ -1,3 +1,14 @@
|
|||||||
|
//! `leptos_form_tool` offers a declaritve way to create forms for
|
||||||
|
//! [leptos](https://leptos.dev/).
|
||||||
|
//!
|
||||||
|
//! To learn more, see the
|
||||||
|
//! [README.md](https://github.com/MitchellMarinoDev/leptos_form_tool/src/branch/main/README.md)
|
||||||
pub mod controls;
|
pub mod controls;
|
||||||
pub mod form;
|
mod form;
|
||||||
|
mod form_builder;
|
||||||
pub mod styles;
|
pub mod styles;
|
||||||
|
mod validation_builder;
|
||||||
|
|
||||||
|
pub use form::{Form, FormToolData, FormValidator};
|
||||||
|
pub use form_builder::FormBuilder;
|
||||||
|
pub use validation_builder::ValidationBuilder;
|
||||||
|
|||||||
+354
-94
@@ -1,155 +1,415 @@
|
|||||||
use super::FormStyle;
|
use super::FormStyle;
|
||||||
use crate::controls::{
|
use crate::{
|
||||||
heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData,
|
controls::{
|
||||||
text_input::TextInputData, ControlData, ControlRenderData,
|
button::ButtonData, checkbox::CheckboxData, heading::HeadingData, hidden::HiddenData,
|
||||||
|
output::OutputData, radio_buttons::RadioButtonsData, select::SelectData,
|
||||||
|
slider::SliderData, spacer::SpacerData, stepper::StepperData, submit::SubmitData,
|
||||||
|
text_area::TextAreaData, text_input::TextInputData, ControlData, ControlRenderData,
|
||||||
|
},
|
||||||
|
FormToolData,
|
||||||
};
|
};
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
use leptos_router::Form;
|
use std::rc::Rc;
|
||||||
|
use web_sys::MouseEvent;
|
||||||
|
|
||||||
pub enum GridFormStylingAttributes {
|
/// Styling attributes for the [`GridFormStyle`].
|
||||||
|
#[derive(Clone, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
pub enum GFStyleAttr {
|
||||||
|
/// Set the width of the control out of 12.
|
||||||
|
/// Defaults to 12/12 (full width).
|
||||||
Width(u32),
|
Width(u32),
|
||||||
|
/// Adds a tooltip to the control.
|
||||||
|
/// This sets the html title attribute, which shows the text when the
|
||||||
|
/// user hovers their mouse over the control for a couple seconds.
|
||||||
|
Tooltip(String),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A complete useable example for defining a form style.
|
||||||
|
///
|
||||||
|
/// This can be used directly in by your form, or you can copy `grid_form.rs`
|
||||||
|
/// into your project and make any neccesary change. You will also want to
|
||||||
|
/// copy `grid_form.scss` from the git repo and put that in the `styles`
|
||||||
|
/// directory for your leptos project to get all the styling.
|
||||||
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
pub struct GridFormStyle;
|
pub struct GridFormStyle;
|
||||||
|
|
||||||
impl FormStyle for GridFormStyle {
|
impl FormStyle for GridFormStyle {
|
||||||
type StylingAttributes = GridFormStylingAttributes;
|
type StylingAttributes = GFStyleAttr;
|
||||||
|
|
||||||
|
fn form_frame(&self, form: ControlRenderData<Self, View>) -> View {
|
||||||
|
view! { <div class="form_grid">{form.data}</div> }.into_view()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A common function that wraps the given view in the styles
|
||||||
|
fn custom_component(&self, styles: &[Self::StylingAttributes], inner: View) -> View {
|
||||||
|
let mut width = 12;
|
||||||
|
let mut tooltip = None;
|
||||||
|
for style in styles.iter() {
|
||||||
|
match style {
|
||||||
|
GFStyleAttr::Width(w) => width = *w,
|
||||||
|
GFStyleAttr::Tooltip(t) => tooltip = Some(t),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TODO: something about an on-submit thing
|
|
||||||
fn form_frame(&self, children: View) -> View {
|
|
||||||
view! {
|
view! {
|
||||||
<Form action="" class="form_grid">
|
<div style:grid-column=format!("span {}", width) title=tooltip>
|
||||||
{children}
|
{inner}
|
||||||
</Form>
|
</div>
|
||||||
}
|
}
|
||||||
.into_view()
|
.into_view()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View {
|
fn group(&self, group: Rc<ControlRenderData<Self, View>>) -> View {
|
||||||
|
let view = view! { <div class="form_group form_grid">{&group.data}</div> }.into_view();
|
||||||
|
|
||||||
|
self.custom_component(&group.styles, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spacer(&self, control: Rc<ControlRenderData<Self, SpacerData>>) -> View {
|
||||||
|
self.custom_component(
|
||||||
|
&control.styles,
|
||||||
|
view! { <div style:height=control.data.height.as_ref()></div> }.into_view(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn heading(&self, control: Rc<ControlRenderData<Self, HeadingData>>) -> View {
|
||||||
|
self.custom_component(
|
||||||
|
&control.styles,
|
||||||
|
view! { <h2 class="form_heading">{control.data.title.clone()}</h2> }.into_view(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn submit(&self, control: Rc<ControlRenderData<Self, SubmitData>>) -> View {
|
||||||
|
self.custom_component(
|
||||||
|
&control.styles,
|
||||||
|
view! { <input type="submit" value=&control.data.text class="form_submit"/> }
|
||||||
|
.into_view(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn button<FD: FormToolData>(
|
||||||
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, ButtonData<FD>>>,
|
||||||
|
data_signal: RwSignal<FD>,
|
||||||
|
) -> View {
|
||||||
|
let action = control.data.action.clone();
|
||||||
|
let on_click = move |ev: MouseEvent| {
|
||||||
|
if let Some(action) = action.clone() {
|
||||||
|
action(ev, data_signal)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let view = view! {
|
||||||
|
<button type="button" class="form_button" on:click=on_click>
|
||||||
|
{&control.data.text}
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
.into_view();
|
||||||
|
|
||||||
|
self.custom_component(&control.styles, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn output(
|
||||||
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, OutputData>>,
|
||||||
|
value_getter: Option<Signal<String>>,
|
||||||
|
) -> View {
|
||||||
|
let view = view! { <span>{move || value_getter.map(|g| g.get())}</span> }.into_view();
|
||||||
|
self.custom_component(&control.styles, view)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hidden(
|
||||||
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, HiddenData>>,
|
||||||
|
value_getter: Option<Signal<String>>,
|
||||||
|
) -> View {
|
||||||
|
let value_getter = move || value_getter.map(|g| g.get());
|
||||||
view! {
|
view! {
|
||||||
<h2 class="form_heading">
|
<input
|
||||||
{&control.data.title}
|
name=&control.data.name
|
||||||
</h2>
|
prop:value=value_getter
|
||||||
|
style="visibility: hidden; column-span: none"
|
||||||
|
/>
|
||||||
}
|
}
|
||||||
.into_view()
|
.into_view()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_input(
|
fn text_input(
|
||||||
&self,
|
&self,
|
||||||
control: ControlRenderData<Self, TextInputData>,
|
control: Rc<ControlRenderData<Self, TextInputData>>,
|
||||||
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
|
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
|
||||||
value_setter: Box<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
|
value_setter: Rc<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> View {
|
) -> View {
|
||||||
view! {
|
let view = view! {
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<label for=&control.data.name class="form_label">
|
||||||
<label for={&control.data.name} class="form_label">
|
|
||||||
{control.data.label.as_ref()}
|
{control.data.label.as_ref()}
|
||||||
</label>
|
</label>
|
||||||
<span class="form_error">
|
<span class="form_error">{move || validation_state.get().err()}</span>
|
||||||
{move || format!("{}", validation_state.get().err().unwrap_or_default())}
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
// TODO:
|
|
||||||
type=control.data.input_type
|
type=control.data.input_type
|
||||||
id=&control.data.name
|
id=&control.data.name
|
||||||
name=control.data.name
|
name=&control.data.name
|
||||||
placeholder=control.data.placeholder
|
placeholder=control.data.placeholder.as_ref()
|
||||||
prop:value=move || value_getter.get()
|
|
||||||
on:change=move |ev| {
|
|
||||||
value_setter(event_target_value(&ev));
|
|
||||||
}
|
|
||||||
class="form_input"
|
class="form_input"
|
||||||
class=("form_input_invalid", move || validation_state.get().is_err())
|
class=("form_input_invalid", move || validation_state.get().is_err())
|
||||||
/>
|
prop:value=move || value_getter.get()
|
||||||
</div>
|
on:focusout=move |ev| {
|
||||||
}
|
|
||||||
.into_view()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn select(
|
|
||||||
&self,
|
|
||||||
control: ControlRenderData<Self, SelectData>,
|
|
||||||
value_getter: Signal<<SelectData as ControlData>::ReturnType>,
|
|
||||||
value_setter: Box<dyn Fn(<SelectData as ControlData>::ReturnType)>,
|
|
||||||
validation_state: Signal<Result<(), String>>,
|
|
||||||
) -> View {
|
|
||||||
let options_view = control
|
|
||||||
.data
|
|
||||||
.options
|
|
||||||
.into_iter()
|
|
||||||
.map(|value| {
|
|
||||||
// let value = value;
|
|
||||||
let cloned_value = value.clone();
|
|
||||||
view! {
|
|
||||||
<option
|
|
||||||
value={value}
|
|
||||||
selected=move || value_getter.get() == *cloned_value
|
|
||||||
>
|
|
||||||
*value
|
|
||||||
</option>
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect_view();
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<div>
|
|
||||||
{move || format!("{:?}", validation_state.get())}
|
|
||||||
<select
|
|
||||||
id=&control.data.name
|
|
||||||
name=control.data.name
|
|
||||||
class="form_input"
|
|
||||||
on:change=move |ev| {
|
|
||||||
value_setter(event_target_value(&ev));
|
value_setter(event_target_value(&ev));
|
||||||
}
|
}
|
||||||
>
|
|
||||||
{options_view}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
.into_view()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View {
|
|
||||||
view! {
|
|
||||||
<input
|
|
||||||
type="submit"
|
|
||||||
value=control.data.text
|
|
||||||
class="w-full form_submit"
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
.into_view()
|
.into_view();
|
||||||
|
|
||||||
|
self.custom_component(&control.styles, view)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn text_area(
|
fn text_area(
|
||||||
&self,
|
&self,
|
||||||
control: ControlRenderData<Self, TextAreaData>,
|
control: Rc<ControlRenderData<Self, TextAreaData>>,
|
||||||
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
|
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
|
||||||
value_setter: Box<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
|
value_setter: Rc<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> View {
|
) -> View {
|
||||||
view! {
|
let view = view! {
|
||||||
<div>
|
<div>
|
||||||
{move || format!("{:?}", validation_state.get())}
|
<label for=&control.data.name class="form_label">
|
||||||
|
{control.data.label.as_ref()}
|
||||||
|
</label>
|
||||||
|
<span class="form_error">{move || validation_state.get().err()}</span>
|
||||||
|
</div>
|
||||||
<textarea
|
<textarea
|
||||||
id=&control.data.name
|
id=&control.data.name
|
||||||
name=control.data.name
|
name=&control.data.name
|
||||||
placeholder=control.data.placeholder
|
placeholder=control.data.placeholder.as_ref()
|
||||||
|
prop:value=move || value_getter.get()
|
||||||
class="form_input"
|
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()
|
prop:value=move || value_getter.get()
|
||||||
on:change=move |ev| {
|
on:change=move |ev| {
|
||||||
value_setter(event_target_value(&ev));
|
value_setter(event_target_value(&ev));
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
.into_view()
|
.into_view();
|
||||||
|
|
||||||
|
self.custom_component(&control.styles, view)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn custom_component(&self, view: View) -> View {
|
fn slider(
|
||||||
view
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, SliderData>>,
|
||||||
|
value_getter: Signal<<SliderData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Rc<dyn Fn(<SliderData as ControlData>::ReturnType)>,
|
||||||
|
validation_state: Signal<Result<(), String>>,
|
||||||
|
) -> View {
|
||||||
|
let view = view! {
|
||||||
|
<div>
|
||||||
|
<label for=&control.data.name class="form_label">
|
||||||
|
{control.data.label.as_ref()}
|
||||||
|
</label>
|
||||||
|
<span class="form_error">{move || validation_state.get().err()}</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="range"
|
||||||
|
id=&control.data.name
|
||||||
|
name=&control.data.name
|
||||||
|
min=control.data.min
|
||||||
|
max=control.data.max
|
||||||
|
class="form_input"
|
||||||
|
class=("form_input_invalid", move || validation_state.get().is_err())
|
||||||
|
prop:value=move || value_getter.get()
|
||||||
|
on:input=move |ev| {
|
||||||
|
let value = event_target_value(&ev).parse::<i32>().ok();
|
||||||
|
if let Some(value) = value {
|
||||||
|
value_setter(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
.into_view();
|
||||||
|
|
||||||
|
self.custom_component(&control.styles, view)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+153
-23
@@ -1,40 +1,170 @@
|
|||||||
mod grid_form;
|
mod grid_form;
|
||||||
pub use grid_form::{GridFormStyle, GridFormStylingAttributes};
|
|
||||||
|
|
||||||
use crate::controls::{
|
use crate::{
|
||||||
heading::HeadingData, select::SelectData, submit::SubmitData, text_area::TextAreaData,
|
controls::{
|
||||||
text_input::TextInputData, ControlData, ControlRenderData,
|
button::ButtonData, checkbox::CheckboxData, heading::HeadingData, hidden::HiddenData,
|
||||||
|
output::OutputData, radio_buttons::RadioButtonsData, select::SelectData,
|
||||||
|
slider::SliderData, spacer::SpacerData, stepper::StepperData, submit::SubmitData,
|
||||||
|
text_area::TextAreaData, text_input::TextInputData, ControlData, ControlRenderData,
|
||||||
|
},
|
||||||
|
FormToolData,
|
||||||
};
|
};
|
||||||
use leptos::{Signal, View};
|
use leptos::{RwSignal, Signal, View};
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
pub trait FormStyle: Default + 'static {
|
pub use grid_form::{GFStyleAttr, GridFormStyle};
|
||||||
|
|
||||||
|
/// Defines a way to style a form.
|
||||||
|
///
|
||||||
|
/// Provides methods for rendering all the controls.
|
||||||
|
/// This provider is in charge of figuring out what html elements should be
|
||||||
|
/// rendered and how they should be styled.
|
||||||
|
pub trait FormStyle: 'static {
|
||||||
|
/// The type of styling attributes that this [`FormStyle`] takes.
|
||||||
|
///
|
||||||
|
/// These styling attributes can be applied to any of the controls.
|
||||||
type StylingAttributes;
|
type StylingAttributes;
|
||||||
|
|
||||||
fn form_frame(&self, children: View) -> View;
|
/// Render any containing components for the form.
|
||||||
fn heading(&self, control: ControlRenderData<Self, HeadingData>) -> View;
|
///
|
||||||
|
/// This allows you to wrap all the form components
|
||||||
|
/// in another component if neccisary.
|
||||||
|
///
|
||||||
|
/// Do NOT wrap it in an actual `form` element; any
|
||||||
|
/// wrapping should be done with `div` or similar elements.
|
||||||
|
fn form_frame(&self, form: ControlRenderData<Self, View>) -> View;
|
||||||
|
|
||||||
|
/// Wraps the view of a custom component.
|
||||||
|
///
|
||||||
|
/// The rendering of the custom component is given by the `inner` view.
|
||||||
|
/// Here the styler has a chance wrap the view with other components, or
|
||||||
|
/// applying the styling attributes.
|
||||||
|
///
|
||||||
|
/// This method does not need to be called by the custom component, but
|
||||||
|
/// the custom component may make use of this method for the
|
||||||
|
/// aforementioned reasons.
|
||||||
|
fn custom_component(&self, style: &[Self::StylingAttributes], inner: View) -> View;
|
||||||
|
|
||||||
|
/// Renders a group.
|
||||||
|
///
|
||||||
|
/// The inner view for the group's components is provided.
|
||||||
|
/// This method should wrap the group in any visual grouping elements,
|
||||||
|
/// and apply the styles.
|
||||||
|
fn group(&self, group: Rc<ControlRenderData<Self, View>>) -> View;
|
||||||
|
|
||||||
|
/// Renders a spacer.
|
||||||
|
///
|
||||||
|
/// See [`SpacerData`].
|
||||||
|
fn spacer(&self, control: Rc<ControlRenderData<Self, SpacerData>>) -> View;
|
||||||
|
|
||||||
|
/// Renders a heading for a section of the form.
|
||||||
|
fn heading(&self, control: Rc<ControlRenderData<Self, HeadingData>>) -> View;
|
||||||
|
|
||||||
|
/// Renders a submit button.
|
||||||
|
///
|
||||||
|
/// See [`SubmitData`].
|
||||||
|
fn submit(&self, control: Rc<ControlRenderData<Self, SubmitData>>) -> View;
|
||||||
|
|
||||||
|
/// Renders a button.
|
||||||
|
///
|
||||||
|
/// See [`BuuttonData`]
|
||||||
|
fn button<FD: FormToolData>(
|
||||||
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, ButtonData<FD>>>,
|
||||||
|
data_signal: RwSignal<FD>,
|
||||||
|
) -> View;
|
||||||
|
|
||||||
|
/// Renders some output text.
|
||||||
|
///
|
||||||
|
/// See [`OutputData`].
|
||||||
|
fn output(
|
||||||
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, OutputData>>,
|
||||||
|
value_getter: Option<Signal<String>>,
|
||||||
|
) -> View;
|
||||||
|
|
||||||
|
/// Renders a input control that should be hidden from the user.
|
||||||
|
///
|
||||||
|
/// See [`HiddenData`].
|
||||||
|
fn hidden(
|
||||||
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, HiddenData>>,
|
||||||
|
value_getter: Option<Signal<String>>,
|
||||||
|
) -> View;
|
||||||
|
|
||||||
|
/// Renders a text input control.
|
||||||
|
///
|
||||||
|
/// See [`TextInputData`].
|
||||||
fn text_input(
|
fn text_input(
|
||||||
&self,
|
&self,
|
||||||
control: ControlRenderData<Self, TextInputData>,
|
control: Rc<ControlRenderData<Self, TextInputData>>,
|
||||||
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
|
value_getter: Signal<<TextInputData as ControlData>::ReturnType>,
|
||||||
value_setter: Box<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
|
value_setter: Rc<dyn Fn(<TextInputData as ControlData>::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> View;
|
) -> View;
|
||||||
fn select(
|
|
||||||
&self,
|
/// Renders a text area control.
|
||||||
control: ControlRenderData<Self, SelectData>,
|
///
|
||||||
value_getter: Signal<<SelectData as ControlData>::ReturnType>,
|
/// See [`TextAreaData`].
|
||||||
value_setter: Box<dyn Fn(<SelectData as ControlData>::ReturnType)>,
|
|
||||||
validation_state: Signal<Result<(), String>>,
|
|
||||||
) -> View;
|
|
||||||
fn submit(&self, control: ControlRenderData<Self, SubmitData>) -> View;
|
|
||||||
fn text_area(
|
fn text_area(
|
||||||
&self,
|
&self,
|
||||||
control: ControlRenderData<Self, TextAreaData>,
|
control: Rc<ControlRenderData<Self, TextAreaData>>,
|
||||||
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
|
value_getter: Signal<<TextAreaData as ControlData>::ReturnType>,
|
||||||
value_setter: Box<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
|
value_setter: Rc<dyn Fn(<TextAreaData as ControlData>::ReturnType)>,
|
||||||
|
validation_state: Signal<Result<(), String>>,
|
||||||
|
) -> View;
|
||||||
|
|
||||||
|
/// Renders a group of radio buttons.
|
||||||
|
///
|
||||||
|
/// See [`RadioButtonsData`].
|
||||||
|
fn radio_buttons(
|
||||||
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, RadioButtonsData>>,
|
||||||
|
value_getter: Signal<<RadioButtonsData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Rc<dyn Fn(<RadioButtonsData as ControlData>::ReturnType)>,
|
||||||
|
validation_state: Signal<Result<(), String>>,
|
||||||
|
) -> View;
|
||||||
|
|
||||||
|
/// Renders a select (or dropdown) control.
|
||||||
|
///
|
||||||
|
/// See [`SelectData`].
|
||||||
|
fn select(
|
||||||
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, SelectData>>,
|
||||||
|
value_getter: Signal<<SelectData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Rc<dyn Fn(<SelectData as ControlData>::ReturnType)>,
|
||||||
|
validation_state: Signal<Result<(), String>>,
|
||||||
|
) -> View;
|
||||||
|
|
||||||
|
/// Renders a checkbox control.
|
||||||
|
///
|
||||||
|
/// See [`CheckboxData`].
|
||||||
|
fn checkbox(
|
||||||
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, CheckboxData>>,
|
||||||
|
value_getter: Signal<<CheckboxData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Rc<dyn Fn(<CheckboxData as ControlData>::ReturnType)>,
|
||||||
|
) -> View;
|
||||||
|
|
||||||
|
/// Renders a stepper control.
|
||||||
|
///
|
||||||
|
/// See [`StepperData`].
|
||||||
|
fn stepper(
|
||||||
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, StepperData>>,
|
||||||
|
value_getter: Signal<<StepperData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Rc<dyn Fn(<StepperData as ControlData>::ReturnType)>,
|
||||||
|
validation_state: Signal<Result<(), String>>,
|
||||||
|
) -> View;
|
||||||
|
|
||||||
|
/// Renders a slider control.
|
||||||
|
///
|
||||||
|
/// See [`SliderData`].
|
||||||
|
fn slider(
|
||||||
|
&self,
|
||||||
|
control: Rc<ControlRenderData<Self, SliderData>>,
|
||||||
|
value_getter: Signal<<SliderData as ControlData>::ReturnType>,
|
||||||
|
value_setter: Rc<dyn Fn(<SliderData as ControlData>::ReturnType)>,
|
||||||
validation_state: Signal<Result<(), String>>,
|
validation_state: Signal<Result<(), String>>,
|
||||||
) -> View;
|
) -> View;
|
||||||
// TODO: test custom component
|
|
||||||
fn custom_component(&self, view: View) -> View;
|
|
||||||
// TODO: add group
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,171 @@
|
|||||||
|
use crate::{controls::ValidationFn, FormToolData};
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
/// A function that validates a field.
|
||||||
|
///
|
||||||
|
/// This is similar to [`ValidationFn`](crate::controls::ValidationFn)
|
||||||
|
/// but takes a &str for the name of the field for improved error messages.
|
||||||
|
type ValidationBuilderFn<T> = dyn Fn(&str, &T) -> Result<(), String> + 'static;
|
||||||
|
|
||||||
|
/// A helper builder that allows you to specify a validation function
|
||||||
|
/// declaritivly
|
||||||
|
///
|
||||||
|
/// Using this builder is not required as validation functions can just be
|
||||||
|
/// closures, but for simple validation function this builder can be helpful
|
||||||
|
///
|
||||||
|
/// Validations are run in the order that they are called in the builder.
|
||||||
|
pub struct ValidationBuilder<FD: FormToolData, T: ?Sized + 'static> {
|
||||||
|
/// The name of the field, for error messages.
|
||||||
|
name: String,
|
||||||
|
/// The getter function for the field to validate.
|
||||||
|
field_fn: Box<dyn Fn(&FD) -> &T + 'static>,
|
||||||
|
/// The functions to be called when validating.
|
||||||
|
functions: Vec<Box<ValidationBuilderFn<T>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData, T: ?Sized + 'static> ValidationBuilder<FD, T> {
|
||||||
|
/// Creates a new empty [`ValidationBuilder`] on the given field.
|
||||||
|
pub fn for_field(field_fn: impl Fn(&FD) -> &T + 'static) -> Self {
|
||||||
|
ValidationBuilder {
|
||||||
|
name: String::from("Field"),
|
||||||
|
field_fn: Box::new(field_fn),
|
||||||
|
functions: Vec::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name of the field that is being validated.
|
||||||
|
///
|
||||||
|
/// This is the name that will be used for error messages.
|
||||||
|
pub fn named(mut self, name: impl ToString) -> Self {
|
||||||
|
self.name = name.to_string();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Adds a custom validation function.
|
||||||
|
///
|
||||||
|
/// The function should take the value as an argument and return
|
||||||
|
/// a [`Result<(), String>`], just like any other validation function.
|
||||||
|
pub fn custom(mut self, f: impl ValidationFn<T>) -> Self {
|
||||||
|
self.functions.push(Box::new(move |_name, value| f(value)));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Builds the action validation function.
|
||||||
|
pub fn build(self) -> impl ValidationFn<FD> {
|
||||||
|
move |form_data| {
|
||||||
|
let value = (self.field_fn)(form_data);
|
||||||
|
for f in self.functions.iter() {
|
||||||
|
match f(self.name.as_str(), value) {
|
||||||
|
Ok(()) => {}
|
||||||
|
err => return err,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData> ValidationBuilder<FD, str> {
|
||||||
|
/// Requires the field to not be empty.
|
||||||
|
pub fn required(mut self) -> Self {
|
||||||
|
self.functions.push(Box::new(move |name, value| {
|
||||||
|
if value.is_empty() {
|
||||||
|
Err(format!("{} is required", name))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requires the field's length to be at least `min_len`.
|
||||||
|
pub fn min_len(mut self, min_len: usize) -> Self {
|
||||||
|
self.functions.push(Box::new(move |name, value| {
|
||||||
|
if value.len() < min_len {
|
||||||
|
Err(format!("{} must be >= {} characters", name, min_len))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requires the field's length to be less than or equal to `min_len`.
|
||||||
|
pub fn max_len(mut self, max_len: usize) -> Self {
|
||||||
|
self.functions.push(Box::new(move |name, value| {
|
||||||
|
if value.len() > max_len {
|
||||||
|
Err(format!("{} must be <= {} characters", name, max_len))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requires the field to contain `pattern`.
|
||||||
|
pub fn contains(mut self, pattern: impl ToString) -> Self {
|
||||||
|
let pattern = pattern.to_string();
|
||||||
|
self.functions.push(Box::new(move |name, value| {
|
||||||
|
if !value.contains(&pattern) {
|
||||||
|
Err(format!("{} must contain {}", name, &pattern))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData, T: PartialOrd<T> + Display + 'static> ValidationBuilder<FD, T> {
|
||||||
|
/// Requires the value to be at least `min_value` according to
|
||||||
|
/// `PartialOrd`.
|
||||||
|
pub fn min_value(mut self, min_value: T) -> Self {
|
||||||
|
self.functions.push(Box::new(move |name, value| {
|
||||||
|
if value < &min_value {
|
||||||
|
Err(format!("{} mut be >= {}", name, min_value))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requires the value to be at most `max_value` according to
|
||||||
|
/// `PartialOrd`.
|
||||||
|
pub fn max_value(mut self, max_value: T) -> Self {
|
||||||
|
self.functions.push(Box::new(move |name, value| {
|
||||||
|
if value > &max_value {
|
||||||
|
Err(format!("{} mut be <= {}", name, max_value))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<FD: FormToolData, T: PartialEq<T> + Display + 'static> ValidationBuilder<FD, T> {
|
||||||
|
/// Requires the field to be in the provided whitelist.
|
||||||
|
pub fn whitelist(mut self, whitelist: Vec<T>) -> Self {
|
||||||
|
self.functions.push(Box::new(move |name, value| {
|
||||||
|
if !whitelist.contains(value) {
|
||||||
|
Err(format!("{} cannot be {}", name, value))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Requires the field to not be in the provided blacklist.
|
||||||
|
pub fn blacklist(mut self, blacklist: Vec<T>) -> Self {
|
||||||
|
self.functions.push(Box::new(move |name, value| {
|
||||||
|
if blacklist.contains(value) {
|
||||||
|
Err(format!("{} cannot be {}", name, value))
|
||||||
|
} else {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user