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.
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
ts
import type {
VbenFormSchema as FormSchema,
VbenFormProps,
} 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',
},
},
defineRules: {
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;
},
},
});
const useVbenForm = useForm<ComponentType>;
export { useVbenForm, z };
export type VbenFormSchema = FormSchema<ComponentType>;
export type { VbenFormProps };Component Adapter Example
ts
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 'ant-design-vue';
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:
vue
<script lang="ts" setup>
import { message } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
const [BaseForm] = useVbenForm({
// 所有表单项共用,可单独在表单内覆盖
commonConfig: {
// 所有表单项
componentProps: {
class: 'w-full',
},
},
// 提交函数
handleSubmit: onSubmit,
// 垂直布局,label和input在不同行,值为vertical
// 水平布局,label和input在同一行
layout: 'horizontal',
schema: [
{
// 组件需要在 #/adapter.ts内注册,并加上类型
component: 'Input',
// 对应组件的参数
componentProps: {
placeholder: '请输入用户名',
},
// 字段名
fieldName: 'username',
// 界面显示的label
label: '字符串',
},
{
component: 'InputPassword',
componentProps: {
placeholder: '请输入密码',
},
fieldName: 'password',
label: '密码',
},
{
component: 'InputNumber',
componentProps: {
placeholder: '请输入',
},
fieldName: 'number',
label: '数字(带后缀)',
suffix: () => '¥',
},
{
component: 'Select',
componentProps: {
allowClear: true,
filterOption: true,
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
placeholder: '请选择',
showSearch: true,
},
fieldName: 'options',
label: '下拉选',
},
{
component: 'RadioGroup',
componentProps: {
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
fieldName: 'radioGroup',
label: '单选组',
},
{
component: 'Radio',
fieldName: 'radio',
label: '',
renderComponentContent: () => {
return {
default: () => ['Radio'],
};
},
},
{
component: 'CheckboxGroup',
componentProps: {
name: 'cname',
options: [
{
label: '选项1',
value: '1',
},
{
label: '选项2',
value: '2',
},
],
},
fieldName: 'checkboxGroup',
label: '多选组',
},
{
component: 'Checkbox',
fieldName: 'checkbox',
label: '',
renderComponentContent: () => {
return {
default: () => ['我已阅读并同意'],
};
},
},
{
component: 'Mentions',
componentProps: {
options: [
{
label: 'afc163',
value: 'afc163',
},
{
label: 'zombieJ',
value: 'zombieJ',
},
],
placeholder: '请输入',
},
fieldName: 'mentions',
label: '提及',
},
{
component: 'Rate',
fieldName: 'rate',
label: '评分',
},
{
component: 'Switch',
componentProps: {
class: 'w-auto',
},
fieldName: 'switch',
label: '开关',
},
{
component: 'DatePicker',
fieldName: 'datePicker',
label: '日期选择框',
},
{
component: 'RangePicker',
fieldName: 'rangePicker',
label: '范围选择器',
},
{
component: 'TimePicker',
fieldName: 'timePicker',
label: '时间选择框',
},
{
component: 'TreeSelect',
componentProps: {
allowClear: true,
placeholder: '请选择',
showSearch: true,
treeData: [
{
label: 'root 1',
value: 'root 1',
children: [
{
label: 'parent 1',
value: 'parent 1',
children: [
{
label: 'parent 1-0',
value: 'parent 1-0',
children: [
{
label: 'my leaf',
value: 'leaf1',
},
{
label: 'your leaf',
value: 'leaf2',
},
],
},
{
label: 'parent 1-1',
value: 'parent 1-1',
},
],
},
{
label: 'parent 2',
value: 'parent 2',
},
],
},
],
treeNodeFilterProp: 'label',
},
fieldName: 'treeSelect',
label: '树选择',
},
],
wrapperClass: 'grid-cols-1',
});
function onSubmit(values: Record<string, any>) {
message.success({
content: `form values: ${JSON.stringify(values)}`,
});
}
</script>
<template>
<BaseForm />
</template>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
vue
<script lang="ts" setup>
import { computed, nextTick, onMounted, ref, watch } from 'vue';
import { Button, Card, message, Space, Tag } from 'ant-design-vue';
import { useVbenForm } from '#/adapter/form';
const transformedValues = ref<Record<string, any>>({});
const liveValues = ref<Record<string, any>>({});
const [Form, formApi] = useVbenForm({
commonConfig: {
componentProps: {
class: 'w-full',
},
},
handleSubmit,
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',
componentProps: {
placeholder: '请输入关键字',
},
fieldName: 'keyword',
label: '关键字',
},
],
wrapperClass: 'grid-cols-1 md:grid-cols-2',
});
const liveValuesPreview = computed(() => formatJsonPreview(liveValues.value));
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;
}
async function handleInspectValues() {
await syncPreviewValues();
message.success('已刷新 getValues 输出');
}
function handleSubmit(values: Record<string, any>) {
transformedValues.value = values;
message.success({
content: `getValues output: ${JSON.stringify(values)}`,
});
}
async function syncPreviewValues(values?: Record<string, any>) {
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,
},
);
});
</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>
</div>
<Card title="valueFormat 示例">
<template #extra>
<Space wrap>
<Button type="primary" @click="handleInspectValues">
查看 getValues 输出
</Button>
</Space>
</template>
<Form />
</Card>
<div class="grid gap-4 lg:grid-cols-2">
<Card title="原始 form.values(组件值)">
<pre class="bg-muted overflow-auto rounded-md p-4 text-sm">{{
liveValuesPreview
}}</pre>
</Card>
<Card title="getValues / submit 输出(valueFormat 后)">
<pre class="bg-muted overflow-auto rounded-md p-4 text-sm">{{
transformedValuesPreview
}}</pre>
</Card>
</div>
</div>
</template>Key API Notes
useVbenFormreturns[Form, formApi]formApi.getFieldComponentRef()andformApi.getFocusedField()are available in current versionshandleValuesChange(values, fieldsChanged)includes the second parameter in newer versionsfieldMappingTimeandscrollToFirstErrorare part of the current form propsschema.valueFormatletsgetValues()transform UI values into backend-friendly payloads
Reference
For the complete Chinese API tables and more examples, see the Chinese component page if you need the full parameter matrix.

vben
Li Kui