Pular para o conteúdo principal

Examples

Complete, working examples covering real-world use cases.


Example 1 — Simple contact form

Schema with 1 step, 3 fields, no external dependencies.

import type { FormSchema } from "@schema-forms-data/core";
import { FieldType } from "@schema-forms-data/core";
import { FormRenderer } from "@schema-forms-data/renderer";

const schema: FormSchema = {
id: "contato",
nome: "Fale Conosco",
status: "ativo",
steps: [
{
id: "s1",
titulo: "Sua mensagem",
ordem: 1,
containers: [
{
id: "c1",
ordem: 1,
campos: [
{
id: "f1",
nome: "nome",
label: "Nome",
tipo: FieldType.TEXTO,
obrigatorio: true,
tamanho: 6,
ordem: 1,
},
{
id: "f2",
nome: "email",
label: "E-mail",
tipo: FieldType.EMAIL,
obrigatorio: true,
tamanho: 6,
ordem: 2,
},
{
id: "f3",
nome: "mensagem",
label: "Mensagem",
tipo: FieldType.TEXTAREA,
obrigatorio: true,
tamanho: 12,
ordem: 3,
validacao: { minLength: 20, maxLength: 500 },
},
],
},
],
},
],
};

export function ContactForm() {
return (
<FormRenderer
schema={schema}
formTitle="Contact Us"
template="moderno"
onComplete={async (values) => {
await fetch("/api/contact", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(values),
});
}}
/>
);
}

Example 2 — Multi-step registration with async validation

Schema with 2 steps, server-side CPF uniqueness check, and conditional fields.

import type { FormSchema } from "@schema-forms-data/core";
import { FieldType, generateId } from "@schema-forms-data/core";
import { FormRenderer } from "@schema-forms-data/renderer";
import { useState } from "react";
import { myApi } from "./api";

const schema: FormSchema = {
id: "cadastro",
nome: "Cadastro de Membro",
status: "ativo",
template: "corporativo",
steps: [
{
id: "step-dados",
titulo: "Dados pessoais",
descricao: "Informe seus dados para criar o cadastro",
ordem: 1,
containers: [
{
id: "c-dados",
ordem: 1,
campos: [
{
id: "f-nome",
nome: "nome",
label: "Nome completo",
tipo: FieldType.TEXTO,
obrigatorio: true,
tamanho: 8,
ordem: 1,
},
{
id: "f-cpf",
nome: "cpf",
label: "CPF",
tipo: FieldType.CPF,
obrigatorio: true,
tamanho: 4,
ordem: 2,
validate: [{ type: "cpfUnico" }],
},
{
id: "f-email",
nome: "email",
label: "E-mail",
tipo: FieldType.EMAIL,
obrigatorio: true,
tamanho: 6,
ordem: 3,
validate: [{ type: "emailUnico" }],
},
{
id: "f-tel",
nome: "telefone",
label: "Telefone",
tipo: FieldType.TELEFONE,
obrigatorio: true,
tamanho: 6,
ordem: 4,
},
{
id: "f-nasc",
nome: "data_nascimento",
label: "Data de nascimento",
tipo: FieldType.DATE,
obrigatorio: true,
tamanho: 4,
ordem: 5,
validacao: { minAge: 14 },
},
{
id: "f-resp",
nome: "tem_responsavel",
label: "Sou menor de 18 anos e tenho um responsável",
tipo: FieldType.CHECKBOX,
obrigatorio: false,
tamanho: 12,
ordem: 6,
},
],
},
{
// Este container só aparece se "tem_responsavel" for true
id: "c-resp",
titulo: "Dados do responsável",
ordem: 2,
condicional: {
campo: "tem_responsavel",
operador: "igual",
valor: true,
},
campos: [
{
id: "f-resp-nome",
nome: "responsavel_nome",
label: "Nome do responsável",
tipo: FieldType.TEXTO,
obrigatorio: true,
tamanho: 8,
ordem: 1,
},
{
id: "f-resp-tel",
nome: "responsavel_telefone",
label: "Telefone do responsável",
tipo: FieldType.TELEFONE,
obrigatorio: true,
tamanho: 4,
ordem: 2,
},
],
},
],
},
{
id: "step-endereco",
titulo: "Endereço",
ordem: 2,
containers: [
{
id: "c-end",
ordem: 1,
campos: [
{
id: "f-cep",
nome: "cep",
label: "CEP",
tipo: FieldType.CEP,
obrigatorio: true,
tamanho: 3,
ordem: 1,
cepFillMap: {
logradouro: "logradouro",
bairro: "bairro",
cidade: "cidade",
estado: "estado",
},
},
{
id: "f-log",
nome: "logradouro",
label: "Logradouro",
tipo: FieldType.TEXTO,
obrigatorio: true,
tamanho: 7,
ordem: 2,
},
{
id: "f-num",
nome: "numero",
label: "Número",
tipo: FieldType.TEXTO,
obrigatorio: true,
tamanho: 2,
ordem: 3,
},
{
id: "f-comp",
nome: "complemento",
label: "Complemento",
tipo: FieldType.TEXTO,
obrigatorio: false,
tamanho: 4,
ordem: 4,
},
{
id: "f-bai",
nome: "bairro",
label: "Bairro",
tipo: FieldType.TEXTO,
obrigatorio: true,
tamanho: 4,
ordem: 5,
},
{
id: "f-cid",
nome: "cidade",
label: "Cidade",
tipo: FieldType.TEXTO,
obrigatorio: true,
tamanho: 3,
ordem: 6,
},
{
id: "f-est",
nome: "estado",
label: "Estado",
tipo: FieldType.TEXTO,
obrigatorio: true,
tamanho: 1,
ordem: 7,
},
],
},
],
},
],
};

export function MemberRegistrationForm() {
const [serverErrors, setServerErrors] = useState<Record<string, string>>({});

return (
<FormRenderer
schema={schema}
formTitle="Member Registration"
fieldErrors={serverErrors}
validatorMapper={{
cpfUnico: async (value) => {
const exists = await myApi.checkCpf(String(value));
return exists ? "CPF already registered" : undefined;
},
emailUnico: async (value) => {
const exists = await myApi.checkEmail(String(value));
return exists ? "Email already registered" : undefined;
},
}}
onSubmitStep={async (stepIndex, data) => {
// Save draft per step
await myApi.saveDraft(stepIndex, data);
}}
onComplete={async (values) => {
try {
await myApi.finalizeRegistration(values);
} catch (err) {
if (err.status === 422) {
setServerErrors(err.errors);
}
}
}}
/>
);
}

Example 3 — Event registration form (advanced)

Schema with file upload, template variables, payment types, and FormSpy for autosave.

import { FormRenderer, FormSpy } from "@schema-forms-data/renderer";
import { FieldType } from "@schema-forms-data/core";
import type { FormSchema } from "@schema-forms-data/core";
import { useCallback } from "react";
import { myApi } from "./api";
import { currentEvent } from "./state";

const schema: FormSchema = {
id: "inscricao-evento",
nome: "Inscrição no Evento",
status: "ativo",
template: "acampamento",
steps: [
{
id: "s-dados",
titulo: "Seus dados",
descricao: "Para se inscrever no {{evento.nome}}",
ordem: 1,
containers: [
{
id: "c-pessoal",
titulo: "Dados pessoais",
ordem: 1,
campos: [
{
id: "f-nome",
nome: "nome",
label: "Nome completo",
tipo: FieldType.TEXTO,
obrigatorio: true,
tamanho: 8,
ordem: 1,
},
{
id: "f-cpf",
nome: "cpf",
label: "CPF",
tipo: FieldType.CPF,
obrigatorio: true,
tamanho: 4,
ordem: 2,
validate: [{ type: "cpfNoEvento" }],
},
{
id: "f-email",
nome: "email",
label: "E-mail",
tipo: FieldType.EMAIL,
obrigatorio: true,
tamanho: 6,
ordem: 3,
},
{
id: "f-tel",
nome: "telefone",
label: "Telefone / WhatsApp",
tipo: FieldType.TELEFONE,
obrigatorio: true,
tamanho: 6,
ordem: 4,
},
{
id: "f-nasc",
nome: "data_nascimento",
label: "Data de nascimento",
tipo: FieldType.DATE,
obrigatorio: true,
tamanho: 4,
ordem: 5,
validacao: { minAge: 14 },
},
{
id: "f-tam",
nome: "tamanho_camiseta",
label: "Tamanho da camiseta",
tipo: FieldType.SELECT,
obrigatorio: true,
tamanho: 4,
ordem: 6,
opcoesFromVar: "evento.tamanhosCamiseta",
},
{
id: "f-foto",
nome: "foto_documento",
label: "Foto do documento (RG ou CNH)",
tipo: FieldType.FILE,
obrigatorio: true,
tamanho: 4,
ordem: 7,
validacao: {
fileTypes: ["image/jpeg", "image/png"],
maxFileSize: 5242880, // 5MB
},
},
],
},
],
},
{
id: "s-saude",
titulo: "Saúde",
ordem: 2,
containers: [
{
id: "c-saude",
ordem: 1,
campos: [
{
id: "f-sang",
nome: "tipo_sanguineo",
label: "Tipo sanguíneo",
tipo: FieldType.SELECT,
obrigatorio: true,
tamanho: 4,
ordem: 1,
opcoes: [
{ valor: "A+", label: "A+" },
{ valor: "A-", label: "A-" },
{ valor: "B+", label: "B+" },
{ valor: "B-", label: "B-" },
{ valor: "AB+", label: "AB+" },
{ valor: "AB-", label: "AB-" },
{ valor: "O+", label: "O+" },
{ valor: "O-", label: "O-" },
],
},
{
id: "f-alerg",
nome: "alergias",
label: "Possui alergias?",
tipo: FieldType.CHECKBOX,
tamanho: 12,
obrigatorio: false,
ordem: 2,
},
{
id: "f-alerg-desc",
nome: "descricao_alergias",
label: "Descreva suas alergias",
tipo: FieldType.TEXTAREA,
obrigatorio: true,
tamanho: 12,
ordem: 3,
condicional: {
campo: "alergias",
operador: "igual",
valor: true,
},
clearedValue: null,
},
],
},
{
id: "c-emergencia",
titulo: "Contato de emergência",
ordem: 2,
campos: [
{
id: "f-emg-nome",
nome: "contato_emergencia_nome",
label: "Nome",
tipo: FieldType.TEXTO,
obrigatorio: true,
tamanho: 8,
ordem: 1,
},
{
id: "f-emg-tel",
nome: "contato_emergencia_telefone",
label: "Telefone",
tipo: FieldType.TELEFONE,
obrigatorio: true,
tamanho: 4,
ordem: 2,
},
],
},
],
},
{
id: "s-pagamento",
titulo: "Pagamento",
ordem: 3,
containers: [
{
id: "c-pag",
ordem: 1,
campos: [
{
id: "f-pag",
nome: "forma_pagamento",
label: "Forma de pagamento",
tipo: FieldType.PAYMENT_METHOD,
obrigatorio: true,
tamanho: 12,
ordem: 1,
},
{
id: "f-termos",
nome: "aceite_termos",
label: "Li e aceito os termos de participação do {{evento.nome}}",
tipo: FieldType.TERMS,
obrigatorio: true,
tamanho: 12,
ordem: 2,
termoPdfUploadId: "termos-upload-id",
},
],
},
],
},
],
};

export function EventRegistrationForm() {
// Autosave to sessionStorage
const handleValuesChange = useCallback((values: Record<string, unknown>) => {
sessionStorage.setItem(`draft-${currentEvent.id}`, JSON.stringify(values));
}, []);

// Restore draft
const draft = JSON.parse(
sessionStorage.getItem(`draft-${currentEvent.id}`) ?? "{}",
);

return (
<FormRenderer
schema={schema}
formTitle={`Registration — ${currentEvent.nome}`}
externalData={{
"evento.nome": currentEvent.nome,
"evento.valor": currentEvent.valor,
"evento.id": currentEvent.id,
"evento.tamanhosCamiseta": currentEvent.tamanhosCamiseta,
}}
initialValues={draft}
uploadFile={async (file, fieldName, onProgress) => {
return await myApi.uploadFile(file, fieldName, onProgress);
}}
resolveTermsUploadUrl={async (uploadId) => {
return await myApi.resolveTermsUrl(uploadId);
}}
validatorMapper={{
cpfNoEvento: async (value, _all, _cfg, external) => {
const alreadyRegistered = await myApi.checkCpfForEvent(
String(value),
String(external["evento.id"]),
);
return alreadyRegistered
? "CPF already registered for this event"
: undefined;
},
}}
onValuesChange={handleValuesChange}
onSubmitStep={async (stepIndex, data) => {
await myApi.saveRegistrationDraft(currentEvent.id, stepIndex, data);
}}
onComplete={async (values) => {
await myApi.finalizeRegistration(currentEvent.id, values);
sessionStorage.removeItem(`draft-${currentEvent.id}`);
}}
/>
);
}

Example 4 — Builder integrated in an admin panel

import {
BuilderWrapper,
BuilderDndContext,
Palette,
Canvas,
ConfigPanel,
LivePreview,
useBuilder,
builderToFormSchema,
} from "@schema-forms-data/builder";
import type { FormSchema } from "@schema-forms-data/core";
import { useState } from "react";
import { myApi } from "./api";

function BuilderControls({
schemaId,
onSaved,
}: {
schemaId: string;
onSaved: (schema: FormSchema) => void;
}) {
const { containers, configs, stepConfig, canUndo, canRedo, undo, redo } =
useBuilder();
const [saving, setSaving] = useState(false);

const save = async () => {
setSaving(true);
try {
const schema = builderToFormSchema({ containers }, configs, {
id: schemaId,
nome: "Form",
status: "ativo",
stepConfig,
});
await myApi.saveSchema(schema);
onSaved(schema);
} finally {
setSaving(false);
}
};

return (
<div className="builder-controls">
<button onClick={undo} disabled={!canUndo}>
↩ Undo
</button>
<button onClick={redo} disabled={!canRedo}>
↪ Redo
</button>
<button onClick={save} disabled={saving}>
{saving ? "Saving…" : "Save"}
</button>
</div>
);
}

export function FormEditorPanel({
schema,
onSaved,
}: {
schema: FormSchema | null;
onSaved: (schema: FormSchema) => void;
}) {
return (
<BuilderWrapper
schema={schema}
uploadTermsPdf={async (file, schemaId, onProgress) => {
return await myApi.uploadTermsPdf(file, schemaId, onProgress);
}}
deleteTermsPdf={async (uploadId, schemaId) => {
await myApi.deleteTermsPdf(uploadId, schemaId);
}}
>
<BuilderDndContext>
<div className="editor-layout">
<BuilderControls schemaId={schema?.id ?? "new"} onSaved={onSaved} />
<div className="editor-body">
<Palette />
<Canvas />
<ConfigPanel />
<LivePreview />
</div>
</div>
</BuilderDndContext>
</BuilderWrapper>
);
}

Example 5 — Autosave with FormSpy and useFormApi

import { FormRenderer, FormSpy, useFormApi } from "@schema-forms-data/renderer";

// Button that lives inside FormRenderer (e.g. via componentMapper or as a child)
function SaveDraftButton({ onSave }: { onSave: () => void }) {
const api = useFormApi();

const save = () => {
const state = api.getState();
localStorage.setItem("draft", JSON.stringify(state.values));
onSave();
};

return (
<button type="button" onClick={save}>
Save draft
</button>
);
}

export function FormWithAutosave() {
return (
<FormRenderer schema={schema} onComplete={handleComplete}>
{/* FormSpy monitors changes and saves automatically */}
<FormSpy
onChange={({ values, dirty }) => {
if (dirty) {
sessionStorage.setItem("autosave", JSON.stringify(values));
}
}}
>
{({ dirty, valid }) => (
<div className="form-footer">
{dirty && (
<span className="warning">You have unsaved changes</span>
)}
<button type="submit" disabled={!valid}>
Submit
</button>
</div>
)}
</FormSpy>
</FormRenderer>
);
}