Roadmap & Planned Improvements
This document consolidates concrete DX (developer experience), API consistency, and extensibility improvement suggestions, based on an analysis of the library's current source code.
Note: Nothing here implies the library is broken — these are evolution opportunities for broader adoption and long-term maintainability.
1. Portuguese field naming
Problem
Schema properties use Portuguese names: nome, tamanho, obrigatorio, opcoes, valor, campos, passos, etc. This creates a barrier for:
- Adoption in international projects or multilingual teams
- Integration with libraries and tools that expect English identifiers
- Unfamiliar IntelliSense for non-Portuguese-speaking developers
Suggestion
Keep PT names as default (compatibility), but provide English aliases in the same type with a soft deprecation:
// Maintain backward-compatible type adapter
export interface FormFieldEN {
id: string; // alias for `nome`
size?: number; // alias for `tamanho`
required?: boolean; // alias for `obrigatorio`
options?: FieldOption[]; // alias for `opcoes`
fields?: FormField[]; // alias for `campos`
}
Or alternatively create a defineField() function that normalises both forms:
const field = defineField({ id: "email", type: "email", required: true });
// or equivalently:
const field = defineField({ nome: "email", tipo: "email", obrigatorio: true });
2. Portuguese FieldType enum values
Problem
The FieldType enum has values like 'texto', 'numero', 'selecionar', 'multipla_escolha'. This is unusual in the React/TS ecosystem and makes LLM inference and IntelliSense harder in international projects.
Suggestion
Keep the current values for compatibility, but also export a second enum or constant with English aliases:
export const FieldTypeAlias = {
TEXT: FieldType.TEXTO,
NUMBER: FieldType.NUMERO,
SELECT: FieldType.SELECIONAR,
MULTI_SELECT: FieldType.MULTIPLA_ESCOLHA,
// ...
} as const;
This would not break anything existing and would make usage in international projects more natural.
3. FieldOption.valor vs .value
Problem
The value field of an option is valor (PT), while the label field is label (EN). There is an internal inconsistency:
interface FieldOption {
label: string; // English
valor: string; // Portuguese
}
This causes confusion especially when creating component mappers or integrating with external data sources that return { label, value }.
Suggestion
Accept both forms in the type with preference for valor, or rename to value in the next major version:
interface FieldOption {
label: string;
value?: string; // new alias, preferred
/** @deprecated Use `value` instead */
valor?: string;
}
4. BuilderWrapper vs BuilderProvider naming convention
Problem
In the React ecosystem, the Provider suffix is the convention for context components. BuilderWrapper is less intuitive than BuilderProvider.
Additionally, BuilderWrapper and BuilderDndContext are two components that must be used together, but their names don't clearly communicate this relationship.
Suggestion
Keep BuilderWrapper for compatibility, but also export BuilderProvider as an alias:
// packages/builder/src/index.ts
export { BuilderWrapper as BuilderProvider } from "./BuilderWrapper";
Or create a Builder component that combines both by default:
// New all-in-one component
<Builder schema={schema} componentMapper={...} onSave={...}>
<BuilderCanvas />
<Palette />
{/* DnD already configured internally */}
</Builder>
5. No global onChange callback on FormRenderer
Problem
FormRenderer has onValuesChange that fires with the current step's values, but does not expose a callback that receives the accumulated values from all steps as the user advances. This makes integrations harder where the form state needs to be synced with external state (e.g., draft autosave).
Suggestion
Add a top-level callback:
interface FormRendererProps {
// ...existing...
onFormChange?: (
allValues: Record<string, unknown>,
currentStep: number,
) => void;
}
While this callback doesn't exist, the current workaround (documented in guides/hooks.md) is to use FormSpy inside componentMapper, which works but is less intuitive.
6. BuilderWrapper without direct access to the serialised schema
Problem
To get a FormSchema from the builder state, you must call builderToFormSchema() inside a child component that uses useBuilder(). It's not possible to pass an external onSchemaChange prop directly to BuilderWrapper.
Suggestion
Add an onSchemaChange prop to BuilderWrapper:
<BuilderWrapper
schema={schema}
componentMapper={componentMapper}
onSchemaChange={(formSchema) => {
// called automatically on every change
save(formSchema);
}}
>
This would eliminate the need for a child wrapper component just to capture the schema via useBuilder().
7. Domain-specific event fields without an abstraction layer
Problem
FieldType includes very specific event management fields such as:
'participacao'— attendance/contribution control'pagamento'— payment gateway integration'voucher'— coupon validation
These fields have business logic embedded in the renderer, which makes the library harder to use in non-event contexts.
Suggestion
Create a plugin/extension layer for registering custom FieldTypes:
// packages/renderer
registerFieldType({
type: "pagamento",
component: PaymentField,
validate: paymentValidator,
icon: CreditCard,
label: "Payment",
});
This would allow these fields to be moved to a separate package (@schema-forms-data/event-fields) and let users register their own types.
8. Untyped externalData
Problem
externalData is passed as Record<string, unknown> to validators, conditionals, and templates. There is no TypeScript generic that allows the developer to type external data:
// Today: no type-safe externalData
const validator: FieldValidator = (value, allFields, schema, externalData) => {
const data = externalData as { eventId: string }; // manual type assertion
};
Suggestion
Make FormRenderer and related types generic over externalData:
interface FormRendererProps<TExternalData = Record<string, unknown>> {
// ...
externalData?: TExternalData;
}
type FieldValidator<TExternalData = Record<string, unknown>> = (
value: unknown,
allFields: Record<string, unknown>,
schema: FormSchema,
externalData: TExternalData,
) => string | undefined | Promise<string | undefined>;
9. Type errors without unique field identifiers
Problem
fieldErrors is a Record<string, string> object where the key must be the field name. However, in repeatable containers, the same field can appear multiple times with indices. There is no clear contract for how to map errors to fields in repeatable arrays.
Suggestion
Explicitly document and support index notation for repeatable arrays:
// Support keys in the format: "field[0].subfield"
fieldErrors={{
'responsible[0].cpf': 'Invalid CPF',
'responsible[1].cpf': 'CPF already registered',
}}
10. No native read-only mode
Problem
There is no global readOnly mode for FormRenderer that disables all inputs and submission. To display a filled form as a "view", the developer must:
- Create an alternative
componentMapperwith all fields disabled, or - Implement custom logic in
onCompleteto prevent submission
Suggestion
Add a readOnly prop to FormRenderer:
<FormRenderer
schema={schema}
componentMapper={componentMapper}
initialValues={savedValues}
readOnly {/* ← all fields disabled, submit button hidden */}
/>
Impact Summary
| Suggestion | DX Impact | Estimated Effort | Breaking change? |
|---|---|---|---|
| English aliases for props | High (international adoption) | Medium | No |
FieldType English aliases | Medium | Low | No |
FieldOption.value alias | High | Low | Not in next minor |
BuilderProvider alias | Low (cosmetic) | Low | No |
Global onFormChange | High | Medium | No |
onSchemaChange on BuilderWrapper | High | Medium | No |
| Custom FieldType plugin | High (extensibility) | High | No (additive) |
Generic externalData | Medium | Medium | No |
fieldErrors for arrays | Medium | Medium | No |
Native readOnly mode | High | Medium | No |
Este documento consolida sugestões concretas de melhoria de DX (experiência do desenvolvedor), consistência de API e extensibilidade, baseadas na análise do código-fonte atual da biblioteca.
Nota: Nenhum item aqui implica que a biblioteca está quebrada — são oportunidades de evolução para uso mais amplo e manutenção de longo prazo.
1. Nomenclatura dos campos em português
Problema
Propriedades de schema usam nomes em português: nome, tamanho, obrigatorio, opcoes, valor, campos, passos, etc. Isso cria uma barreira para:
- Adoção em projetos internacionais ou equipes multilíngues
- Integração com bibliotecas e ferramentas que esperam inglês
- Intellisense pouco familiar para desenvolvedores não-PT
Sugestão
Manter os nomes PT como padrão (compatibilidade), mas criar aliases em inglês no mesmo tipo, com deprecação suave:
// Manter retrocompatibilidade com um adaptador de tipo
export interface FormFieldEN {
id: string; // alias de `nome`
size?: number; // alias de `tamanho`
required?: boolean; // alias de `obrigatorio`
options?: FieldOption[]; // alias de `opcoes`
fields?: FormField[]; // alias de `campos`
}
Ou, alternativamente, criar uma função defineField() que normaliza ambas as formas:
const field = defineField({ id: "email", type: "email", required: true });
// ou equivalentemente:
const field = defineField({ nome: "email", tipo: "email", obrigatorio: true });
2. Valores de FieldType enum em português
Problema
O enum FieldType tem valores como 'texto', 'numero', 'selecionar', 'multipla_escolha'. Isso é incomum no ecossistema React/TS e dificulta inferência por LLMs e intellisense em projetos internacionais.
Sugestão
Manter os valores atuais para compatibilidade, mas exportar um segundo enum ou constante com aliases em inglês:
export const FieldTypeAlias = {
TEXT: FieldType.TEXTO,
NUMBER: FieldType.NUMERO,
SELECT: FieldType.SELECIONAR,
MULTI_SELECT: FieldType.MULTIPLA_ESCOLHA,
// ...
} as const;
Isso não quebraria nada existente e tornaria o uso em projetos internacionais mais natural.
3. FieldOption.valor vs .value
Problema
O campo de valor de uma opção é valor (PT), enquanto o campo de label é label (EN). Há inconsistência interna:
interface FieldOption {
label: string; // inglês
valor: string; // português
}
Isso gera confusão especialmente ao criar component mappers ou ao integrar com fontes de dados externas que retornam { label, value }.
Sugestão
Aceitar ambas as formas no tipo com preferência por valor, ou renomear para value na próxima major version:
interface FieldOption {
label: string;
value?: string; // novo alias, preferido
/** @deprecated Use `value` instead */
valor?: string;
}
4. Convenção de nomenclatura BuilderWrapper vs BuilderProvider
Problema
No ecossistema React, o sufixo Provider é a convenção para componentes de contexto. BuilderWrapper é menos intuitivo do que BuilderProvider.
Adicionalmente, BuilderWrapper e BuilderDndContext são dois componentes que precisam ser usados juntos, mas têm nomes que não comunicam claramente essa relação.
Sugestão
Manter BuilderWrapper por compatibilidade, mas exportar também BuilderProvider como alias:
// packages/builder/src/index.ts
export { BuilderWrapper as BuilderProvider } from "./BuilderWrapper";
Ou criar um componente Builder que combina ambos por padrão:
// Novo componente "all-in-one"
<Builder schema={schema} componentMapper={...} onSave={...}>
<BuilderCanvas />
<Palette />
{/* DnD já configurado internamente */}
</Builder>
5. Ausência de callback onChange global no FormRenderer
Problema
FormRenderer tem onValuesChange que dispara com os valores do passo atual, mas não expõe um callback que receba os valores acumulados de todos os passos conforme o usuário avança. Isso dificulta integrações onde o estado do formulário precisa ser sincronizado com um estado externo (ex: autosave de rascunho).
Sugestão
Adicionar um callback de nível superior:
interface FormRendererProps {
// ...existentes...
onFormChange?: (
allValues: Record<string, unknown>,
currentStep: number,
) => void;
}
Enquanto esse callback não existe, a alternativa atual (documentada em guides/hooks.md) é usar FormSpy dentro do componentMapper, o que é funcional mas menos intuitivo.
6. BuilderWrapper sem acesso direto ao schema serializado
Problema
Para obter o FormSchema a partir do estado do builder, é necessário chamar builderToFormSchema() dentro de um componente filho que use useBuilder(). Não é possível passar um onSchemaChange externo para BuilderWrapper diretamente.
Sugestão
Adicionar prop onSchemaChange ao BuilderWrapper:
<BuilderWrapper
schema={schema}
componentMapper={componentMapper}
onSchemaChange={(formSchema) => {
// chamado automaticamente a cada mudança
save(formSchema);
}}
>
Isso eliminaria a necessidade de um wrapper filho apenas para capturar o schema via useBuilder().
7. Campos de domínio específico de eventos without abstração
Problema
FieldType inclui campos muito específicos de gestão de eventos, como:
'participacao'— controle de participação (presença/contribuição)'pagamento'— integração com gateway de pagamento'voucher'— validação de cupom
Esses campos têm lógica de negócio embutida no renderer, o que torna a biblioteca difícil de usar em contextos não relacionados a eventos.
Sugestão
Criar uma camada de plugin/extensão para registrar FieldTypes customizados:
// packages/renderer
registerFieldType({
type: "pagamento",
component: PaymentField,
validate: paymentValidator,
icon: CreditCard,
label: "Pagamento",
});
Isso permitiria que esses campos sejam movidos para um pacote separado (@schema-forms-data/event-fields) e que usuários registrem seus próprios tipos.
8. Tipagem de externalData sem validação
Problema
externalData é passado como Record<string, unknown> para validadores, condicionais e templates. Não há TypeScript genérico que permita ao desenvolvedor tipar os dados externos:
// Hoje: sem tipagem segura de externalData
const validator: FieldValidator = (value, allFields, schema, externalData) => {
const data = externalData as { eventId: string }; // type assertion manual
};
Sugestão
Tornar FormRenderer e os tipos relacionados genéricos em relação a externalData:
interface FormRendererProps<TExternalData = Record<string, unknown>> {
// ...
externalData?: TExternalData;
}
type FieldValidator<TExternalData = Record<string, unknown>> = (
value: unknown,
allFields: Record<string, unknown>,
schema: FormSchema,
externalData: TExternalData,
) => string | undefined | Promise<string | undefined>;
9. Erros de tipo sem identificador único de campo
Problema
fieldErrors é um objeto Record<string, string> onde a chave deve ser o nome do campo. Porém, em containers repetíveis, o mesmo campo pode aparecer múltiplas vezes com índices. Não há um contrato claro de como mapear erros para campos em arrays repetíveis.
Sugestão
Documentar e suportar explicitamente a sintaxe de índice para arrays repetíveis:
// Suporte a chaves no formato: "campo[0].subcampo"
fieldErrors={{
'responsible[0].cpf': 'CPF inválido',
'responsible[1].cpf': 'CPF já cadastrado',
}}
10. Ausência de modo "read-only" nativo
Problema
Não existe um modo readOnly global para o FormRenderer que desabilite todos os inputs e submissão. Para exibir um formulário preenchido como "visualização", o desenvolvedor precisa:
- Criar um
componentMapperalternativo com todos os campos desabilitados, ou - Implementar lógica customizada no
onCompletepara impedir submissão
Sugestão
Adicionar prop readOnly ao FormRenderer:
<FormRenderer
schema={schema}
componentMapper={componentMapper}
initialValues={savedValues}
readOnly // ← todos os campos desabilitados, botão de submit oculto
/>
Resumo de Impacto
| Sugestão | Impacto para DX | Esforço estimado | Breaking change? |
|---|---|---|---|
| Aliases em inglês para props | Alto (adoção internacional) | Médio | Não |
FieldType aliases em inglês | Médio | Baixo | Não |
FieldOption.value alias | Alto | Baixo | Não na próxima minor |
BuilderProvider alias | Baixo (cosmético) | Baixo | Não |
onFormChange global | Alto | Médio | Não |
onSchemaChange no BuilderWrapper | Alto | Médio | Não |
| Plugin de FieldTypes customizados | Alto (extensibilidade) | Alto | Não (aditivo) |
Genérico para externalData | Médio | Médio | Não |
fieldErrors para arrays | Médio | Médio | Não |
Modo readOnly nativo | Alto | Médio | Não |