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 blocks array 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 TypePurposeCommon Children
PageBlockTop-level page containerPageHeader, PageContent, PageFooter
PageHeaderBlockPage header sectionHeader, Navigation
PageContentBlockMain content areaCard, Row, Paragraph
RowBlockHorizontal layoutAny blocks side-by-side
CardBlockContent cardText, inputs, buttons

Input Blocks (Form Elements)

These blocks typically don't contain children:

Block TypePurposeKey Metadata
InputBlockText inputlabel, placeholder, contextPath
SelectBlockDropdownlabel, options, contextPath
CheckboxBlockCheckboxlabel, contextPath
DateBlockDate pickerlabel, contextPath

Best Practices

✅ DO

  • Always use BaseBlock as the root of custom components
  • Register all blocks before using them
  • Use unique IDs for every block
  • Leverage metadata for configuration
  • Follow naming conventions: BlockType should 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" }
      ]
    }
  ]
}