Skip to main content

Builder Setup

The builder is split into two independent layers:

LayerComponentResponsibility
StateBuilderWrapper / BuilderProviderForm structure, configs, undo/redo history
Drag-and-dropBuilderDndContextDndContext, DragOverlay, drag-end handlers

BuilderDndContext is optionalBuilderWrapper works perfectly without it. Use BuilderDndContext when you want the full DnD experience out of the box, or bring your own DndContext if you need custom sensors or drag behaviour.

Install dependencies

npm install @schema-forms-data/builder @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities

Component structure

BuilderWrapper          ← state (BuilderProvider + TooltipProvider)
└── BuilderDndContext ← optional — DndContext + DragOverlay + handlers
├── Palette ← draggable field types and preset blocks
├── Canvas ← drop area (steps → containers → fields)
├── ConfigPanel ← configuration for the selected item
└── LivePreview ← live preview with FormRenderer

Standard usage

Wrap your own editor component with BuilderWrapper + BuilderDndContext. Your inner components access state via useBuilder() — they don't need to know anything about DnD:

// Page / route component
import { BuilderWrapper, BuilderDndContext } from "@schema-forms-data/builder";

export function BuilderPage({ schema, onSave, onBack }) {
const { showError } = useMessage();

return (
<BuilderWrapper schema={schema}>
<BuilderDndContext onError={showError}>
<BuilderEditor schema={schema} onSave={onSave} onBack={onBack} />
</BuilderDndContext>
</BuilderWrapper>
);
}
// BuilderEditor — save/undo/redo logic via useBuilder(), no DnD knowledge needed
import {
useBuilder,
builderToFormSchema,
validateBuilderIntegrity,
} from "@schema-forms-data/builder";

export function BuilderEditor({ schema, onSave, onBack }) {
const {
configs,
containers,
previewTemplateId,
canUndo,
canRedo,
undo,
redo,
} = useBuilder();
const [nome, setNome] = useState(schema?.nome ?? "");
const [isSaving, setIsSaving] = useState(false);

const handleSave = async () => {
const { errors } = validateBuilderIntegrity({ containers }, configs);
if (errors.length > 0) {
showError(errors.join("\n"));
return;
}

setIsSaving(true);
try {
const result = builderToFormSchema({ containers }, configs, {
nome,
status: "ativo",
template: previewTemplateId,
id: schema?.id,
});
await onSave(result);
} finally {
setIsSaving(false);
}
};

return (
<div style={{ display: "flex", flexDirection: "column", height: "100vh" }}>
<BuilderToolbar
nome={nome}
onNomeChange={setNome}
canUndo={canUndo}
canRedo={canRedo}
onUndo={undo}
onRedo={redo}
isSaving={isSaving}
onSave={handleSave}
onBack={onBack}
/>
<BuilderCanvas />
</div>
);
}
// BuilderCanvas — layout only, no logic
import {
Palette,
Canvas,
ConfigPanel,
LivePreview,
} from "@schema-forms-data/builder";

export function BuilderCanvas({
externalData,
}: {
externalData?: Record<string, unknown>;
}) {
return (
<div style={{ display: "flex", flex: 1, overflow: "hidden" }}>
<aside style={{ width: 200, overflowY: "auto" }}>
<Palette />
</aside>
<main style={{ flex: 1, overflowY: "auto" }}>
<Canvas />
</main>
<aside style={{ width: 280, overflowY: "auto" }}>
<ConfigPanel />
</aside>
<div style={{ width: 380, overflowY: "auto" }}>
<LivePreview eventoData={externalData} />
</div>
</div>
);
}

This structure keeps concerns separated:

ComponentResponsibility
BuilderWrapperConverts the schema, provides BuilderProvider context
BuilderDndContextAll DnD — sensors, drag events, DragOverlay
BuilderEditorSave / validate / undo / redo via useBuilder()
BuilderCanvasLayout of Palette, Canvas, ConfigPanel, LivePreview

Using your own DndContext

Replace BuilderDndContext entirely if you need custom sensors or drag behaviour. Palette, Canvas, and ConfigPanel work inside any DndContext as long as BuilderWrapper is an ancestor:

import {
DndContext,
DragOverlay,
PointerSensor,
useSensors,
useSensor,
} from "@dnd-kit/core";
import {
BuilderWrapper,
useBuilder,
Palette,
Canvas,
ConfigPanel,
} from "@schema-forms-data/builder";

function MyDndLayer({ children }: { children: React.ReactNode }) {
const { addItem, moveCanvasItem, reorderInContainer } = useBuilder();
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 8 } }),
);

return (
<DndContext
sensors={sensors}
onDragEnd={(event) => {
// your drag-end logic using addItem / moveCanvasItem / reorderInContainer
}}
>
{children}
<DragOverlay>{/* your ghost */}</DragOverlay>
</DndContext>
);
}

export function CustomBuilder({ schema }: { schema: FormSchema | null }) {
return (
<BuilderWrapper schema={schema}>
<MyDndLayer>
<Palette />
<Canvas />
<ConfigPanel />
</MyDndLayer>
</BuilderWrapper>
);
}

What BuilderDndContext handles internally

Drag actionWhat happens
Field from Palette → CanvasaddItem(type, containerId)
Preset block from Palette → StepaddPresetBlock(block, stepId)
Field on Canvas → different positionreorderInContainer or moveCanvasItem

Drag data shapes

Each draggable item sets data.current with one of these shapes:

ShapeWhenPayload
isPaletteItem: trueDragging a field type from Palette{ isPaletteItem: true, componentType: string }
isPalettePreset: trueDragging a preset block{ isPalettePreset: true, presetId: string }
isCanvasItem: trueReordering an item already on the canvas{ isCanvasItem: true, id: string, fromContainer: string }

Why DragOverlay is required

The Canvas uses overflow: auto on several nested panels. Without a DragOverlay (rendered into a portal on document.body), the dragged ghost is clipped at scroll boundaries. Always render it as a sibling of DndContext, not inside it.

Drag ghost component

DragGhost is exported for use inside a custom DragOverlay:

import { DragGhost, useBuilder } from "@schema-forms-data/builder";

// inside your DragOverlay:
const { getConfig } = useBuilder();
<DragOverlay>
{activeData ? <DragGhost data={activeData} getConfig={getConfig} /> : null}
</DragOverlay>;

Load an existing schema

BuilderWrapper handles conversion automatically when you pass a schema prop. For lower-level access:

import {
formSchemaToBuilder,
BuilderProvider,
} from "@schema-forms-data/builder";

const { configs, dndState, templateId, stepConfig } =
formSchemaToBuilder(existingSchema);

<BuilderProvider
initialConfigs={configs}
initialContainers={dndState.containers}
initialTemplateId={templateId}
initialStepConfig={stepConfig}
>
<BuilderInner />
</BuilderProvider>;