ForgeForm

Wizard Forms

ForgeForm's Wizard Forms feature, introduced in v1.2.2, dramatically simplifies the creation of multi-step forms. Wizards guide users through complex processes by breaking them into sequential, validated steps.

warning

Important: Wizard Forms is still in beta. Some features might break or change as we continue to improve the functionality. Always test thoroughly before using in production.

Contents

When to Use Wizard Forms:

Wizard forms are ideal for scenarios like:

  • Onboarding processes: Guiding new users through account setup, profile creation, and initial settings.
  • Complex surveys or questionnaires: Breaking long surveys into logical sections for better user engagement and completion rates.
  • E-commerce checkout flows: Stepping users through shipping address, billing information, payment details, and order confirmation.
  • Configuration wizards: Setting up complex software or services with a step-by-step approach.
  • Multi-stage applications: Collecting data across multiple related forms that need to be completed in a specific order.

Creating a Wizard with createWizard

The createWizard function is the starting point for building multi-step forms. It takes three main arguments:

  • steps: WizardStepConfig[] (Required): An array defining each step of the wizard. Each step is a WizardStepConfig object with:
    • id: string: A unique identifier for the step. This id is crucial for programmatically controlling the wizard and referencing steps in lifecycle callbacks.
    • schema: FormSchema<StepData>: A ForgeForm schema (created using createSchema) that defines the data and validation rules for the step. <StepData> should be an interface or type representing the data structure for that specific step.
  • initialData?: Partial<T> (Optional): An object providing initial data for the wizard. This data will be pre-populated in the form fields and is useful for scenarios like editing existing data in a wizard format. <T> is the type that represents the combined data across all wizard steps.
  • options?: WizardOptions<T> (Optional): An object to configure wizard behavior and lifecycle events. It accepts the following callback functions:
    • onStepChange?: (stepId: string, index: number) => void: Called whenever the active step changes. Provides the stepId and its index (0-based). Useful for updating UI elements like progress bars.

      const wizard = createWizard(steps, {}, {
        onStepChange: (stepId, index) => console.log(`Step ${index + 1} activated: ${stepId}`),
      });
    • onValidationSuccess?: (stepId: string, data: any) => void: Invoked after a step validates successfully when nextStep is called. Provides the stepId and the validated data for that step. Useful for saving step data or triggering UI updates on successful validation.

      const wizard = createWizard(steps, {}, {
        onValidationSuccess: (stepId, data) => console.log(`Step ${stepId} data validated:`, data),
      });
    • onValidationError?: (stepId: string, errors: FormErrors) => void: Called when validation fails for a step during nextStep. Provides the stepId and the errors object. Useful for displaying step-specific error messages to the user.

      const wizard = createWizard(steps, {}, {
        onValidationError: (stepId, errors) => console.error(`Step ${stepId} validation errors:`, errors),
      });
    • onComplete?: (finalData: any) => void: Triggered when the wizard is successfully completed (all steps validated and nextStep called on the last step). Provides the aggregated finalData from all steps. This is where you would typically handle form submission to a server.

      const wizard = createWizard(steps, {}, {
        onComplete: (finalData) => alert('Wizard Completed! Check console for data'),
      });

Wizard Instance Methods and Properties: Controlling Wizard Flow

Once you create a wizard instance using createWizard(), you gain access to several methods and properties to control its behavior:

  • wizard.nextStep(stepData?: object): Promise<boolean>: Moves the wizard to the next step.

    • stepData (Optional): Data for the current step. If provided, it's merged into the wizard's overall data.
    • Returns a Promise<boolean>: Resolves to true if navigation is successful (current step validates), false if validation fails.
  • wizard.nextStep(stepData?: object): Promise<boolean>: Moves the wizard to the next step.

    • stepData (Optional): Data for the current step. If provided, it's merged into the wizard's overall data.

    • Returns a Promise<boolean>: Resolves to true if navigation is successful (current step validates), false if validation fails.

      import React, { useState, useMemo } from 'react';
      import { createSchema, createWizard } from 'forgeform';
       
      interface Step1Data { name?: string; }
      interface Step2Data { email?: string; }
      interface Step3Data { message?: string; }
      interface WizardFormData extends Step1Data, Step2Data, Step3Data {}
       
      const step1Schema = createSchema<Step1Data>({ fields: { name: { type: 'string', required: true, requiredErrorMessage: 'Name is required' } } });
      const step2Schema = createSchema<Step2Data>({ fields: { email: { type: 'email', required: true, requiredErrorMessage: 'Email is required', formatErrorMessage: 'Invalid email' } } });
      const step3Schema = createSchema<Step3Data>({ fields: { message: { type: 'textarea', required: true, minLength: 10, maxLength: 200, requiredErrorMessage: 'Message is required', maxLengthErrorMessage: 'Message too long' } } });
       
      const wizardInstanceExample = createWizard<WizardFormData>([
        { id: 'step1', schema: step1Schema },
        { id: 'step2', schema: step2Schema },
        { id: 'step3', schema: step3Schema }
      ]);
       
      const NextStepExample = () => {
        const [wizard] = useState(() => wizardInstanceExample);
        const [formData, setFormData] = useState({});
       
        const handleNextClick = async () => {
          const success = await wizard.nextStep(formData);
          if (success) {
            alert('Step Validated and Moved to Next! (Check console)');
            console.log('Wizard Data after nextStep:', wizard.data);
            // In real app, you would update UI to show the next step
          } else {
            alert('Validation Failed for Current Step!');
            // Handle validation errors (display in UI)
          }
        };
       
        return (
          <div>
            <p>Demonstrates <b>wizard.nextStep()</b></p>
            <button onClick={handleNextClick}>Next Step</button>
          </div>
        );
      };
       
      export default NextStepExample;
  • wizard.previousStep(): void: Moves the wizard to the preceding step. Has no effect if already at the first step.

    import React, { useState, useMemo } from 'react';
    import { createSchema, createWizard } from 'forgeform';
     
    // ... (Step schemas as defined in nextStep example) ...
    const wizardInstanceExample2 = createWizard<WizardFormData>([
      { id: 'step1', schema: step1Schema },
      { id: 'step2', schema: step2Schema },
      { id: 'step3', schema: step3Schema }
    ]);
     
    const PreviousStepExample = () => {
      const [wizard] = useState(() => wizardInstanceExample2);
      const [currentStepIndex, setCurrentStepIndex] = useState(0);
     
      const handlePreviousClick = () => {
        wizard.previousStep();
        setCurrentStepIndex(wizard.currentStepIndex);
      };
     
      // Simulate moving to step 2 initially (for demo purposes)
      useMemo(() => { wizard.nextStep({}); setCurrentStepIndex(1); }, []); // runs once on mount
     
      return (
        <div>
          <p>Demonstrates <b>wizard.previousStep()</b>. Currently on Step {currentStepIndex + 1}.</p>
          {currentStepIndex > 0 && <button onClick={handlePreviousClick}>Previous Step</button>}
        </div>
      );
    };
     
    export default PreviousStepExample;
  • wizard.goToStep(stepId: string): void: Navigates the wizard directly to the step with the provided stepId. Useful for implementing step navigation menus or conditional branching in wizards.

    import React, { useState, useMemo } from 'react';
    import { createSchema, createWizard } from 'forgeform';
     
    // ... (Step schemas as defined in nextStep example) ...
    const wizardInstanceExample3 = createWizard<WizardFormData>([
      { id: 'step1', schema: step1Schema },
      { id: 'step2', schema: step2Schema },
      { id: 'step3', schema: step3Schema }
    ]);
     
    const GoToStepExample = () => {
      const [wizard] = useState(() => wizardInstanceExample3);
      const [currentStepId, setCurrentStepId] = useState(wizardInstanceExample3.currentStepId);
     
      const handleGoToStep2 = () => {
        wizard.goToStep('step2');
        setCurrentStepId(wizard.currentStepId);
      };
     
      const handleGoToStep3 = () => {
        wizard.goToStep('step3');
        setCurrentStepId(wizard.currentStepId);
      };
     
      return (
        <div>
          <p>Demonstrates <b>wizard.goToStep(stepId)</b>. Currently on Step ID: {currentStepId}.</p>
          <button onClick={handleGoToStep2}>Go to Step 2 (Email)</button>
          <button onClick={handleGoToStep3}>Go to Step 3 (Message)</button>
        </div>
      );
    };
     
    export default GoToStepExample;
  • wizard.reset(): void: Resets the wizard to its initial state. Clears all collected data, sets the current step back to the first step, and resets validation history. Useful for allowing users to restart the wizard from the beginning.

    import React, { useState, useMemo } from 'react';
    import { createSchema, createWizard } from 'forgeform';
     
    // ... (Step schemas as defined in nextStep example) ...
    const wizardInstanceExample4 = createWizard<WizardFormData>([
      { id: 'step1', schema: step1Schema },
      { id: 'step2', schema: step2Schema },
      { id: 'step3', schema: step3Schema }
    ], { name: 'Initial Name' }); // Initial data example
     
    const ResetWizardExample = () => {
      const [wizard, setWizard] = useState(() => wizardInstanceExample4);
      const [currentStepIndex, setCurrentStepIndex] = useState(wizardInstanceExample4.currentStepIndex);
      const [wizardData, setWizardData] = useState(wizardInstanceExample4.data);
     
      const handleResetClick = () => {
        wizard.reset();
        setCurrentStepIndex(wizard.currentStepIndex);
        setWizardData(wizard.data); // Update state to reflect reset data
        alert('Wizard Reset!');
      };
     
      useMemo(() => { wizard.nextStep({ email: '[email address removed]' }); setCurrentStepIndex(1); setWizardData(wizard.data); }, []); // Simulate progressing for demo
     
      return (
        <div>
          <p>Demonstrates <b>wizard.reset()</b>. Currently on Step {currentStepIndex + 1}.<br />Data (before reset): {JSON.stringify(wizardData)}</p>
          <button onClick={handleResetClick}>Reset Wizard</button>
        </div>
      );
    };
     
    export default ResetWizardExample;
  • wizard.getProgress(): number: Returns the current progress of the wizard as a number between 0 and 1. Useful for displaying a progress bar or percentage indicator in the UI. Calculates progress based on the current step index and total steps.

    import React, { useState, useMemo } from 'react';
    import { createSchema, createWizard } from 'forgeform';
     
    // ... (Step schemas as defined in nextStep example) ...
    const wizardInstanceExample5 = createWizard<WizardFormData>([
      { id: 'step1', schema: step1Schema },
      { id: 'step2', schema: step2Schema },
      { id: 'step3', schema: step3Schema }
    ]);
     
    const ProgressExample = () => {
      const [wizard] = useState(() => wizardInstanceExample5);
      const [progress, setProgress] = useState(wizard.getProgress());
     
      const handleNextClick = async () => {
        await wizard.nextStep({});
        setProgress(wizard.getProgress());
      };
     
      return (
        <div>
          <p>Demonstrates <b>wizard.getProgress()</b>. Current Progress: {(progress * 100).toFixed(0)}%</p>
          <button onClick={handleNextClick}>Next Step</button>
        </div>
      );
    };
     
    export default ProgressExample;
  • wizard.getState(): { currentStepId: string; currentStepIndex: number; data: Partial<T>; completedSteps: boolean[]; }: Returns an object containing the current state of the wizard, including: currentStepId, currentStepIndex, data, and completedSteps (an array of booleans indicating if each step has been completed and validated). Useful for persisting and restoring wizard state, or for advanced UI logic that depends on the wizard's internal state.

    import React, { useState, useMemo } from 'react';
    import { createSchema, createWizard } from 'forgeform';
     
    // ... (Step schemas as defined in nextStep example) ...
    const wizardInstanceExample6 = createWizard<WizardFormData>([
      { id: 'step1', schema: step1Schema },
      { id: 'step2', schema: step2Schema },
      { id: 'step3', schema: step3Schema }
    ]);
     
    const StateExample = () => {
      const [wizard] = useState(() => wizardInstanceExample6);
      const [wizardState, setWizardState] = useState(wizard.getState());
     
      const handleNextClick = async () => {
        await wizard.nextStep({});
        setWizardState(wizard.getState()); // Update state after step change
      };
     
      return (
        <div>
          <p>Demonstrates <b>wizard.getState()</b>. Current State: <pre style={{ fontSize: '0.8em' }}>{JSON.stringify(wizardState, null, 2)}</pre></p>
          <button onClick={handleNextClick}>Next Step</button>
        </div>
      );
    };
     
    export default StateExample;

Properties:

  • wizard.currentStepIndex: number: Read-only property that provides the 0-based index of the current active step.
  • wizard.currentStepId: string: Read-only property that returns the id of the current step.
  • wizard.totalSteps: number: Read-only property indicating the total number of steps defined in the wizard.
  • wizard.data: Partial<T>: Read-write property (though typically updated through nextStep and other methods) that holds all the data collected across the wizard's steps. This object aggregates data from each step, keyed by the field names defined in your schemas.

Comprehensive 3-Step Wizard Example (Revisited)

Let's revisit and enhance the 3-step wizard example to demonstrate the methods and properties in a more complete context:

// src/components/ComprehensiveWizardExample.tsx
import React, { useState, useMemo } from 'react';
import { createSchema, createWizard } from 'forgeform';
 
interface ComprehensiveWizardData {
  name?: string;
  email?: string;
  message?: string;
}
 
// Define schemas for each step (using Regex for email validation)
const step1SchemaComp = createSchema<ComprehensiveWizardData>({ fields: { name: { type: 'string', required: true, requiredErrorMessage: 'Name is required' } } });
const step2SchemaComp = createSchema<ComprehensiveWizardData>({ fields: { email: { type: 'email', required: true, requiredErrorMessage: 'Email is required', formatErrorMessage: 'Invalid email format' } } });
const step3SchemaComp = createSchema<ComprehensiveWizardData>({ fields: { message: { type: 'textarea', required: true, minLength: 10, maxLength: 200, requiredErrorMessage: 'Message is required', maxLengthErrorMessage: 'Message too long' } } });
 
const wizardInstanceComprehensive = createWizard<ComprehensiveWizardData>(
  [
    { id: 'step-name', schema: step1SchemaComp },
    { id: 'step-email', schema: step2SchemaComp },
    { id: 'step-message', schema: step3SchemaComp },
  ],
  {}, // Initial data
  {
    onStepChange: (stepId, index) => console.log(`[Callback] Step changed to: ${stepId}, index: ${index}`),
    onComplete: (finalData) => {
      console.log('[Callback] Wizard Completed! Final Data:', finalData);
      alert('Wizard Completed! Check console for final data.');
    },
    onValidationError: (stepId, errors) => {
      console.error(`[Callback] Validation failed for step ${stepId} with errors:`, errors);
      alert(`Validation errors on step ${stepId}. See console for details.`);
    },
    onValidationSuccess: (stepId, data) => console.log(`[Callback] Step ${stepId} validated successfully with data:`, data),
  }
);
 
const ComprehensiveWizardExample = () => {
  const [currentStepIndexState, setCurrentStepIndexState] = useState(0);
  const [formData, setFormData] = useState({});
  const wizard = useMemo(() => wizardInstanceComprehensive, []); // Memoize wizard instance
  const [progress, setProgress] = useState(wizard.getProgress()); // Track progress
 
  const handleInputChange = (e) => {
    setFormData({ ...formData, [e.target.name]: e.target.value });
  };
 
  const handleNext = async (e) => {
    e.preventDefault();
    const success = await wizard.nextStep(formData);
    if (success) {
      setFormData({});
      setCurrentStepIndexState(wizard.currentStepIndex);
      setProgress(wizard.getProgress()); // Update progress
    }
  };
 
  const handlePrevious = (e) => {
    e.preventDefault();
    wizard.previousStep();
    setCurrentStepIndexState(wizard.currentStepIndex);
    setProgress(wizard.getProgress()); // Update progress
  };
 
  const handleSubmit = async (e) => {
    e.preventDefault();
    const success = await wizard.nextStep(formData); // Validate final step
    if (success) {
      wizard.reset(); // Reset wizard for next use after completion
      setCurrentStepIndexState(0);
      setProgress(0); // Reset progress
    }
  };
 
  const handleGoToStep2 = () => {
    wizard.goToStep('step-email');
    setCurrentStepIndexState(wizard.currentStepIndex);
    setProgress(wizard.getProgress()); // Update progress
  };
 
  return (
    <div>
      <h3>Comprehensive 3-Step Wizard Demo</h3>
      <p>Step {currentStepIndexState + 1} of {wizard.totalSteps} ({ (progress * 100).toFixed(0)}% Complete)</p>
 
      <button onClick={handleGoToStep2} style={{ marginBottom: '10px' }}>Go to Step 2 (Email - Programmatic Navigation)</button>
 
      <form onSubmit={wizard.currentStepIndex === wizard.totalSteps - 1 ? handleSubmit : handleNext}>
        {wizard.currentStepIndex === 0 && (
          <div>
            <label htmlFor="name">Name:</label>
            <input type="text" id="name" name="name" onChange={handleInputChange} />
          </div>
        )}
 
        {wizard.currentStepIndex === 1 && (
          <div>
            <label htmlFor="email">Email:</label>
            <input type="email" id="email" name="email" onChange={handleInputChange} />
          </div>
        )}
 
        {wizard.currentStepIndex === 2 && (
          <div>
            <label htmlFor="message">Message:</label>
            <textarea id="message" name="message" onChange={handleInputChange} />
          </div>
        )}
 
        <div>
          {currentStepIndexState > 0 && <button type="button" onClick={handlePrevious}>Previous</button>}
          <button type="submit">{wizard.currentStepIndex === wizard.totalSteps - 1 ? 'Submit' : 'Next'}</button>
        </div>
      </form>
    </div>
  );
};
 
export default ComprehensiveWizardExample;

Wizard Forms with React Hook Form Resolver

ForgeForm Wizard Forms can be seamlessly integrated with React Hook Form using forgeFormResolver. This powerful combination allows you to leverage React Hook Form's excellent form state management and ForgeForm's robust schema-based validation within your multi-step wizards.

Example: 3-Step Wizard with React Hook Form Resolver

Let's create a 3-step wizard similar to the previous example, but now integrated with React Hook Form for form state and validation handling within each step:

// src/components/WizardHookFormExample.tsx
import React, { useState, useMemo } from 'react';
import { useForm, useFormContext, FormProvider } from 'react-hook-form';
import { createSchema, createWizard, forgeFormResolver } from 'forgeform';
 
// Define data interfaces for each step
interface Step1HookFormData { name?: string; }
interface Step2HookFormData { email?: string; }
interface Step3HookFormData { message?: string; }
interface WizardHookFormData extends Step1HookFormData, Step2HookFormData, Step3HookFormData {}
 
// Define schemas for each step
const step1HookSchema = createSchema<Step1HookFormData>({ fields: { name: { type: 'string', required: true, requiredErrorMessage: 'Name is required' } } });
const step2HookSchema = createSchema<Step2HookFormData>({ fields: { email: { type: 'email', required: true, requiredErrorMessage: 'Email is required', formatErrorMessage: 'Invalid email' } } });
const step3HookSchema = createSchema<Step3HookFormData>({ fields: { message: { type: 'textarea', required: true, minLength: 10, maxLength: 200, requiredErrorMessage: 'Message is required', maxLengthErrorMessage: 'Message too long' } } });
 
// Create Wizard Instance
const wizardHookFormInstance = createWizard<WizardHookFormData>([
  { id: 'step-name', schema: step1HookSchema },
  { id: 'step-email', schema: step2HookSchema },
  { id: 'step-message', schema: step3HookSchema },
], {});
 
// Step Components (using useFormContext to access React Hook Form methods)
const Step1Component = () => {
  const { register, formState: { errors } } = useFormContext<Step1HookFormData>();
  return (
    <div>
      <label htmlFor="name">Name:</label>
      <input {...register('name')} id="name" />
      {errors.name && <p style={{ color: 'red' }}>{errors.name.message}</p>}
    </div>
  );
};
 
const Step2Component = () => {
  const { register, formState: { errors } } = useFormContext<Step2HookFormData>();
  return (
    <div>
      <label htmlFor="email">Email:</label>
      <input {...register('email')} type="email" id="email" />
      {errors.email && <p style={{ color: 'red' }}>{errors.email.message}</p>}
    </div>
  );
};
 
const Step3Component = () => {
  const { register, formState: { errors } } = useFormContext<Step3HookFormData>();
  return (
    <div>
      <label htmlFor="message">Message:</label>
      <textarea {...register('message')} id="message" />
      {errors.message && <p style={{ color: 'red' }}>{errors.message.message}</p>}
    </div>
  );
};
 
const WizardHookFormExample = () => {
  const [currentStepIndexState, setCurrentStepIndexState] = useState(0);
  const wizard = useMemo(() => wizardHookFormInstance, []);
  const [progress, setProgress] = useState(wizard.getProgress());
 
  // Initialize React Hook Form instance - outside of the component to persist across steps
  const formMethods = useForm<WizardHookFormData>({
    resolver: forgeFormResolver(wizard.getCurrentStepSchema()), // Resolver for initial step
    mode: 'onSubmit' // or 'onBlur', etc. as needed
  });
  const { handleSubmit, setError, clearErrors } = formMethods; // Extract methods
 
  const handleNext = async (e) => {
    e.preventDefault();
    clearErrors(); // Clear previous step errors before validation
 
    const currentStepId = wizard.currentStepId; // Get current step id for context
    const stepData = formMethods.getValues(); // Get data from React Hook Form for current step
 
    const success = await wizard.nextStep(stepData); // Validate with ForgeForm Wizard
    if (success) {
      formMethods.reset(stepData, { keepValues: true, keepErrors: false }); // Keep valid values, clear errors for next step
      setCurrentStepIndexState(wizard.currentStepIndex);
      setProgress(wizard.getProgress());
      formMethods.rebuildMode('onSubmit', forgeFormResolver(wizard.getCurrentStepSchema())); // Rebuild resolver for next step's schema
    } else {
      // Set errors to React Hook Form - using errors from ForgeForm validation
      const validationResult = await wizard.validateCurrentStep(); // Get validation result to access errors
      if (validationResult && validationResult.errors) {
        Object.entries(validationResult.errors).forEach(([fieldName, errorObj]) => {
          setError(fieldName, { type: 'manual', message: errorObj.error });
        });
      }
      alert('Validation errors on current step. See errors below.');
    }
  };
 
  const handlePrevious = (e) => {
    e.preventDefault();
    wizard.previousStep();
    setCurrentStepIndexState(wizard.currentStepIndex);
    setProgress(wizard.getProgress());
    formMethods.rebuildMode('onSubmit', forgeFormResolver(wizard.getCurrentStepSchema())); // Rebuild resolver for prev step's schema
    clearErrors(); // Clear errors when going back
  };
 
  const handleSubmitFinal = handleSubmit(async (data) => { // React Hook Form's handleSubmit
    const success = await wizard.nextStep(data); // Final step validation
    if (success) {
      alert('Wizard Completed with React Hook Form! Check console for final data.');
      console.log('Wizard Final Data (React Hook Form Integration):', wizard.data);
      wizard.reset();
      setCurrentStepIndexState(0);
      setProgress(0);
      formMethods.reset({}, { keepValues: false, keepErrors: false }); // Reset form completely
      formMethods.rebuildMode('onSubmit', forgeFormResolver(wizard.getCurrentStepSchema())); // Reset resolver to first step schema
    } else {
      alert('Validation errors on final step.');
    }
  });
 
  return (
    <FormProvider {...formMethods}> {/* FormProvider to make form methods available to steps */}
      <div>
        <h3>Wizard Form with React Hook Form Resolver</h3>
        <p>Step {currentStepIndexState + 1} of {wizard.totalSteps} ({ (progress * 100).toFixed(0)}% Complete)</p>
 
        <form onSubmit={wizard.currentStepIndex === wizard.totalSteps - 1 ? handleSubmitFinal : handleNext}>
          {wizard.currentStepIndex === 0 && <Step1Component />}
          {wizard.currentStepIndex === 1 && <Step2Component />}
          {wizard.currentStepIndex === 2 && <Step3Component />}
 
          <div>
            {currentStepIndexState > 0 && <button type="button" onClick={handlePrevious}>Previous</button>}
            <button type="submit">{wizard.currentStepIndex === wizard.totalSteps - 1 ? 'Submit' : 'Next'}</button>
          </div>
        </form>
      </div>
    </FormProvider>
  );
};
 
export default WizardHookFormExample;
  • Import necessary modules:

    import { useForm, useFormContext, FormProvider } from 'react-hook-form';
    import { createSchema, createWizard, forgeFormResolver } from 'forgeform';

    We import useForm, useFormContext, FormProvider from react-hook-form and ForgeForm related functions.

  • Define Step Schemas and Wizard: Schemas and the wizard are defined similarly to the previous Wizard example, but now schemas are specific to each step's data interface (e.g., Step1HookFormData).

  • Step Components using useFormContext: Each step component (Step1Component, Step2Component, Step3Component) uses useFormContext<StepData>() to access React Hook Form's register function and errors object. This allows each step to manage its own input fields and display errors within the React Hook Form context.

  • FormProvider: The main WizardHookFormExample component wraps the entire form with <FormProvider {...formMethods}>. This makes the React Hook Form methods (register, handleSubmit, errors, etc.) available to all step components rendered within it, via useFormContext.

  • useForm Initialization with forgeFormResolver:

    const formMethods = useForm<WizardHookFormData>({
      resolver: forgeFormResolver(wizard.getCurrentStepSchema()),
      mode: 'onSubmit'
    });

    We initialize React Hook Form's useForm outside the component's render to persist the form state across step changes. Crucially, we use forgeFormResolver(wizard.getCurrentStepSchema()) to link the resolver to the current step's schema from the ForgeForm wizard. wizard.getCurrentStepSchema() is a helper method (you'd need to add this to your createWizard implementation, or track current schema manually) to get the schema for the active step. Initially, it's the schema of the first step.

  • handleNext function:

    • Calls clearErrors() to reset errors from previous steps.
    • Gets current step data using formMethods.getValues().
    • Calls wizard.nextStep(stepData) to validate the step using ForgeForm Wizard and move to the next step.
    • If validation succeeds (success is true):
      • formMethods.reset is called to clear errors and optionally keep the valid values for the next step's form (using keepValues: true).
      • setCurrentStepIndexState and setProgress are updated.
      • formMethods.rebuildMode is essential: it updates React Hook Form's resolver to use the schema of the new current step after a successful nextStep.
    • If validation fails (success is false):
      • wizard.validateCurrentStep() is called to get detailed validation errors from ForgeForm.
      • setError from React Hook Form is used to manually set errors for each field based on the errors from ForgeForm, so React Hook Form can display them correctly.
  • handlePrevious function:

    • Calls wizard.previousStep() to move to the previous step.
    • Updates setCurrentStepIndexState and setProgress.
    • Crucially, calls formMethods.rebuildMode to update the resolver to the previous step's schema.
    • clearErrors() is called to clear any errors as the user goes back.
  • handleSubmitFinal function:

    • This is passed to React Hook Form's handleSubmit for the final step's submission.
    • Calls wizard.nextStep(data) to perform final validation on the last step.
    • If final validation is successful, it alerts success, logs final data, resets the wizard and React Hook Form, and resets the resolver to the first step's schema for the next wizard usage.
    • Otherwise, it alerts the user of validation errors.
  • Conditional Rendering of Step Components: The step components (Step1Component, Step2Component, Step3Component) are conditionally rendered based on wizard.currentStepIndex to show the appropriate step UI.

  • Rebuilding Resolver: The rebuildMode method in useForm is key to dynamically updating the resolver as the wizard progresses through steps with different schemas. This ensures React Hook Form always validates against the correct schema for the active step.

On this page