Configuração do Builder
O builder é dividido em duas camadas independentes:
| Camada | Componente | Responsabilidade |
|---|---|---|
| Estado | BuilderWrapper / BuilderProvider | Estrutura do formulário, configs, histórico de undo |
| Drag-and-drop | BuilderDndContext | DndContext, DragOverlay, handlers de drag-end |
BuilderDndContext é opcional — BuilderWrapper funciona perfeitamente sem ele. Use BuilderDndContext quando quiser a experiência DnD completa pronta para uso, ou traga seu próprio DndContext se precisar de sensores ou comportamentos de arrasto customizados.
Instalar dependências
npm install @schema-forms-data/builder @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
Estrutura de componentes
BuilderWrapper ← estado (BuilderProvider + TooltipProvider)
└── BuilderDndContext ← opcional — DndContext + DragOverlay + handlers
├── Palette ← tipos de campo e blocos preset arrastáveis
├── Canvas ← área de drop (steps → containers → fields)
├── ConfigPanel ← configuração do item selecionado
└── LivePreview ← preview em tempo real com FormRenderer
Uso padrão
Envolva seu próprio componente de editor com BuilderWrapper + BuilderDndContext. Os componentes internos acessam o estado via useBuilder() — eles não precisam saber nada sobre DnD:
// Componente de página / rota
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 — lógica de salvar/undo/redo via useBuilder(), sem conhecimento de DnD
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 — apenas layout, sem lógica
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>
);
}
Essa estrutura mantém as responsabilidades separadas:
| Componente | Responsabilidade |
|---|---|
BuilderWrapper | Converte o schema, fornece o contexto BuilderProvider |
BuilderDndContext | Todo o DnD — sensores, eventos de drag, DragOverlay |
BuilderEditor | Salvar / validar / undo / redo via useBuilder() |
BuilderCanvas | Layout de Palette, Canvas, ConfigPanel, LivePreview |
Usando seu próprio DndContext
Substitua BuilderDndContext completamente se precisar de sensores ou comportamentos de arrasto customizados. Palette, Canvas e ConfigPanel funcionam dentro de qualquer DndContext desde que BuilderWrapper seja um ancestral:
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) => {
// sua lógica de drag-end usando addItem / moveCanvasItem / reorderInContainer
}}
>
{children}
<DragOverlay>{/* seu ghost */}</DragOverlay>
</DndContext>
);
}
export function CustomBuilder({ schema }: { schema: FormSchema | null }) {
return (
<BuilderWrapper schema={schema}>
<MyDndLayer>
<Palette />
<Canvas />
<ConfigPanel />
</MyDndLayer>
</BuilderWrapper>
);
}
O que o BuilderDndContext trata internamente
| Ação de drag | O que acontece |
|---|---|
| Campo da Palette → Canvas | addItem(type, containerId) |
| Bloco preset da Palette → Step | addPresetBlock(block, stepId) |
| Campo no Canvas → posição diferente | reorderInContainer ou moveCanvasItem |
Formatos de dados do drag
Cada item arrastável define data.current com um destes formatos:
| Formato | Quando | Payload |
|---|---|---|
isPaletteItem: true | Arrastando um tipo de campo da Palette | { isPaletteItem: true, componentType: string } |
isPalettePreset: true | Arrastando um bloco preset | { isPalettePreset: true, presetId: string } |
isCanvasItem: true | Reordenando um item já no canvas | { isCanvasItem: true, id: string, fromContainer: string } |
Por que o DragOverlay é obrigatório
O Canvas usa overflow: auto em vários painéis aninhados. Sem um DragOverlay (renderizado via portal no document.body), o ghost do arrasto é cortado nas bordas de scroll. Sempre renderize-o como irmão do DndContext, nunca dentro dele.
Componente de ghost do drag
DragGhost é exportado para uso dentro de um DragOverlay customizado:
import { DragGhost, useBuilder } from "@schema-forms-data/builder";
// dentro do seu DragOverlay:
const { getConfig } = useBuilder();
<DragOverlay>
{activeData ? <DragGhost data={activeData} getConfig={getConfig} /> : null}
</DragOverlay>;
Carregar um schema existente
BuilderWrapper trata a conversão automaticamente quando você passa a prop schema. Para acesso de nível mais baixo:
import {
formSchemaToBuilder,
BuilderProvider,
} from "@schema-forms-data/builder";
const { configs, dndState, templateId, stepConfig } =
formSchemaToBuilder(schemaExistente);
<BuilderProvider
initialConfigs={configs}
initialContainers={dndState.containers}
initialTemplateId={templateId}
initialStepConfig={stepConfig}
>
<BuilderInner />
</BuilderProvider>;