Custom Blocks Guide

Learn how to create your own custom blocks to extend the form builder with any functionality you need.

Quick Example

Here's a complete example of creating a custom Alert block:

1. Create Your Block Component

// components/blocks/AlertBlock.tsx
import React from 'react';
import BaseBlock from '@seanblock/form-builder/blocks/BaseBlock';
import { Block } from '@seanblock/form-builder';

interface AlertBlockProps {
  block: Block;
}

export default function AlertBlock({ block }: AlertBlockProps) {
  const { metadata = {} } = block;
  const { 
    variant = 'info',  // info | warning | error | success
    message = '' 
  } = metadata;

  const colors = {
    info: 'bg-blue-100 border-blue-500 text-blue-700',
    warning: 'bg-yellow-100 border-yellow-500 text-yellow-700',
    error: 'bg-red-100 border-red-500 text-red-700',
    success: 'bg-green-100 border-green-500 text-green-700'
  };

  return (
    <BaseBlock 
      block={block}
      as="div"
      className={`border-l-4 p-4 ${colors[variant]}`}
    >
      <p>{message}</p>
      {/* Any nested blocks will render here automatically */}
    </BaseBlock>
  );
}

2. Register the Block

// app/page.tsx
import FormBuilderV3 from '@seanblock/form-builder';
import AlertBlock from './components/blocks/AlertBlock';

const customBlocks = {
  AlertBlock  // Key must match blockType in JSON
};

export default function MyForm() {
  return (
    <FormBuilderV3
      config={formConfig}
      context={initialContext}
      additionalBlocks={customBlocks}
    />
  );
}

3. Use in JSON

{
  "id": "welcome-alert",
  "blockType": "AlertBlock",
  "metadata": {
    "variant": "success",
    "message": "Welcome to the form!"
  },
  "blocks": [
    {
      "id": "alert-link",
      "blockType": "LinkBlock",
      "metadata": {
        "label": "Learn More",
        "href": "/docs"
      }
    }
  ]
}

Block Anatomy

Required Props

Every block component receives a block prop:

interface BlockProps {
  block: Block;  // The JSON block definition
}

function MyBlock({ block }: BlockProps) {
  // ...
}

Using BaseBlock

⚠️ Always wrap your content in BaseBlock

BaseBlock handles rendering nested blocks, applies className/style, evaluates displayConditions, and manages event delegation.

function MyBlock({ block }: BlockProps) {
  return (
    <BaseBlock block={block} as="section">
      {/* Your content */}
    </BaseBlock>
  );
}

BaseBlock Props

PropTypeDescription
blockBlock✅ Required - The block definition
asstringHTML tag or component (default: "div")
classNamestringAdditional CSS classes
childrenReactNodeExplicit children
disabledbooleanDisable interactions

Common Patterns

1. Simple Display Block

No state, just display content:

function BadgeBlock({ block }: BlockProps) {
  const { metadata = {} } = block;
  const { label, color = 'blue' } = metadata;

  return (
    <BaseBlock 
      block={block}
      as="span"
      className={`inline-block px-2 py-1 text-xs rounded bg-${color}-500 text-white`}
    >
      {label}
    </BaseBlock>
  );
}

2. Container Block

Wraps nested blocks:

function SectionBlock({ block }: BlockProps) {
  const { metadata = {} } = block;
  const { title, subtitle } = metadata;

  return (
    <BaseBlock block={block} as="section" className="space-y-4">
      {title && <h2 className="text-2xl font-bold">{title}</h2>}
      {subtitle && <p className="text-gray-600">{subtitle}</p>}
      {/* Nested blocks render here automatically */}
    </BaseBlock>
  );
}

3. Input Block with Context

Reads/writes to form context:

import { useFormBuilder } from '@seanblock/form-builder';

function SliderBlock({ block }: BlockProps) {
  const { id, metadata = {} } = block;
  const { label, min = 0, max = 100, contextPath } = metadata;
  
  const { context, updateContextValue } = useFormBuilder();
  const value = context[contextPath] || min;

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const newValue = Number(e.target.value);
    updateContextValue(contextPath, newValue);
  };

  return (
    <BaseBlock block={block} as="div" className="space-y-2">
      <label htmlFor={id}>{label}: {value}</label>
      <input
        id={id}
        type="range"
        min={min}
        max={max}
        value={value}
        onChange={handleChange}
        className="w-full"
      />
    </BaseBlock>
  );
}

4. Block with Actions

Trigger actions on events:

function ToggleBlock({ block }: BlockProps) {
  const { metadata = {} } = block;
  const { label, contextPath } = metadata;
  
  const { context, updateContextValue, executeActions } = useFormBuilder();
  const [isOn, setIsOn] = useState(context[contextPath] || false);

  const handleToggle = async () => {
    const newValue = !isOn;
    setIsOn(newValue);
    updateContextValue(contextPath, newValue);
    
    // Execute onChange actions
    const onChangeActions = block.actions?.filter(a => a.trigger === 'onChange');
    if (onChangeActions?.length) {
      await executeActions(onChangeActions);
    }
  };

  return (
    <BaseBlock block={block} as="div">
      <button
        onClick={handleToggle}
        className={`toggle ${isOn ? 'on' : 'off'}`}
      >
        {label}: {isOn ? 'ON' : 'OFF'}
      </button>
    </BaseBlock>
  );
}

Best Practices

✅ DO

  • Always use BaseBlock as the root element
  • Type your metadata for better autocomplete
  • Provide sensible defaults for metadata values
  • Use contextPath for storing form data
  • Handle loading/error states gracefully
  • Make blocks responsive by default

❌ DON'T

  • Don't manually render block.blocks
  • Don't mutate context directly
  • Don't skip BaseBlock
  • Don't use global state
  • Don't forget accessibility

useFormBuilder Hook

Access form state and methods in custom blocks:

import { useFormBuilder } from '@seanblock/form-builder';

function CustomBlock() {
  const {
    context,              // Current form data
    updateContextValue,   // Update form data
    executeAction,        // Trigger actions
    executeActions,       // Trigger multiple actions
    currentPage,          // Current page ID
    setCurrentPage,       // Navigate to page
    invalidList,          // Validation errors
    errors,               // General errors
    isLoading,           // API loading state
    config,              // Form configuration
    importedBlocks       // Registered blocks
  } = useFormBuilder();
}