Skip to main content

Compiler Foundation

DynamicForm 3.0 adds a field module and compiler layer before the existing FormConfig pipeline.

Field Modules
-> compileFormConfig
-> FormConfig
-> processFormConfig
-> DynamicForm

This is additive. Existing FormConfig usage does not need to change.

Field Modules

A field module packages reusable business-field behavior:

import type { FieldModule } from '@whynotsnow/dynamic-form';

interface UserSelectorOptions {
label?: string;
options?: Array<{ label: string; value: string }>;
}

export const UserSelectorModule: FieldModule<UserSelectorOptions> = {
type: 'UserSelector',
defaultProps: {
allowClear: true,
showSearch: true
},
dependencies: ['departmentId'],
createConfig: (options) => ({
id: 'userId',
label: options?.label ?? 'User',
component: 'Select',
componentProps: {
options: options?.options ?? []
}
})
};

Modules describe domain fields. Render components are still resolved by the existing component registry.

FieldModule<TOptions> and ModuleConfig<TOptions> provide module-level option typing. Their default remains Record<string, unknown>, so existing callers do not need to migrate. The registry still resolves modules by runtime type; this version does not attempt a global type-to-options mapping across heterogeneous config arrays.

Registry

Use ModuleRegistryManager for isolated registries or defaultModuleRegistry for shared registration.

import { ModuleRegistryManager } from '@whynotsnow/dynamic-form';

const registry = new ModuleRegistryManager();

registry.register(UserSelectorModule);
registry.has('UserSelector');
registry.get('UserSelector');
registry.list();
registry.unregister('UserSelector');

Duplicate module types are rejected by default. Pass { override: true } when replacement is intentional.

Compiler

Compile module config into standard FormConfig:

import { Form } from 'antd';
import { DynamicForm, compileFormConfig } from '@whynotsnow/dynamic-form';

const compiled = compileFormConfig(
{
fields: [
{
type: 'UserSelector',
id: 'ownerId',
options: { label: 'Owner' },
overrides: {
required: true
}
}
]
},
{ registry }
);

export function Example() {
const [form] = Form.useForm();

return (
<DynamicForm
form={form}
formConfig={compiled.formConfig}
componentRegistry={{ customComponents: compiled.componentRegistry }}
/>
);
}

The higher-level CompiledDynamicForm is recommended when rendering compiler output because it automatically wires the returned component registry:

import { CompiledDynamicForm } from '@whynotsnow/dynamic-form';

<CompiledDynamicForm form={form} compiled={compiled} />;

An additional componentRegistry may still be provided; an explicitly supplied custom component wins on name conflicts. The lower-level manual DynamicForm integration remains available when config and registry need separate control.

The compiler returns:

{
formConfig: FormConfig;
componentRegistry: ComponentRegistry;
}

formConfig can be passed to processFormConfig() or DynamicForm.

Mixed Fields And Groups

Compiler input is unified as ModuleFormConfig. Fields remain flat declarations and join a group through groupId; fields without groupId remain at the top level. Group rules only support show and hide. Manual effect runs first, and rules run afterward with matching result keys taking precedence.

compileFormConfig({
fields: [
{ type: 'AccountType', id: 'accountType' },
{ type: 'CompanyName', id: 'companyName', groupId: 'companyInfo' }
],
groups: [
{
id: 'companyInfo',
title: 'Company Information',
initialVisible: false,
rules: [{ when: { field: 'accountType', equals: 'company' }, then: { action: 'show' } }]
}
]
});

The compiler validates globally unique IDs, valid group references, and non-empty groups.

Hooks

Compiler hooks allow compile-time extension:

compileFormConfig(moduleFormConfig, {
registry,
hooks: {
beforeCompile(context) {},
beforeModuleExpand(context) {},
afterModuleExpand(context) {},
afterCompile(context) {}
}
});

Hooks operate on compiler context only. They should not mutate Ant Design Form instances or React runtime state.

Boundaries

  • compileFormConfig() creates FormConfig.
  • processFormConfig() keeps preparing runtime data.
  • DynamicForm props are unchanged.
  • compileFormConfig() supports flat, grouped, and mixed output; groups do not change field value paths.
  • Ant Design Form remains the source of truth for values and validation state.