@matthew.ngo/reform
Version:
A flexible and powerful React form management library with advanced validation, state observation, and multi-group support
394 lines (335 loc) • 16.9 kB
Markdown
# Reform Form Wizard
Reform provides a powerful form wizard system that allows you to create multi-step forms with navigation, validation, and state management. This document covers the form wizard capabilities of Reform.
## Table of Contents
- [Overview](#overview)
- [Basic Usage](#basic-usage)
- [Step Configuration](#step-configuration)
- [Navigation](#navigation)
- [Validation](#validation)
- [Persistence](#persistence)
- [API Reference](#api-reference)
## Overview
Reform's form wizard system allows you to:
1. **Create multi-step forms** with a clear navigation flow
2. **Validate each step** before proceeding to the next
3. **Track completion status** of steps
4. **Customize step behavior** with lifecycle hooks
5. **Persist wizard state** across sessions
## Basic Usage
Here's a basic example of creating a multi-step form wizard:
```tsx
import { useReform, useReformWizard } from '@reform/core';
type UserForm = {
// Step 1: Personal Information
firstName: string;
lastName: string;
email: string;
// Step 2: Address
street: string;
city: string;
state: string;
zipCode: string;
// Step 3: Account
username: string;
password: string;
confirmPassword: string;
};
const RegistrationForm = () => {
// Initialize Reform form
const form = useReform<UserForm>({
defaultData: {
firstName: '',
lastName: '',
email: '',
street: '',
city: '',
state: '',
zipCode: '',
username: '',
password: '',
confirmPassword: '',
},
minGroups: 1,
});
// Initialize wizard with steps
const wizard = useReformWizard(form, {
steps: [
{
id: 'personal',
title: 'Personal Information',
groupIndices: [0], // Use the first form group
fields: ['firstName', 'lastName', 'email'],
},
{
id: 'address',
title: 'Address',
groupIndices: [0],
fields: ['street', 'city', 'state', 'zipCode'],
},
{
id: 'account',
title: 'Account Setup',
groupIndices: [0],
fields: ['username', 'password', 'confirmPassword'],
},
],
validateOnNext: true,
});
// Handle form submission
const handleSubmit = (data: any) => {
console.log('Form submitted:', data);
};
return (
<div className="wizard-container">
{/* Step indicator */}
<div className="wizard-steps">
{wizard.getSteps().map(step => (
<div
key={step.id}
className={`wizard-step ${step.isActive ? 'active' : ''} ${
step.isCompleted ? 'completed' : ''
} ${!step.isEnabled ? 'disabled' : ''}`}
onClick={() => step.isEnabled && wizard.goToStep(step.id)}
>
{step.title}
</div>
))}
</div>
{/* Current step content */}
<form onSubmit={form.formMethods.handleSubmit(handleSubmit)}>
{wizard.currentStep.id === 'personal' && (
<div className="step-content">
<h2>Personal Information</h2>
<div>
<label>First Name</label>
<input {...form.register(0, 'firstName')} />
</div>
<div>
<label>Last Name</label>
<input {...form.register(0, 'lastName')} />
</div>
<div>
<label>Email</label>
<input {...form.register(0, 'email')} type="email" />
</div>
</div>
)}
{wizard.currentStep.id === 'address' && (
<div className="step-content">
<h2>Address</h2>
<div>
<label>Street</label>
<input {...form.register(0, 'street')} />
</div>
<div>
<label>City</label>
<input {...form.register(0, 'city')} />
</div>
<div>
<label>State</label>
<input {...form.register(0, 'state')} />
</div>
<div>
<label>Zip Code</label>
<input {...form.register(0, 'zipCode')} />
</div>
</div>
)}
{wizard.currentStep.id === 'account' && (
<div className="step-content">
<h2>Account Setup</h2>
<div>
<label>Username</label>
<input {...form.register(0, 'username')} />
</div>
<div>
<label>Password</label>
<input {...form.register(0, 'password')} type="password" />
</div>
<div>
<label>Confirm Password</label>
<input {...form.register(0, 'confirmPassword')} type="password" />
</div>
</div>
)}
{/* Navigation buttons */}
<div className="wizard-navigation">
{wizard.canGoPrev && (
<button type="button" onClick={wizard.prev}>
Previous
</button>
)}
{wizard.canGoNext ? (
<button type="button" onClick={wizard.next}>
Next
</button>
) : (
<button type="submit">Submit</button>
)}
</div>
</form>
</div>
);
};
```
## Step Configuration
Each step in the wizard can be configured with various options:
```tsx
const wizard = useReformWizard(form, {
steps: [
{
id: 'personal',
title: 'Personal Information',
groupIndices: [0], // Which form groups this step applies to
// Optional: Custom validation for this step
validate: groups => {
const data = groups[0].data;
if (!data.email.includes('@')) {
form.setError(0, 'email', {
type: 'manual',
message: 'Invalid email format',
});
return false;
}
return true;
},
// Optional: Custom completion check
isCompleted: groups => {
const data = groups[0].data;
return !!data.firstName && !!data.lastName && !!data.email;
},
// Optional: Custom enabled check
isEnabled: (allGroups, completedSteps) => {
// This is the first step, so always enabled
return true;
},
// Optional: Lifecycle hooks
onEnter: groups => {
console.log('Entering personal information step');
// You can initialize data or perform side effects
},
onLeave: (groups, direction) => {
console.log(`Leaving personal information step (${direction})`);
// Return false to prevent leaving the step
if (direction === 'next' && !groups[0].data.email) {
alert('Please enter your email before proceeding');
return false;
}
return true;
},
},
// Other steps...
],
});
```
## Navigation
The wizard provides several methods for navigation:
```tsx
// Go to the next step (with validation)
wizard.next();
// Go to the previous step
wizard.prev();
// Go to a specific step by ID
wizard.goToStep('address');
// Go to a specific step by index
wizard.goToStepByIndex(1);
// Reset the wizard
wizard.resetWizard({
resetFormData: true, // Also reset form data
clearCompletedSteps: true, // Clear completed steps status
});
```
## Validation
You can control validation behavior with configuration options:
```tsx
const wizard = useReformWizard(form, {
steps: [...],
// Validate the current step before proceeding to the next
validateOnNext: true,
// Allow skipping to any step (even if previous steps are not completed)
allowSkipSteps: false,
// Mark steps as completed when leaving them (if valid)
markCompletedOnLeave: true,
});
```
## Persistence
The wizard can persist its state across sessions:
```tsx
const wizard = useReformWizard(form, {
steps: [...],
// Enable state persistence
persistState: true,
// Custom storage key
persistStateKey: 'my-registration-wizard',
// Callbacks
onStepCompleted: (stepId, groups) => {
console.log(`Step ${stepId} completed`);
analytics.trackStepCompletion(stepId);
},
onWizardCompleted: (groups) => {
console.log('Wizard completed');
analytics.trackWizardCompletion();
},
});
```
## API Reference
### useReformWizard
```typescript
function useReformWizard<T extends Record<string, any>>(
reform: ReformReturn<T>,
config: FormWizardConfig<T>
): FormWizardReturn<T>;
```
#### FormWizardConfig
| Option | Type | Default | Description |
| -------------------- | -------------------------------------------------- | ----------------------- | --------------------------------------- |
| steps | `FormStep<T>[]` | Required | Array of step configurations |
| initialStepId | `string` | First step ID | ID of the initial step |
| validateOnNext | `boolean` | `true` | Validate current step before proceeding |
| allowSkipSteps | `boolean` | `false` | Allow skipping to any step |
| markCompletedOnLeave | `boolean` | `true` | Mark steps as completed when leaving |
| persistState | `boolean` | `false` | Persist wizard state to localStorage |
| persistStateKey | `string` | `'reform-wizard-state'` | Storage key for persistence |
| onStepCompleted | `(stepId: string, groups: FormGroup<T>[]) => void` | `undefined` | Callback when a step is completed |
| onWizardCompleted | `(groups: FormGroup<T>[]) => void` | `undefined` | Callback when all steps are completed |
#### FormStep
| Property | Type | Description |
| ------------ | -------------------------------------------------------------------------------------- | ------------------------------------------- |
| id | `string` | Unique identifier for the step |
| title | `string` | Display title for the step |
| groupIndices | `number[]` | Indices of form groups this step applies to |
| fields | `(keyof T)[]` | Optional list of fields in this step |
| validate | `(groups: FormGroup<T>[]) => boolean \| Promise<boolean>` | Custom validation function |
| isCompleted | `(groups: FormGroup<T>[]) => boolean` | Custom completion check |
| isEnabled | `(allGroups: FormGroup<T>[], completedSteps: string[]) => boolean` | Custom enabled check |
| onEnter | `(groups: FormGroup<T>[]) => void \| Promise<void>` | Called when entering the step |
| onLeave | `(groups: FormGroup<T>[], direction: 'next' \| 'prev') => boolean \| Promise<boolean>` | Called when leaving the step |
#### FormWizardReturn
| Property/Method | Type | Description |
| -------------------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------- |
| steps | `FormStep<T>[]` | Array of step configurations |
| currentStep | `FormStep<T>` | Current active step |
| currentStepId | `string` | ID of current step |
| completedSteps | `string[]` | IDs of completed steps |
| isCompleted | `boolean` | Whether all steps are completed |
| isValidating | `boolean` | Whether validation is in progress |
| isTransitioning | `boolean` | Whether transitioning between steps |
| isStepCompleted | `(stepId: string) => boolean` | Check if a step is completed |
| isStepEnabled | `(stepId: string) => boolean` | Check if a step is enabled |
| isCurrentStep | `(stepId: string) => boolean` | Check if a step is the current step |
| markStepCompleted | `(stepId: string) => void` | Mark a step as completed |
| markStepNotCompleted | `(stepId: string) => void` | Mark a step as not completed |
| validateStep | `(stepId: string) => Promise<boolean>` | Validate a specific step |
| goToStep | `(stepId: string) => Promise<boolean>` | Navigate to a specific step |
| next | `() => Promise<boolean>` | Navigate to the next step |
| prev | `() => Promise<boolean>` | Navigate to the previous step |
| canGoNext | `boolean` | Whether can navigate to next step |
| canGoPrev | `boolean` | Whether can navigate to previous step |
| resetWizard | `(options?: { resetFormData?: boolean; clearCompletedSteps?: boolean }) => void` | Reset the wizard |
| getSteps | `() => Array<FormStep<T> & { isActive: boolean; isCompleted: boolean; isEnabled: boolean }>` | Get all steps with status |
| getStep | `(stepId: string) => (FormStep<T> & { isActive: boolean; isCompleted: boolean; isEnabled: boolean }) \| undefined` | Get a specific step with status |
| getCurrentStep | `() => FormStep<T> & { isActive: boolean; isCompleted: boolean; isEnabled: boolean }` | Get current step with status |
| getStepGroups | `(step: FormStep<T>) => FormGroup<T>[]` | Get form groups for a step |
| getStepIndex | `(stepId: string) => number` | Get index of a step |
| getCurrentStepIndex | `() => number` | Get index of current step |
| goToStepByIndex | `(index: number) => Promise<boolean>` | Navigate to a step by index |