Builder Pattern
The Form Engine uses a Builder Pattern to provide a type-safe, explicit, and flexible way to configure your form components, layouts, and steppers.
Why the Builder Pattern?
The Builder Pattern offers several key advantages:
- Type Safety: Full TypeScript support with autocomplete and compile-time validation
- Explicit Configuration: See exactly what components are registered in your engine
- Tree-Shaking: Only bundle the components you actually use
- Flexibility: Easy to add custom components or override defaults
- Immutability: Once built, the engine instance is immutable and safe to reuse
Core Concepts
FormComposer
The FormComposer class is your entry point for configuring the form engine. It provides a fluent API for registering components:
import { createReactFormComposer } from '@schema-engine/forms-react/registries/builder-registry';
const formEngine = createReactFormComposer().addElement(/* ... */).addLayout(/* ... */).addStepper(/* ... */).build();Registration Objects
Each component (element, layout, or stepper) is defined as a registration object that contains:
id/id/id: Unique identifiercomponent: React component to renderschema: Zod schema for configuration validationmetadata: Human-readable information (name, description, category, etc.)
Example element registration:
import type { ReactElementRegistration } from '@schema-engine/forms-react';
import { z } from 'zod';
export const CustomInputElement: ReactElementRegistration = {
id: 'custom-input',
component: CustomInput,
schema: z.object({
id: z.string(),
name: z.string(),
label: z.string().optional(),
placeholder: z.string().optional(),
}),
metadata: {
name: 'Custom Input',
description: 'A customized input field',
category: 'input',
},
};Converter Functions
Since React uses a component property but the core engine expects a render function, we provide converter functions:
- ``: Convert React element to core format
- ``: Convert React layout to core format
- ``: Convert React stepper to core format
import { createReactFormComposer } from '@schema-engine/forms-react/registries/builder-registry';
import { CustomInputElement } from './custom-input';
const formEngine = createReactFormComposer().addElement(CustomInputElement).build();Basic Usage
Step 1: Create Your Form Engine
Create a central form engine instance for your application:
// lib/form-engine.ts
import { createReactFormComposer } from '@schema-engine/forms-react/registries/builder-registry';
import {
InputElement,
TextareaElement,
SelectElement,
CardLayoutRegistration,
DefaultStepperRegistration,
} from '@schema-engine/forms-react/registrations';
export const formEngine = createReactFormComposer()
.addElement(InputElement)
.addElement(TextareaElement)
.addElement(SelectElement)
.addLayout(CardLayoutRegistration)
.addStepper(DefaultStepperRegistration)
.build();Step 2: Use in Your Components
Import and use the form engine in your components:
import { FormRenderer } from '@schema-engine/forms-react';
import { formEngine } from './lib/form-engine';
import type { FormConfig } from '@schema-engine/forms/schemas';
function MyForm() {
const formConfig: FormConfig = {
id: 'my-form',
title: 'Contact Form',
steps: [{
id: 'step-1',
elements: [{
id: 'input',
id: 'email',
name: 'email',
label: 'Email Address',
rules: [
{ type: 'required', message: 'Email is required' },
{ type: 'email', message: 'Invalid email address' },
],
}],
}],
};
return <FormRenderer config={formConfig} engine={formEngine} />;
}Available Components
Elements
All input and content elements that can be used in form steps:
InputElement- Text, email, password, tel, url, etc.TextareaElement- Multi-line text inputSelectElement- Dropdown selectionCheckboxElement- Single checkbox or checkbox groupRadioElement- Radio button groupSwitchElement- Toggle switchNumberElement- Numeric inputArrayElement- Dynamic array of elements
Layouts
Visual layouts that wrap form content:
CardLayoutRegistration- Centered card with shadowSplitScreenLayoutRegistration- Two-column split layoutHeroBackgroundLayoutRegistration- Full-width hero with background image
Steppers
Step indicators for multi-step forms:
DefaultStepperRegistration- Horizontal step indicatorVerticalStepperRegistration- Vertical step indicatorMinimalistStepperRegistration- Minimal progress indicatorDebuggerStepperRegistration- Debug stepper with form state
Creating Custom Components
1. Define Your Component
// components/custom-rating.tsx
import type { ReactElementRegistration } from '@schema-engine/forms-react';
import { z } from 'zod';
const RatingConfigSchema = z.object({
id: z.string(),
name: z.string(),
label: z.string().optional(),
maxRating: z.number().default(5),
rules: z.array(z.any()).optional(),
});
type RatingConfig = z.infer<typeof RatingConfigSchema>;
function RatingInput({ config }: { config: RatingConfig }) {
const [rating, setRating] = useState(0);
return (
<div>
{config.label && <label>{config.label}</label>}
<div>
{Array.from({ length: config.maxRating }, (_, i) => (
<button
key={i}
onClick={() => setRating(i + 1)}
className={rating > i ? 'text-yellow-500' : 'text-gray-300'}
>
[star]
</button>
))}
</div>
</div>
);
}
export const RatingElement: ReactElementRegistration = {
id: 'rating',
component: RatingInput,
schema: RatingConfigSchema,
metadata: {
name: 'Rating Input',
description: 'Star rating input field',
category: 'input',
},
};2. Register Your Component
import { RatingElement } from './components/custom-rating';
const formEngine = createReactFormComposer()
.addElement(RatingElement)
// ... other components
.build();3. Use in Form Config
const formConfig: FormConfig = {
id: 'feedback-form',
steps: [
{
elements: [
{
id: 'rating',
id: 'satisfaction',
name: 'satisfaction',
label: 'How satisfied are you?',
maxRating: 5,
rules: [{ type: 'required', message: 'Please rate your experience' }],
},
],
},
],
};Advanced Usage
Conditional Registration
Register components based on feature flags or environment:
const formEngine = createReactFormComposer().addElement(InputElement).addElement(TextareaElement);
if (process.env.ENABLE_ADVANCED_FIELDS) {
formEngine.addElement(RichTextElement).addElement(CodeEditorElement);
}
export const finalEngine = formEngine.build();Multiple Engine Instances
Create different engine instances for different use cases:
// Simple contact form engine
export const contactFormEngine = createReactFormComposer()
.addElement(InputElement)
.addElement(TextareaElement)
.addLayout(CardLayoutRegistration)
.build();
// Complex survey engine
export const surveyEngine = createReactFormComposer()
.addElement(InputElement)
.addElement(SelectElement)
.addElement(RadioElement)
.addElement(CheckboxElement)
.addElement(RatingElement)
.addLayout(SplitScreenLayoutRegistration)
.addStepper(DefaultStepperRegistration)
.build();Extending Registration Objects
Create custom registration builders:
function createInputElement(id: string, displayName: string, inputType: string): ReactElementRegistration {
return {
id,
component: FormInput,
schema: InputFieldSchema,
metadata: {
name: displayName,
description: `${displayName} input field`,
category: 'input',
},
};
}
const formEngine = createReactFormComposer()
.addElement(createInputElement('email', 'Email', 'email'))
.addElement(createInputElement('phone', 'Phone', 'tel'))
.build();Type Safety
The Builder Pattern provides full type safety throughout:
// TypeScript knows about all registered components
const formEngine = createReactFormComposer().addElement(InputElement).build();
// Type-safe retrieval
const inputElement = formEngine.getElement('input'); // Type: ElementRegistration<InputFieldConfig>
const unknownElement = formEngine.getElement('unknown'); // Compile error if strict mode
// Type-safe form configs
const config: FormConfig = {
steps: [
{
elements: [
{
id: 'input', // [check-mark] Valid
// id: 'invalid', // [x-mark] Would cause runtime error
},
],
},
],
};Best Practices
-
Create a Single Engine Instance: Define your form engine in one place and export it for reuse.
-
Register All Components Upfront: Register all components during initialization, not dynamically at runtime.
-
Use TypeScript: Leverage type safety for configuration and component registration.
-
Organize Registrations: Group related components together for better maintainability.
-
Document Custom Components: Add clear metadata to custom components for form builder UIs.
-
Validate Schemas: Use Zod schemas to validate component configurations at runtime.
Migration from Old Pattern
If you're migrating from the old registry pattern:
Old Pattern:
import '@schema-engine/forms-react/registrations';
// Components auto-registered via side effectsNew Pattern:
import { createReactFormComposer } from '@schema-engine/forms-react/registries/builder-registry';
import { InputElement } from '@schema-engine/forms-react/registrations';
export const formEngine = createReactFormComposer().addElement(InputElement).build();See the Migration Guide for detailed instructions.
Next Steps
- Learn about Element Categories
- Explore Creating Custom Layouts
- Understand Validation Rules
- Read about Actions System