跳到主要内容

Form

基于 @tanstack/react-form 构建的类型安全、无样式表单系统,集成了 Ant Design 字段组件。

注意: VEF 重新导出 Ant Design 的 Form 组件,而是提供 useForm——一个完全类型安全的表单 Hook,内置字段组件。

何时使用

  • VEF 应用中的任意数据录入表单。
  • 需要通过 TypeScript 类型推断获得类型安全的表单状态时。
  • 需要校验、异步提交以及字段级错误展示时。

基础用法

import { useForm } from '@vef-framework-react/components';

interface LoginForm {
username: string;
password: string;
}

export default function LoginPage() {
// The form data type is inferred from `defaultValues` (annotate it rather
// than passing explicit generics — `useForm` has one generic per validator slot).
const form = useForm({
defaultValues: { username: '', password: '' } as LoginForm,
onSubmit: async ({ value }) => {
await login(value);
},
});

return (
<form.AppForm>
<form.Form layout="vertical">
<form.AppField
name="username"
validators={{ onChange: ({ value }) => !value ? 'Required' : undefined }}
>
{(field) => (
<field.Input label="Username" placeholder="Enter username" />
)}
</form.AppField>

<form.AppField name="password">
{(field) => (
<field.Password label="Password" placeholder="Enter password" />
)}
</form.AppField>

<form.SubmitButton>Login</form.SubmitButton>
</form.Form>
</form.AppForm>
);
}

form.AppForm 是表单上下文的 provider:内置表单组件(form.Formform.SubmitButtonform.ResetButton)与 useFormContext 都必须渲染在它内部。form.AppField 绑定一个字段,并通过其 render prop 暴露字段组件。

可用字段组件

所有字段组件都可以在 form.AppField 的渲染函数中通过 field.* 访问:

Field Component说明
field.Input文本输入
field.Password密码输入
field.TextArea多行文本
field.InputNumber数字输入
field.Select下拉选择器
field.TreeSelect树形选择器
field.AutoComplete自动完成输入
field.Cascader级联选择器
field.DatePicker日期选择器
field.DateRangePicker日期范围选择器
field.TimePicker时间选择器
field.TimeRangePicker时间范围选择器
field.Checkbox单个复选框
field.CheckboxGroup复选框组
field.Radio单选框组
field.Bool布尔值输入(开关/单选/复选框,由 variant 属性决定具体形式,默认为 "switch"
field.Slider滑块输入
field.Rate星级评分
field.ColorPicker颜色选择器
field.CodeEditor代码编辑器
field.IconPicker图标选择器
field.Mentions提及输入
field.Transfer穿梭框
field.Upload文件上传

不存在单独的 field.Switch。如需开关样式的布尔值输入,请使用 field.Bool(其 variant 属性默认值为 "switch")。

校验

<form.AppField
name="email"
validators={{
onChange: ({ value }) => {
if (!value) return 'Email is required';
if (!/\S+@\S+\.\S+/.test(value)) return 'Invalid email';
return undefined;
},
onBlurAsync: async ({ value }) => {
const taken = await checkEmailTaken(value);
return taken ? 'Email already in use' : undefined;
},
}}
>
{(field) => <field.Input label="Email" />}
</form.AppField>

表单布局

form.Form 为其内部的每个字段项建立布局上下文:layout'horizontal' | 'vertical')、labelAlignlabelWidth。默认是水平布局、标签右对齐、100px 标签列。单个字段项可以通过自身的 FormItemProps 覆盖其中任意一项:

// Vertical layout for the whole form
<form.Form layout="vertical">
<form.AppField name="name">
{(field) => <field.Input label="Name" />}
</form.AppField>
</form.Form>

// Horizontal layout with a wider label column
<form.Form layout="horizontal" labelWidth={140}>
<form.AppField name="name">
{(field) => <field.Input label="Name" />}
</form.AppField>
</form.Form>

这三项设置共同构成 FormLayout 类型,FormModalFormDrawerCrud 都以 formLayout 属性接受它:

interface FormLayout {
layout?: 'horizontal' | 'vertical';
labelAlign?: 'left' | 'right';
labelWidth?: number;
}

字段组

使用 withFieldGroup 创建绑定到表单值某个子树的可复用字段组。它接受 { defaultValues, render, props? }render 收到一个 group API,其 AppField 的 name 相对于字段组挂载的位置:

import { withFieldGroup } from '@vef-framework-react/components';

const AddressGroup = withFieldGroup({
defaultValues: { city: '', zip: '' },
render: ({ group }) => (
<>
<group.AppField name="city">
{(field) => <field.Input label="City" />}
</group.AppField>
<group.AppField name="zip">
{(field) => <field.Input label="ZIP" />}
</group.AppField>
</>
),
});

// Mount the group on any form whose values contain a matching sub-tree:
<AddressGroup form={form} fields="shippingAddress" />

createFormOptions

使用 createFormOptions 定义可复用的表单配置:

import { createFormOptions } from '@vef-framework-react/components';

const loginFormOptions = createFormOptions({
defaultValues: { username: '', password: '' } as LoginForm,
onSubmit: async ({ value }) => { /* ... */ },
});

// In component:
const form = useForm(loginFormOptions);

API

useForm(options)

接受所有 @tanstack/react-form 的 FormOptions,返回一个经过扩展的 FormApi:附加了 VEF 表单组件(form.* / form.AppForm 下的 FormSubmitButtonResetButton)、字段组件(通过 form.AppField 的 render prop 提供),以及 createField 辅助函数。

选项类型说明
defaultValuesTFormData表单初始值
onSubmit({ value }) => Promise<void>提交处理函数
onSubmitInvalid({ value, formApi }) => void提交未通过校验时调用
validatorsFormValidators表单级校验器

FormItemProps(所有字段组件共用)

每个字段组件都接受其所包裹控件自身的 props,外加以下表单项 props(FieldComponentProps<TFieldProps> = TFieldProps & FormItemProps):

PropTypeDefault说明
labelReactNode字段标签
labelWidthnumber继承自表单布局(默认 100标签宽度(像素)
labelAlign'left' | 'right'继承(默认 'right'标签文字对齐方式
layout'horizontal' | 'vertical'继承(默认 'horizontal'覆盖该字段项的表单布局
extraReactNode字段下方的额外提示
requiredboolean显示必填标记
noWrapperbooleanfalse渲染裸控件,不带表单项包裹(label/错误骨架)——用于搜索栏和表格编辑器

form.Form 属性

一个多态的布局/上下文 provider——默认渲染原生 <form>,并提供布局与禁用两个上下文。渲染原生 <form> 时,submit 和 reset 事件会自动接到表单 API 上(preventDefault + handleSubmit() / reset());props 中排除了 onSubmit / onSubmitCapture

PropTypeDefault说明
layout'horizontal' | 'vertical''horizontal'表单项布局
labelAlign'left' | 'right''right'标签文字对齐方式
labelWidthnumber100标签列宽度(像素)
disabledbooleanfalse禁用内部所有字段(通过上下文提供)
componentElementType"form"渲染的元素类型;非 form 元素会跳过原生 submit/reset 接线
(rest)ComponentPropsWithoutRef<TComponent>所渲染元素的任意 prop(classNamestyle 等)

form.SubmitButton 属性

一个绑定到表单状态的 Button:渲染为 type="primary"htmlType="submit",在表单 isSubmitting 期间显示加载状态,并在表单 canSubmitfalse 或外围表单被禁用时自行禁用。

PropTypeDefault说明
onSubmit() => void点击处理函数;仅在原生 <form> 之外需要(例如接到 form.handleSubmit
childrenReactNode'提交'按钮文案
(rest)ButtonProps except htmlType / onClick / onClickCapture其他任意 Button prop(iconsizedanger 等)

form.ResetButton 属性

一个 htmlType="reset"Button,在表单 isSubmitting 或外围表单被禁用时处于禁用状态。

PropTypeDefault说明
onReset() => void点击处理函数;仅在原生 <form> 之外需要(例如接到 form.reset
childrenReactNode'重置'按钮文案
(rest)ButtonProps except htmlType / onClick / onClickCapture其他任意 Button prop

useFormContext<TFormData>()

从上下文读取最近的表单 API——供渲染在 form.* render props 之外的字段组件使用,例如 ProSearch 内的搜索字段或自定义表单操作。返回与 useForm 相同的 FormApi,类型由你传入的 TFormData 决定。

useFormStore(form.store, selector)

TanStack Form useStore 的重导出,用于以渲染优化的方式订阅表单状态切片(值、错误、canSubmit 等):

const username = useFormStore(form.store, (state) => state.values.username);

withForm / withFieldGroup

TanStack Form 高阶组件的重导出,预先绑定了 VEF 的字段与表单组件。withForm 围绕共享的表单选项构建可复用的表单组件;withFieldGroup 为某个值子树构建可复用的字段组(见上文"字段组"一节)。

form.createField / restoreFieldOptions

form.createField(name, options) 把字段定义成数据——用于把表单定义组织为同构数组(例如配置驱动的表单引擎)。options 扩展自绑定的 <form.AppField> 的全部选项(validatorslistenersdefaultValueasyncDebounceMsmode 等),并新增:

选项类型说明
render(field: typeof fieldComponents) => ReactNoderender prop 的主体,作用域限定为注入的字段组件
renderMetaTMeta extends AnyObject由外围遍历消费的任意元数据(例如布局跨度、可见性)

它返回一个 FormFieldItem<TFormData, TMeta>{ name, fieldOptions?, render, renderMeta? },其中 fieldOptions 做了类型擦除(ErasedFieldOptions<TFormData>),让不同字段名的项可以放进同一个数组。在渲染处用 restoreFieldOptions(item.fieldOptions) 恢复后展开:

items.map((item) => (
<form.AppField key={item.name} name={item.name} {...restoreFieldOptions(item.fieldOptions)}>
{item.render}
</form.AppField>
));

最佳实践

  • 定义 defaultValues 时包含表单数据的完整结构,以获得正确的 TypeScript 类型推断。
  • 使用 validators.onChange 提供即时反馈,使用 validators.onBlurAsync 进行服务端校验。
  • 使用 form.SubmitButton 而非普通的 Button——它会在提交期间自动禁用并显示加载状态。
  • 使用 form.ResetButton 将表单重置为 defaultValues