最近做 vue2 项目,用到了 element-ui,发现 element-ui 的表单组件用起来不是很方便,每个表单项都要写一大堆代码,于是就想着封装一个表单组件,支持一键配置表单项,这样就可以减少很多重复代码。
一、看组件效果
原本的 el-form,各表单项代码繁多
实现以上效果,官方文档给出的代码如下:
<template>
<el-form
:model="ruleForm"
:rules="rules"
ref="ruleForm"
label-width="100px"
class="demo-ruleForm"
>
<el-form-item label="活动名称" prop="name">
<el-input v-model="ruleForm.name"></el-input>
</el-form-item>
<el-form-item label="活动区域" prop="region">
<el-select v-model="ruleForm.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item label="活动时间" required>
<el-col :span="11">
<el-form-item prop="date1">
<el-date-picker
type="date"
placeholder="选择日期"
v-model="ruleForm.date1"
style="width: 100%;"
></el-date-picker>
</el-form-item>
</el-col>
<el-col class="line" :span="2">-</el-col>
<el-col :span="11">
<el-form-item prop="date2">
<el-time-picker
placeholder="选择时间"
v-model="ruleForm.date2"
style="width: 100%;"
></el-time-picker>
</el-form-item>
</el-col>
</el-form-item>
<el-form-item label="即时配送" prop="delivery">
<el-switch v-model="ruleForm.delivery"></el-switch>
</el-form-item>
<el-form-item label="活动性质" prop="type">
<el-checkbox-group v-model="ruleForm.type">
<el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>
<el-checkbox label="地推活动" name="type"></el-checkbox>
<el-checkbox label="线下主题活动" name="type"></el-checkbox>
<el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="特殊资源" prop="resource">
<el-radio-group v-model="ruleForm.resource">
<el-radio label="线上品牌商赞助"></el-radio>
<el-radio label="线下场地免费"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="活动形式" prop="desc">
<el-input type="textarea" v-model="ruleForm.desc"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')"
>立即创建</el-button
>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</el-form>
</template>
<script>
export default {
data() {
return {
ruleForm: {
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: '',
},
rules: {
name: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{ min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' },
],
region: [
{ required: true, message: '请选择活动区域', trigger: 'change' },
],
date1: [
{
type: 'date',
required: true,
message: '请选择日期',
trigger: 'change',
},
],
date2: [
{
type: 'date',
required: true,
message: '请选择时间',
trigger: 'change',
},
],
type: [
{
type: 'array',
required: true,
message: '请至少选择一个活动性质',
trigger: 'change',
},
],
resource: [
{ required: true, message: '请选择活动资源', trigger: 'change' },
],
desc: [{ required: true, message: '请填写活动形式', trigger: 'blur' }],
},
};
},
methods: {
submitForm(formName) {
this.$refs[formName].validate((valid) => {
if (valid) {
alert('submit!');
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm(formName) {
this.$refs[formName].resetFields();
},
},
};
</script>
增强版的 el-form - 去掉表单项组件代码,改成schema动态配置
增强版的 el-form,只需要配置表单项的 schema,就可以实现以上效果,代码如下:
<template>
<!-- <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm" > 增加了schema 去掉了rules 去掉了内部的表单项 -->
<EnhancedElForm
:model="formData"
:schema="schema"
ref="ruleForm"
label-width="100px"
class="demo-ruleForm"
>
<el-form-item>
<el-button type="primary" @click="submitForm('ruleForm')"
>立即创建</el-button
>
<el-button @click="resetForm('ruleForm')">重置</el-button>
</el-form-item>
</EnhancedElForm>
</template>
<script>
export default {
data() {
return {
// 不变
ruleForm: {
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: '',
},
// 表单项配置 rules合并在单个表单项的配置中
schema: [
// 输入框
// <el-form-item label="活动名称" prop="name"> <el-input v-model="ruleForm.name"></el-input> </el-form-item>
{
component: 'el-input',
modelKey: 'name',
label: '活动名称',
// 验证规则
rules: [
{ required: true, message: '请输入活动名称', trigger: 'blur' },
{
min: 3,
max: 5,
message: '长度在 3 到 5 个字符',
trigger: 'blur',
},
],
},
// 选择器
// <el-form-item label="活动区域" prop="region"> <el-select v-model="ruleForm.region" placeholder="请选择活动区域"> <el-option label="区域一" value="shanghai"></el-option> <el-option label="区域二" value="beijing"></el-option> </el-select> </el-form-item>
{
component: 'el-select',
modelKey: 'region',
label: '活动区域',
props: {
placeholder: '请选择活动区域',
options: [
{
value: 'shanghai',
label: '区域一',
},
{
value: 'beijing',
label: '区域二',
},
],
},
rules: [
{ required: true, message: '请选择活动区域', trigger: 'change' },
],
},
// 日期选择器 以及 同一个label对应多个输入框
// <el-form-item label="活动时间" required> <el-col :span="11"> <el-form-item prop="date1"> <el-date-picker type="date" placeholder="选择日期" v-model="ruleForm.date1" style="width: 100%;"></el-date-picker> </el-form-item> </el-col> <el-col class="line" :span="2">-</el-col> <el-col :span="11"> <el-form-item prop="date2"> <el-time-picker placeholder="选择时间" v-model="ruleForm.date2" style="width: 100%;"></el-time-picker> </el-form-item> </el-col> </el-form-item>
{
label: '活动时间',
props: {
required: true,
},
cols: [
{
span: 11,
component: 'el-date-picker',
modelKey: 'date1',
props: {
placeholder: '选择日期',
},
rules: [
{ required: true, message: '请选择日期', trigger: 'change' },
],
},
{
span: 2,
class: 'line',
text: '-',
},
{
span: 11,
component: 'el-time-picker',
modelKey: 'date2',
props: {
placeholder: '选择时间',
},
rules: [
{ required: true, message: '请选择时间', trigger: 'change' },
],
},
],
},
// 开关
// <el-form-item label="即时配送" prop="delivery"> <el-switch v-model="ruleForm.delivery"></el-switch> </el-form-item>
{
component: 'el-switch',
modelKey: 'delivery',
label: '即时配送',
},
// 复选框
// <el-form-item label="活动性质" prop="type"> <el-checkbox-group v-model="ruleForm.type"> <el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox> <el-checkbox label="地推活动" name="type"></el-checkbox> <el-checkbox label="线下主题活动" name="type"></el-checkbox> <el-checkbox label="单纯品牌曝光" name="type"></el-checkbox> </el-checkbox-group> </el-form-item>
{
component: 'el-checkbox-group',
modelKey: 'type',
label: '活动性质',
props: {
options: [
{
value: '美食/餐厅线上活动',
label: '美食/餐厅线上活动',
},
{
value: '地推活动',
label: '地推活动',
},
{
value: '线下主题活动',
label: '线下主题活动',
},
{
value: '单纯品牌曝光',
label: '单纯品牌曝光',
},
],
},
rules: [
{
type: 'array',
required: true,
message: '请至少选择一个活动性质',
trigger: 'change',
},
],
},
// 单选框
// <el-form-item label="特殊资源" prop="resource"> <el-radio-group v-model="ruleForm.resource"> <el-radio label="线上品牌商赞助"></el-radio> <el-radio label="线下场地免费"></el-radio> </el-radio-group> </el-form-item>
{
component: 'el-radio-group',
modelKey: 'resource',
label: '特殊资源',
props: {
options: [
{
value: '线上品牌商赞助',
label: '线上品牌商赞助',
},
{
value: '线下场地免费',
label: '线下场地免费',
},
],
},
rules: [
{ required: true, message: '请选择特殊资源', trigger: 'change' },
],
},
// 文本区域输入框
// <el-form-item label="活动形式" prop="desc"> <el-input type="textarea" v-model="ruleForm.desc"></el-input> </el-form-item>
{
component: 'el-input',
modelKey: 'desc',
label: '活动形式',
props: {
type: 'textarea',
},
rules: [
{ required: true, message: '请填写活动形式', trigger: 'blur' },
],
},
],
};
},
methods: {
// ...不变
},
};
</script>
二、EnhancedElForm组件实现
2.1 基本思路
- 通过 schema 配置表单项,schema 是一个数组,每个元素代表一个表单项的配置。
- 通过 v-for 遍历 schema,根据配置生成对应的表单项。
- 表单项的配置包括:component、modelKey、label、rules、props、cols等属性。
- 通过 component 区分不同的表单项,如 el-input、el-select、el-switch、el-checkbox-group、el-radio-group 等
- 通过 modelKey 绑定表单项的值
- 通过 label 设置表单项的标签
- 通过 rules 配置表单项的验证规则
- 通过 props 配置表单组件上的属性。
- 通过 cols 配置同一个 label 对应多个属性的情况
2.2 实现逻辑
新建 EnhancedElForm.vue 组件,接收 model 和 schema 两个 props,model 是表单数据,schema 是表单项配置。在组件中遍历 schema,根据配置生成对应的表单项,同时将 rules 从 schema 中提取出来,传递给 el-form 组件。
2.3 基础实现
先不考虑复杂的情况,
表单其他属性和事件用v-bind和v-on直接传递给 el-form 组件。
model 直接放在 form 上,schema 遍历生成每个表单项。
表单每项都是el-form-item,只是内部有所区分,使用component组件,表单项本身的属性和使用通过props和listeners进行直接传递。
所以 EnhancedElForm.vue 组件如下:
<template>
<el-form :model="model" v-bind="$attrs" v-on="$listeners">
<template v-for="config in schema">
<el-form-item
:label="config.label"
:prop="config.modelKey"
:key="config.modelKey"
>
<component
:is="config.component"
v-model="model[config.modelKey]"
v-bind="config.props"
v-on="config.listeners"
/>
</el-form-item>
</template>
<slot name="default"></slot>
</el-form>
</template>
<script>
export default {
name: 'enhanced-el-form',
props: {
model: {
type: Object,
default() {
return {};
},
},
schema: {
type: Array,
default() {
return {};
},
},
},
};
</script>
2.4 增加选项类组件
对于 el-select、el-checkbox-group、el-radio-group 这类组件,因为内部实现不太一样,但是希望使用的时候,统一配置 options 就行,所以需要区分这几个组件。
<template>
<!-- 不变 -->
<component
:is="config.component"
v-model="model[config.modelKey]"
v-bind="config.props"
v-on="config.listeners"
>
<template v-if="config.component === 'el-radio-group'">
<el-radio
v-for="(item, index) in config.props.options"
:key="index"
:label="typeof item === 'object' ? item.value : item"
>{{ typeof item === 'object' ? item.label : item }}</el-radio
>
</template>
<template v-else-if="config.component === 'el-checkbox-group'">
<el-checkbox
v-for="(item, index) in config.props.options"
:key="index"
:label="typeof item === 'object' ? item.value : item"
>{{ typeof item === 'object' ? item.label : item }}</el-checkbox
>
</template>
<template v-else-if="config.component === 'el-select'">
<el-option
v-for="(item, index) in config.props.options"
:key="index"
:value="typeof item === 'object' ? item.value : item"
:label="typeof item === 'object' ? item.label : item"
></el-option>
</template>
</component>
<!-- 不变 -->
</template>
2.5 增加验证规则
将 rules 从 schema 中提取出来,传递给 el-form 组件。
<template>
<el-form :model="model" :rules="rules" v-bind="$attrs" v-on="$listeners">
<!-- 不变 -->
</el-form>
</template>
<script>
export default {
// ...
computed: {
computed: {
rules() {
return this.schema.reduce((acc, cur) => {
acc[cur.modelKey] = cur.rules;
return acc;
}, {});
},
},
},
};
</script>
2.6 增加 cols 配置
对于一些特殊的组件,比如日期选择器,需要配置多个组件,这时候需要使用 cols 配置。 或者对于一些表单项,需要多个组件,比如同一个 label 对应多个输入框,这时候也需要使用 cols 配置。 这里考虑到 cols 的时候,component 可能没有值,所以默认加个 div。
<template>
<!-- 不变 -->
<component
:is="config.component || 'div'"
v-model="model[config.modelKey]"
v-bind="config.props"
v-on="config.listeners"
>
<template v-if="config.cols">
<el-col v-for="(col, index) in config.cols" :key="index" :span="col.span">
<component
v-if="col.component"
:is="col.component"
v-model="model[col.modelKey]"
v-bind="col.props"
v-on="col.listeners"
/>
{{ col.text }}
</el-col>
</template>
</component>
<!-- 不变 -->
</template>
2.7 增加 自定义组件
对于一些需要自定义组件,封装的时候,props 需要 value 值,值发生变化的时候,需要触发 input 事件,使用 v-model 进行双向绑定。配置的时候,component 设置为自定义组件的名称即可。
比如定义一个输入名字的NameInput组件:
<template>
<input
type="text"
:value="innerValue"
@input="
($event) => {
innerValue = $event.target.value;
}
"
v-bind="$attrs"
/>
</template>
<script>
export default {
name: 'NameInput',
props: {
value: {
type: String,
required: true,
},
},
computed: {
innerValue: {
get() {
return this.value;
},
set(value) {
this.$emit('input', value);
},
},
},
};
</script>
使用的时候,只需要配置 component 为NameInput即可。
import NameInput from './NameInput.vue';
const schema = [
{
component: NameInput,
modelKey: 'name',
label: '姓名',
rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
},
];
如果不想单独抽离成一个组件,也可以使用 slot 来处理。
<template>
<slot v-if="config.slotName" :name="config.slotName" v-bind="config"></slot>
<component v-else ... >
</template>
使用的时候,要配置 slotName,然后在组件内部写逻辑。
const schema = [
{
slotName: 'username',
label: '姓名',
rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
},
];
组件中使用slot,并且传递配置。
<template>
<EnhancedElForm
:model="formData"
:schema="schema"
ref="dataForm"
label-width="100px"
class="demo-ruleForm"
>
<template #username="config">
<el-input v-model="formData[config.modelKey]"></el-input>
</template>
</EnhancedElForm>
</template>
2.7 直接访问 el-form 的方法
通常来说, 想要访问 el-form 的方法,是需要通过定义在EnhancedElForm组件上的ref,然后通过ref访问到EnhancedElForm组件,再通过$refs访问到ElForm组件上的方法。比如访问validate方法,需要这样:
this.$refs.ruleForm.$refs.elForm.validate((valid) => {
// ...
});
怎么能够直接访问到el-form的方法呢?直接将el-form的方法暴露出来,这样就可以直接访问到el-form的方法了。下次访问就是this.$refs.ruleForm.validate
<template>
<el-form
ref="elForm"
:model="model"
:rules="rules"
v-bind="$attrs"
v-on="$listeners"
>
<!-- 不变 -->
</el-form>
</template>
<script>
export default {
// ...
methods: {
// 等同于 validate(...args) { return this.$refs.elForm.validate(...args) },...
...['validate', 'validateField', 'resetFields', 'clearValidate'].reduce(
(methods, method) => {
methods[method] = function (...args) {
return this.$refs.elForm && this.$refs.elForm[method](...args);
};
return methods;
},
{}
),
},
};
</script>
2.8 配置查询表单
后台系统中,查询表单通常是横向的,且会有查询和重置按钮,这里通过属性isQueryForm来控制是否是查询表单。
<template>
<el-form :inline="isQueryForm" ..>
>
<!-- 不变 -->
<el-form-item class="query-form-buttons" v-if="isQueryForm">
<el-button type="primary" @click="$emit('query')">查询</el-button>
<el-button v-if="showReset" @click="$emit('reset')">重置</el-button>
<slot name="default" />
</el-form-item>
<slot v-else name="default" />
</el-form>
</template>
<script>
export default {
props: {
isQueryForm: {
type: Boolean,
default: false,
},
showReset: {
type: Boolean,
default: true,
},
},
};
</script>
2.9 EnhancedElForm的全部代码
<template>
<el-form
ref="elForm"
:model="model"
:rules="rules"
:inline="isQueryForm"
v-bind="$attrs"
v-on="$listeners"
>
<template v-for="config in schema">
<el-form-item
:label="config.label"
:prop="config.modelKey"
:key="config.modelKey"
>
<slot
v-if="config.slotName"
:name="config.slotName"
v-bind="config"
></slot>
<component
v-else
:is="config.component || 'div'"
v-model="model[config.modelKey]"
v-bind="config.props"
v-on="config.listeners"
>
<template v-if="config.cols">
<el-col
v-for="(col, index) in config.cols"
:key="index"
:span="col.span"
>
<component
v-if="col.component"
:is="col.component"
v-model="model[col.modelKey]"
v-bind="col.props"
v-on="col.listeners"
/>
{{ col.text }}
</el-col>
</template>
<template v-else-if="config.component === 'el-radio-group'">
<el-radio
v-for="(item, index) in config.props.options"
:key="index"
:label="typeof item === 'object' ? item.value : item"
>{{ typeof item === 'object' ? item.label : item }}</el-radio
>
</template>
<template v-else-if="config.component === 'el-checkbox-group'">
<el-checkbox
v-for="(item, index) in config.props.options"
:key="index"
:label="typeof item === 'object' ? item.value : item"
>{{ typeof item === 'object' ? item.label : item }}</el-checkbox
>
</template>
<template v-else-if="config.component === 'el-select'">
<el-option
v-for="(item, index) in config.props.options"
:key="index"
:value="typeof item === 'object' ? item.value : item"
:label="typeof item === 'object' ? item.label : item"
></el-option>
</template>
</component>
</el-form-item>
</template>
<el-form-item class="query-form-buttons" v-if="isQueryForm">
<el-button type="primary" @click="$emit('query')">查询</el-button>
<el-button v-if="showReset" @click="$emit('reset')">重置</el-button>
<slot name="default" />
</el-form-item>
<slot v-else name="default" />
</el-form>
</template>
<script>
export default {
name: 'enhanced-el-form',
props: {
model: {
type: Object,
default() {
return {};
},
},
schema: {
type: Array,
default() {
return {};
},
},
isQueryForm: {
type: Boolean,
default: false,
},
showReset: {
type: Boolean,
default: true,
},
},
computed: {
rules() {
return this.schema.reduce((acc, cur) => {
acc[cur.modelKey] = cur.rules;
return acc;
}, {});
},
},
methods: {
...['validate', 'validateField', 'resetFields', 'clearValidate'].reduce(
(methods, method) => {
methods[method] = function (...args) {
return this.$refs.elForm && this.$refs.elForm[method](...args);
};
return methods;
},
{}
),
},
};
</script>
三、实战应用
3.1 新建和编辑表单
<template>
<EnhancedElForm
:model="formData"
:schema="schema"
ref="dataForm"
label-width="100px"
class="demo-ruleForm"
>
<el-form-item>
<el-button type="primary" @click="submitForm">立即创建</el-button>
<el-button @click="resetForm">重置</el-button>
</el-form-item>
</EnhancedElForm>
</template>
<script>
import EnhancedElForm from './EnhancedElForm.vue';
export default {
data() {
return {
formData: {
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: '',
},
schema: [] /** schema懒得重复写了 */,
};
},
methods: {
submitForm() {
this.$refs.dataForm.validate((valid) => {
if (valid) {
alert('submit!');
} else {
console.log('error submit!!');
return false;
}
});
},
resetForm() {
this.$refs.dataForm.resetFields();
},
},
};
</script>
3.2 查询表单
<template>
<EnhancedElForm
:model="queryData"
:schema="querySchema"
isQueryForm
ref="dataForm"
@query="query"
@reset="reset"
>
<el-button>创建桌面</el-button>
</EnhancedElForm>
</template>
<script>
import EnhancedElForm from './EnhancedElForm.vue';
export default {
data() {
return {
formData: {
name: '',
state: '',
},
schema: []/** schema懒得重复写了 */,
};
},
methods: {
query() {
console.log('query');
},
reset() {
console.log('reset');
},
},
};