9.7 KiB
| outline |
|---|
| deep |
Vben Form
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 internally for state and validation lifecycles, with Zod 4 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 before upgrading an existing project.
If some details are not obvious from the docs, check the live demos as well.
Adapter Setup
Each app keeps its own adapter layer under src/adapter/form.ts and src/adapter/component/index.ts.
The current adapter pattern is:
- initialize the shared component adapter first
- call
setupVbenForm(...) - map special
v-model:*prop names throughmodelPropNameMap - keep the form empty state aligned with the actual UI library behavior
Form Adapter Example
import type {
FormValues,
VbenFormProps as FormProps,
VbenFormSchema as FormSchema,
} from '@vben/common-ui';
import type { ComponentType } from './component';
import { setupVbenForm, useVbenForm as useForm, z } from '@vben/common-ui';
import { $t } from '@vben/locales';
import { initComponentAdapter } from './component';
initComponentAdapter();
setupVbenForm<ComponentType>({
config: {
baseModelPropName: 'value',
emptyStateValue: null,
modelPropNameMap: {
Checkbox: 'checked',
Radio: 'checked',
Switch: 'checked',
Upload: 'fileList',
},
},
rules: {
required: (value, _params, ctx) => {
if (value === undefined || value === null || value.length === 0) {
return $t('ui.formRules.required', [ctx.label]);
}
return true;
},
selectRequired: (value, _params, ctx) => {
if (value === undefined || value === null) {
return $t('ui.formRules.selectRequired', [ctx.label]);
}
return true;
},
},
});
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<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
import type { Component, SetupContext } from 'vue';
import type { BaseFormComponentType } from '@vben/common-ui';
import { h } from 'vue';
import { globalShareState } from '@vben/common-ui';
import { $t } from '@vben/locales';
import {
AutoComplete,
Button,
Checkbox,
CheckboxGroup,
DatePicker,
Divider,
Input,
InputNumber,
InputPassword,
Mentions,
notification,
Radio,
RadioGroup,
RangePicker,
Rate,
Select,
Space,
Switch,
Textarea,
TimePicker,
TreeSelect,
Upload,
} from 'antdv-next';
const withDefaultPlaceholder = <T extends Component>(
component: T,
type: 'input' | 'select',
) => {
return (props: any, { attrs, slots }: Omit<SetupContext, 'expose'>) => {
const placeholder = props?.placeholder || $t(`ui.placeholder.${type}`);
return h(component, { ...props, ...attrs, placeholder }, slots);
};
};
export type ComponentType =
| 'AutoComplete'
| 'Checkbox'
| 'CheckboxGroup'
| 'DatePicker'
| 'DefaultButton'
| 'Divider'
| 'Input'
| 'InputNumber'
| 'InputPassword'
| 'Mentions'
| 'PrimaryButton'
| 'Radio'
| 'RadioGroup'
| 'RangePicker'
| 'Rate'
| 'Select'
| 'Space'
| 'Switch'
| 'Textarea'
| 'TimePicker'
| 'TreeSelect'
| 'Upload'
| BaseFormComponentType;
async function initComponentAdapter() {
const components: Partial<Record<ComponentType, Component>> = {
AutoComplete,
Checkbox,
CheckboxGroup,
DatePicker,
DefaultButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'default' }, slots);
},
Divider,
Input: withDefaultPlaceholder(Input, 'input'),
InputNumber: withDefaultPlaceholder(InputNumber, 'input'),
InputPassword: withDefaultPlaceholder(InputPassword, 'input'),
Mentions: withDefaultPlaceholder(Mentions, 'input'),
PrimaryButton: (props, { attrs, slots }) => {
return h(Button, { ...props, attrs, type: 'primary' }, slots);
},
Radio,
RadioGroup,
RangePicker,
Rate,
Select: withDefaultPlaceholder(Select, 'select'),
Space,
Switch,
Textarea: withDefaultPlaceholder(Textarea, 'input'),
TimePicker,
TreeSelect: withDefaultPlaceholder(TreeSelect, 'select'),
Upload,
};
globalShareState.setComponents(components);
globalShareState.defineMessage({
copyPreferencesSuccess: (title, content) => {
notification.success({
description: content,
message: title,
placement: 'bottomRight',
});
},
});
}
export { initComponentAdapter };
Basic Usage
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:
<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.
- return a value to write back to the current field
- call
setValue(key, nextValue)to write derived fields - return
undefinedto keep the original field removed after decomposition
Key API Notes
useVbenFormreturns[Form, formApi]useVbenForm<TValues>propagates values through APIs, callbacks, schema callbacks, and slots- prefer
reset,submit,validateAndSubmit, andclearValidation resetForm,submitForm,validateAndSubmitForm, andresetValidateremain deprecated aliases that warn once in developmentclearValidationinvalidates in-flight async results before clearing errorsformApi.getFieldComponentRef()andformApi.getFocusedField()are available in current versionshandleValuesChange(values, fieldsChanged)receives readonly raw form state beforevalueFormat,fieldMappingTime, or array-to-string conversion- its third
getFormattedValuesargument 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, andgetValueSnapshot()returns bothhandleSubmit(values, rawValues)receives the formatted payload and its corresponding raw snapshotfieldMappingTimeandscrollToFirstErrorare part of the current form propsschema.valueFormatletsgetValues()transform UI values into backend-friendly payloadsformApi.formis the stableFormContextApi; raw TanStack generics are intentionally not exposed- prefer
formApi.form.useFieldValue,useFieldValues, anduseFieldErrorfor fine-grained subscriptions; useuseValuesonly when the whole form is required useSelectorremains the compatibility selector for combined{ values, errors, meta }state- legacy
setupVbenForm({ defineRules })still works, warns once in development, and is silent in production; userulesfor 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, andrenderComponentContentfunctions receiveFormSchemaContext; value-dependent rendering belongs independencies.resolve - use
formFieldProps.validateOnwithblurand/orchange; submit always validates, andasyncDebounceMsdebounces async validators - use
changeEventFallback: trueonly for components that emitchangewithout anupdate:*event
Reference
For the complete Chinese API tables and more examples, see the Chinese component page if you need the full parameter matrix.