Pular para o conteúdo principal

Configuração do Builder

O builder é dividido em duas camadas independentes:

CamadaComponenteResponsabilidade
EstadoBuilderWrapper / BuilderProviderEstrutura do formulário, configs, histórico de undo
Drag-and-dropBuilderDndContextDndContext, DragOverlay, handlers de drag-end

BuilderDndContext é opcionalBuilderWrapper 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:

ComponenteResponsabilidade
BuilderWrapperConverte o schema, fornece o contexto BuilderProvider
BuilderDndContextTodo o DnD — sensores, eventos de drag, DragOverlay
BuilderEditorSalvar / validar / undo / redo via useBuilder()
BuilderCanvasLayout 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 dragO que acontece
Campo da Palette → CanvasaddItem(type, containerId)
Bloco preset da Palette → StepaddPresetBlock(block, stepId)
Campo no Canvas → posição diferentereorderInContainer ou moveCanvasItem

Formatos de dados do drag

Cada item arrastável define data.current com um destes formatos:

FormatoQuandoPayload
isPaletteItem: trueArrastando um tipo de campo da Palette{ isPaletteItem: true, componentType: string }
isPalettePreset: trueArrastando um bloco preset{ isPalettePreset: true, presetId: string }
isCanvasItem: trueReordenando 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>;