feat(@vben-core/form-ui): add typed form value codecs (#8189)
* feat(@vben-core/form-ui): add typed form value codecs * test(@vben-core/form-ui): cover codec value boundaries * refactor(project): propagate form codec generics * feat(@vben/plugins): propagate form codecs through vxe grids * refactor(@vben/playground): migrate form codec examples * refactor(@vben/playground): migrate query forms to codecs * docs(@vben/docs): document form value codecs
This commit is contained in:
@@ -41,18 +41,30 @@ setupVbenForm<ComponentType>({
|
||||
},
|
||||
});
|
||||
|
||||
function useVbenForm<TValues extends FormValues = FormValues>(
|
||||
options: FormProps<ComponentType, Record<never, never>, TValues>,
|
||||
function useVbenForm<
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
>(
|
||||
options: FormProps<
|
||||
ComponentType,
|
||||
Record<never, never>,
|
||||
TFormValues,
|
||||
TSubmitValues
|
||||
>,
|
||||
) {
|
||||
return useForm<TValues, ComponentType, Record<never, never>>(options);
|
||||
return useForm<
|
||||
TFormValues,
|
||||
ComponentType,
|
||||
Record<never, never>,
|
||||
TSubmitValues
|
||||
>(options);
|
||||
}
|
||||
|
||||
export { useVbenForm, z };
|
||||
|
||||
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
|
||||
>;
|
||||
export type VbenFormProps<
|
||||
TFormValues extends FormValues = FormValues,
|
||||
TSubmitValues extends FormValues = TFormValues,
|
||||
> = FormProps<ComponentType, Record<never, never>, TFormValues, TSubmitValues>;
|
||||
|
||||
@@ -244,13 +244,28 @@ export { initComponentAdapter };
|
||||
|
||||
<DemoPreview dir="demos/vben-form/query" />
|
||||
|
||||
## 值格式化
|
||||
## 表单值编解码
|
||||
|
||||
当组件的展示值与后端真正需要的 payload 不一致时,可以在 schema 上使用 `valueFormat`。它会在 `getValues()`、提交、以及依赖这些输出的方法中生效。
|
||||
当组件值与后端 payload 不一致时,使用表单级 `codec` 统一定义双向转换。`encode` 接收完整 `TFormValues` 并返回完整 `TSubmitValues`;`decode` 执行反向转换。多字段拆分、合并和删除都在一个纯函数边界完成,不依赖 schema 顺序或字符串路径写入。
|
||||
|
||||
- `return xxx`:回写当前字段
|
||||
- `setValue('startTime', xxx)`:写入其他字段
|
||||
- `return undefined`:保持当前字段已被移除,适合把一个字段拆成多个字段
|
||||
`codec` 直接写在 `useVbenForm` 选项中即可。只需标注 `encode` 的表单值入参,`TSubmitValues` 会从返回对象自动推导,并传递给 `decode`、`getValues()` 和提交回调:
|
||||
|
||||
```ts
|
||||
const [Form, formApi] = useVbenForm({
|
||||
codec: {
|
||||
decode(values) {
|
||||
return { period: [values.startTime, values.endTime] };
|
||||
},
|
||||
encode(values: Readonly<FormValues>) {
|
||||
return {
|
||||
endTime: values.period[1],
|
||||
startTime: values.period[0],
|
||||
};
|
||||
},
|
||||
},
|
||||
schema,
|
||||
});
|
||||
```
|
||||
|
||||
<DemoPreview dir="demos/vben-form/value-format" />
|
||||
|
||||
@@ -305,7 +320,7 @@ const [Form, formApi] = useVbenForm({
|
||||
|
||||
### 类型传递与插槽
|
||||
|
||||
通过 `useVbenForm<TValues>` 定义一次表单值类型后,`getValues`、`setValues`、`setFieldValue`、`handleSubmit`、`handleValuesChange`、`formApi.form.values` 和 selector 都会沿用该类型,可直接作为 API 请求参数:
|
||||
使用 `useVbenForm<TFormValues, TSubmitValues>` 分别声明组件表单值和提交值。schema、slots、`setValues`、`formApi.form.values` 使用 `TFormValues`;`getValues`、submit 和 `handleSubmit` 第一参数使用 `TSubmitValues`。两种结构相同时只传一个泛型即可。
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
@@ -362,15 +377,16 @@ useVbenForm 返回的第二个参数,是一个对象,包含了一些表单
|
||||
|
||||
| 方法名 | 描述 | 类型 | 版本号 |
|
||||
| --- | --- | --- | --- |
|
||||
| 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` | - |
|
||||
| submit | 提交表单 | `(e?: Event) => Promise<TSubmitValues>` | - |
|
||||
| validateAndSubmit | 校验通过后提交表单 | `() => Promise<TSubmitValues \| undefined>` | - |
|
||||
| reset | 重置表单 | `(state?: FormResetState<TFormValues>, options?: FormResetOptions) => Promise<void>` | - |
|
||||
| clearValidation | 清空指定字段或全部校验,并取消进行中的异步校验 | `(fieldNames?: FormFieldName<TFormValues> \| FormFieldName<TFormValues>[]) => Promise<void>` | - |
|
||||
| setValues | 设置表单组件值,默认会过滤不在 schema 中定义的字段 | `(fields: Partial<TFormValues>, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
|
||||
| setSubmitValues | 通过 codec.decode 回填完整提交值 | `(values: TSubmitValues, filterFields?: boolean, shouldValidate?: boolean) => Promise<void>` | - |
|
||||
| getValues | 获取经过 codec.encode 或旧格式化管道的提交值 | `() => Promise<TSubmitValues>` | - |
|
||||
| getRawValues | 获取未格式化的独立表单值快照 | `() => Promise<TFormValues>` | - |
|
||||
| getValueSnapshot | 一次获取表单值和提交值 | `() => Promise<FormValueSnapshot<TFormValues, TSubmitValues>>` | - |
|
||||
| formatValues | 编码指定的表单值快照 | `(rawValues: Readonly<TFormValues>) => TSubmitValues` | - |
|
||||
| validate | 表单校验 | `() => Promise<FormValidationResult>` | - |
|
||||
| validateField | 校验指定字段 | `(fieldName: string) => Promise<FormValidationResult>` | - |
|
||||
| isFieldValid | 检查某个字段是否已通过校验 | `(fieldName: string)=>Promise<boolean>` | - |
|
||||
@@ -415,8 +431,9 @@ const submitting = formApi.form.useSelector((state) => state.meta.submitting);
|
||||
| actionLayout | 表单操作按钮位置 | `'newLine' \| 'rowEnd' \| 'inline'` | `rowEnd` |
|
||||
| actionPosition | 表单操作按钮对齐方式 | `'left' \| 'center' \| 'right'` | `right` |
|
||||
| handleReset | 表单重置回调 | `(values: Record<string, any>,) => Promise<void> \| void` | - |
|
||||
| handleSubmit | 表单提交回调 | `(values: TValues, rawValues: Readonly<TValues>) => Promise<void> \| void` | - |
|
||||
| handleValuesChange | 表单值变化回调 | `(rawValues: Readonly<TValues>, fieldsChanged: string[], getFormattedValues: () => TValues) => void` | - |
|
||||
| codec | 表单值与提交值的双向编解码器 | `FormCodec<TFormValues, TSubmitValues>` | - |
|
||||
| handleSubmit | 表单提交回调 | `(values: TSubmitValues, rawValues: Readonly<TFormValues>) => Promise<void> \| void` | - |
|
||||
| handleValuesChange | 表单值变化回调 | `(rawValues: Readonly<TFormValues>, fieldsChanged: string[], getFormattedValues: () => TSubmitValues) => void` | - |
|
||||
| handleCollapsedChange | 表单收起展开状态变化回调 | `(collapsed: boolean) => void` | - |
|
||||
| actionButtonsReverse | 调换操作按钮位置 | `boolean` | `false` |
|
||||
| resetButtonOptions | 重置按钮组件参数 | `ActionButtonOptions` | - |
|
||||
@@ -435,41 +452,15 @@ const submitting = formApi.form.useSelector((state) => state.meta.submitting);
|
||||
|
||||
::: tip handleValuesChange
|
||||
|
||||
`handleValuesChange` 的第一个参数是未经过 `valueFormat`、`fieldMappingTime` 或 array-to-string 转换的只读当前值,第二个参数是本次发生变化的 schema 字段名。第三个参数 `getFormattedValues` 是惰性函数:不调用就不会执行深拷贝和格式化,适合只在少数变化场景读取提交结构。字段映射生成的目标字段不会出现在 `fieldsChanged` 中。
|
||||
`handleValuesChange` 的第一个参数是未编码的只读 `TFormValues`,第二个参数是本次发生变化的 schema 字段名。第三个参数 `getFormattedValues` 是惰性函数:不调用就不会执行 codec 或旧格式化管道。
|
||||
|
||||
`getRawValues()` 和 `getValues()` 分别只生成一份目标快照;确实需要同时比较两种结构时再调用 `getValueSnapshot()`。`handleSubmit(values, rawValues)` 会在提交边界同时提供格式化结果和对应的原始快照。
|
||||
|
||||
:::
|
||||
|
||||
::: tip fieldMappingTime
|
||||
::: tip 旧格式化 API
|
||||
|
||||
此属性用于将表单内的数组值映射成 2 个字段,它应当传入一个数组,数组的每一项是一个映射规则,规则的第一个成员是一个字符串,表示需要映射的字段名,第二个成员是一个数组,表示映射后的字段名,第三个成员是一个可选的格式掩码,用于格式化日期时间字段;也可以提供一个格式化函数(参数分别为当前值和当前字段名,返回格式化后的值)。如果明确地将格式掩码设为null,则原值映射而不进行格式化(适用于非日期时间字段)。例如:`[['timeRange', ['startTime', 'endTime'], 'YYYY-MM-DD']]`,`timeRange`应当是一个至少具有2个成员的数组类型的值。Form会将`timeRange`的值前两个值分别按照格式掩码`YYYY-MM-DD`格式化后映射到`startTime`和`endTime`字段上。每一项的第三个参数是一个可选的格式掩码,
|
||||
|
||||
:::
|
||||
|
||||
::: tip valueFormat
|
||||
|
||||
`valueFormat` 适合处理“组件值”和“提交值”不一致的场景。例如:
|
||||
|
||||
- `RangePicker` 返回 `[dayjs, dayjs]`,但后端需要 `{ startTime, endTime }`
|
||||
- `DatePicker` 返回 `dayjs`,但后端只需要时间戳
|
||||
|
||||
`valueFormat` 会在 `getValues()` 过程中执行:
|
||||
|
||||
- 返回 `undefined`:当前字段保持删除状态
|
||||
- 返回其他值:回写当前字段
|
||||
- 调用 `setValue(key, nextValue)`:写入一个或多个新字段
|
||||
|
||||
```ts
|
||||
{
|
||||
component: 'RangePicker',
|
||||
fieldName: 'reportRange',
|
||||
valueFormat(value, setValue) {
|
||||
setValue('startTime', value?.[0]?.valueOf());
|
||||
setValue('endTime', value?.[1]?.valueOf());
|
||||
},
|
||||
}
|
||||
```
|
||||
`schema.valueFormat`、`fieldMappingTime` 和 `arrayToStringFields` 仍保持原运行时行为,但已经标记为 `@deprecated`,开发环境首次使用时会提示迁移。配置 codec 后只执行 codec;同时存在的旧配置会被忽略,避免重复转换。
|
||||
|
||||
:::
|
||||
|
||||
@@ -604,7 +595,7 @@ export interface FormSchema<
|
||||
rules?: FormSchemaRuleType;
|
||||
/** 后缀 */
|
||||
suffix?: CustomRenderType;
|
||||
/** 获取 getValues() 输出时格式化当前字段 */
|
||||
/** @deprecated 使用表单级 codec */
|
||||
valueFormat?: FormValueFormat;
|
||||
}
|
||||
```
|
||||
@@ -615,6 +606,8 @@ export interface FormSchema<
|
||||
|
||||
::: details FormValueFormat
|
||||
|
||||
`FormValueFormat` 是兼容类型,已标记为 `@deprecated`。新代码应使用 `FormCodec<TFormValues, TSubmitValues>`。
|
||||
|
||||
```ts
|
||||
type FormValueFormat = (
|
||||
value: any,
|
||||
|
||||
@@ -1,47 +1,80 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, nextTick, onMounted, ref, watch } from 'vue';
|
||||
import { computed, nextTick, onMounted, ref } from 'vue';
|
||||
|
||||
import { Button, Card, message, Space, Tag } from 'antdv-next';
|
||||
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
const transformedValues = ref<Record<string, any>>({});
|
||||
const liveValues = ref<Record<string, any>>({});
|
||||
interface ValueFormatFormValues {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
tags?: string[];
|
||||
}
|
||||
|
||||
function encodeValueFormatValues(values: Readonly<ValueFormatFormValues>) {
|
||||
return {
|
||||
fullName: [values.firstName, values.lastName].filter(Boolean).join(' '),
|
||||
tags: (values.tags ?? []).join(','),
|
||||
};
|
||||
}
|
||||
|
||||
type ValueFormatSubmitValues = ReturnType<typeof encodeValueFormatValues>;
|
||||
|
||||
function decodeValueFormatValues(
|
||||
values: Readonly<ValueFormatSubmitValues>,
|
||||
): ValueFormatFormValues {
|
||||
const [firstName = '', ...lastNameParts] = values.fullName
|
||||
.trim()
|
||||
.split(/\s+/);
|
||||
return {
|
||||
firstName,
|
||||
lastName: lastNameParts.join(' '),
|
||||
tags: values.tags ? values.tags.split(',') : [],
|
||||
};
|
||||
}
|
||||
|
||||
const transformedValues = ref<Partial<ValueFormatSubmitValues>>({});
|
||||
const liveValues = ref<Partial<ValueFormatFormValues>>({});
|
||||
|
||||
const [Form, formApi] = useVbenForm({
|
||||
codec: {
|
||||
decode: decodeValueFormatValues,
|
||||
encode: encodeValueFormatValues,
|
||||
},
|
||||
commonConfig: {
|
||||
componentProps: {
|
||||
class: 'w-full',
|
||||
},
|
||||
},
|
||||
handleSubmit,
|
||||
handleValuesChange,
|
||||
schema: [
|
||||
{
|
||||
component: 'RangePicker',
|
||||
fieldName: 'reportRange',
|
||||
help: '通过 setValue 拆分为 startTime / endTime,并移除原字段',
|
||||
label: '统计时间范围',
|
||||
valueFormat(value, setValue) {
|
||||
setValue('startTime', value?.[0]?.valueOf());
|
||||
setValue('endTime', value?.[1]?.valueOf());
|
||||
},
|
||||
},
|
||||
{
|
||||
component: 'DatePicker',
|
||||
fieldName: 'deadline',
|
||||
help: '直接 return 时间戳,保留原字段名',
|
||||
label: '截止时间',
|
||||
valueFormat(value) {
|
||||
return value?.valueOf();
|
||||
},
|
||||
component: 'Input',
|
||||
fieldName: 'firstName',
|
||||
help: '与姓氏一起编码为 fullName',
|
||||
label: '名字',
|
||||
},
|
||||
{
|
||||
component: 'Input',
|
||||
fieldName: 'lastName',
|
||||
help: '与名字一起编码为 fullName',
|
||||
label: '姓氏',
|
||||
},
|
||||
{
|
||||
component: 'Select',
|
||||
componentProps: {
|
||||
placeholder: '请输入关键字',
|
||||
mode: 'multiple',
|
||||
options: [
|
||||
{ label: '管理员', value: 'admin' },
|
||||
{ label: '审核员', value: 'reviewer' },
|
||||
{ label: '访客', value: 'guest' },
|
||||
],
|
||||
placeholder: '请选择标签',
|
||||
},
|
||||
fieldName: 'keyword',
|
||||
label: '关键字',
|
||||
fieldName: 'tags',
|
||||
help: '数组编码为逗号分隔字符串',
|
||||
label: '标签',
|
||||
},
|
||||
],
|
||||
wrapperClass: 'grid-cols-1 md:grid-cols-2',
|
||||
@@ -53,22 +86,8 @@ const transformedValuesPreview = computed(() => {
|
||||
return formatJsonPreview(transformedValues.value);
|
||||
});
|
||||
|
||||
function formatJsonPreview(value: Record<string, any>) {
|
||||
return JSON.stringify(
|
||||
value,
|
||||
(_key, currentValue) => {
|
||||
return isFormattableDateValue(currentValue)
|
||||
? currentValue.format('YYYY-MM-DD HH:mm:ss')
|
||||
: currentValue;
|
||||
},
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
function isFormattableDateValue(
|
||||
value: unknown,
|
||||
): value is { format: (template: string) => string } {
|
||||
return !!value && typeof value === 'object' && 'format' in value;
|
||||
function formatJsonPreview(value: unknown) {
|
||||
return JSON.stringify(value, null, 2);
|
||||
}
|
||||
|
||||
async function handleInspectValues() {
|
||||
@@ -76,44 +95,54 @@ async function handleInspectValues() {
|
||||
message.success('已刷新 getValues 输出');
|
||||
}
|
||||
|
||||
function handleSubmit(values: Record<string, any>) {
|
||||
async function handleSetSubmitValues() {
|
||||
await formApi.setSubmitValues({
|
||||
fullName: 'Ada Lovelace',
|
||||
tags: 'admin,reviewer',
|
||||
});
|
||||
await syncPreviewValues();
|
||||
message.success('已通过 codec.decode 回填提交值');
|
||||
}
|
||||
|
||||
function handleSubmit(values: ValueFormatSubmitValues) {
|
||||
transformedValues.value = values;
|
||||
message.success({
|
||||
content: `getValues output: ${JSON.stringify(values)}`,
|
||||
});
|
||||
}
|
||||
|
||||
async function syncPreviewValues(values?: Record<string, any>) {
|
||||
liveValues.value = values ?? formApi.form?.values ?? {};
|
||||
function handleValuesChange(
|
||||
values: Readonly<ValueFormatFormValues>,
|
||||
_fieldsChanged: string[],
|
||||
getFormattedValues: () => ValueFormatSubmitValues,
|
||||
) {
|
||||
liveValues.value = { ...values };
|
||||
transformedValues.value = getFormattedValues();
|
||||
}
|
||||
|
||||
async function syncPreviewValues(values?: Readonly<ValueFormatFormValues>) {
|
||||
liveValues.value = { ...(values ?? formApi.form?.values ?? {}) };
|
||||
transformedValues.value = await formApi.getValues();
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await nextTick();
|
||||
watch(
|
||||
() => formApi.form?.values,
|
||||
async (values) => {
|
||||
await syncPreviewValues(values);
|
||||
},
|
||||
{
|
||||
deep: true,
|
||||
immediate: true,
|
||||
},
|
||||
);
|
||||
await syncPreviewValues();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="space-y-4">
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<Tag color="processing">return 值:回写当前字段</Tag>
|
||||
<Tag color="success">setValue:拆分写入其他字段</Tag>
|
||||
<Tag color="warning">return undefined:保持原字段删除</Tag>
|
||||
<Tag color="processing">encode:生成完整提交值</Tag>
|
||||
<Tag color="success">decode:恢复完整表单值</Tag>
|
||||
<Tag color="warning">多字段转换原子执行</Tag>
|
||||
</div>
|
||||
|
||||
<Card title="valueFormat 示例">
|
||||
<Card title="Codec 示例">
|
||||
<template #extra>
|
||||
<Space wrap>
|
||||
<Button @click="handleSetSubmitValues">从提交值回填</Button>
|
||||
<Button type="primary" @click="handleInspectValues">
|
||||
查看 getValues 输出
|
||||
</Button>
|
||||
@@ -128,7 +157,7 @@ onMounted(async () => {
|
||||
liveValuesPreview
|
||||
}}</pre>
|
||||
</Card>
|
||||
<Card title="getValues / submit 输出(valueFormat 后)">
|
||||
<Card title="getValues / submit 输出(codec.encode 后)">
|
||||
<pre class="bg-muted overflow-auto rounded-md p-4 text-sm">{{
|
||||
transformedValuesPreview
|
||||
}}</pre>
|
||||
|
||||
@@ -207,7 +207,7 @@ Create the form through `useVbenForm`:
|
||||
|
||||
## Typed Values and Slots
|
||||
|
||||
Declare the value shape once with `useVbenForm<TValues>`. The same type flows through value APIs, callbacks, selectors, and field/default/action slots:
|
||||
Use `useVbenForm<TFormValues, TSubmitValues>` to declare component-facing form values and submission values separately. Schema, slots, selectors, and `setValues` use `TFormValues`; `getValues`, submit, and the first `handleSubmit` argument use `TSubmitValues`. Pass one generic when both shapes are identical.
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
@@ -254,30 +254,47 @@ async function fillForm() {
|
||||
|
||||
Named field slots expose `field`, `componentField`, `modelValue`, `name`, `disabled`, `isInValid`, `values`, and `formApi`. The default slot exposes `shapes`, `values`, and `formApi`; action slots expose `values` and `formApi`. Forms without an explicit `TValues` remain compatible with arbitrary slot names and broad props.
|
||||
|
||||
## Value Formatting
|
||||
## Form Codec
|
||||
|
||||
Use `schema.valueFormat` when the component value is convenient for the UI but the final payload returned by `getValues()` should use a different shape.
|
||||
Use the form-level `codec` when component values and the backend payload have different shapes. `encode` converts the complete `TFormValues` object to `TSubmitValues`; `decode` performs the inverse conversion. Multi-field splits and merges are atomic and do not depend on schema order or string-path writes.
|
||||
|
||||
- return a value to write back to the current field
|
||||
- call `setValue(key, nextValue)` to write derived fields
|
||||
- return `undefined` to keep the original field removed after decomposition
|
||||
Define `codec` directly in the `useVbenForm` options. Annotate only the form-value input of `encode`; `TSubmitValues` is inferred from its return object and flows into `decode`, `getValues()`, and submit callbacks:
|
||||
|
||||
```ts
|
||||
const [Form, formApi] = useVbenForm({
|
||||
codec: {
|
||||
decode(values) {
|
||||
return { period: [values.startTime, values.endTime] };
|
||||
},
|
||||
encode(values: Readonly<FormValues>) {
|
||||
return {
|
||||
endTime: values.period[1],
|
||||
startTime: values.period[0],
|
||||
};
|
||||
},
|
||||
},
|
||||
schema,
|
||||
});
|
||||
```
|
||||
|
||||
<DemoPreview dir="demos/vben-form/value-format" />
|
||||
|
||||
`schema.valueFormat`, `fieldMappingTime`, and `arrayToStringFields` remain runtime-compatible but are deprecated. When a codec is configured it takes precedence and deprecated transforms are ignored.
|
||||
|
||||
## Key API Notes
|
||||
|
||||
- `useVbenForm` returns `[Form, formApi]`
|
||||
- `useVbenForm<TValues>` propagates values through APIs, callbacks, schema callbacks, and slots
|
||||
- `useVbenForm<TFormValues, TSubmitValues>` keeps component values and submission values distinct
|
||||
- prefer `reset`, `submit`, `validateAndSubmit`, and `clearValidation`
|
||||
- `resetForm`, `submitForm`, `validateAndSubmitForm`, and `resetValidate` remain deprecated aliases that warn once in development
|
||||
- `clearValidation` invalidates in-flight async results before clearing errors
|
||||
- `formApi.getFieldComponentRef()` and `formApi.getFocusedField()` are available in current versions
|
||||
- `handleValuesChange(values, fieldsChanged)` receives readonly raw form state before `valueFormat`, `fieldMappingTime`, or array-to-string conversion
|
||||
- `handleValuesChange(values, fieldsChanged)` receives readonly `TFormValues` before codec or legacy formatting
|
||||
- its third `getFormattedValues` argument formats lazily, so raw-only change handlers avoid clone and transform work
|
||||
- `getRawValues()` returns only an independent raw snapshot, `getValues()` returns only the formatted payload, and `getValueSnapshot()` returns both
|
||||
- `handleSubmit(values, rawValues)` receives the formatted payload and its corresponding raw snapshot
|
||||
- `fieldMappingTime` and `scrollToFirstError` are part of the current form props
|
||||
- `schema.valueFormat` lets `getValues()` transform UI values into backend-friendly payloads
|
||||
- `fieldMappingTime`, `arrayToStringFields`, and `schema.valueFormat` are deprecated compatibility options
|
||||
- `codec.encode` defines the `getValues()` payload and `codec.decode` powers complete `setSubmitValues()` fills
|
||||
- `formApi.form` is the stable `FormContextApi`; raw TanStack generics are intentionally not exposed
|
||||
- prefer `formApi.form.useFieldValue`, `useFieldValues`, and `useFieldError` for fine-grained subscriptions; use `useValues` only when the whole form is required
|
||||
- `useSelector` remains the compatibility selector for combined `{ values, errors, meta }` state
|
||||
|
||||
@@ -23,7 +23,7 @@ The following Vben APIs remain supported:
|
||||
|
||||
- `useVbenForm(options)` returning `[Form, formApi]`
|
||||
- existing `FormApi` methods for values, reset, validation, submission, schema updates, and component refs
|
||||
- existing `FormSchema` fields, dependencies, `valueFormat`, and array schema structure
|
||||
- existing `FormSchema` fields, dependencies, deprecated `valueFormat`, and array schema structure
|
||||
- application adapters and the re-exported `z` namespace
|
||||
- the existing `componentField` slot and binding shape
|
||||
|
||||
@@ -42,9 +42,9 @@ New code uses `reset`, `submit`, `validateAndSubmit`, and `clearValidation`. The
|
||||
| `useFieldValue(fieldName)` | `FormContextApi` | Subscribes to one field value without reacting to unrelated fields. |
|
||||
| `useFieldValues(fieldNames)` | `FormContextApi` | Subscribes to a declared group of field values. |
|
||||
| `useFieldError(fieldName)` | `FormContextApi` | Subscribes to one field error without consuming the full error object. |
|
||||
| `getRawValues()` | `FormApi` | Returns an independent raw snapshot before field mapping and `valueFormat`. |
|
||||
| `getRawValues()` | `FormApi` | Returns an independent form-value snapshot before codec or legacy formatting. |
|
||||
| `formatValues(rawValues)` | `FormApi` | Runs the unified formatting pipeline on a supplied raw snapshot. |
|
||||
| `getValueSnapshot()` | `FormApi` | Returns `{ rawValues, values }`, where `values` is the formatted payload. |
|
||||
| `getValueSnapshot()` | `FormApi` | Returns `{ rawValues, values }`, where `values` is `TSubmitValues`. |
|
||||
| `asyncDebounceMs` | `FormFieldOptions` | Configures TanStack Field async validation debounce. |
|
||||
| `changeEventFallback` | `FormCommonConfig` / adapter config | Enables fallback for legacy components that emit `change` without `update:*`; defaults to `false`. |
|
||||
|
||||
@@ -60,7 +60,7 @@ New code uses `reset`, `submit`, `validateAndSubmit`, and `clearValidation`. The
|
||||
| Change-event compatibility | `disabledOnChangeListener: false` enabled fallback | `changeEventFallback: true` enables fallback with positive semantics. |
|
||||
| Top-level render callbacks | `componentProps(values, actions, ctx)`, `help(values, actions, ctx)`, `renderComponentContent(values, actions, ctx)` | Receive only lightweight `FormSchemaContext`; value-dependent behavior moves to `dependencies.resolve`. |
|
||||
| `validateAndSubmit()` | Repeated low-level validation/scroll handling and could validate again during submit | Delegates to canonical `validate()` and shared submission logic; invalid forms do not submit. |
|
||||
| `getValues()` | Implicitly returned transformed values | Still returns the formatted payload; use `getRawValues()` for raw state. |
|
||||
| `getValues()` | Implicitly returned transformed values | Returns codec-encoded `TSubmitValues`; without a codec it preserves legacy formatting. |
|
||||
|
||||
### Removed APIs
|
||||
|
||||
@@ -86,7 +86,7 @@ New code uses `reset`, `submit`, `validateAndSubmit`, and `clearValidation`. The
|
||||
- Field components use fine-grained value/error selectors; full error aggregation is no longer on the normal input path.
|
||||
- Async validators discard stale Promises through a Vben generation without reading private TanStack AbortController or meta fields.
|
||||
- New and legacy dependencies share one atomic executor, so stale async results cannot overwrite newer state.
|
||||
- Formatting runs in a fixed array-to-string, range mapping, schema `valueFormat` order and performs one deep clone per formatted snapshot.
|
||||
- New code uses one form-level codec to encode the complete object atomically; legacy array-to-string, range mapping, and schema `valueFormat` remain compatible but deprecated.
|
||||
|
||||
## Typed Values and Slots
|
||||
|
||||
@@ -198,7 +198,7 @@ Required markers are derived from whether the schema accepts `undefined`.
|
||||
|
||||
Do not read `_def`, `_zod.def`, or `typeName`. Use public `.unwrap()` APIs and public pipe inputs. Delegate intersection defaults to the Zod 4-compatible `zod-defaults` package.
|
||||
|
||||
Standard Schema validation does not write transform/coerce output back into TanStack Form state. Keep using `valueFormat` for submission payload conversion, or explicitly call `parseAsync` at the submission boundary when transformed schema output is required.
|
||||
Standard Schema validation does not write transform/coerce output back into TanStack Form state. Use the form-level codec for submission payload conversion, or explicitly call `parseAsync` inside the codec `encode` boundary when transformed schema output is required.
|
||||
|
||||
Also review these changes:
|
||||
|
||||
@@ -220,7 +220,7 @@ The shadcn form primitives now use a Vben-owned field context. Labels, controls,
|
||||
|
||||
`dependencies.resolve(context)` is the recommended API: it evaluates once and atomically commits one dynamic-state patch, while stale async results are discarded as a unit. Legacy `if/show/disabled/required/rules/componentProps/trigger` callbacks remain supported through the same normalized executor, but are marked `@deprecated` and emit one development warning. Both APIs react only to declared `triggerFields`.
|
||||
|
||||
`handleValuesChange(rawValues, fieldsChanged, getFormattedValues)` receives readonly raw values and formats only when its third argument is called. `getRawValues()` and `getValues()` each create only the requested snapshot; use `getValueSnapshot()` when both are required. `handleSubmit(values, rawValues)` receives both forms at submission. The formatter performs one deep clone, then applies array-to-string, range mapping, and schema `valueFormat` in order. Array fields keep using TanStack push/remove operations and stable row identity.
|
||||
`handleValuesChange(rawValues, fieldsChanged, getFormattedValues)` receives readonly `TFormValues` and runs codec or legacy formatting only when its third argument is called. `getRawValues()` returns form values and `getValues()` returns `TSubmitValues`; use `getValueSnapshot()` when both are required. `handleSubmit(values, rawValues)` receives both forms at submission. Legacy array-to-string, range mapping, and schema `valueFormat` remain compatible but deprecated. Array fields keep using TanStack push/remove operations and stable row identity.
|
||||
|
||||
## Test and Acceptance Matrix
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ outline: deep
|
||||
|
||||
- `useVbenForm(options)` 仍返回 `[Form, formApi]`
|
||||
- `FormApi` 的值、校验、提交、重置、schema 更新和组件引用能力
|
||||
- `FormSchema` 的 `fieldName`、`component`、`componentProps`、`rules`、`dependencies`、`defaultValue`、`valueFormat` 和数组字段结构
|
||||
- `FormSchema` 的 `fieldName`、`component`、`componentProps`、`rules`、`dependencies`、`defaultValue`、已弃用的 `valueFormat` 和数组字段结构
|
||||
- `dependencies.triggerFields` 与回调参数
|
||||
- 组件适配器和 `z` 重导出路径
|
||||
- 自定义 slot 中原有的 `componentField` 绑定对象
|
||||
@@ -43,9 +43,9 @@ outline: deep
|
||||
| `useFieldValue(fieldName)` | `FormContextApi` | 订阅单字段值,避免无关字段变化触发组件更新。 |
|
||||
| `useFieldValues(fieldNames)` | `FormContextApi` | 订阅一组字段值,主要用于声明式依赖计算。 |
|
||||
| `useFieldError(fieldName)` | `FormContextApi` | 订阅单字段错误,不再依赖全量错误对象。 |
|
||||
| `getRawValues()` | `FormApi` | 返回未执行字段映射与 `valueFormat` 的独立原始值快照。 |
|
||||
| `getRawValues()` | `FormApi` | 返回未执行 codec 或旧格式化管道的独立表单值快照。 |
|
||||
| `formatValues(rawValues)` | `FormApi` | 对指定原始值执行统一格式化流水线。 |
|
||||
| `getValueSnapshot()` | `FormApi` | 同时返回 `{ rawValues, values }`,其中 `values` 为格式化结果。 |
|
||||
| `getValueSnapshot()` | `FormApi` | 同时返回 `{ rawValues, values }`,其中 `values` 为 `TSubmitValues`。 |
|
||||
| `asyncDebounceMs` | `FormFieldOptions` | 设置 TanStack Field 异步校验防抖时间。 |
|
||||
| `changeEventFallback` | `FormCommonConfig` / adapter config | 为只发送 `change`、不发送 `update:*` 的旧组件启用事件回退,默认 `false`。 |
|
||||
|
||||
@@ -61,7 +61,7 @@ outline: deep
|
||||
| change 事件兼容 | `disabledOnChangeListener: false` 表示启用 | `changeEventFallback: true` 表示启用,改为正向语义。 |
|
||||
| 顶层动态渲染回调 | `componentProps(values, actions, ctx)`、`help(values, actions, ctx)`、`renderComponentContent(values, actions, ctx)` | 仅接收轻量 `FormSchemaContext`。依赖表单值的动态逻辑迁移到 `dependencies.resolve`。 |
|
||||
| `validateAndSubmit()` | 自行调用底层校验并重复实现错误滚动,提交阶段可能再次校验 | 委托统一 `validate()` 与共享提交逻辑,无效时不提交,错误滚动只有一个实现。 |
|
||||
| `getValues()` | 隐式完成所有字段转换 | 语义保持为“返回格式化值”;需要原始值时显式使用 `getRawValues()`。 |
|
||||
| `getValues()` | 隐式完成所有字段转换 | 返回 codec 编码后的 `TSubmitValues`;无 codec 时保持旧格式化行为。 |
|
||||
|
||||
### 删除的 API
|
||||
|
||||
@@ -87,7 +87,7 @@ outline: deep
|
||||
- 字段组件改用细粒度 value/error selector;全量错误聚合退出普通输入热路径。
|
||||
- async validator 通过 Vben generation 丢弃过期 Promise,不读取 TanStack 私有 AbortController 或 meta 字段。
|
||||
- dependencies 新旧语法共用一个原子执行器,异步旧结果不会覆盖新状态。
|
||||
- 值格式化按 array-to-string、时间范围映射、schema `valueFormat` 的固定顺序执行,并且每次格式化只深拷贝一次。
|
||||
- 新代码使用表单级 codec 原子编码完整对象;旧 array-to-string、时间范围映射和 schema `valueFormat` 继续兼容但已弃用。
|
||||
|
||||
## 值类型与插槽类型
|
||||
|
||||
@@ -212,7 +212,7 @@ Vben 表单按以下优先级生成初值:
|
||||
|
||||
不要读取 `_def`、`_zod.def` 或 `typeName`。公共包装器使用 `.unwrap()`;Zod 4 的 transform/pipe 使用公开的输入 schema。intersection 的默认值交给支持 Zod 4 的 `zod-defaults` 处理。
|
||||
|
||||
TanStack Form 使用 Standard Schema 校验时不会自动把 transform/coerce 的输出写回当前表单 state。提交 payload 需要转换时,继续使用 `valueFormat`;如果必须提交 schema transform 后的结果,应在提交边界显式调用 `parseAsync`。
|
||||
TanStack Form 使用 Standard Schema 校验时不会自动把 transform/coerce 的输出写回当前表单 state。提交 payload 需要转换时,使用表单级 codec;如果必须提交 schema transform 后的结果,应在 codec 的 `encode` 边界显式调用 `parseAsync`。
|
||||
|
||||
### 其他需要复核的 API
|
||||
|
||||
@@ -245,7 +245,7 @@ shadcn form primitive 使用 Vben 自有字段上下文,不再注入 vee 的 `
|
||||
|
||||
`dependencies.resolve(context)` 是推荐语法:一次求值并原子提交完整动态 patch,过期异步结果整体丢弃。旧的 `if/show/disabled/required/rules/componentProps/trigger` 语法仍兼容,但已标记为 `@deprecated` 并在开发环境首次使用时提示迁移;内部仍归一到同一个执行器。两种语法都只根据 `triggerFields` 重算,无关字段变化不会执行回调。
|
||||
|
||||
`handleValuesChange(rawValues, fieldsChanged, getFormattedValues)` 接收未格式化的只读当前值,第三个参数仅在调用时执行格式化。`getRawValues()` 和 `getValues()` 分别只生成原始或格式化快照;需要同时比较时使用 `getValueSnapshot()`。提交回调通过 `handleSubmit(values, rawValues)` 同时取得两种结构。格式化流水线只深拷贝一次,并按 array-to-string、时间范围映射、schema `valueFormat` 的顺序执行。数组字段继续使用 TanStack push/remove 操作和稳定行身份。
|
||||
`handleValuesChange(rawValues, fieldsChanged, getFormattedValues)` 接收只读 `TFormValues`,第三个参数仅在调用时执行 codec 或旧格式化管道。`getRawValues()` 返回表单值,`getValues()` 返回 `TSubmitValues`;需要同时比较时使用 `getValueSnapshot()`。提交回调通过 `handleSubmit(values, rawValues)` 同时取得两种结构。旧 array-to-string、时间范围映射和 schema `valueFormat` 继续兼容但已弃用。数组字段继续使用 TanStack push/remove 操作和稳定行身份。
|
||||
|
||||
## 测试与验收
|
||||
|
||||
|
||||
Reference in New Issue
Block a user