Skip to content

πŸͺš Lightweight form builder for React that lets you dynamically render form fields from validation schemas, manage multi-step flows, and simplify validation handling.

Notifications You must be signed in to change notification settings

Wildhoney/Formikate

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

64 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Formikate Formikate

Checks

Lightweight form builder for React that lets you dynamically render form fields from validation schemas, manage multi-step flows, and simplify validation handling.

View Live Demo

Features

  • Dynamically render form fields using zod validation schemas
  • Declarative multi-step forms via useFields configuration
  • Inactive fields are automatically reset to their default values
  • Steps without active fields are automatically skipped during navigation
  • Per-step validation β€” only fields on the current step (or earlier) are validated on submit

Getting started

Begin by defining your validation schema and field descriptors:

import * as z from 'zod';

export const schema = z.object({
    name: z.string().min(1, 'Name is required'),
    address: z.string().min(1, 'Address is required'),
    guest: z.boolean(),
});

export type Schema = z.infer<typeof schema>;

export const fields = {
    name: { step: 'personal' as const, validate: schema.shape.name, value: '' },
    address: {
        step: 'delivery' as const,
        validate: schema.shape.address,
        value: '',
    },
    guest: {
        step: 'personal' as const,
        validate: schema.shape.guest,
        value: false,
    },
};

Import useForm – it accepts all of the same useFormik (Formik) arguments (except validate, validationSchema, and initialValues which are handled internally). Initial values are derived from each field's value property:

import { useForm, useFields, Position } from 'formikate';
import { fields } from './utils';

const form = useForm<Schema>({
    fields,
    validateOnBlur: false,
    validateOnChange: false,
    onSubmit(values) {
        if (!form.status.progress.last)
            return void form.status.navigate.to(Position.Next);
        console.log('Submitting', values);
    },
});

You can use form to access all of the usual Formik properties such as form.values and form.errors.

Defining Steps and Fields

Use useFields to declare the step structure and field configuration. The step property on each field is strongly typed β€” it must match one of the identifiers in the steps array:

useFields(form, () => ({
    steps: ['personal', 'delivery', 'review'],
    fields: {
        ...fields,
        address: {
            ...fields.address,
            active: form.values.guest === false,
        },
    },
}));

Config Shape

Property Type Description
steps (string | number | symbol)[] Ordered list of step identifiers
fields Record<string, FieldConfig> Map of field names to their configuration

Field Config

Property Type Description
step string | number | symbol Which step this field belongs to β€” must match one of the identifiers in steps
validate ZodType Zod schema used for validation
value unknown Default/reset value for the field β€” also used as the initial value when passed to useForm
active boolean? Whether the field is active (default true). Inactive fields are excluded from validation and reset to value

Automatic Step Skipping

Steps where all fields have active: false are automatically skipped during navigation:

useFields(form, () => ({
    steps: ['personal', 'delivery', 'review'],
    fields: {
        ...fields,
        address: {
            ...fields.address,
            active: form.values.guest === false,
        },
    },
}));

When guest is true, the address field is inactive, so the delivery step is skipped.

Status

After calling useFields, the computed state is available on form.status:

form.status.empty; // boolean β€” true when no fields/steps are configured
form.status.field; // Record<string, { exists(), required, optional }>
form.status.progress; // step progression state
form.status.navigate; // navigation controls

Field State

form.status.field.name.exists(); // true if the field is active
form.status.field.name.required; // true if the Zod schema rejects `undefined`
form.status.field.name.optional; // inverse of required

Progress

form.status.progress.current; // identifier of the current step
form.status.progress.position; // zero-based index within visible steps
form.status.progress.total; // total number of visible steps
form.status.progress.first; // whether on the first visible step
form.status.progress.last; // whether on the last visible step
form.status.progress.steps; // array of { id, index } for visible steps
form.status.progress.step; // map of step id β†’ { visible, current }

Navigation

import { Position } from 'formikate';

form.status.navigate.to(Position.Next); // go to next step
form.status.navigate.to(Position.Previous); // go to previous step
form.status.navigate.to(Position.First); // go to first step
form.status.navigate.to(Position.Last); // go to last step
form.status.navigate.to('review'); // go to a specific step by id

form.status.navigate.exists(Position.Next); // true if a next step exists
form.status.navigate.exists(Position.Previous); // true if a previous step exists
form.status.navigate.exists('review'); // true if a specific step is reachable

Rendering

Use Formikate's Form component to provide the form to child components:

import { Form, Position } from 'formikate';

<Form value={form}>
    <form onSubmit={form.handleSubmit}>
        {form.status.field.name.exists() && (
            <input type="text" {...form.getFieldProps('name')} />
        )}

        {form.status.field.address.exists() && (
            <input type="text" {...form.getFieldProps('address')} />
        )}

        <button
            type="button"
            disabled={!form.status.navigate.exists(Position.Previous)}
            onClick={() => form.status.navigate.to(Position.Previous)}
        >
            Back
        </button>

        <button type="submit">
            {form.status.progress.last ? 'Submit' : 'Next'}
        </button>
    </form>
</Form>;

Accessing Form in Child Components

Use the useFormContext hook in child components to access the form with properly typed status:

import { useFormContext } from 'formikate';
import type { Schema } from './types';

function NameField() {
    const form = useFormContext<Schema>();

    if (!form.status.field.name.exists()) return null;

    return <input type="text" {...form.getFieldProps('name')} />;
}

Empty State

When all fields are inactive, form.status.empty is true:

{
    form.status.empty ? (
        <p>No fields available</p>
    ) : (
        <form onSubmit={form.handleSubmit}>{/* ... */}</form>
    );
}

About

πŸͺš Lightweight form builder for React that lets you dynamically render form fields from validation schemas, manage multi-step flows, and simplify validation handling.

Topics

Resources

Stars

Watchers

Forks

Packages

No packages published