ridstack-form 0.1.0

End-to-End Type-safe form handling for Dioxus applications
docs.rs failed to build ridstack-form-0.1.0
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.

Ridstack-Form

Type-safe form handling for Dioxus.

Quickstart

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

Let's Deep Dive

Architecture

Check 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:

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.
#[derive(Store, PartialEq, Eq)]
struct TestData {
    name: String,
    // more fields...
}
  1. 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
    }
}
  1. Create a struct for storing your fields. Use Store macro on it.
#[derive(Clone, Store, Default)]
struct TestFieldState {
    name: FieldState<String, FormError>,
    // more fields...
}
  1. Create form state. Its advised (and mandatory also), to use Store on the TestFieldState and FormState.
#[derive(Store, Clone)]
struct TestForm {

    field_state: Store<TestFieldState>,
    // required, for managing the form state.
    form_state: Store<FormState>,
}
  1. Let's start applying traits.

    1. FormSubmitInput returns 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(),
            })
        }
    }
    
    1. GetFieldRegistry returns the field_registry
    impl GetFieldRegistry for TestForm {
        type FieldRegistry = Self;
        fn get_field_registry(&self) -> Self::FieldRegistry {
            self.clone()
        }
    }
    
    1. 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.
    #[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"
        }
    }
    
    1. 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;
        }
    }
    
    1. 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
    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,
            }
        }
    }
    
    1. We need to manually check which field validators to be run on submit. For this we apply ValidateOnSubmit trait.
    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 {})
        }
    }
    
    
    1. 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.

    2. 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.

    3. 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.

    4. 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.

    
    #[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.

  1. 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.

  1. 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.

  1. 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.

  1. 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.