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:
@@ -6,6 +6,10 @@ outline: deep
|
||||
|
||||
`Vben Form` is the shared form abstraction used across different UI-library variants such as `Ant Design Vue`, `Element Plus`, `Naive UI`, and other adapters added inside this repository.
|
||||
|
||||
It uses [TanStack Form](https://tanstack.com/form/latest/docs/framework/vue/overview) internally for state and validation lifecycles, with [Zod 4](https://zod.dev/v4) schemas. Application code should continue using `useVbenForm`, `FormApi`, and the adapter layer instead of depending on the raw TanStack instance.
|
||||
|
||||
Read the [Zod 4 and TanStack Form migration guide](/en/guide/in-depth/zod-v4-form-migration) before upgrading an existing project.
|
||||
|
||||
> If some details are not obvious from the docs, check the live demos as well.
|
||||
|
||||
## Adapter Setup
|
||||
@@ -23,8 +27,9 @@ The current adapter pattern is:
|
||||
|
||||
```ts
|
||||
import type {
|
||||
FormValues,
|
||||
VbenFormProps as FormProps,
|
||||
VbenFormSchema as FormSchema,
|
||||
VbenFormProps,
|
||||
} from '@vben/common-ui';
|
||||
|
||||
import type { ComponentType } from './component';
|
||||
@@ -46,7 +51,7 @@ setupVbenForm<ComponentType>({
|
||||
Upload: 'fileList',
|
||||
},
|
||||
},
|
||||
defineRules: {
|
||||
rules: {
|
||||
required: (value, _params, ctx) => {
|
||||
if (value === undefined || value === null || value.length === 0) {
|
||||
return $t('ui.formRules.required', [ctx.label]);
|
||||
@@ -62,11 +67,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
|
||||
>;
|
||||
```
|
||||
|
||||
### Component Adapter Example
|
||||
@@ -191,6 +205,55 @@ Create the form through `useVbenForm`:
|
||||
|
||||
<DemoPreview dir="demos/vben-form/basic" />
|
||||
|
||||
## 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:
|
||||
|
||||
```vue
|
||||
<script setup lang="ts">
|
||||
import { useVbenForm } from '#/adapter/form';
|
||||
|
||||
interface AccountFormValues {
|
||||
email: string;
|
||||
nickname: string;
|
||||
}
|
||||
|
||||
const [Form, formApi] = useVbenForm<AccountFormValues>({
|
||||
handleSubmit(values) {
|
||||
return addAccount(values); // AccountFormValues
|
||||
},
|
||||
schema: [
|
||||
{ component: 'Input', fieldName: 'email', label: 'Email' },
|
||||
{ component: 'Input', fieldName: 'nickname', label: 'Nickname' },
|
||||
],
|
||||
});
|
||||
|
||||
async function fillForm() {
|
||||
await formApi.setValues({ email: 'user@example.com' });
|
||||
return formApi.getValues(); // Promise<AccountFormValues>
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Form>
|
||||
<template #email="{ componentField, field, formApi, values }">
|
||||
<!-- field.state.value and componentField.modelValue are strings -->
|
||||
<input v-bind="componentField" :data-email="values.email" />
|
||||
<button type="button" @click="formApi.clearValidation('email')">
|
||||
Clear
|
||||
</button>
|
||||
</template>
|
||||
<template #default="{ formApi, shapes, values }">
|
||||
<button type="button" @click="formApi.submit()">
|
||||
Submit {{ shapes.length }} fields for {{ values.email }}
|
||||
</button>
|
||||
</template>
|
||||
</Form>
|
||||
</template>
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
Use `schema.valueFormat` when the component value is convenient for the UI but the final payload returned by `getValues()` should use a different shape.
|
||||
@@ -204,10 +267,25 @@ Use `schema.valueFormat` when the component value is convenient for the UI but t
|
||||
## Key API Notes
|
||||
|
||||
- `useVbenForm` returns `[Form, formApi]`
|
||||
- `useVbenForm<TValues>` propagates values through APIs, callbacks, schema callbacks, and slots
|
||||
- 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)` includes the second parameter in newer versions
|
||||
- `handleValuesChange(values, fieldsChanged)` receives readonly raw form state before `valueFormat`, `fieldMappingTime`, or array-to-string conversion
|
||||
- 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
|
||||
- `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
|
||||
- legacy `setupVbenForm({ defineRules })` still works, warns once in development, and is silent in production; use `rules` for new code
|
||||
- prefer `dependencies: { triggerFields, resolve(context) }` for one atomic dynamic-state patch; legacy dependency callbacks remain supported but are deprecated and warn once in development
|
||||
- top-level `componentProps`, `help`, and `renderComponentContent` functions receive `FormSchemaContext`; value-dependent rendering belongs in `dependencies.resolve`
|
||||
- use `formFieldProps.validateOn` with `blur` and/or `change`; submit always validates, and `asyncDebounceMs` debounces async validators
|
||||
- use `changeEventFallback: true` only for components that emit `change` without an `update:*` event
|
||||
|
||||
## Reference
|
||||
|
||||
|
||||
Reference in New Issue
Block a user