Form Builder Architecture: Nested Blocks System
Overview
The @seanblock/form-builder uses a Notion-like nested block architecture where blocks can contain other blocks infinitely deep. The entire form is defined in JSON and rendered recursively.
Core Concepts
1. Everything is a Block
A block is the fundamental building unit. Every element in your form is a block:
interface Block {
id: string; // Unique identifier
blockType: string; // Component to render (e.g., "InputBlock", "CardBlock")
className?: string; // CSS classes
style?: React.CSSProperties;
metadata?: BlockMetadata; // Block-specific configuration
displayConditions?: Condition[]; // Show/hide logic
blocks?: Block[]; // 🎯 Nested child blocks
actions?: Action[]; // Event handlers
validations?: ValidateRule[]; // Validation rules
}2. Recursive Rendering
Blocks render their children automatically via BaseBlock:
// BaseBlock.tsx (simplified)
function BaseBlock({ block, children, as: Tag = "div" }) {
const { blocks } = block;
const renderChild = (child: Block) => {
const ChildComponent = importedBlocks[child.blockType];
return <ChildComponent key={child.id} block={child} />;
};
return (
<Tag>
{children}
{blocks?.map(renderChild)} {/* 🎯 Recursive rendering */}
</Tag>
);
}Key Points:
- Each block can have a
blocksarray of children - Children are rendered recursively
- Missing block types show helpful error messages
- Infinite nesting is supported
3. Block Registry
All blocks must be registered in the FormBuilderV3Provider:
import FormBuilderV3 from '@seanblock/form-builder';
import * as defaultBlocks from '@seanblock/form-builder/blocks';
// Standard blocks are pre-registered
const registry = {
InputBlock: defaultBlocks.InputBlock,
CardBlock: defaultBlocks.CardBlock,
// ... all default blocks
};
// Add custom blocks
const customBlocks = {
MyCustomBlock: MyCustomBlockComponent
};
<FormBuilderV3
config={config}
context={context}
additionalBlocks={customBlocks} {/* 🎯 Extend the registry */}
/>Standard Block Types
Layout Blocks (Containers)
These blocks typically contain other blocks:
| Block Type | Purpose | Common Children |
|---|---|---|
| PageBlock | Top-level page container | PageHeader, PageContent, PageFooter |
| PageHeaderBlock | Page header section | Header, Navigation |
| PageContentBlock | Main content area | Card, Row, Paragraph |
| RowBlock | Horizontal layout | Any blocks side-by-side |
| CardBlock | Content card | Text, inputs, buttons |
Input Blocks (Form Elements)
These blocks typically don't contain children:
| Block Type | Purpose | Key Metadata |
|---|---|---|
| InputBlock | Text input | label, placeholder, contextPath |
| SelectBlock | Dropdown | label, options, contextPath |
| CheckboxBlock | Checkbox | label, contextPath |
| DateBlock | Date picker | label, contextPath |
Best Practices
✅ DO
- Always use
BaseBlockas the root of custom components - Register all blocks before using them
- Use unique IDs for every block
- Leverage metadata for configuration
- Follow naming conventions:
BlockTypeshould end with "Block"
❌ DON'T
- Don't manually render
block.blocks(BaseBlock handles it) - Don't mutate context directly (use
updateContextValue) - Don't forget to handle
displayConditions - Don't use reserved props (
id,className,style)
Advanced Features
1. Display Conditions
Show/hide blocks based on context:
{
"id": "business-field",
"blockType": "InputBlock",
"metadata": { "label": "Business Name" },
"displayConditions": [
{
"matchType": "exact",
"field": "entityType",
"value": "business"
}
]
}2. Validation
Add validation rules to input blocks:
{
"id": "email",
"blockType": "InputBlock",
"validations": [
{
"rule": "pattern",
"value": "/^[^@]+@[^@]+$/",
"message": "Invalid email"
}
]
}3. Actions
Trigger behaviors on events:
{
"id": "submit-btn",
"blockType": "ButtonBlock",
"actions": [
{
"type": "conditional",
"conditions": [
{ "matchType": "isValid", "field": "email" }
],
"then": [
{ "type": "callApi", "service": "submitForm" }
],
"else": [
{ "type": "toast", "message": "Please fix errors" }
]
}
]
}