refactor(@vben-core/form-ui): migrate to Zod 4 and TanStack Form (#8176)
* build(@vben-core/form-ui): update form validation dependencies * refactor(@vben-core/form-ui): replace vee-validate with TanStack Form * test(@vben-core/form-ui): cover TanStack Form migration * docs(@vben/docs): document Zod 4 form migration * refactor(project): update form adapters * refactor(project): migrate application form consumers * refactor(@vben/playground): migrate form examples * docs(@vben/docs): fix validate return type and required rule empty check in form docs * fix(@vben-core/form-ui): resolve oxlint and eslint errors - rewrite nested ternary expressions as if/else in form-field-array.vue and form-runtime.ts - sort @tanstack/vue-form before @vben-core/composables in package.json * chore: fix merge artifacts and formatting - move dependencies before devDependencies in root package.json - update preferences snapshot for widget positioning fields - format form-array demo README * refactor(@vben-core/form-ui): optimize form runtime and value flow - add fine-grained field selectors and atomic dependency resolution - expose raw and formatted value snapshots with focused regression coverage * fix(@vben/layouts): dispose sortable instance on unmount * docs(@vben/docs): document form runtime API changes - list added, changed, removed, and deprecated form APIs - document atomic dependencies and raw versus formatted values
This commit is contained in:
@@ -16,7 +16,9 @@ outline: deep
|
||||
|
||||
## 适配器
|
||||
|
||||
表单底层使用 [vee-validate](https://vee-validate.logaretm.com/v4/) 进行表单验证,所以你可以使用 `vee-validate` 的所有功能。对于不同的 UI 框架,我们提供了适配器,以便更好的适配不同的 UI 框架。
|
||||
表单内部使用 [TanStack Form](https://tanstack.com/form/latest/docs/framework/vue/overview) 管理状态与校验生命周期,并使用 [Zod 4](https://zod.dev/v4) 描述 schema。业务侧仍通过 `useVbenForm`、`FormApi` 和组件适配器使用表单,不应直接依赖底层 TanStack 实例。
|
||||
|
||||
从 Zod 3 或旧表单引擎升级时,请先阅读 [Zod 4 与 TanStack Form 迁移指南](/guide/in-depth/zod-v4-form-migration)。
|
||||
|
||||
### 适配器说明
|
||||
|
||||
@@ -26,8 +28,9 @@ outline: deep
|
||||
|
||||
```ts
|
||||
import type {
|
||||
FormValues,
|
||||
VbenFormProps as FormProps,
|
||||
VbenFormSchema as FormSchema,
|
||||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { ComponentType } from './component';
|
||||
@@ -42,6 +45,8 @@ setupVbenForm<ComponentType>({
|
||||
config: {
|
||||
// ant design vue组件库默认都是 v-model:value
|
||||
baseModelPropName: 'value',
|
||||
// 仅当组件不发送 update:*、只发送 change 时启用
|
||||
changeEventFallback: false,
|
||||
// 一些组件库空值为 null,重置表单时需要和实际组件行为保持一致
|
||||
emptyStateValue: null,
|
||||
// 一些组件是 v-model:checked 或者 v-model:fileList
|
||||
@@ -52,7 +57,7 @@ setupVbenForm<ComponentType>({
|
||||
Upload: 'fileList',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
rules: {
|
||||
// 输入项目必填国际化适配
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
@@ -70,11 +75,20 @@ setupVbenForm<ComponentType>({
|
||||
},
|
||||
});
|
||||
|
||||
const useVbenForm = useForm<ComponentType>;
|
||||
function useVbenForm<TValues extends FormValues = FormValues>(
|
||||
options: FormProps<ComponentType, Record<never, never>, TValues>,
|
||||
) {
|
||||
return useForm<TValues, ComponentType, Record<never, never>>(options);
|
||||
}
|
||||
|
||||
export { useVbenForm, z };
|
||||
export type VbenFormSchema = FormSchema<ComponentType>;
|
||||
export type { VbenFormProps };
|
||||
export type VbenFormSchema<TValues extends FormValues = FormValues> =
|
||||
FormSchema<ComponentType, Record<never, never>, TValues>;
|
||||
export type VbenFormProps<TValues extends FormValues = FormValues> = FormProps<
|
||||
ComponentType,
|
||||
Record<never, never>,
|
||||
TValues
|
||||
>;
|
||||
```
|
||||
|
||||
:::
|
||||
@@ -252,6 +266,8 @@ export { initComponentAdapter };
|
||||
|
||||
_注意_ 需要指定 `dependencies` 的 `triggerFields` 属性,设置由谁的改动来触发,以便表单组件能够正确的联动。
|
||||
|
||||
新代码推荐使用 `dependencies.resolve(context)` 一次返回完整动态状态。它只在 `triggerFields` 变化时执行,并原子更新 `if`、`show`、`disabled`、`required`、`rules`、`componentProps`、`help` 和 `renderComponentContent`,避免多个异步回调产生中间状态。原有多回调结构继续兼容。
|
||||
|
||||
<DemoPreview dir="demos/vben-form/dynamic" />
|
||||
|
||||
## 自定义组件
|
||||
@@ -287,29 +303,105 @@ const [Form, formApi] = useVbenForm({
|
||||
</template>
|
||||
```
|
||||
|
||||
### 类型传递与插槽
|
||||
|
||||
通过 `useVbenForm<TValues>` 定义一次表单值类型后,`getValues`、`setValues`、`setFieldValue`、`handleSubmit`、`handleValuesChange`、`formApi.form.values` 和 selector 都会沿用该类型,可直接作为 API 请求参数:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
interface AccountFormValues {
|
||||
email: string;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm<AccountFormValues>({
|
||||
handleSubmit(values, rawValues) {
|
||||
// values: AccountFormValues
|
||||
// rawValues: Readonly<AccountFormValues>
|
||||
return addAccount(values);
|
||||
},
|
||||
schema: [
|
||||
{ component: 'Input', fieldName: 'email', label: 'Email' },
|
||||
{ component: 'Input', fieldName: 'nickname', label: 'Nickname' },
|
||||
],
|
||||
});
|
||||
|
||||
async function fillForm() {
|
||||
await formApi.setValues({ email: 'user@example.com' });
|
||||
const values = await formApi.getValues(); // AccountFormValues
|
||||
return values;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form>
|
||||
<template #email="{ componentField, field, formApi, values }">
|
||||
<!-- field.state.value、componentField.modelValue 均为 string -->
|
||||
<input v-bind="componentField" :data-email="values.email" />
|
||||
<button type="button" @click="formApi.clearValidation('email')">
|
||||
Clear
|
||||
</button>
|
||||
</template>
|
||||
<template #default="{ formApi, shapes, values }">
|
||||
<!-- values: AccountFormValues -->
|
||||
<button type="button" @click="formApi.submit()">
|
||||
Submit {{ shapes.length }} fields for {{ values.email }}
|
||||
</button>
|
||||
</template>
|
||||
</Form>
|
||||
</template>
|
||||
```
|
||||
|
||||
字段命名插槽提供 `field`、`componentField`、`modelValue`、`name`、`disabled`、`isInValid`、`values` 和 `formApi`。默认插槽提供 `shapes`、`values` 和 `formApi`;`reset-before`、`submit-before`、`expand-before`、`expand-after` 提供 `values` 和 `formApi`。未声明 `TValues` 时仍兼容任意字段名,但 slot props 会回退为宽泛类型。
|
||||
|
||||
### FormApi
|
||||
|
||||
useVbenForm 返回的第二个参数,是一个对象,包含了一些表单的方法。
|
||||
|
||||
| 方法名 | 描述 | 类型 | 版本号 |
|
||||
| --- | --- | --- | --- |
|
||||
| submitForm | 提交表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
|
||||
| validateAndSubmitForm | 提交并校验表单 | `(e:Event)=>Promise<Record<string,any>>` | - |
|
||||
| resetForm | 重置表单 | `()=>Promise<void>` | - |
|
||||
| setValues | 设置表单值, 默认会过滤不在schema中定义的field, 可通过filterFields形参关闭过滤 | `(fields: Record<string, any>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
|
||||
| getValues | 获取表单值 | `(fields:Record<string, any>,shouldValidate: boolean = false)=>Promise<void>` | - |
|
||||
| validate | 表单校验 | `()=>Promise<void>` | - |
|
||||
| validateField | 校验指定字段 | `(fieldName: string)=>Promise<ValidationResult<unknown>>` | - |
|
||||
| submit | 提交表单 | `(e?: Event) => Promise<TValues>` | - |
|
||||
| validateAndSubmit | 校验通过后提交表单 | `() => Promise<TValues \| undefined>` | - |
|
||||
| reset | 重置表单 | `(state?: FormResetState<TValues>, options?: FormResetOptions) => Promise<void>` | - |
|
||||
| clearValidation | 清空指定字段或全部校验,并取消进行中的异步校验 | `(fieldNames?: FormFieldName<TValues> \| FormFieldName<TValues>[]) => Promise<void>` | - |
|
||||
| setValues | 设置表单值,默认会过滤不在 schema 中定义的字段 | `(fields: Partial<TValues>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
|
||||
| getValues | 获取经过字段映射和 valueFormat 的值 | `() => Promise<TValues>` | - |
|
||||
| getRawValues | 获取未格式化的独立值快照 | `() => Promise<TValues>` | - |
|
||||
| getValueSnapshot | 一次获取原始值和格式化值 | `() => Promise<FormValueSnapshot<TValues>>` | - |
|
||||
| formatValues | 格式化指定的原始值快照 | `(rawValues: Readonly<TValues>) => TValues` | - |
|
||||
| validate | 表单校验 | `() => Promise<FormValidationResult>` | - |
|
||||
| validateField | 校验指定字段 | `(fieldName: string) => Promise<FormValidationResult>` | - |
|
||||
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` | - |
|
||||
| resetValidate | 重置表单校验 | `()=>Promise<void>` | - |
|
||||
| updateSchema | 更新formSchema | `(schema:FormSchema[])=>void` | - |
|
||||
| setFieldValue | 设置字段值 | `(field: string, value: any, shouldValidate?: boolean)=>Promise<void>` | - |
|
||||
| setState | 设置组件状态(props) | `(stateOrFn:\| ((prev: VbenFormProps) => Partial<VbenFormProps>)\| Partial<VbenFormProps>)=>Promise<void>` | - |
|
||||
| getState | 获取组件状态(props) | `()=>Promise<VbenFormProps>` | - |
|
||||
| form | 表单对象实例,可以操作表单,见 [useForm](https://vee-validate.logaretm.com/v4/api/use-form/) | - | - |
|
||||
| form | 稳定的 `FormContextApi`,提供 values、errors、set/reset/validate/submit 与数组字段操作,不暴露底层 TanStack 泛型 | `FormContextApi` | - |
|
||||
| getFieldComponentRef | 获取指定字段的组件实例 | `<T=unknown>(fieldName: string)=>T` | >5.5.3 |
|
||||
| getFocusedField | 获取当前已获得焦点的字段 | `()=>string\|undefined` | >5.5.3 |
|
||||
|
||||
旧命名 `submitForm`、`validateAndSubmitForm`、`resetForm`、`resetValidate` 分别对应 `submit`、`validateAndSubmit`、`reset`、`clearValidation`。它们仍可调用,但已标记 `@deprecated`,开发环境每个旧名称只警告一次,生产环境静默。
|
||||
|
||||
### FormContextApi 响应式读取
|
||||
|
||||
`formApi.form` 提供细粒度 selector。字段组件应优先使用字段级方法,避免订阅整份 values 或 errors:
|
||||
|
||||
| 方法 | 返回值 | 用途 |
|
||||
| --- | --- | --- |
|
||||
| `useFieldValue(fieldName)` | `Readonly<Ref<FormFieldValue>>` | 订阅一个字段值。 |
|
||||
| `useFieldValues(fieldNames)` | `Readonly<Ref<FormFieldValue[]>>` | 订阅一组声明字段值。 |
|
||||
| `useFieldError(fieldName)` | `Readonly<Ref<string \| undefined>>` | 订阅一个字段错误。 |
|
||||
| `useValues()` | `Readonly<Ref<TValues>>` | 订阅整份表单值。 |
|
||||
| `useSelector(selector)` | `Readonly<Ref<TResult>>` | 兼容入口,可从 `{ values, errors, meta }` 组合选择状态。 |
|
||||
|
||||
```ts
|
||||
const email = formApi.form.useFieldValue('email');
|
||||
const emailError = formApi.form.useFieldError('email');
|
||||
const submitting = formApi.form.useSelector((state) => state.meta.submitting);
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
所有属性都可以传入 `useVbenForm` 的第一个参数中。
|
||||
@@ -323,8 +415,8 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
| actionLayout | 表单操作按钮位置 | `'newLine' \| 'rowEnd' \| 'inline'` | `rowEnd` |
|
||||
| actionPosition | 表单操作按钮对齐方式 | `'left' \| 'center' \| 'right'` | `right` |
|
||||
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleSubmit | 表单提交回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleValuesChange | 表单值变化回调 | `(values: Record<string, any>, fieldsChanged: string[]) => void` | - |
|
||||
| handleSubmit | 表单提交回调 | `(values: TValues, rawValues: Readonly<TValues>) => Promise<void> \| void` | - |
|
||||
| handleValuesChange | 表单值变化回调 | `(rawValues: Readonly<TValues>, fieldsChanged: string[], getFormattedValues: () => TValues) => void` | - |
|
||||
| handleCollapsedChange | 表单收起展开状态变化回调 | `(collapsed: boolean) => void` | - |
|
||||
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
|
||||
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
|
||||
@@ -343,7 +435,9 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
|
||||
::: tip handleValuesChange
|
||||
|
||||
`handleValuesChange` 回调函数的第一个参数`values`装载了表单改变后的当前值对象,第二个参数`fieldsChanged`是一个数组,包含了所有被改变的字段名。注意:第二个参数仅在v5.5.4(不含)以上版本可用,并且传递的是已在schema中定义的字段名。如果你使用了字段映射并且需要检查是哪些字段发生了变化的话,请注意该参数并不会包含映射后的字段名。
|
||||
`handleValuesChange` 的第一个参数是未经过 `valueFormat`、`fieldMappingTime` 或 array-to-string 转换的只读当前值,第二个参数是本次发生变化的 schema 字段名。第三个参数 `getFormattedValues` 是惰性函数:不调用就不会执行深拷贝和格式化,适合只在少数变化场景读取提交结构。字段映射生成的目标字段不会出现在 `fieldsChanged` 中。
|
||||
|
||||
`getRawValues()` 和 `getValues()` 分别只生成一份目标快照;确实需要同时比较两种结构时再调用 `getValueSnapshot()`。`handleSubmit(values, rawValues)` 会在提交边界同时提供格式化结果和对应的原始快照。
|
||||
|
||||
:::
|
||||
|
||||
@@ -410,6 +504,11 @@ export interface ActionButtonOptions {
|
||||
|
||||
```ts
|
||||
export interface FormCommonConfig {
|
||||
/**
|
||||
* 仅当组件不发送 update:*、只发送 change 时启用兼容回退
|
||||
* @default false
|
||||
*/
|
||||
changeEventFallback?: boolean;
|
||||
/**
|
||||
* 所有表单项的props
|
||||
*/
|
||||
@@ -431,7 +530,7 @@ export interface FormCommonConfig {
|
||||
* 所有表单项的控件样式
|
||||
* @default {}
|
||||
*/
|
||||
formFieldProps?: Partial<typeof Field>;
|
||||
formFieldProps?: FormFieldOptions;
|
||||
/**
|
||||
* 所有表单项的栅格布局
|
||||
* @default ""
|
||||
@@ -475,11 +574,14 @@ export interface FormCommonConfig {
|
||||
```ts
|
||||
export interface FormSchema<
|
||||
T extends BaseFormComponentType = BaseFormComponentType,
|
||||
TValues extends FormValues = FormValues,
|
||||
> extends FormCommonConfig {
|
||||
/** 组件 */
|
||||
component: Component | T;
|
||||
/** 组件参数 */
|
||||
componentProps?: ComponentProps;
|
||||
componentProps?:
|
||||
| MaybeComponentProps
|
||||
| ((ctx: FormSchemaContext<TValues>) => MaybeComponentProps);
|
||||
/** 默认值 */
|
||||
defaultValue?: any;
|
||||
/** 依赖 */
|
||||
@@ -489,13 +591,15 @@ export interface FormSchema<
|
||||
/** 字段名,也作为自定义插槽的名称 */
|
||||
fieldName: string;
|
||||
/** 帮助信息 */
|
||||
help?: CustomRenderType;
|
||||
help?: string | ((ctx: FormSchemaContext<TValues>) => Component | string);
|
||||
/** 是否隐藏表单项 */
|
||||
hide?: boolean;
|
||||
/** 表单的标签(如果是一个string,会用于默认必选规则的消息提示) */
|
||||
label?: CustomRenderType;
|
||||
/** 自定义组件内部渲染 */
|
||||
renderComponentContent?: RenderComponentContentType;
|
||||
renderComponentContent?: (
|
||||
ctx: FormSchemaContext<TValues>,
|
||||
) => Record<string, any>;
|
||||
/** 字段规则 */
|
||||
rules?: FormSchemaRuleType;
|
||||
/** 后缀 */
|
||||
@@ -505,6 +609,8 @@ export interface FormSchema<
|
||||
}
|
||||
```
|
||||
|
||||
顶层 `componentProps`、`help` 和 `renderComponentContent` 函数只接收轻量 `FormSchemaContext`,适合数组行索引、字段路径等 schema 信息。需要读取表单值时,使用 `dependencies.resolve({ values, ... })`,避免每个字段订阅整份 values。
|
||||
|
||||
:::
|
||||
|
||||
::: details FormValueFormat
|
||||
@@ -529,29 +635,37 @@ type FormValueFormat = (
|
||||
|
||||
```ts
|
||||
dependencies: {
|
||||
// 触发字段。只有这些字段值变动时,联动才会触发
|
||||
triggerFields: ['name'],
|
||||
// 动态判断当前字段是否需要显示,不显示则直接销毁
|
||||
if(values,formApi){},
|
||||
// 动态判断当前字段是否需要显示,不显示用css隐藏
|
||||
show(values,formApi){},
|
||||
// 动态判断当前字段是否需要禁用
|
||||
disabled(values,formApi){},
|
||||
// 字段变更时,都会触发该函数
|
||||
trigger(values,formApi){},
|
||||
// 动态rules
|
||||
rules(values,formApi){},
|
||||
// 动态必填
|
||||
required(values,formApi){},
|
||||
// 动态组件参数
|
||||
componentProps(values,formApi){},
|
||||
triggerFields: ['type', 'role'],
|
||||
resolve({ values, actions, controller, schema }) {
|
||||
const editable = values.type === 'editable';
|
||||
return {
|
||||
componentProps: { placeholder: schema.fieldName },
|
||||
disabled: !editable,
|
||||
required: values.role === 'owner',
|
||||
rules: editable ? 'required' : null,
|
||||
show: values.type !== 'hidden',
|
||||
};
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
`resolve` 返回的字段会一次性提交;支持 `if`、`show`、`disabled`、`required`、`rules`、`componentProps`、`help` 和 `renderComponentContent`。未返回 `rules` 时继续使用静态规则,显式返回 `rules: null` 时关闭静态规则。`actions` 是稳定的 `FormContextApi`,`controller` 是高层 FormApi,`schema` 包含字段名和数组行上下文。
|
||||
|
||||
旧的 `if/show/disabled/required/rules/componentProps/trigger` 回调语法仍完整兼容并保持原求值顺序,但已标记为 `@deprecated`,开发环境首次使用时会提示迁移。新旧语法在同一个 dependencies 对象中互斥;绕过类型同时传入时以 `resolve` 为准。
|
||||
|
||||
### 表单校验
|
||||
|
||||
表单校验需要通过 schema 内的 `rules` 属性进行配置。
|
||||
|
||||
字段默认在 blur、change 和 submit 时校验。使用 `formFieldProps.validateOn` 限制交互触发时机,submit 始终校验;异步校验可通过 `asyncDebounceMs` 防抖:
|
||||
|
||||
```ts
|
||||
formFieldProps: {
|
||||
asyncDebounceMs: 300,
|
||||
validateOn: ['blur'],
|
||||
}
|
||||
```
|
||||
|
||||
rules的值可以是字符串(预定义的校验规则名称),也可以是一个zod的schema。
|
||||
|
||||
#### 预定义的校验规则
|
||||
|
||||
Reference in New Issue
Block a user