Multi-Step Form
A complex multi-step form with validation and navigation using individual stage components and StageRenderer.
Overview
This example demonstrates a comprehensive multi-step form with the following features:
- Multi-Stage Flow: Progression through stages: 'personal' → 'contact' → 'preferences' → 'confirmation' → 'complete'
- Individual Stage Components: Each stage is a separate React component
- StageRenderer: Automatic stage rendering based on current stage
- Form Validation: Real-time validation with error handling and display
- Data Persistence: Form data persists across stage transitions
- Error Handling: Graceful handling of validation errors
- Navigation: Back/forward navigation between stages
- Progress Indicator: Visual progress indicator at the top
- Debug Interface: Interactive debug panel for real-time state monitoring and manipulation
Key Features
This example demonstrates several advanced Stage Flow patterns:
1. Data Management with setStageData
The form uses engine.setStageData()
to update form data without triggering stage transitions:
const handleNameChange = React.useCallback(
e => {
const updatedData = { ...(data || {}), name: e.target.value };
engine.setStageData(updatedData); // Updates data without stage change
},
[data, engine]
);
This allows for:
- Real-time form updates: Data changes immediately without transitions
- Validation feedback: Error states can be updated instantly
- Performance: No unnecessary stage transitions for data updates
2. Multi-stage Form Flow
The form progresses through multiple stages with data persistence:
// Personal stage
{ name: 'personal', transitions: [{ target: 'contact', event: 'next' }] }
// Contact stage
{ name: 'contact', transitions: [
{ target: 'personal', event: 'back' },
{ target: 'review', event: 'next' }
]}
// Review stage
{ name: 'review', transitions: [
{ target: 'contact', event: 'back' },
{ target: 'success', event: 'submit' }
]}
3. Error Handling
Each stage validates data and shows appropriate errors:
const handleNext = React.useCallback(() => {
if (!data?.name || data.name.trim() === "") {
const dataWithError = { ...(data || {}), errors: { personal: "Name is required" } };
engine.setStageData(dataWithError); // Update with error state
return;
}
// Proceed to next stage
send("next", updatedData);
}, [data, send, engine]);
4. Progress Tracking
The form shows current progress and allows navigation:
function ProgressIndicator({ currentStage }) {
const stages = ['personal', 'contact', 'review', 'success'];
const currentIndex = stages.indexOf(currentStage);
return (
<div className="progress-bar">
{stages.map((stage, index) => (
<div key={stage} className={`step ${index <= currentIndex ? 'active' : ''}`}>
{stage}
</div>
))}
</div>
);
}
Usage
import { StageFlowEngine } from '@stage-flow/core';
import { StageFlowProvider, useStageFlow } from '@stage-flow/react';
// Create engine with form configuration
const engine = new StageFlowEngine(formConfig);
function App() {
return (
<StageFlowProvider engine={engine}>
<MultiStepForm />
</StageFlowProvider>
);
}
Benefits
- Type-safe data updates:
setStageData
ensures data consistency - Reactive UI: Changes are immediately reflected in components
- Validation integration: Error states update without transitions
- Performance optimized: No unnecessary re-renders or transitions
- Developer experience: Clean, predictable data flow
Live Example
Live Editor
// import { StageFlowEngine } from '@stage-flow/core'; // import { StageFlowProvider, useStageFlow } from '@stage-flow/react'; function MultiStepForm() { // Common UI Components function ProgressIndicator({ currentStage }) { const stages = ["personal", "contact", "preferences", "confirmation"]; return ( <div style={{ display: "flex", marginBottom: "20px", gap: "5px" }}> {stages.map((stage, index) => ( <div key={stage} style={{ padding: "10px", backgroundColor: stage === currentStage ? "#007bff" : "#f8f9fa", color: stage === currentStage ? "white" : "#495057", borderRadius: "5px", fontSize: "14px", fontWeight: stage === currentStage ? "600" : "400", }} > {index + 1}. {stage} </div> ))} </div> ); } function ErrorDisplay({ error }) { if (!error) return null; return ( <div style={{ color: "#721c24", marginBottom: "10px", padding: "10px", backgroundColor: "#f8d7da", borderRadius: "4px", border: "1px solid #f5c6cb", }} > {error} </div> ); } function FormButton({ onClick, children, variant = "primary", disabled = false }) { const buttonStyles = { primary: { backgroundColor: "#007bff", color: "white", }, secondary: { backgroundColor: "#6c757d", color: "white", }, success: { backgroundColor: "#28a745", color: "white", }, }; const style = { padding: "10px 20px", border: "none", borderRadius: "4px", cursor: disabled ? "not-allowed" : "pointer", fontSize: "14px", fontWeight: "500", opacity: disabled ? 0.6 : 1, ...buttonStyles[variant], }; return ( <button onClick={onClick} style={style} disabled={disabled}> {children} </button> ); } function FormContainer({ children, title = "Multi-Step Form" }) { return ( <div style={{ padding: "20px", border: "1px solid #ddd", borderRadius: "8px", maxWidth: "600px", backgroundColor: "white", }} > <h2 style={{ margin: "0 0 20px 0", color: "#2c3e50" }}>{title}</h2> {children} </div> ); } function FormInput({ type = "text", placeholder, value, onChange, style = {} }) { const inputStyle = { padding: "10px", border: "1px solid #ddd", borderRadius: "4px", width: "100%", marginBottom: "10px", color: "#2c3e50", fontSize: "14px", backgroundColor: "white", ...style, }; return <input type={type} placeholder={placeholder} value={value || ""} onChange={onChange} style={inputStyle} />; } function FormLabel({ children, style = {} }) { const labelStyle = { display: "block", marginBottom: "10px", color: "#495057", fontSize: "14px", ...style, }; return <label style={labelStyle}>{children}</label>; } function DebugInfo() { const { currentStage, data } = useStageFlow(); return ( <div style={{ marginBottom: "20px", padding: "15px", backgroundColor: "#f8f9fa", borderRadius: "4px", border: "1px solid #e9ecef", maxWidth: "600px", width: "100%", }} > <div style={{ marginBottom: "10px" }}> <h4 style={{ margin: "0", color: "#495057" }}>Debug Info:</h4> </div> <div> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Current Stage:</strong> {currentStage} </p> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Stage Components:</strong> personal, contact, preferences, confirmation, complete </p> <p style={{ margin: "5px 0", fontSize: "12px", color: "#6c757d" }}> <strong>Current Data:</strong> {JSON.stringify(data)} </p> </div> </div> ); } // Personal stage component function PersonalStage({ data, send }) { const { engine } = useStageFlow(); const handleNameChange = React.useCallback( e => { const updatedData = { ...(data || {}), name: e.target.value }; engine.setStageData(updatedData); }, [data, engine] ); const handleNext = React.useCallback(() => { if (!data?.name || data.name.trim() === "") { const dataWithError = { ...(data || {}), errors: { personal: "Name is required" } }; engine.setStageData(dataWithError); return; } const updatedData = { ...(data || {}), errors: {} }; send("next", updatedData); }, [data, send, engine]); return ( <FormContainer> <ProgressIndicator currentStage="personal" /> <ErrorDisplay error={data?.errors?.personal} /> <div> <h3 style={{ margin: "0 0 15px 0", color: "#2c3e50" }}>Personal Information</h3> <FormInput placeholder="Name" value={data?.name} onChange={handleNameChange} /> </div> <div style={{ marginTop: "20px", display: "flex", gap: "10px" }}> <FormButton onClick={handleNext}>Next</FormButton> </div> </FormContainer> ); } // Contact stage component function ContactStage({ data, send }) { const { engine } = useStageFlow(); const handleEmailChange = React.useCallback( e => { const updatedData = { ...(data || {}), email: e.target.value }; engine.setStageData(updatedData); }, [data, engine] ); const handlePhoneChange = React.useCallback( e => { const updatedData = { ...(data || {}), phone: e.target.value }; engine.setStageData(updatedData); }, [data, engine] ); const handleNext = React.useCallback(() => { if (!data?.email || data.email.trim() === "") { const dataWithError = { ...(data || {}), errors: { contact: "Email is required" } }; engine.setStageData(dataWithError); return; } if (!data.email.includes("@")) { const dataWithError = { ...(data || {}), errors: { contact: "Invalid email format" } }; engine.setStageData(dataWithError); return; } const updatedData = { ...(data || {}), errors: {} }; send("next", updatedData); }, [data, send, engine]); const handleBack = React.useCallback(() => { send("back", data || {}); }, [data, send]); return ( <FormContainer> <ProgressIndicator currentStage="contact" /> <ErrorDisplay error={data?.errors?.contact} /> <div> <h3 style={{ margin: "0 0 15px 0", color: "#2c3e50" }}>Contact Information</h3> <FormInput type="email" placeholder="Email" value={data?.email} onChange={handleEmailChange} /> <FormInput type="tel" placeholder="Phone (optional)" value={data?.phone} onChange={handlePhoneChange} style={{ marginBottom: "0" }} /> </div> <div style={{ marginTop: "20px", display: "flex", gap: "10px" }}> <FormButton onClick={handleBack} variant="secondary"> Back </FormButton> <FormButton onClick={handleNext}>Next</FormButton> </div> </FormContainer> ); } // Preferences stage component function PreferencesStage({ data, send }) { const { engine } = useStageFlow(); const handlePreferenceChange = React.useCallback( (pref, checked) => { const prefs = data?.preferences || []; const updatedPrefs = checked ? [...prefs, pref] : prefs.filter(p => p !== pref); const updatedData = { ...(data || {}), preferences: updatedPrefs }; engine.setStageData(updatedData); }, [data, engine] ); const handleNext = React.useCallback(() => { const updatedData = { ...(data || {}), errors: {} }; send("next", updatedData); }, [data, send]); const handleBack = React.useCallback(() => { send("back", data || {}); }, [data, send]); return ( <FormContainer> <ProgressIndicator currentStage="preferences" /> <div> <h3 style={{ margin: "0 0 15px 0", color: "#2c3e50" }}>Preferences</h3> <FormLabel> <input type="checkbox" checked={data?.preferences?.includes("newsletter") || false} onChange={e => handlePreferenceChange("newsletter", e.target.checked)} style={{ marginRight: "8px" }} /> Newsletter </FormLabel> <FormLabel> <input type="checkbox" checked={data?.preferences?.includes("notifications") || false} onChange={e => handlePreferenceChange("notifications", e.target.checked)} style={{ marginRight: "8px" }} /> Notifications </FormLabel> </div> <div style={{ marginTop: "20px", display: "flex", gap: "10px" }}> <FormButton onClick={handleBack} variant="secondary"> Back </FormButton> <FormButton onClick={handleNext}>Next</FormButton> </div> </FormContainer> ); } // Confirmation stage component function ConfirmationStage({ data, send }) { const handleSubmit = React.useCallback(() => { const updatedData = { ...(data || {}), errors: {} }; send("next", updatedData); }, [data, send]); const handleBack = React.useCallback(() => { send("back", data || {}); }, [data, send]); return ( <FormContainer> <ProgressIndicator currentStage="confirmation" /> <div> <h3 style={{ margin: "0 0 15px 0", color: "#2c3e50" }}>Confirmation</h3> <div style={{ backgroundColor: "#f8f9fa", padding: "15px", borderRadius: "4px", border: "1px solid #e9ecef", }} > <p style={{ margin: "5px 0", color: "#495057" }}> <strong style={{ color: "#2c3e50" }}>Name:</strong> {data?.name} </p> <p style={{ margin: "5px 0", color: "#495057" }}> <strong style={{ color: "#2c3e50" }}>Email:</strong> {data?.email} </p> <p style={{ margin: "5px 0", color: "#495057" }}> <strong style={{ color: "#2c3e50" }}>Phone:</strong> {data?.phone || "Not provided"} </p> <p style={{ margin: "5px 0", color: "#495057" }}> <strong style={{ color: "#2c3e50" }}>Preferences:</strong> {data?.preferences?.join(", ") || "None"} </p> </div> </div> <div style={{ marginTop: "20px", display: "flex", gap: "10px" }}> <FormButton onClick={handleBack} variant="secondary"> Back </FormButton> <FormButton onClick={handleSubmit}>Submit</FormButton> </div> </FormContainer> ); } // Complete stage component function CompleteStage({ data, send }) { const handleStartOver = React.useCallback(() => { const resetData = { name: "", email: "", phone: "", preferences: [], errors: {}, }; send("restart", resetData); }, [send]); return ( <FormContainer> <div> <h3 style={{ margin: "0 0 15px 0", color: "#2c3e50" }}>Thank you!</h3> <p style={{ color: "#28a745", fontSize: "16px", fontWeight: "500" }}>Your information has been submitted successfully.</p> <FormButton onClick={handleStartOver} variant="success" style={{ marginTop: "10px" }}> Start Over </FormButton> </div> </FormContainer> ); } // Create engine directly without useRef const engine = new StageFlowEngine({ initial: "personal", data: { name: "", email: "", phone: "", preferences: [], errors: {}, }, stages: [ { name: "personal", transitions: [ { target: "contact", event: "next" }, ], }, { name: "contact", transitions: [ { target: "preferences", event: "next" }, { target: "personal", event: "back" }, ], }, { name: "preferences", transitions: [ { target: "confirmation", event: "next" }, { target: "contact", event: "back" }, ], }, { name: "confirmation", transitions: [ { target: "complete", event: "next" }, { target: "preferences", event: "back" }, ], }, { name: "complete", transitions: [{ target: "personal", event: "restart" }], }, ], }); engine.start(); return ( <StageFlowProvider engine={engine}> <DebugInfo /> <StageRenderer stageComponents={{ personal: PersonalStage, contact: ContactStage, preferences: PreferencesStage, confirmation: ConfirmationStage, complete: CompleteStage, }} /> </StageFlowProvider> ); }
Result
Loading...