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:
dream-weave
2026-07-23 15:09:31 +08:00
committed by GitHub
parent f43de252b5
commit 98dea92225
43 changed files with 1678 additions and 413 deletions

View File

@@ -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>;

View File

@@ -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,

View File

@@ -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>

View File

@@ -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

View File

@@ -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

View File

@@ -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 操作和稳定行身份。
## 测试与验收