Type-safe form handling for Dioxus.
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}
}
}
}
}
}
}
- Validify feature is still in WIP. So it should be disabled.
- 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.
- Ready to be used just docs and readme needs to be updated.
Check architecture.svg in the repo.
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.
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:
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.
- Create your data struct.
#[derive(Store, PartialEq, Eq)]
struct TestData {
name: String,
// more fields...
}- 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.
#[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
}
}- Create a struct for storing your fields. Use Store macro on it.
#[derive(Clone, Store, Default)]
struct TestFieldState {
name: FieldState<String, FormError>,
// more fields...
}- Create form state. Its advised (and mandatory also),
to use Store on the
TestFieldStateandFormState.
#[derive(Store, Clone)]
struct TestForm {
field_state: Store<TestFieldState>,
// required, for managing the form state.
form_state: Store<FormState>,
}-
Let's start applying traits.
FormSubmitInputreturns the original data you've created.
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(), }) } }
GetFieldRegistryreturns the field_registry
impl GetFieldRegistry for TestForm { type FieldRegistry = Self; fn get_field_registry(&self) -> Self::FieldRegistry { self.clone() } }
- 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
FieldStateProvidertaking this accessor as generic over theFieldRegistry. We could've returned and made theTestFieldStatetheFieldStateProviderbut the API doesn't allow us and also it may break granular reactivity provided by the store.
#[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" } }
- Now, we apply calculate can submit because it can't be auto-checked whether the fields are ready to be submitted or not.
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; } }
- We will apply some traits required for Form management.
The first is
GetFormStatewhich makes us return the form state. The second isInitFormwhich returns a hook used to initialize the form
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, } } }
- We need to manually check which field validators to be run on submit. For this we apply
ValidateOnSubmittrait.
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 {}) } }
-
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.
-
Now the final piece. That's using this form. To get started, use the
use_form_providerhook and pass your form type (hereTestForm) in the turbofish operator. -
Then use the
Formcomponent 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. -
Then use the
Fieldcomponent to access the fields. Use the accessor structs to ensure that theFieldcomponent points to the specified field. You can use thevalidatorsandasync_validatorsprop to set sync and async validators respectively. You can also use thevalidate_onprop to make sure that validators run before submit, on blur, on input or all.Note:
FormandFieldcomponents need heavy generics of the Form struct (hereTestForm). It is a better practice to already put the generics in a separate component and use that instead. Here's the last part.
#[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 }} } } } }
Note: This section will be updated slowly
- 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
FieldStateProvidertrait, to provide field states. But its better to let the macro do the work. - If you're building form manually, then you've to use the
use_form_providerfn 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 throughuse_formfn outside theFormor html form component.
- 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.
- 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.
- 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.
- 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.
- 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.