useForm
useForm 在 items 数组中设置表单项。setModel 用于更新表单数据,setItems 用于更新表单项。instance.value 可以调用 validate、resetFields 和 clearValidate 等原生方法。
典型表单
vue
<script setup lang="ts">
import { useForm } from 'element-hooks';
import {
ElCheckboxGroup,
ElInput,
ElRadioGroup,
ElSelect,
ElSwitch,
} 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: 'actions',
},
],
});
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 #actions>
<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 { ElDatePicker, ElInput, ElSelect } 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 { type FormItem, useForm } from 'element-hooks';
import { type FormItemProps, type FormProps, ElInput } from 'element-plus';
import { ref } from 'vue';
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="setState(prev => ({ ...prev, labelPosition }))"
>
<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="setItems(buildItems(itemLabelPosition))"
>
<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 {
type FormRules,
ElCheckboxGroup,
ElInput,
ElRadioGroup,
ElSegmented,
ElSelect,
ElSelectV2,
ElSwitch,
} 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: 'actions',
},
],
});
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 #actions>
<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 { type FormItemRule, type FormRules, ElInput } from 'element-plus';
type Validator = NonNullable<FormItemRule['validator']>;
const validatePass: Validator = (_rule, value, callback) => {
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: Validator = (_rule, value, callback) => {
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: Validator = (_rule, value, callback) => {
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: 'actions',
},
],
});
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 #actions>
<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 { type FormItem, useForm } from 'element-hooks';
import { ElInput } from 'element-plus';
import { ref, watch } from 'vue';
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: 'actions',
});
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 #actions>
<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: 'actions',
},
],
});
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 #actions>
<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 { useForm } from 'element-hooks';
import {
type ComponentSize,
type FormProps,
ElCheckboxGroup,
ElInput,
ElRadioGroup,
ElSelect,
} from 'element-plus';
import { ref } from 'vue';
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: 'actions',
},
],
});
const size = ref<ComponentSize>('default');
const labelPosition = ref<FormProps['labelPosition']>('right');
const onSizeChange = (val: string | number | boolean | undefined) => {
size.value = val as ComponentSize;
setState(prev => ({ ...prev, size: val as ComponentSize }));
};
const onLabelPositionChange = (
val: string | number | boolean | undefined,
) => {
labelPosition.value = val as FormProps['labelPosition'];
setState(prev => ({
...prev,
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 #actions>
<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>纯元素节点
raw: true 用于渲染不带 el-form-item 的节点,适用于提示文字和分割线。
vue
<script setup lang="ts">
import { useForm } from 'element-hooks';
import { ElAlert, ElDivider, ElInput } from 'element-plus';
import { h } from 'vue';
const [Form] = useForm({
labelWidth: '100px',
model: {
username: '',
password: '',
},
items: [
{
raw: true,
render: {
component: () =>
h(ElAlert, {
title: 'Warning alert',
type: 'warning',
style: 'margin-bottom: 18px;',
}),
},
},
{
label: '用户名',
prop: 'username',
render: { component: ElInput },
},
{
label: '密码',
prop: 'password',
render: { component: ElInput, props: { type: 'password' } },
},
{
raw: true,
render: { component: ElDivider },
},
{
slot: 'footer',
},
],
});
</script>
<template>
<Form>
<template #footer>
<el-button type="primary">Login</el-button>
</template>
</Form>
</template>API
useForm 支持 ElForm 的属性,并增加以下配置。调用 useForm(options) 时,FormOptions<T> 必须包含 model。
items— 用于声明表单项的FormItem<T>[]。model— 初始表单模型T。调用useForm(options)时必填。无参调用useForm()时,getModel()返回null。
无参调用时,T 默认使用 object。需要通过 getModel() 访问模型字段时,应为 useForm 指定泛型。
FormItem
| 字段 | 说明 | 类型 |
|---|---|---|
key | 表单项的唯一标识 | string | number |
raw | 是否为纯元素节点(不包裹 el-form-item) | boolean |
slot | 默认插槽名(slots.default 的简写) | string |
slots | 具名插槽配置 | Partial<Record<FormItemSlotName, string>> |
render | 渲染组件配置 | RenderOptions<T> |
render.component
render.component 可以使用全局组件名称,例如 input 可以映射到 ElInput。
render.props
render.props 可以是属性对象,也可以是返回属性对象的函数。函数参数是当前表单模型 model,组件会在渲染时执行该函数。函数中使用的响应式数据变化后,组件属性会自动更新。
事件、格式化器等函数属性会直接传给组件。
ts
import { useForm } from 'element-hooks';
import { ElSelect } from 'element-plus';
import { ref } from 'vue';
const selectOptions = ref([]);
const [Form] = useForm({
model: { status: 'active' },
items: [
{
render: {
component: ElSelect,
props: model => ({
options: selectOptions.value,
// model 为当前表单数据
disabled: model.status !== 'active',
}),
},
},
],
});slot
slot 是 slots.default 的简写形式。当同一个表单项同时定义了 slot 和 render 时,插槽优先。
Controller
| 成员 | 说明 | 类型 |
|---|---|---|
setState | 更新整体配置 | (state: FormOptions<T> | ((prev: FormOptions<T>) => FormOptions<T>)) => void |
setItems | 更新表单项 | (items: FormItem<T>[] | ((prev: FormItem<T>[]) => FormItem<T>[])) => void |
getItems | 获取当前表单项 | () => FormItem<T>[] |
setModel | 更新或初始化模型数据 | (model: T | ((prev: T) => T)) => void |
getModel | 获取当前模型数据 | () => T | null |
instance | ElForm 组件实例 | Ref<FormInstance | null> |
模型尚未初始化时,应先通过 setModel 传入模型对象。