Directory
- Welcome
Multi Step Form Component
The Multi Step Form component provides a powerful foundation for building complex, multi-step forms with built-in validation, data persistence, and smooth animations. Perfect for surveys, wizards, and complex data collection workflows.
Key Features
-
Step Management: Flexible step configuration with back/forward navigation
-
Data Persistence: Optional auto-save functionality with customizable storage and form flow history
-
Validation: Built-in Zod schema validation with custom validation support
-
Dynamic Step Management: Customize form flow through the schema, conditional steps made easy
Configuration Overview
The Multi Step Form requires configuration across several files to work properly:
1. Form Schema Definition
First, define your form’s schema and steps in a dedicated file (e.g., src/schemas/form-flows/your-form.ts
)
import { z } from 'zod';
export const flatSchema = z.object({ field1: z.string().min(2), field2: z.string().email(), field3: z.string(),});
The schema here will be used to validate the form data in the Astro action and local storage.
2. Form Steps Definition
Use the defineFormSchema
function to define your form’s steps. The function accepts an object where the keys are the step IDs and the values are the step configurations. The file should be in src/lib/client/form-flows/your-form.ts
Each step configuration is defined using the defineStep
function. This function accepts a configuration object that describes the step’s schema, validation, and other options.
DefineStep arguments
Prop | Type | Description |
---|---|---|
schema | ZodSchema | The Zod schema for the step's fields |
onComplete | (formData: FormData) => Promise<string | undefined> | Handler called when step is complete, returns next step ID |
showSubmitButton | boolean | Show submit button instead of next |
changeOnSelect | boolean | Advance to next step on selection |
selectHandler | (form: HTMLElement) => selectHandlerResult | null | Custom changeOnSelect behavior handler |
processInput | Record<string, (value: string) => Promise<unknown> | unknown> | Pre-process field values before validation |
validate | Record<string, (formData: FormData) => Promise<{ success: boolean, errors?: string[] }>> | Custom validation functions |
onReturn | (form: HTMLFormElement) => Promise<void> | Handler called when returning to step |
onEnter | (form: HTMLFormElement) => Promise<void> | Handler called when entering step |
import { defineFormSchema, defineStep } from '@/schemas/multistep';import { flatSchema } from './basic-schema';
export const yourFormSteps = defineFormSchema({ stepOne: defineStep({ schema: flatSchema.pick({ field1: true, field2: true, }), async onComplete(formData) { if (formData.get('field1') === 'helloworld') { return 'secretStep'; // Return next step ID } return 'stepOne'; // Return same step ID }, }), stepTwo: defineStep({ schema: flatSchema.pick({ field3: true, }), showSubmitButton: true, async onComplete() { return undefined; // Form complete }, }), secretStep: defineStep({ schema: flatSchema.pick({ field3: true, }), showSubmitButton: true, async onComplete() { return undefined; // Form complete }, }),});
selectHandler
The selectHandler
option is used to handle the selection for the step when using the changeOnSelect
option. It should return an object with the following properties:
Prop | Type | Description |
---|---|---|
getData | () => FormData | A function that is called to get the data for the step. It should return a `FormData` object. |
subscribe | (callback: (value?: string) => void) => void | A function that is called to setup any event listeners to the data for the step. It's argument is a callback function that is called when the data changes via the event listener. |
cleanup | () => void | A function that is called to cleanup the event listeners for the step. |
import { defineFormSchema, defineStep } from '@/schemas/multistep';import { flatSchema } from './basic-schema';
export const yourFormSteps = defineFormSchema({ stepOne: defineStep({ schema: flatSchema.pick({ field1: true, field2: true, }), async onComplete(formData) { if (formData.get('field1') === 'helloworld') { return 'secretStep'; // Return next step ID } return 'stepOne'; // Return same step ID }, }), stepTwo: defineStep({ schema: flatSchema.pick({ field3: true, }), showSubmitButton: true, async onComplete() { return undefined; // Form complete }, }), secretStep: defineStep({ schema: flatSchema.pick({ field3: true, }), showSubmitButton: true, async onComplete() { return undefined; // Form complete }, }),});
3. Local Storage Schema
Add your form’s schema to schemas/local-data.ts
to enable data persistence:
import { z } from 'zod';import { flatSchema } from './basic-schema';
export const ZLocalDataSchemas = z.object({ // ... existing schemas ... ['your-form']: flatSchema,});
4. Form HTML
Create your form page component (e.g., pages/your-form.astro
) and use the MultistepForm
component for each step.
---import MSForm from '@/components/NST-components/UI/multistep-form/MultistepForm.astro';import MSFormSection from '@/components/NST-components/UI/multistep-form/FormSection.astro';import Input from '@/components/NST-components/UI/input/Input.astro';---
<MSForm name="your-form" animation={{ type: 'slide', direction: 'horizontal' }} scrollToTop> <MSFormSection name="stepOne"> <Input type="text" name="field1" required /> <Input type="email" name="field2" required /> </MSFormSection>
<MSFormSection name="secretStep"> <Input type="text" name="field3" required /> </MSFormSection>
<MSFormSection name="stepTwo"> <Input type="text" name="field3" required /> </MSFormSection></MSForm>
5. Form Script
Create your form page script (e.g., scripts/your-form.ts
):
<script> //eslint-disable-next-line // @ts-nocheck import { yourFormSteps } from './basic-steps'; import { ready } from '@/components/NST-components/base'; import { genericSaveDataHandler } from '@/lib/client/storage'; import { ActionError } from 'astro:actions';
async function init() { const form = await ready( document.querySelector('multistep-form') );
if (!form) return;
form.setStepConfig({ config: yourFormSteps, firstStep: 'stepOne', localDataKey: 'your-form', });
form.submitHandler = async (formData) => { try { await submitFormData(formData); return undefined; } catch { return new ActionError({ message: 'Submission failed', code: 'SUBMIT_ERROR', }); } };
// Optional: Enable auto-save form.saveDataHandler = genericSaveDataHandler('your-form'); }
init();</script>
Auto-save Functionality
Implement auto-save functionality with debouncing. You can set the debounce rate in milliseconds, and define the function that runs on form auto-save. There’s three ways to set this up.
import { ready } from '@/components/NST-components/base';import type { MSFElement } from '@/components/NST-components/UI/multistep-form/data';import { getData, setData } from '@/lib/client/storage';
const contactForm = await ready( document.querySelector<MSFElement>('multistep-form[name="contact-form"]'));
if (contactForm) { contactForm.saveDataHandler = (data) => { const contactFormData = { name: data.get('name')?.toString(), email: data.get('email')?.toString(), message: data.get('message')?.toString(), };
setData('contact-form', { ...getData('contact-form'), ...contactFormData, }); }; //If youre not saving all the data, you can disable the step history so the user will start on the first step if the page refreshes. But the data will still be saved. contactForm.saveStepHistory = false;}
import { ready } from '@/components/NST-components/base';import type { MSFElement } from '@/components/NST-components/UI/multistep-form/data';
const localDataKey = 'contact-form';
const contactForm = await ready( document.querySelector<MSFElement>('multistep-form[name="contact-form"]'));
if (contactForm) { //Can also be set in the forms component props without needing a script contactForm.localDataKey = localDataKey;}
import { ready } from '@/components/NST-components/base';import type { MSFElement } from '@/components/NST-components/UI/multistep-form/data';import { genericSaveDataHandler } from '@/lib/client/storage';
const localDataKey = 'contact-form';
const contactForm = await ready( document.querySelector<MSFElement>('multistep-form[name="contact-form"]'));
if (contactForm) { contactForm.saveDataHandler = genericSaveDataHandler(localDataKey, { dontSave: ['add_to_newsletter'], }); contactForm.saveStepHistory = false;}
Form Functions API
The Form component provides the following functions:
Name | Type | Description |
---|---|---|
get: data | () => FormData | A function that is called to get the data for the form. |
set: stepConfig | () => void | A function that is called to setup the step config for the form. |
set: saveDataHandler | (data: { data: Record<string, string | File>; stepHistory: string[]; currentStep: string; }) => void | A function that is called to setup the save data handler for the form. |
set: submitHandler | (formData: FormData) => Promise<ActionError | undefined> | A function that is called to submit the form. |
Advanced Features
Data Persistence
Enable auto-save and form flow history functionality:
form.saveDataHandler = (data) => { localStorage.setItem('form-data', JSON.stringify(data));};
Custom Validation
Add custom validation to any step:
defineStep({ schema: z.object({ username: z.string(), }), validate: { username: async (formData) => { const username = formData.get('username'); const isAvailable = await checkUsernameAvailability(username); return { success: isAvailable, errors: isAvailable ? undefined : ['Username already taken'], }; }, },});
Select Handlers
Create interactive selection steps:
defineStep({ schema: z.object({ plan: z.enum(['basic', 'pro', 'enterprise']) }), changeOnSelect: true, // Advance on selection selectHandler: (form) => ({ subscribe: (callback) => { form.addEventListener('click', (e) => { const plan = e.target.closest('[data-plan]')?.dataset.plan; if (plan) callback(plan); }); }, cleanup: () => { // Cleanup event listeners } })
MultistepForm Props API
Prop | Type | Default | Description |
---|---|---|---|
name | string | - | Unique identifier for the form |
animation | { type: "fade" | "slide", direction: "horizontal" | "vertical", duration: number } | { type: "fade", direction: "horizontal", duration: 300 } | Animation configuration |
scrollToTop | boolean | false | Scroll to top on step change |
submitButtonText | string | Submit | Submit button text |
submitButtonTextSubmitting | string | Submitting... | Text during submission |
submitButtonProps | ComponentProps<typeof Button> | {} | Submit button props |
backButtonProps | ComponentProps<typeof Button> | {} | Back button props |
nextButtonProps | ComponentProps<typeof Button> | {} | Next button props |
FormSection Element
The FormSection
element is used to wrap the form fields for each step. It extends the <form>
element and is used to wrap the form fields for each step.
FormSection Props API
Prop | Type | Default | Description |
---|---|---|---|
name | string | - | Unique identifier for the form |
Limited Time Launch Sale
Get started with our Multi Step Form component and other powerful UI components in our AstroJS starter template!
GET 60% OFF!