antd-meta-form
v0.1.0
Published
Meta driven Ant Design form helper for React with AntD v5/v6 support.
Downloads
185
Maintainers
Readme
Antd meta Form
Meta-driven Ant Design form helper for React, with AntD v5 and v6 support.
Inspired by eBay/nice-form-react, implemented fresh and focused on Ant Design only. It fixes the two long-standing gaps of the reference project:
condition/renderwork withdependencies— they are evaluated inside the AntDForm.Itemdependency lifecycle, so only the dependent field area re-renders (nice-form-react#8).- Multiple fields per
form-listitem —listItemMetaaccepts a single field, an array of fields, or a function returning either (nice-form-react#14).
Why
Forms are repetitive. Describe them as metadata — possibly loaded from an API — and render real AntD forms with validation, layout, conditional visibility, and dynamic lists, without giving up access to any underlying Form.Item prop.
Installation
pnpm add antd-meta-form
# or: npm install / yarn addPeer dependencies
| Package | Range | Notes |
| --- | --- | --- |
| antd | >=5 <7 | v5 and v6 supported |
| react | >=18 | |
| react-dom | >=18 | |
| @ant-design/icons | >=5 <7 | optional |
The package ships ESM + CJS + TypeScript declarations and never bundles React or AntD. No AntD CSS is imported — your app keeps full control over styling and theming.
Example apps
Runnable Vite playgrounds for both supported AntD majors live in examples/:
examples/vite-antd5 (antd 5 + React 18) and examples/vite-antd6 (antd 6 + React 19).
cd examples/vite-antd6
pnpm install
pnpm devBasic example
import { AntdMetaForm, type MetaFormMeta } from 'antd-meta-form';
import { Button, Form } from 'antd';
const meta: MetaFormMeta = {
columns: 2,
layout: 'vertical',
fields: [
{
key: 'favoriteFruit',
label: 'Favorite Fruit',
widget: 'select',
required: true,
options: ['Apple', 'Orange', 'Other'],
},
{
key: 'otherFruit',
label: 'Other Fruit',
widget: 'input',
dependencies: ['favoriteFruit'],
condition: ({ form }) => form.getFieldValue('favoriteFruit') === 'Other',
},
{
key: 'submit',
render: () => (
<Button type="primary" htmlType="submit">
Submit
</Button>
),
},
],
};
export default function Page() {
const [form] = Form.useForm();
return <AntdMetaForm form={form} meta={meta} onFinish={(values) => console.log(values)} />;
}Rendering inside an existing AntD Form
import { Form } from 'antd';
import { MetaFormItems } from 'antd-meta-form';
function MyForm() {
const [form] = Form.useForm();
return (
<Form form={form} layout="vertical" onFinish={console.log}>
<MetaFormItems form={form} meta={meta} />
</Form>
);
}Dynamic visibility with dependencies
condition, visibleWhen, and render are executed inside a no-style Form.Item that carries the field's dependencies (or shouldUpdate). When a dependency changes, only that field area re-renders — exactly how AntD intends dependencies to work.
{
key: 'otherFruit',
label: 'Other Fruit',
widget: 'input',
dependencies: ['favoriteFruit'],
condition: ({ form }) => form.getFieldValue('favoriteFruit') === 'Other',
}Do not combine dependencies and shouldUpdate on the same field — AntD warns that they conflict. If both are present, this package logs a development warning and uses dependencies.
Note: like AntD itself, dependency re-evaluation is triggered by field updates flowing through the form store (user input,
Form.Itemcontrols). It is not a watcher on arbitrary external state.
JSON-safe conditions with visibleWhen
Metadata that comes from an API cannot carry functions. Use visibleWhen instead of condition:
{
key: 'otherFruit',
label: 'Other Fruit',
widget: 'input',
dependencies: ['favoriteFruit'],
visibleWhen: { field: 'favoriteFruit', op: 'eq', value: 'Other' },
}Operators: eq, neq, in, notIn, exists, empty, gt, gte, lt, lte.
Pass an array of expressions to combine them with AND.
Multi-field form-list
{
key: 'addresses',
label: 'Addresses',
widget: 'form-list',
listItemMeta: [
{ key: 'street', label: 'Street', widget: 'input', required: true },
{ key: 'city', label: 'City', widget: 'select', options: ['Beijing', 'Shanghai'] },
],
}Values map to addresses[0].street, addresses[0].city, …
The classic single-field form (nice-form-react compatible) still works — the field binds to the list item value itself:
{
key: 'addresses',
label: 'Addresses',
widget: 'form-list',
listItemMeta: { widget: 'select', options: ['Beijing', 'Shanghai', 'Nanjing'] },
}listItemMeta can also be a function (ctx) => FieldMeta | FieldMeta[] receiving { listField, index, operation, errors, parentField, form, meta }.
List options: minItems, maxItems, allowMove, addItemButtonLabel, addItemButtonProps, removeItemButtonLabel, listItemLayout (columns/columnGap/rowGap/layout for the row grid).
Nested form-list
List item fields can themselves be form-list fields:
{
key: 'contacts',
label: 'Contacts',
widget: 'form-list',
listItemMeta: [
{ key: 'name', label: 'Name', widget: 'input' },
{ key: 'phones', label: 'Phones', widget: 'form-list', listItemMeta: { widget: 'input' } },
],
}Custom widgets
import { defineWidget } from 'antd-meta-form';
const ColorSwatch = ({ value, onChange }) => /* value/onChange injected by Form.Item */ null;
defineWidget('color-swatch', ColorSwatch);
// optional convertField hook, e.g. for checked-style widgets:
defineWidget('my-switch', MySwitch, (field) => ({ ...field, valuePropName: 'checked' }));A field's widget can also be a component directly: { key: 'x', widget: MyWidget }.
createWidgetRegistry() is exported for building isolated registries.
View mode
Set viewMode: true on the whole meta or on a single field to render read-only values. Customize with renderView(value, ctx) or viewWidget/viewWidgetProps. Option values are mapped back to their labels automatically.
API reference
<AntdMetaForm form? meta {...formProps} />
Creates (or reuses) an AntD form and renders the metadata. All other props are forwarded to AntD Form.
<MetaFormItems form meta pathPrefix? />
Renders metadata fields inside an existing AntD Form.
MetaFormMeta
| Prop | Type | Description |
| --- | --- | --- |
| fields | FieldMeta[] | Field definitions |
| columns | number | Grid columns (default 1) |
| columnGap / rowGap | number \| string | Grid gaps |
| layout | 'horizontal' \| 'vertical' \| 'inline' | AntD form layout |
| labelWidth | number \| string | Fixed label column width |
| initialValues | object | Initial form values |
| viewMode | boolean | Render all fields read-only |
| disabled | boolean | Disable all widgets |
FieldMeta
Extends AntD FormItemProps (so rules, tooltip, extra, validateTrigger, valuePropName, getValueFromEvent, normalize, preserve, hidden, … all pass through), plus:
| Prop | Type | Description |
| --- | --- | --- |
| key | string | Required. Becomes name; dots create nested paths (user.name → ['user','name']; prefix !!! for a literal key) |
| name | NamePath | Explicit AntD name path override |
| widget | string \| Component \| null | Widget name or component (default 'input') |
| widgetProps | object | Props for the widget |
| options | (string \| number \| object)[] | For select / radio-group / checkbox-group; scalars are auto-converted |
| required | boolean | Adds a required rule if none exists |
| condition | (ctx) => boolean | Function visibility (use with dependencies) |
| visibleWhen | expression or array | JSON-safe visibility (use with dependencies) |
| render | (ctx) => ReactNode | Replace the whole field rendering |
| renderView / viewWidget | | View-mode rendering |
| fullWidth / colSpan / rowSpan | | Grid placement |
| listItemMeta + list props | | See form-list above |
Built-in widgets
Every AntD data-entry component is registered:
input, text, search, password, textarea, input-otp, number, select, auto-complete, cascader, tree-select, mentions, radio-group, checkbox, checkbox-group, switch, segmented, slider, rate, color-picker, date-picker, range-picker, time-picker, time-range-picker, transfer, upload, form-list, view-text.
Widget-specific notes:
upload— automatically wired withvaluePropName: 'fileList'and agetValueFromEventthat unwraps the upload event. Provide the trigger viachildren(e.g. a button) and Upload props viawidgetProps.transfer— automatically wired withvaluePropName: 'targetKeys'; passdataSource/renderviawidgetProps.tree-select—optionsare mapped ontotreeData(labels/values/children align with treeData's default field names); awidgetProps.treeDatatakes precedence.color-pickerrequires antd ≥ 5.5 andinput-otprequires antd ≥ 5.16; on older 5.x versions these names are simply not registered.
AntD v5/v6 compatibility
- Only stable public AntD APIs are used:
Form,Form.Item,Form.List,Form.ErrorList, plus the standard input widgets. - CI runs the test matrix against AntD 5 + React 18 and AntD 6 + React 18/19.
- AntD is a peer dependency and never bundled, so your app's AntD version (and theme) wins.
Contributing & releasing
Development setup, the CI matrix, and the release/publishing process are documented in CONTRIBUTING.md.
License
MIT
