useForm
useForm 为 ElForm 提供了声明式配置能力。通过 items 数组即可描述表单结构,并配合控制器完成模型更新与实例访问,显著减少模板层重复代码。
在保留原始表单能力(校验、重置、清空校验等)的同时,useForm 更适合在中后台页面中快速构建可维护的表单模块。
典型表单
vue
<script setup lang="ts">
import { useForm } from 'element-hooks';
import {
ElInput,
ElSelect,
ElSwitch,
ElCheckboxGroup,
ElRadioGroup,
} from 'element-plus';
const [Form] = useForm({
labelWidth: 'auto',
model: {
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: '',
},
items: [
{
label: 'Activity name',
prop: 'name',
render: { component: ElInput },
},
{
label: 'Activity zone',
prop: 'region',
render: {
component: ElSelect,
props: {
placeholder: 'please select your zone',
options: [
{ label: 'Zone one', value: 'shanghai' },
{ label: 'Zone two', value: 'beijing' },
],
},
},
},
{
label: 'Activity time',
slot: 'activityTime',
},
{
label: 'Instant delivery',
prop: 'delivery',
render: { component: ElSwitch },
},
{
label: 'Activity type',
prop: 'type',
render: {
component: ElCheckboxGroup,
props: {
options: [
{
label: 'Online activities',
value: 'Online activities',
name: 'type',
},
{
label: 'Promotion activities',
value: 'Promotion activities',
name: 'type',
},
{
label: 'Offline activities',
value: 'Offline activities',
name: 'type',
},
{
label: 'Simple brand exposure',
value: 'Simple brand exposure',
name: 'type',
},
],
},
},
},
{
label: 'Resources',
prop: 'resource',
render: {
component: ElRadioGroup,
props: {
options: [
{ label: 'Sponsor', value: 'Sponsor' },
{ label: 'Venue', value: 'Venue' },
],
},
},
},
{
label: 'Activity form',
prop: 'desc',
render: { component: ElInput, props: { type: 'textarea' } },
},
{
slot: 'buttons',
},
],
});
const onSubmit = () => {
console.log('submit!');
};
</script>
<template>
<Form style="max-width: 600px">
<template #activityTime="{ model }">
<el-col :span="11">
<el-date-picker
v-model="model.date1"
type="date"
placeholder="Pick a date"
style="width: 100%"
/>
</el-col>
<el-col :span="2" class="text-center">
<span class="text-gray-500">-</span>
</el-col>
<el-col :span="11">
<el-time-picker
v-model="model.date2"
placeholder="Pick a time"
style="width: 100%"
/>
</el-col>
</template>
<template #buttons>
<el-button type="primary" @click="onSubmit">Create</el-button>
<el-button>Cancel</el-button>
</template>
</Form>
</template>行内表单
vue
<script setup lang="ts">
import { useForm } from 'element-hooks';
import { ElInput, ElSelect, ElDatePicker } from 'element-plus';
const [Form] = useForm({
inline: true,
model: {
user: '',
region: '',
date: '',
},
items: [
{
label: 'Approved by',
prop: 'user',
render: {
component: ElInput,
props: {
placeholder: 'Approved by',
clearable: true,
},
},
},
{
label: 'Activity zone',
prop: 'region',
render: {
component: ElSelect,
props: {
placeholder: 'Activity zone',
clearable: true,
options: [
{ label: 'Zone one', value: 'shanghai' },
{ label: 'Zone two', value: 'beijing' },
],
},
},
},
{
label: 'Activity time',
prop: 'date',
render: {
component: ElDatePicker,
props: {
type: 'date',
placeholder: 'Pick a date',
clearable: true,
},
},
},
{
slot: 'operations',
},
],
});
const onSubmit = () => {
console.log('submit!');
};
</script>
<template>
<Form class="demo-form-inline">
<template #operations>
<el-button type="primary" @click="onSubmit">Query</el-button>
</template>
</Form>
</template>
<style>
.demo-form-inline .el-input {
--el-input-width: 190px;
}
.demo-form-inline .el-select {
--el-select-width: 190px;
}
</style>对齐方式
vue
<script setup lang="ts">
import { ref } from 'vue';
import { useForm } from 'element-hooks';
import { ElInput } from 'element-plus';
import type { FormItem } from 'element-hooks';
import type { FormItemProps, FormProps } from 'element-plus';
const labelPosition = ref<FormProps['labelPosition']>('right');
const itemLabelPosition = ref<FormItemProps['labelPosition']>('');
const buildItems = (pos: FormItemProps['labelPosition'] = ''): FormItem[] => [
{
label: 'Form Align',
labelPosition: 'right',
slot: 'formAlign',
},
{
label: 'Form Item Align',
labelPosition: 'right',
slot: 'formItemAlign',
},
{
label: 'Name',
prop: 'name',
labelPosition: pos || undefined,
render: { component: ElInput },
},
{
label: 'Activity zone',
prop: 'region',
labelPosition: pos || undefined,
render: { component: ElInput },
},
{
label: 'Activity form',
prop: 'type',
labelPosition: pos || undefined,
render: { component: ElInput },
},
];
const [Form, { setState, setItems }] = useForm({
labelPosition: 'right',
labelWidth: 'auto',
model: {
name: '',
region: '',
type: '',
},
items: buildItems(),
});
</script>
<template>
<Form style="max-width: 600px">
<template #formAlign>
<el-radio-group
v-model="labelPosition"
aria-label="label position"
@change="
(val: FormProps['labelPosition']) => setState({ labelPosition: val })
"
>
<el-radio-button value="left">Left</el-radio-button>
<el-radio-button value="right">Right</el-radio-button>
<el-radio-button value="top">Top</el-radio-button>
</el-radio-group>
</template>
<template #formItemAlign>
<el-radio-group
v-model="itemLabelPosition"
aria-label="item label position"
@change="
(val: FormItemProps['labelPosition']) => setItems(buildItems(val))
"
>
<el-radio-button value="">Empty</el-radio-button>
<el-radio-button value="left">Left</el-radio-button>
<el-radio-button value="right">Right</el-radio-button>
<el-radio-button value="top">Top</el-radio-button>
</el-radio-group>
</template>
</Form>
</template>表单校验
vue
<script setup lang="ts">
import { useForm } from 'element-hooks';
import {
ElInput,
ElSelect,
ElSelectV2,
ElSwitch,
ElCheckboxGroup,
ElRadioGroup,
ElSegmented,
} from 'element-plus';
import type { FormRules } from 'element-plus';
interface RuleForm {
name: string;
region: string;
count: string;
date1: string;
date2: string;
delivery: boolean;
location: string;
type: string[];
resource: string;
desc: string;
}
const rules: FormRules<RuleForm> = {
name: [
{
required: true,
message: 'Please input Activity name',
trigger: 'blur',
},
{ min: 3, max: 5, message: 'Length should be 3 to 5', trigger: 'blur' },
],
region: [
{
required: true,
message: 'Please select Activity zone',
trigger: 'change',
},
],
count: [
{
required: true,
message: 'Please select Activity count',
trigger: 'change',
},
],
date1: [
{
type: 'date',
required: true,
message: 'Please pick a date',
trigger: 'change',
},
],
date2: [
{
type: 'date',
required: true,
message: 'Please pick a time',
trigger: 'change',
},
],
location: [
{
required: true,
message: 'Please select a location',
trigger: 'change',
},
],
type: [
{
type: 'array',
required: true,
message: 'Please select at least one activity type',
trigger: 'change',
},
],
resource: [
{
required: true,
message: 'Please select activity resource',
trigger: 'change',
},
],
desc: [
{
required: true,
message: 'Please input activity form',
trigger: 'blur',
},
],
};
const locationOptions = ['Home', 'Company', 'School'];
const options = Array.from({ length: 10000 }).map((_, idx) => ({
value: `${idx + 1}`,
label: `${idx + 1}`,
}));
const [Form, { instance }] = useForm({
labelWidth: 'auto',
model: {
name: 'Hello',
region: '',
count: '',
date1: '',
date2: '',
delivery: false,
location: '',
type: [],
resource: '',
desc: '',
},
rules,
items: [
{
label: 'Activity name',
prop: 'name',
render: { component: ElInput },
},
{
label: 'Activity zone',
prop: 'region',
render: {
component: ElSelect,
props: {
placeholder: 'Activity zone',
options: [
{ label: 'Zone one', value: 'shanghai' },
{ label: 'Zone two', value: 'beijing' },
],
},
},
},
{
label: 'Activity count',
prop: 'count',
render: {
component: ElSelectV2,
props: {
placeholder: 'Activity count',
options,
},
},
},
{
label: 'Activity time',
required: true,
slot: 'activityTime',
},
{
label: 'Instant delivery',
prop: 'delivery',
render: { component: ElSwitch },
},
{
label: 'Activity location',
prop: 'location',
render: {
component: ElSegmented,
props: {
options: locationOptions,
},
},
},
{
label: 'Activity type',
prop: 'type',
render: {
component: ElCheckboxGroup,
props: {
options: [
{
label: 'Online activities',
value: 'Online activities',
name: 'type',
},
{
label: 'Promotion activities',
value: 'Promotion activities',
name: 'type',
},
{
label: 'Offline activities',
value: 'Offline activities',
name: 'type',
},
{
label: 'Simple brand exposure',
value: 'Simple brand exposure',
name: 'type',
},
],
},
},
},
{
label: 'Resources',
prop: 'resource',
render: {
component: ElRadioGroup,
props: {
options: [
{ label: 'Sponsorship', value: 'Sponsorship' },
{ label: 'Venue', value: 'Venue' },
],
},
},
},
{
label: 'Activity form',
prop: 'desc',
render: { component: ElInput, props: { type: 'textarea' } },
},
{
slot: 'buttons',
},
],
});
const submitForm = async () => {
if (!instance.value) return;
await instance.value.validate((valid, fields) => {
if (valid) {
console.log('submit!');
} else {
console.log('error submit!', fields);
}
});
};
const resetForm = () => {
if (!instance.value) return;
instance.value.resetFields();
};
</script>
<template>
<Form style="max-width: 600px">
<template #activityTime="{ model }">
<el-col :span="11">
<el-form-item prop="date1">
<el-date-picker
v-model="model.date1"
type="date"
aria-label="Pick a date"
placeholder="Pick a date"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col class="text-center" :span="2">
<span class="text-gray-500">-</span>
</el-col>
<el-col :span="11">
<el-form-item prop="date2">
<el-time-picker
v-model="model.date2"
aria-label="Pick a time"
placeholder="Pick a time"
style="width: 100%"
/>
</el-form-item>
</el-col>
</template>
<template #buttons>
<el-button type="primary" @click="submitForm"> Create </el-button>
<el-button @click="resetForm">Reset</el-button>
</template>
</Form>
</template>自定义校验规则
vue
<script setup lang="ts">
import { useForm } from 'element-hooks';
import { ElInput } from 'element-plus';
import type { FormRules } from 'element-plus';
const validatePass = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('Please input the password'));
} else {
const model = getModel();
if (model?.checkPass !== '') {
if (!instance.value) return;
instance.value.validateField('checkPass');
}
callback();
}
};
const validatePass2 = (rule: any, value: any, callback: any) => {
if (value === '') {
callback(new Error('Please input the password again'));
} else {
const model = getModel();
if (value !== model?.pass) {
callback(new Error("Two inputs don't match!"));
} else {
callback();
}
}
};
const checkAge = (rule: any, value: any, callback: any) => {
if (!value) {
return callback(new Error('Please input the age'));
}
setTimeout(() => {
const num = Number(value);
if (!Number.isInteger(num)) {
callback(new Error('Please input digits'));
} else {
if (num < 18) {
callback(new Error('Age must be greater than 18'));
} else {
callback();
}
}
}, 1000);
};
const rules: FormRules = {
pass: [{ validator: validatePass, trigger: 'blur' }],
checkPass: [{ validator: validatePass2, trigger: 'blur' }],
age: [{ validator: checkAge, trigger: 'blur' }],
};
const [Form, { instance, getModel }] = useForm({
labelWidth: 'auto',
statusIcon: true,
model: {
pass: '',
checkPass: '',
age: '',
},
rules,
items: [
{
label: 'Password',
prop: 'pass',
render: {
component: ElInput,
props: { type: 'password', autocomplete: 'off' },
},
},
{
label: 'Confirm',
prop: 'checkPass',
render: {
component: ElInput,
props: { type: 'password', autocomplete: 'off' },
},
},
{
label: 'Age',
prop: 'age',
render: {
component: ElInput,
},
},
{
slot: 'buttons',
},
],
});
const submitForm = () => {
if (!instance.value) return;
instance.value.validate(valid => {
if (valid) {
console.log('submit!');
} else {
console.log('error submit!');
}
});
};
const resetForm = () => {
if (!instance.value) return;
instance.value.resetFields();
};
</script>
<template>
<Form style="max-width: 600px" class="demo-ruleForm">
<template #buttons>
<el-button type="primary" @click="submitForm">Submit</el-button>
<el-button @click="resetForm">Reset</el-button>
</template>
</Form>
</template>添加/删除表单项
vue
<script setup lang="ts">
import { ref, watch } from 'vue';
import { useForm } from 'element-hooks';
import { ElInput } from 'element-plus';
import type { FormItem } from 'element-hooks';
interface DomainItem {
key: number;
value: string;
}
const domains = ref<DomainItem[]>([
{
key: 1,
value: '',
},
]);
const buildItems = (domainsList: DomainItem[]): FormItem[] => {
const items: FormItem[] = [
{
prop: 'email',
label: 'Email',
rules: [
{
required: true,
message: 'Please input email address',
trigger: 'blur',
},
{
type: 'email',
message: 'Please input correct email address',
trigger: ['blur', 'change'],
},
],
render: { component: ElInput },
},
];
domainsList.forEach((domain, index) => {
items.push({
label: 'Domain' + index,
prop: `domains.${index}.value`,
rules: {
required: true,
message: 'domain can not be null',
trigger: 'blur',
},
slot: `domain_${domain.key}`,
});
});
items.push({
slot: 'buttons',
});
return items;
};
const [Form, { instance, setItems, getModel, setModel }] = useForm({
labelWidth: 'auto',
model: {
domains: domains.value,
email: '',
},
items: buildItems(domains.value),
});
watch(
domains,
newDomains => {
const model = getModel();
if (model) {
model.domains = newDomains;
setModel(model);
setItems(buildItems(newDomains));
}
},
{ deep: true },
);
const removeDomain = (item: DomainItem) => {
const index = domains.value.indexOf(item);
if (index !== -1) {
domains.value.splice(index, 1);
}
};
const addDomain = () => {
domains.value.push({
key: Date.now(),
value: '',
});
};
const submitForm = () => {
if (!instance.value) return;
instance.value.validate(valid => {
if (valid) {
console.log('submit!');
} else {
console.log('error submit!');
}
});
};
const resetForm = () => {
if (!instance.value) return;
instance.value.resetFields();
};
</script>
<template>
<Form style="max-width: 600px" class="demo-dynamic">
<template
v-for="domain in domains"
:key="domain.key"
#[`domain_${domain.key}`]
>
<el-input v-model="domain.value" />
<el-button class="mt-2" @click.prevent="removeDomain(domain)">
Delete
</el-button>
</template>
<template #buttons>
<el-button type="primary" @click="submitForm">Submit</el-button>
<el-button @click="addDomain">New domain</el-button>
<el-button @click="resetForm">Reset</el-button>
</template>
</Form>
</template>数字类型验证
vue
<script setup lang="ts">
import { useForm } from 'element-hooks';
import { ElInput } from 'element-plus';
const [Form, { instance }] = useForm({
labelWidth: 'auto',
model: {
age: '',
},
items: [
{
label: 'age',
prop: 'age',
rules: [
{ required: true, message: 'age is required' },
{
type: 'number',
message: 'age must be a number',
transform: (value: string) => Number(value),
},
],
render: {
component: ElInput,
props: {
type: 'text',
autocomplete: 'off',
},
},
},
{
slot: 'buttons',
},
],
});
const submitForm = () => {
if (!instance.value) return;
instance.value.validate(valid => {
if (valid) {
console.log('submit!');
} else {
console.log('error submit!');
}
});
};
const resetForm = () => {
if (!instance.value) return;
instance.value.resetFields();
};
</script>
<template>
<Form style="max-width: 600px" class="demo-ruleForm">
<template #buttons>
<el-button type="primary" @click="submitForm">Submit</el-button>
<el-button @click="resetForm">Reset</el-button>
</template>
</Form>
</template>尺寸控制
vue
<script setup lang="ts">
import { ref } from 'vue';
import { useForm } from 'element-hooks';
import {
ElInput,
ElSelect,
ElCheckboxGroup,
ElRadioGroup,
} from 'element-plus';
import type { ComponentSize, FormProps } from 'element-plus';
const [Form, { setState }] = useForm({
labelWidth: 'auto',
labelPosition: 'right',
size: 'default',
model: {
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: '',
},
items: [
{
label: 'Activity name',
prop: 'name',
render: { component: ElInput },
},
{
label: 'Activity zone',
prop: 'region',
render: {
component: ElSelect,
props: {
placeholder: 'please select your zone',
options: [
{ label: 'Zone one', value: 'shanghai' },
{ label: 'Zone two', value: 'beijing' },
],
},
},
},
{
label: 'Activity time',
slot: 'activityTime',
},
{
label: 'Activity type',
prop: 'type',
render: {
component: ElCheckboxGroup,
props: {
type: 'button',
options: [
{
label: 'Online activities',
value: 'Online activities',
name: 'type',
},
{
label: 'Promotion activities',
value: 'Promotion activities',
name: 'type',
},
],
},
},
},
{
label: 'Resources',
prop: 'resource',
render: {
component: ElRadioGroup,
props: {
options: [
{ label: 'Sponsor', value: 'Sponsor', border: true },
{ label: 'Venue', value: 'Venue', border: true },
],
},
},
},
{
slot: 'buttons',
},
],
});
const size = ref<ComponentSize>('default');
const labelPosition = ref<FormProps['labelPosition']>('right');
const onSizeChange = (val: string | number | boolean | undefined) => {
size.value = val as ComponentSize;
setState({ size: val as ComponentSize });
};
const onLabelPositionChange = (
val: string | number | boolean | undefined,
) => {
labelPosition.value = val as FormProps['labelPosition'];
setState({ labelPosition: val as FormProps['labelPosition'] });
};
const onSubmit = () => {
console.log('submit!');
};
</script>
<template>
<div>
<el-radio-group
:model-value="size"
aria-label="size control"
@update:model-value="onSizeChange"
>
<el-radio-button value="large">large</el-radio-button>
<el-radio-button value="default">default</el-radio-button>
<el-radio-button value="small">small</el-radio-button>
</el-radio-group>
<el-radio-group
:model-value="labelPosition"
aria-label="position control"
@update:model-value="onLabelPositionChange"
>
<el-radio-button value="left">Left</el-radio-button>
<el-radio-button value="right">Right</el-radio-button>
<el-radio-button value="top">Top</el-radio-button>
</el-radio-group>
</div>
<br />
<Form style="max-width: 600px">
<template #activityTime="{ model }">
<el-col :span="11">
<el-date-picker
v-model="model.date1"
type="date"
aria-label="Pick a date"
placeholder="Pick a date"
style="width: 100%"
/>
</el-col>
<el-col class="text-center" :span="1" style="margin: 0 0.5rem">
-
</el-col>
<el-col :span="11">
<el-time-picker
v-model="model.date2"
aria-label="Pick a time"
placeholder="Pick a time"
style="width: 100%"
/>
</el-col>
</template>
<template #buttons>
<el-button type="primary" @click="onSubmit">Create</el-button>
<el-button>Cancel</el-button>
</template>
</Form>
</template>
<style>
.el-radio-group {
margin-right: 12px;
}
</style>无障碍
vue
<script setup lang="ts">
import { useForm } from 'element-hooks';
import { ElInput } from 'element-plus';
const [Form] = useForm({
labelPosition: 'left',
labelWidth: 'auto',
model: {
fullName: '',
firstName: '',
lastName: '',
},
items: [
{
slot: 'fullNameAlert',
},
{
label: 'Full Name',
prop: 'fullName',
render: { component: ElInput },
},
{
slot: 'informationAlert',
},
{
label: 'Your Information',
slot: 'yourInformation',
},
],
});
</script>
<template>
<Form style="max-width: 600px">
<template #fullNameAlert>
<el-space fill style="width: 100%; margin-bottom: 18px">
<el-alert type="info" show-icon :closable="false">
<p>"Full Name" label is automatically attached to the input:</p>
</el-alert>
</el-space>
</template>
<template #informationAlert>
<el-space fill style="width: 100%; margin-bottom: 18px">
<el-alert type="info" show-icon :closable="false">
<p>
"Your Information" serves as a label for the group of inputs. <br />
You must specify labels on the individal inputs. Placeholders are
not replacements for using the "label" attribute.
</p>
</el-alert>
</el-space>
</template>
<template #yourInformation="{ model }">
<el-row :gutter="20">
<el-col :span="12">
<el-input
v-model="model.firstName"
aria-label="First Name"
placeholder="First Name"
/>
</el-col>
<el-col :span="12">
<el-input
v-model="model.lastName"
aria-label="Last Name"
placeholder="Last Name"
/>
</el-col>
</el-row>
</template>
</Form>
</template>API
Options
useForm 的配置项继承自 Element Plus ElForm 的 Props,并额外支持以下字段:
items:FormItem[]—— 核心配置,用于声明表单项。model:Recordable—— 表单数据模型(使用setModel更新)。
FormItem
| 字段 | 说明 | 类型 |
|---|---|---|
slot | 默认插槽名(slots.default 的简写) | string |
slots | 精细化插槽配置 | Record<string, string> |
render | 渲染组件配置 | { component: Component, props?: Recordable } |
NOTE
slot 是 slots.default 的简写形式。当同一个表单项同时定义了 slot 和 render 时,插槽优先。
Controller
| 方法 | 说明 | 参数 |
|---|---|---|
setState | 动态更新整体配置 | (state: Partial<FormOptions>) => void |
setItems | 动态更新表单项 | (items: FormItem[]) => void |
setModel | 动态更新模型数据 | (model: Recordable) => void |
getModel | 获取当前模型快照 | () => Recordable |
instance | 内部 ElForm 实例(可调用 validate 等原生方法) | Ref<FormInstance> |