Pular para o conteúdo principal

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 componentMapper with all fields disabled, or
  • Implement custom logic in onComplete to 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

SuggestionDX ImpactEstimated EffortBreaking change?
English aliases for propsHigh (international adoption)MediumNo
FieldType English aliasesMediumLowNo
FieldOption.value aliasHighLowNot in next minor
BuilderProvider aliasLow (cosmetic)LowNo
Global onFormChangeHighMediumNo
onSchemaChange on BuilderWrapperHighMediumNo
Custom FieldType pluginHigh (extensibility)HighNo (additive)
Generic externalDataMediumMediumNo
fieldErrors for arraysMediumMediumNo
Native readOnly modeHighMediumNo

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 componentMapper alternativo com todos os campos desabilitados, ou
  • Implementar lógica customizada no onComplete para 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ãoImpacto para DXEsforço estimadoBreaking change?
Aliases em inglês para propsAlto (adoção internacional)MédioNão
FieldType aliases em inglêsMédioBaixoNão
FieldOption.value aliasAltoBaixoNão na próxima minor
BuilderProvider aliasBaixo (cosmético)BaixoNão
onFormChange globalAltoMédioNão
onSchemaChange no BuilderWrapperAltoMédioNão
Plugin de FieldTypes customizadosAlto (extensibilidade)AltoNão (aditivo)
Genérico para externalDataMédioMédioNão
fieldErrors para arraysMédioMédioNão
Modo readOnly nativoAltoMédioNão