npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@imadehidiame/react-form-validation

v1.1.5

Published

A react form validation component

Readme

React Form Wrapper Component

A highly configurable React form wrapper built on top of react-hook-form and zod, with support for dynamic form elements, client-side validation, server response handling, and pre/post submission hooks, leveraging React 19 optimizations using the React Compiler. Check out more details on (https://react.dev/learn/react-compiler/introduction). Please check (https://nextjs.org/docs/app/api-reference/config/next-config-js/reactCompiler) for nextjs React Compiler configuration and refer to other react frameworks documentations for enabling this feature.

This component allows you to generate complex forms dynamically from configuration objects instead of hardcoding inputs, making it ideal for reusable, API-driven, and highly customizable forms.


✨ Features

  • Dynamic form generation using a configuration array (form_components).
  • Built-in validation with Zod schemas (zodResolver).
  • Supports multiple input types:
    • Text, Password, Textarea
    • Select dropdown
    • Checkbox group
    • File upload (single/multiple)
    • Rich text editor
    • Number inputs (integer/float)
    • Custom components
  • Custom validation refinement (redefine function).
  • Pre- and post-submit hooks (pre_submit_action, after_submit_action).
  • Built-in loading and error handling.
  • JSON or FormData submission modes.
  • State persistence via Context API.
  • Optional auto-clear form after submission.

📦 Installation

npm install @imadehidiame/react-form-validation

🚀 Quick Start

\* import the @imadehidiame/react-form-validation/dist/styles.css  file in your entry CSS file*\
@import '@imadehidiame/react-form-validation/dist/styles.css';
'use client';
import { Loader2 } from "lucide-react";
import { useState, useRef, useEffect } from "react";
import { z } from "zod";
import { GenerateFormdata, FormProvider, useActionData, useFormState, NumberFormat, FormWrapper } from '@imadehidiame/react-form-validation';
import { Button } from "./components/ui/button";
import { Card, CardHeader, CardTitle, CardContent } from "./components/ui/card";

const validation = z.object({
    password: z.string().nonempty({ message: 'Please enter a value for password' }),
    currency: z.string().nonempty({ message: 'Please select currency' }),
    amount: z.string().refine((value) => {
        return parseFloat((value).replaceAll(',', '')) >= 5000;
    }, 'Value must be greater than or equal to 5,000'),
    calculated: z.any(),
    integer: z.string().nonempty({ message: 'Please enter integer value' }).refine((value) => {
        return parseInt(value) <= 1000;
    }, { message: 'Value must be less than 1001' }),
    images: z.any().array().refine((files) => {
        return files.length > 0
    },{message:'Please upload at least one image'}),
    message: z.any().array(),
    phone_number: z.string().nonempty({ message: 'Enter a valid phone number' }).refine((value) => {
        return value.length === 19;
    }, 'Enter a valid phone number prefixed with "234"'),
    description: z.string().trim().nonempty({ message: 'Enter the description' }).min(5, { message: 'Descriptions should be at least 5 characters long' }),
    family: z.array(z.string()).refine((value) => {
        return value.length >= 5;
    }, { message: 'Please select at least 5 of my family members' })
});

interface LoadedData {
    loaderData?:z.infer<typeof validation>[]
}

interface ActionData {
    data?:any;
    message?:string;
    error?:string
}

export function FormTest({loaderData}:LoadedData) {
    const [isSubmitting, setIsSubmitting] = useState(false);
    //useFormState() hook provides information about the form and is one of, submit or idle. 
    const navigation = useFormState();

    //useActionData() hook provides information about the response from server after form submission and is typed as ActionData above. "message" represents a string value acting as a success message. "data" acts any value probably returned from the server e.g a random ID generated after form submission. message and data values are usually passed to the after_submit_action() function. "error" is a string value that acts as an error message after a form submission and is passed to the notify() function.
    const action_data = useActionData<ActionData>();
    const is_submitting = navigation === 'submit';

    useEffect(()=>{
        //console.log('action data ',action_data);
    },[action_data]);

    useEffect(()=>{
        //console.log('Navigation change ',navigation);
    },[navigation]);
    
    const prices = {
        'btc': 109000,
        'eth': 4000
    }

    const { password, currency, calculated, amount, integer, images, message, phone_number, description, family } = validation.shape;

    const loader = loaderData && loaderData?.length > 0 ? loaderData[0] : undefined

    const [form_data, set_form_data] = loader ? useState<FormElement<any>[]>([

        (new GenerateFormdata).set_class_names('w-full').set_description('Your password').set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_label('Password').set_label_class_names('mb-4 text-amber-300').set_name('password').set_placeholder('Enter your assword').set_type('password').set_show_password_icon(true).set_validation(password).set_value(loader.password).build(),
        
        (new GenerateFormdata).set_class_names('w-full').set_description('Deposit Amount').set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_label('Depost ($)').set_label_class_names('mb-4 text-amber-300').set_name('amount').set_placeholder('Deposit amount').set_type('float').set_validation(amount).set_value(loader.amount).build(),

        (new GenerateFormdata).set_selects([
            {
                name: 'Select currency',
                value: 'null'
            },
            {
                name: 'Bitcoin',
                value: 'btc'
            },
            {
                name: 'Ethereum',
                value: 'eth'
            }
        ]).set_class_names('w-full').set_disabled(is_submitting).set_field_class_names('w-full bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_id('currency').set_label('Currency').set_label_class_names('text-amber-300 mb-4').set_name('currency').set_placeholder('Select from available').set_type('select').set_value(loader.currency).set_validation(currency).build(),
        
        //You can use the NumberFormat class static methods to format a value while setting it when the form loads
        (new GenerateFormdata).set_description('Enter a value that will automatically get formatted as "+(123) 456 7890 123" for example').set_class_names('w-full').set_disabled(is_submitting).set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_id('phone_number').set_label('Phone Number').set_label_class_names('text-amber-300 mb-4').set_name('phone_number').set_placeholder('Enter phone number').set_type('integer').set_value( NumberFormat.numbers_only(loader.phone_number,{
            allow_decimal: false, allow_zero_start: false, format_to_thousand: false, extra_format: {
                format: '+(3)&3&4&3',
                space_character: '&'
            }
        })).set_validation(phone_number).set_flag({
            add_if_empty: false, allow_decimal: false, allow_zero_start: false, format_to_thousand: false, extra_format: {
                format: '+(3)&3&4&3',
                space_character: '&'
            }
        }).build(),

        (new GenerateFormdata).set_type('float').set_validation(calculated).set_class_names('w-full').set_disabled(true).set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_flag({ add_if_empty: false, allow_decimal: true, allow_zero_start: true, format_to_thousand: true, length_after_decimal: 2 }).set_id('calculated').set_label('Calculated Value').set_label_class_names('text-amber-300 mb-4').set_name('calculated').set_placeholder('Calculated value').set_value((parseFloat(loader.amount) / prices[loader.currency as keyof typeof prices]).toString()).build(),

        (new GenerateFormdata).set_class_names('w-full').set_disabled(is_submitting).set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_flag({ add_if_empty: false, allow_decimal: false, allow_zero_start: false, format_to_thousand: false }).set_id('integer').set_label('Integer Without "0" Prefix').set_label_class_names('text-amber-300 mb-4').set_name('integer').set_type('integer').set_validation(integer).set_value(loader.integer).build(),

        (new GenerateFormdata).set_class_names('w-full').set_disabled(is_submitting).set_field_class_names('w-full bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_file_count(3).set_id('images').set_label('Images').set_label_class_names('text-amber-300').set_name('images').set_size_limit(1000).set_type('multiple_file').set_validation(images).set_value(loader.images).set_extensions(['png', 'jpg', 'jpeg', 'svg', 'webp']).build(),

        (new GenerateFormdata).set_class_names('w-full').set_disabled(is_submitting).set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_id('message').set_label('Message').set_label_class_names('mb-4 text-amber-300').set_name('message').set_placeholder('Enter your message').set_type('rich_text').set_insertion(loader.message[0]).set_validation(message).set_value([]).build(),

        (new GenerateFormdata).set_class_names('w-full').set_disabled(is_submitting).set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_id('description').set_label('Description').set_label_class_names('mb-4 text-amber-300').set_name('description').set_name('description').set_placeholder('Description').set_type('textarea').set_validation(description).set_value(loader.description).build(),

        (new GenerateFormdata).set_class_names('w-full').set_checks([
            {
                name: 'React Router v 7',
                value: 'rrv7'
            },
            {
                name: 'Remixjs',
                value: 'remixjs'
            },
            {
                name: 'Nextjs',
                value: 'nextjs'
            },
            {
                name: 'VueJS',
                value: 'vuejs'
            },
            {
                name: 'React',
                value: 'react'
            },
            {
                name: 'Nodejs',
                value: 'nodejs'
            },
            {
                name: 'Expo',
                value: 'expo'
            },
            {
                name: 'React Native',
                value: 'reactnative'
            },
            {
                name: 'Flutter',
                value: 'flutter'
            },
            {
                name: 'Laravel',
                value: 'laravel'
            }
        ]).set_disabled(is_submitting).set_field_class_names('flex flex-row items-start space-x-4 space-y-0 mt-2 flex-wrap').set_id('family').set_label('Family Members').set_label_class_names('mb-4 text-amber-300').set_name('family').set_type('checkbox').set_validation(family).set_value(loader.family).build()
    ])  : useState<FormElement<any>[]>([
        (new GenerateFormdata).set_class_names('w-full').set_description('Your password').set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_label('Password').set_label_class_names('mb-4 text-amber-300').set_name('password').set_placeholder('Enter your assword').set_type('password').set_show_password_icon(true).set_validation(password).set_value('').build(),
        
        (new GenerateFormdata).set_class_names('w-full').set_description('Deposit Amount').set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_label('Depost ($)').set_label_class_names('mb-4 text-amber-300').set_name('amount').set_placeholder('Deposit amount').set_type('float').set_validation(amount).set_value('').build(),

        (new GenerateFormdata).set_selects([
            {
                name: 'Select currency',
                value: 'null'
            },
            {
                name: 'Bitcoin',
                value: 'btc'
            },
            {
                name: 'Ethereum',
                value: 'eth'
            }
        ]).set_class_names('w-full').set_disabled(is_submitting).set_field_class_names('w-full bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_id('currency').set_label('Currency').set_label_class_names('text-amber-300 mb-4').set_name('currency').set_placeholder('Select from available').set_type('select').set_value('').set_validation(currency).build(),

        
        (new GenerateFormdata).set_class_names('w-full').set_description('Enter a value that will automatically get formatted as "+(123) 456 7890 123" for example').set_disabled(is_submitting).set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_id('phone_number').set_label('Phone Number').set_label_class_names('text-amber-300 mb-4').set_name('phone_number').set_placeholder('Enter phone number').set_type('integer').set_value( NumberFormat.numbers_only('2348143944672',{
            allow_decimal: false, allow_zero_start: false, format_to_thousand: false, extra_format: {
                format: '+(3)&3&4&3',
                space_character: '&'
            }
        })).set_validation(phone_number).set_flag({
            add_if_empty: false, allow_decimal: false, allow_zero_start: false, format_to_thousand: false, extra_format: {
                format: '+(3)&3&4&3',
                space_character: '&'
            }
        }).build(),

        (new GenerateFormdata).set_type('float').set_validation(calculated).set_class_names('w-full').set_disabled(true).set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_flag({ add_if_empty: false, allow_decimal: true, allow_zero_start: true, format_to_thousand: true, length_after_decimal: 2 }).set_id('calculated').set_label('Calculated Value').set_label_class_names('text-amber-300 mb-4').set_name('calculated').set_placeholder('Calculated value').set_value('').build(),

        (new GenerateFormdata).set_class_names('w-full').set_disabled(is_submitting).set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_flag({ add_if_empty: false, allow_decimal: false, allow_zero_start: false, format_to_thousand: false }).set_id('integer').set_label('Integer Without "0" Prefix').set_label_class_names('text-amber-300 mb-4').set_name('integer').set_type('integer').set_validation(integer).set_value('').build(),

        (new GenerateFormdata).set_class_names('w-full').set_disabled(is_submitting).set_field_class_names('w-full bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_file_count(3).set_id('images').set_label('Images').set_label_class_names('text-amber-300').set_name('images').set_size_limit(1000).set_type('multiple_file').set_validation(images).set_value([]).set_extensions(['png', 'jpg', 'jpeg', 'svg', 'webp']).build(),

        (new GenerateFormdata).set_class_names('w-full').set_disabled(is_submitting).set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_id('message').set_label('Message').set_label_class_names('mb-4 text-amber-300').set_name('message').set_placeholder('Enter your message').set_type('rich_text')/*.set_insertion('Hello rich text. Happy to be here')*/.set_validation(message).set_value([]).build(),

        (new GenerateFormdata).set_class_names('w-full').set_disabled(is_submitting).set_field_class_names('bg-gray-700 border-gray-600 text-white focus-visible:ring-amber-300 mt-2.5').set_id('description').set_label('Description').set_label_class_names('mb-4 text-amber-300').set_name('description').set_name('description').set_placeholder('Description').set_type('textarea').set_validation(description).set_value('').build(),

        (new GenerateFormdata).set_class_names('w-full').set_checks([
            {
                name: 'React Router v 7',
                value: 'rrv7'
            },
            {
                name: 'Remixjs',
                value: 'remixjs'
            },
            {
                name: 'Nextjs',
                value: 'nextjs'
            },
            {
                name: 'VueJS',
                value: 'vuejs'
            },
            {
                name: 'React',
                value: 'react'
            },
            {
                name: 'Nodejs',
                value: 'nodejs'
            },
            {
                name: 'Expo',
                value: 'expo'
            },
            {
                name: 'React Native',
                value: 'reactnative'
            },
            {
                name: 'Flutter',
                value: 'flutter'
            },
            {
                name: 'Laravel',
                value: 'laravel'
            }
        ]).set_disabled(is_submitting).set_field_class_names('flex flex-row items-start space-x-4 space-y-0 mt-2 flex-wrap').set_id('family').set_label('Family Members').set_label_class_names('mb-4 text-amber-300').set_name('family').set_type('checkbox').set_validation(family).set_value(['remixjs','rrv7']).build()
    ])   ;

    const on_change = (name:string|number|symbol,value: any) => {
        if (name === 'amount') {
            if (value) {
                const {value:other_value} = form_data.find(e=>e.name === 'currency')!;
                if (other_value) {
                    //const calc = parseFloat((value as string).replaceAll(',', '')) / prices[other_value as keyof typeof prices];
                    set_form_data(prev=>prev.map(e => e.name === 'calculated' ? ({...e,value:(parseFloat((value as string).replaceAll(',', '')) / prices[other_value as keyof typeof prices]).toString()}) : e));
                    //set_form_object(prev => ({ ...prev, calculated: (parseFloat((value as string).replaceAll(',', '')) / prices[form_object.currency as keyof typeof prices]).toString() }));
                } else {
                    set_form_data(prev=>prev.map(e => e.name === 'calculated' ? ({...e,value:''}) : e));
                }
            } else {
                set_form_data(prev=>prev.map(e => e.name === 'calculated' ? ({...e,value:''}) : e));
                //set_form_object(prev => ({ ...prev, calculated: '' }));
            }

        } else if (name === 'currency') {
            if (value && (value == 'eth' || value == 'btc')) {
                const {value:other_value} = form_data.find(e=>e.name === 'amount')!;
                if (other_value) {
                    set_form_data(prev=>prev.map(e => e.name === 'calculated' ? ({...e,value:(parseFloat((other_value as string).replaceAll(',', '')) / prices[value as keyof typeof prices]).toString()}) : e));
                } else {
                    set_form_data(prev=>prev.map(e => e.name === 'calculated' ? ({...e,value:''}) : e));
                }
            } else {
                set_form_data(prev=>prev.map(e => e.name === 'calculated' ? ({...e,value:''}) : e));
            }
        }
    }
    
    const submit_form = async () => {

    }
    const form_values_ = form_data.reduce((prev,{name,value})=>{
        return {...prev,[name as string]:value}
    },{}) 
    
    const is_form_valid = validation.safeParse(form_values_)
    

    return (
        
        <Card className="bg-gray-800 py-4 px-2 w-full border border-amber-300/50">
            <CardHeader className="max-sm:p-2">
                <CardTitle className="text-lg font-bold text-amber-300">Kickstart your journey</CardTitle>
                <p className="text-sm text-gray-400">Make a deposit</p>
            </CardHeader>
            <CardContent className="max-sm:p-2">
              <FormProvider>
                <FormWrapper
                    action="/api/test-form"
                    is_json={true}
                    is_clear_form={true}
                    validation_mode="onChange"
                    on_change={on_change}
                    pre_submit_action={async (value)=>{
                        return value;
                    }}
                    method="POST"
                    form_components={form_data}
                    notify={(error)=>{
                        alert(error);
                    }}
                    fetch_options={{credentials:'include'}}
                    request_headers={{'X-CSRF-TOKEN':'csrf_request_token_header'}}
                    validationMode="onChange"
                    set_form_elements={set_form_data}
                    class_names="space-y-1 p-0 flex flex-wrap gap-4 items-center mb-12"
                    validate_values={[]}
                    after_submit_action={(message,data) => {
                        console.log('message ',message);
                        console.log('data ', data);
                    }}
                >
                    <Button
                        type="button"
                        variant="outline"
                        className="ml-auto bg-transparent border-amber-300 text-amber-300 hover:bg-amber-300 hover:text-gray-900 cursor-pointer text-sm py-2 h-9"
                        disabled={!is_form_valid}
                        onClick={async ()=>{
                            
                        }}
                    >
                        {is_submitting && <Loader2 className="animate-spin w-4 h-4 mr-2" />}
                        {is_submitting ? 'Processing Data' : 'Generate'}
                    </Button>
                    <Button
                        variant="outline"
                        className="ml-auto bg-transparent border-amber-300 text-amber-300 hover:bg-amber-300 hover:text-gray-900 cursor-pointer text-sm py-2 h-9"
                        //disabled={!(is_form_valid.success)}
                        disabled={is_submitting}
                        onClick={submit_form}
                    >
                        {is_submitting && <Loader2 className="animate-spin w-4 h-4 mr-2" />}
                        {is_submitting ? 'Processing Deposit' : 'Deposit'}
                    </Button>

                </FormWrapper>
            </FormProvider>

            </CardContent>
        </Card>
        
    );
}

🛠 API Reference Props

| Prop | Type | Required | Default | Description | | ----------------------------------- | --------------------------------------------------------------- | -------- | ---------- | ----------------------------------------------------- | | form_components | FormElement[] | ✅ | — | Array of form field configuration objects. | | set_form_elements | Dispatch<SetStateAction<FormElement[]>> | ✅ | — | State setter to update form_components dynamically. | | action | string | ✅ | — | API endpoint to submit the form data. | | method | 'POST' \| 'PATCH' | ✅ | — | HTTP method for submission. | | is_json | boolean | ❌ | false | Whether to send data as JSON or FormData. | | fetch_options | Partial<Record<string,any>> | ❌ | - | Valid record of fetch request options e.g credentials, cors,etc. | | request_headers | <Record<string,string> | ❌ | - | Request headers to send alongside the request | | is_clear_form | boolean | ❌ | false | Whether to reset form after successful submission. | | notify | (error: string) => void | ❌ | — | Called on validation or submission errors. | | pre_submit_action | (values) => values \| Promise<values> | ❌ | — | Function to modify form values before submission. | | after_submit_action | (message, data) => void | ❌ | — | Called after successful submission. | | load_animation / hide_animation | () => void | ❌ | — | Hooks for triggering loading indicators. | | redefine | (values) => { valid: boolean; error: string; path?: string; } | ❌ | — | Adds custom refinement validation to the Zod schema. | | validation_mode | 'onChange' \| 'onBlur' \| 'onTouched' \| 'onSubmit' \| 'all' | ❌ | 'onBlur' | React Hook Form validation mode. | | on_change | (name: string, value: any) => void | ❌ | — | Called whenever a form field changes. | | children | ReactNode | ❌ | — | Custom JSX inside the <form>. | | class_names | string | ❌ | — | Form-level CSS classes. |

FormElement Interface

Each element in form_components follows and is set using the GenerateFormData object as shown above. Please call build() after setting all the props as shown above

interface FormElement<T extends FieldValues> {
type: FormType;
name?: keyof T | number | string | symbol;
value?: string | number | File | any[] | null | undefined;
class_names?: string;
description?:string;
description_class_names?:string;
error_field_class_names?:string;
disabled?: boolean;
field_class_names?: string;
label_class_names?: string;
label?: string;
form_state?:T;
set_form_state?:React.Dispatch<React.SetStateAction<T>>;
id?: string;
validation?: z.ZodType;
placeholder?: string;
show_password_icon?:boolean;
size_limit?: number;
file_count?: number;
extensions?:string[];
selects?: { name: string; value: string }[];
checks?: { name: string; value: string }[];
component?: React.ReactNode;
refine?: (value: any) => boolean | { valid: boolean; error: string; path?: string };
insertion?:string;
flag?: {
  allow_decimal?: boolean;
  allow_zero_start?: boolean;
  length_after_decimal?: number;
  add_if_empty?: boolean;
  total_length?: number;
  format_to_thousand?: boolean;
  allow_negative_prefix?: boolean;
  extra_format?:{
    format:string|string[],
    space_character?:string
  }
};
}

🏗 Creating Form Elements

You use GenerateFormdata builder to create form components

import { GenerateFormdata } from "@imadehidiame/react-form-validation";
import { z } from "zod";

const usernameField = new GenerateFormdata()
  .set_type("text")
  .set_name("username")
  .set_label("Username")
  .set_placeholder("Enter username")
  .set_validation(z.string().min(3))
  .build();

🔄 Handling Validation

Validation is powered by Zod:

import { z } from "zod";

const passwordField = new GenerateFormdata()
  .set_type("password")
  .set_name("password")
  .set_label("Password")
  .set_validation(z.string().min(6, "Password too short"))
  .build();

✨ Form Submission

📡 Submission Flow

On submit:

Values pass Zod validation.

pre_submit_action runs if provided.

Form data is sent via fetch_request.

If server responds with validation errors, fields are updated accordingly.

If successful:

after_submit_action runs.

Form optionally clears if is_clear_form is set to true.

User submits form → handleFormSubmission runs.

If provided, pre_submit_action(values) runs before sending data and if it is given, it should return the data to be submitted to the server;

navigation state is set to "submit" and this value is usually is provided by the useFormState() hook. This can be used to display a loading to visually tell the user the form is submitting

Payload preparation:

If is_json → JSON.stringify(values)

Else → FormData with all values stringified.

5️⃣ Response Handling

If server returns error:

Errors → Notify with a default message passed to the notify(). Or handle it in your code by using the "error" value of the useActionData() hook

Validation errors:

If validate_values is set, only those fields are updated.

Otherwise, all fields in errors are updated in form state via form.setError.

If server returns without errors:

Calls after_submit_action if provided and passes the "data" and "message" values of the useActionData() hook

Optionally clears form is_clear_form prop is set to true.

License

MIT