# Ridstack-Form
Type-safe form handling for Dioxus.
## Quickstart
```rust, no_run
use dioxus::prelude::*;
use ridstack_form::prelude::*;
use thiserror::Error;
fn main() {
dioxus::launch(App);
}
// reusable component
#[component]
fn TextField<TForm, TField>(
#[props(optional)] id: String,
field_api: ReadSignal<FieldApi<TForm, TField>>,
#[props(extends=GlobalAttributes,extends=input)] attributes: Vec<Attribute>,
) -> Element
where
TForm: FieldStateProvider<TField>,
TField: Clone + PartialEq + 'static,
TForm::FieldValue: FieldValue<PrimitiveValue = String>,
{
rsx! {
label{
r#for: &id
}
input{
id,
value: field_api().value(),
onblur: field_api.peek().handlers().on_blur(),
oninput: move|evt|{
let _ = field_api.peek().handlers().on_input()(Either::Right(evt.value()));
},
..attributes
}
div{if let Some(err) = (field_api().state().errors)().get(0){
p{
{err.error_value()}
}
}
}
}
}
#[derive(Store, GenForm, PartialEq, Eq)]
struct Example {
// You can skip generation of some fields by removing this attribute.
#[field(error(FormError), accessor = "GetName")]
name: String,
// other fields...
}
#[derive(Debug, Error, PartialEq, Eq, PartialOrd, Ord, Clone)]
enum FormError {
#[error("This error should never occur.")]
Infallible,
}
impl FieldError for FormError {}
impl From<Infallible> for FormError {
fn from(_value: Infallible) -> Self {
Self::Infallible
}
}
#[component]
fn App() -> Element {
rsx! {
ExampleForm {
on_submit: |_data|{},
ExampleField {
field:GetName,
component:|field_api|{
rsx! {
TextField { field_api}
}
}
}
}
}
}
```
## Status
<ul>
<li> Validify feature is still in WIP. So it should be disabled.
</li>
<li>Tests aren't present as I'm too lazy to write some but I also have
a lot of other work. The ones in the test folder aren't really tests but a blueprint to write this Readme.
<li>Ready to be used just docs and readme needs to be updated.</li>
</ul>
## Let's Deep Dive
### Architecture
Check [`architecture.svg`](./architecture.svg) in the repo.
### Field Value
For a field to work, the value of that field must apply `FieldValue` trait.
Every `FieldValue` needs to point a primitve field value which is basically `String`, `u8`, `u16`, `bool`, etc.
The primitive field value is required for the components to get data of the field.
Interconversion btw field and primitive field value is needed.
By default `FieldValue` and `PrimitiveFieldValue` is applied for most of rust built-in types.
You can also apply `PrimitiveFieldValue` on your own struct or enum. Then create components
accessing it as field data.
There's also a macro named `FieldValue` which builds `FieldValue` trait for wrapper-like structs.
It is helpful to auto build `FieldValue` if the underlying data implements `PrimitiveFieldValue`.
### Reusable Components
Ridstack form is made on the principle of reusable components. It means you've to make components
or parts of your form only once and you can use it as many times as you want (like Tanstack Form).
You can make a component work only on a particular type of field. (Its based on the `PrimitiveFieldValue` type).
An example is here:
```rust, no_run
use dioxus::prelude::*;
use ridstack_form::prelude::*;
#[component]
fn TextField<TForm, TField>(
#[props(optional)] id: String,
field_api: ReadSignal<FieldApi<TForm, TField>>,
#[props(extends=GlobalAttributes,extends=input)] attributes: Vec<Attribute>,
) -> Element
where
TForm: FieldStateProvider<TField>,
TField: Clone + PartialEq + 'static,
TForm::FieldValue: FieldValue<PrimitiveValue = String>,
{
rsx! {
label{
r#for: &id
}
input{
id,
value: field_api().value(),
onblur: field_api.peek().handlers().on_blur(),
oninput: move|evt|{
let _ = field_api.peek().handlers().on_input()(Either::Right(evt.value()));
},
..attributes
}
div{if let Some(err) = (field_api().state().errors)().get(0){
p{
{err.error_value()}
}
}
}
}
}
```
Here in the form generics, it's specified that only those types whose `PrimitiveFieldValue` is of `String` type.
In this way the type-safety of reusable components is maintained.
For your reference, I'm putting how to make a good components system in a separate dir (`components` dir) inside the **repo's** example dir.
If you want raw errors (errors in `String` form), you can use the `use_raw_field_errors` hook, which will directly return the
error of highest priority, maintained by `Ord` and `PartialOrd` (0 is the hightest, 1 next, ...) in form of `String`.
**Note: This is directly copied from my project and thus many packages are unspecified.**
**Note: Ensure that `use_raw_field_errors` should be used inside Field component or its children as it may give a context error.**
### Manual Setup
1. Create your data struct.
```rust, no_run
#[derive(Store, PartialEq, Eq)]
struct TestData {
name: String,
// more fields...
}
```
2. Create error details for the fields for the form.
it should handle this infallible error returned from string.
and these derive traits are also required.
You can also use many different types of error.
Basically these errors will be used for validation which
will be done by you only.
```rust,no_run
#[derive(Debug, thiserror::Error, PartialEq, Eq, PartialOrd, Ord, Clone)]
enum FormError {
#[error("This error should never occur.")]
Infallible,
#[error("Name already exists.")]
NameAlreadyExists,
#[error("Name shouldn't start with R")]
NameShouldntStartWithR
}
impl FieldError for FormError {}
impl From<Infallible> for FormError {
fn from(_value: Infallible) -> Self {
Self::Infallible
}
}
```
3. Create a struct for storing your fields. Use Store macro on it.
```rust, no_run
#[derive(Clone, Store, Default)]
struct TestFieldState {
name: FieldState<String, FormError>,
// more fields...
}
```
4. Create form state. Its advised (and mandatory also),
to use Store on the `TestFieldState` and `FormState`.
```rust,no_run
#[derive(Store, Clone)]
struct TestForm {
field_state: Store<TestFieldState>,
// required, for managing the form state.
form_state: Store<FormState>,
}
```
5. Let's start applying traits.
1. `FormSubmitInput` returns the original data you've created.
```rust,no_run
impl FormSubmitInput for TestForm {
type SubmitInput = TestData;
fn build_submit_input(&mut self) -> Option<Self::SubmitInput> {
Some(TestData {
name: self.field_state.name().data().peek().cloned(),
})
}
}
```
2. `GetFieldRegistry` returns the field_registry
```rust,no_run
impl GetFieldRegistry for TestForm {
type FieldRegistry = Self;
fn get_field_registry(&self) -> Self::FieldRegistry {
self.clone()
}
}
```
3. Here's the important part. Each field of the form needs an accessor
which is mostly (there are other patterns also) an empty struct with some derive impls.
Then we apply the `FieldStateProvider` taking this accessor as generic over the `FieldRegistry`.
We could've returned and made the `TestFieldState` the `FieldStateProvider` but the API doesn't allow
us and also it may break granular reactivity provided by the store.
```rust,no_run
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
struct GetName;
impl FieldStateProvider<GetName> for TestForm {
type FieldValue = String;
type FieldError = FormError;
fn get_field_state(
&self,
field: &GetName,
) -> Store<FieldState<Self::FieldValue, Self::FieldError>> {
self.field_state.name().into()
}
// name attr of the form element.
fn name(&self, field: &GetName) -> &'static str {
"name"
}
}
```
4. Now, we apply calculate can submit because it can't be auto-checked
whether the fields are ready to be submitted or not.
```rust,no_run
impl CalculateCanSubmit for TestForm {
fn calculate_can_submit(&mut self) -> bool {
let name_can_submit = self.field_state.name().can_submit();
let name_is_validating = self.field_state.name().is_validating()();
if !name_can_submit || name_is_validating {
self.form_state.can_submit().set(false);
return false;
}
return true;
}
}
```
5. We will apply some traits required for Form management.
The first is `GetFormState` which makes us return the form state.
The second is `InitForm` which returns a hook used to initialize the form
```rust,no_run
impl GetFormState for TestForm {
fn get_form_state(&self) -> Store<FormState> {
self.form_state.clone()
}
}
impl InitForm for TestForm {
fn use_form() -> Self {
let form_state = FormState::use_form_state();
let field_state = use_store(|| TestFieldState::default());
Self {
field_state,
form_state,
}
}
}
```
6. We need to manually check which field validators to be run on submit. For this we apply `ValidateOnSubmit` trait.
```rust,no_run
impl ValidateOnSubmit for TestForm {
fn validate_on_submit(&mut self) -> std::pin::Pin<Box<impl Future<Output = ()>>> {
if matches!(
self.field_state.name().validate_on()(),
ValidateOn::All | ValidateOn::Submit
) {
self.field_state.name().validate();
}
Box::pin(async move {})
}
}
```
7. Create some components shown above. We mostly don't directly write component
as we don't get type safety in the arguments of the closure.
8. Now the final piece. That's using this form. To get started, use the `use_form_provider` hook and
pass your form type (here `TestForm`) in the turbofish operator.
9. Then use the `Form` component to provide states to the child. This is the power that if you use modals
or steppers, the form and field states will be preserved due to direct entry to the stores.
10. Then use the `Field` component to access the fields. Use the accessor structs to ensure that the `Field` component
points to the specified field. You can use the `validators` and `async_validators` prop to set sync and async validators
respectively. You can also use the `validate_on` prop to make sure that validators run before submit, on blur, on input or all.
**Note: `Form` and `Field` components need heavy generics of the Form struct (here `TestForm`). It is a better practice to already
put the generics in a separate component and use that instead.**
Here's the last part.
```rust, no_run
#[component]
fn TestFormComponent(on_submit: EventHandler<TestData>, children: Element) -> Element {
rsx! {
Form::<TestForm> {
on_submit,
{children}
}
}
}
#[component]
fn TestFieldComponent<TField>(
component: Callback<FieldApi<TestForm, TField>, Element>,
field: TField,
#[props(optional)] validate_on: ValidateOn,
#[props(optional, default = Vec::with_capacity(0))] validators: Validators<
<<TestForm as GetFieldRegistry>::FieldRegistry as FieldStateProvider<TField>>::FieldValue,
<<TestForm as GetFieldRegistry>::FieldRegistry as FieldStateProvider<TField>>::FieldError,
>,
#[props(optional, default = Vec::with_capacity(0))] async_validators: AsyncValidators<
<<TestForm as GetFieldRegistry>::FieldRegistry as FieldStateProvider<TField>>::FieldValue,
<<TestForm as GetFieldRegistry>::FieldRegistry as FieldStateProvider<TField>>::FieldError,
>,
) -> Element
where
TField: Clone + PartialEq + 'static,
TestForm: FieldStateProvider<TField>,
{
rsx! {
Field::<TestForm, TField> {
validate_on,
validators,
async_validators,
field,
component
}
}
}
// reusable component
#[component]
fn TextField<TForm, TField>(
#[props(optional)] id: String,
field_api: ReadSignal<FieldApi<TForm, TField>>,
#[props(extends=GlobalAttributes,extends=input)] attributes: Vec<Attribute>,
) -> Element
where
TForm: FieldStateProvider<TField>,
TField: Clone + PartialEq + 'static,
TForm::FieldValue: FieldValue<PrimitiveValue = String>,
{
rsx! {
label{
r#for: &id
}
input{
id,
value: field_api().value(),
onblur: field_api.peek().handlers().on_blur(),
oninput: move|evt|{
let _ = field_api.peek().handlers().on_input()(Either::Right(evt.value()));
},
..attributes
}
div{if let Some(err) = (field_api().state().errors)().get(0){
p{
{err.error_value()}
}
}
}
}
}
#[component]
fn Test() -> Element {
use_form_provider::<TestForm>();
rsx! {
TestFormComponent {
on_submit:|d|{
print!("{:?}", d);
},
TestFieldComponent {
field: GetName,
validate_on: ValidateOn::Input,
validators: vec![Validator::new(|input:&String|{
if input.starts_with("R"){
return Err(FormError::NameShouldntStartWithR)
}
Ok(())
}, 0)],
async_validators: vec![AsyncValidator::new(|input:&String|{
let input = input.clone();
Box::pin(async move {
if input.eq(&"John Doe".to_string()) {
return Err(FormError::NameAlreadyExists);
}
Ok(())
})
}, 0)],
component: |field_api| rsx!{TextField { field_api }}
}
}
}
}
```
### Some other patterns:
***Note: This section will be updated slowly***
1. If you're building form manually, you can create an enum instead of accessor structs
if many fields of your struct are of same type. Then use match case in `FieldStateProvider` trait, to provide
field states. But its better to let the macro do the work.
2. If you're building form manually, then you've to use the `use_form_provider` fn to pass context down the tree.
This opens an interesting view that if you're creating, for example someone's profile through form and
want them to show a preview of what it may look, you can directly access the whole form through `use_form` fn
outside the `Form` or html form component.
### FAQs
1. Was this library built with AI?
The answer is no. I don't use AI for some of my personal limitations. But some references
through AI has been definitely taken especially the async validation debounces and updates,
rest is purely built by me.
2. Why do I need this library?
Imagine you've a data-struct. You've got a dioxus server-fn taking that data as input.
Now, don't you think if you get a form having the required fields building you that data with
all the state managements and validations built right for your data so that you can instantly
call your server-fn on its submission, won't that be very nice?
Ridstack-form makes this possible.
You take a data-struct, apply `GenForm` macro, skip the fields you don't want in your form
by doing nothing on them, and add errors and optional accessor names on the fields you want in your
form and you get all the preparations instantly. Now in part of your app, you just instantiate your form
and in the on_submit function, you get the data ready to be put in your server fn.
3. Why was this library built?
Honestly, I found no suitable form library in dioxus meeting my requirements. The form libraries
in dioxus ecosystem is bit primitive and time consuming to build. This library gets you instantly started
(Although, there are some quirks still there), and expand accordingly. Besides its completely built with
dioxus stores thus enabling granular reactivity.
4. Why is there no tests?
First of all, I'm lazy. And since I'm still a student, I've many things to do.
I take some of my time daily to complete this.
And secondly, most of the logic is directly inside the `Field` component as it's a giant fn.
I could find any suitable way to test all of that. But the current app I'm building, I'm using it
there. If any problems arise, I'll make my best effort to solve the problem.
Or else, if you really want tests, just make a pr.
5. Is this library very verbose?
Yes, setting up a form is very tedious.
But you don't have to worry as a giant macro is there to help you.
It'll apply all the traits by itself. You only have apply the FieldValue trait on your field data.