前言
今天我们来聊一聊form表单组件。市面上开源的form表单组件,写法基本上千篇一律,一个form组件内部跟上一大堆的form-item组件,内嵌了select、radio、checkbox之类的这些就不停的需要去写遍历,需要布局的时候写上一堆的class.一个form组件稍微大一点的就朝100-200行甚至更多去了。不方便维护而且看起来头晕,不符合vue的数据驱动视图的概念。基于以上问题遂对form-item组件进行了二次封装,至于为什么不是对form表单进行二次封装,请往下看
问题剖析
- form-item页面元素多的时候代码量大,不便于维护
- form-item子项数据类型为list时,写法太不方便,需要在html中各种v-for + key
- 需要一行四个这样排列布局的时候需要各处写class col
- label字段长度变化就需要去调整label-width的宽度
- 嵌入的元素宽度或者样式上保持一致需要大量的css样式支撑
- rules规则配置麻烦
- form内部全禁用或者全带clearable属性这种情况编写十分麻烦
- 动态显示隐藏部分form-item实现成本高且代码可读性差
解决思路
先说一下为什么抽离form-item组件而不是抽离form组件。
- 第一点:form表单的校验是通过this.$refs.form.validate进行校验的,如果抽离了form,那么这一步难度就无形中会加大很多。
- 第二点:form表单能抽离的内容极少,属性基本上都是直接在标签上编写的,编码成本本身就低,潜入到组件内部会导致form 和form-item的参数发生冲突
- 第三点:form表单内部情况复杂,需要包容性强。潜入组件内容会导致组件限制性太,耦合性强
通过遍历得到代码结构
我们看到这个代码片段:
<el-form-item label="姓名" prop="name">
<el-input v-model="form.name" placeholder="请输入名称"></el-input>
</el-form-item>
<el-form-item label="性别 prop="sex">
<el-select v-model="form.sex" placeholder="请选择性别">
<el-option label="男" value="1"></el-option>
<el-option label="女" value="0"></el-option>
</el-select>
</el-form-item>
以重复的代码就可以优化的原则建立思路; 首先我们可以看到一个form有n项form-item,每一个项外面都包裹着一层el-form-item,那么我们首先就可以想到可以设置整个form-item为一个Array。数组中的每一项对应form中的每一个form-item。每一个form-item 都有label和prop。那么我们最初建立的入参为:
props: {
config: {type: Array, default: () => [
{label: "姓名", prop: "name"},
{label: "性别", prop: "sex"}
]}
}
我们可以在页面上进行遍历
<el-form-item
v-for="(item, index) in realConfig"
:key="'form-item' + index"
:label="item.label"
:prop="item.prop">
</el-form-item>
我们可以得到:
<el-form-item label="姓名" prop="name"></el-form-item>
<el-form-item label="性别 prop="sex"></el-form-item>
接下来我们发现姓名里面是一个输入框input,性别是一个下拉框select,还有可能出现单选框radio、多选框checkbox、时间选择器date-picker、时间日期选择器、时间段选择器daterangepicker(下面全部称之为“子组件“)等等。面对这种情况我们就能想到在config的项中添加相应的参数,我命名为itemType(本意是命名为type,但是type过于常见,容易造成冲突);所以现在数据入参为:
props: {
form: { type: Object, required: true, default: () => {} }, // 传进来的共享的form表单值对象
config: {type: Array, default: () => [
// itemType值的可选项为可能出现的高频组件名
// input\select\radio\checkbox\date-picker等(组件内部需要写清楚,且需要在require中添加入参强制校验)
{itemType: "input", label: "姓名", prop: "name"},
{itemType: "select", label: "性别", prop: "sex"}
]}
}
接下来在进行页面开发的时候会遇到一个问题,我们如何把对应的表单组件(input、select等)放进去,最简单粗暴的方法就是v-if v-else-if v-else这样的方式,这种方式的缺点是每一个子内容可能有着不同的逻辑,包括dom都会有较大的差异,全放在一个页面会导致一个页面代码行数巨大,且极其难以维护,不符合单个文件不超200行的代码原则(这个点有同学感兴趣,可以在评论区@我,组件库系列完成之后详细的写一版本)。所以我们这里采用的是把所有的子组件全部抽离出去,通过import引入,components局部注入。最后以components + :is的形式进行不同组件的渲染,代码和结构图片如下:  
<el-form-item
v-for="(item, index) in realConfig"
:key="'form-item' + index"
:label="item.label"
:prop="item.prop">
<components
:is="'item-' + item.itemType || 'text'"
:form="form"
v-bind="{ ...item, noLabel }"
/>
</el-form-item>
在components中需要把form传递进去,用来做子组件值的双向绑定,config对应项中的其他的参数也需要传递进去,比如说select需要多选,那么对应项的config: {itemType: "select", label: "性别", prop: "sex", multiple: true},然后在子组件层通过v-bind="$attrs"的方式进行数据的透传,以达到兼容所有elementUI原组件功能的目的
异常情况处理
业务稀奇古怪,啥奇葩情况都有,那么页面上出现的表单项我们的子组件内不符合的咋办呢?这样的情况其实就已经带有比较强的业务性了,我们首先肯定我们做的是一个完全不包含业务的基础组件库。面对这样的情况,有且只有一个办法:Slot 想到了办法,我们接下来就得考虑下都有啥问题,如下:
- 哪一个需要插槽
- 怎么保证嵌入的插槽放在了指定的位置
- 插槽怎么拿到对应的项的数据 考虑到了问题,我们就来想想怎么处理: 1.哪一个需要插槽
我们可以通过在config中加上一个值为Boolean的可选参数“isSlot“,情况如下:
props: {
form: { type: Object, required: true, default: () => {} }, // 传进来的共享的form表单值对象
config: {type: Array, default: () => [
// itemType值的可选项为可能出现的高频组件名
// input\select\radio\checkbox\date-picker等(组件内部需要写清楚,且需要在require中添加入参强制校验)
{itemType: "input", label: "姓名", prop: "name"},
{itemType: "select", label: "性别", prop: "sex"},
{itemType: "select", label: "身份证插槽", prop: "idCard", isSlot: true}
]}
}
2.怎么保证嵌入的插槽放在了指定的位置
遍历时如果当前参数item.isSlot为false或者不存在,那么我们使用components is 如果存在那我们使用slot标签,通过具名插槽的方式指定插槽内容所在的位置,具名插槽的名字就采用item.prop的值,结果如下:
<el-form-item
v-for="(item, index) in realConfig"
:key="'form-item' + index"
:label="item.label"
:prop="item.prop">
<components
v-if="!item.isSlot"
:is="'item-' + item.itemType || 'text'"
:form="form"
v-bind="{ ...item, noLabel }"
/>
<slot v-else :name="item.prop" />
</el-form-item>
3.插槽怎么拿到对应的项的数据 我们可以通过作用域具名插槽进行插槽内外的数据传递。代码如下
<el-form-item
v-for="(item, index) in realConfig"
:key="'form-item' + index"
:label="item.label"
:prop="item.prop">
<components
v-if="!item.isSlot"
:is="'item-' + item.itemType || 'text'"
:form="form"
v-bind="{ ...item, noLabel }"
/>
<slot v-else v-bind="item" :name="item.prop" />
</el-form-item>
这样我们的form-item组件到目前为止就支持了子组件的插入,以及异常业务的兼容性。
最后贴一下这个form-item组件最终完成后的代码和效果图(因安全问题,字段有删减和prop值修改):
<el-form ref="form" :model="form" :rules="rules">
<hc-form-item :form="form" :rules="rules" :config="formConfig" :column="3" clearable same-label class="mt-20">
<com-upload slot="certPositive" v-model="form.certPositive" ext=".jpg,.jpeg,.png,.gif" />
</hc-form-item>
</el-form>
computed: {
formConfig() {
return [
{ itemType: 'input', label: '身份证号码', prop: 'aa', readonly: this.type !== 'save' },
{ itemType: 'input', label: '姓名', prop: 'bb', readonly: this.type !== 'save' },
{ itemType: 'radio', label: '性别', prop: 'cc', code: 'staff_extension_sex' },
{ itemType: 'date-picker', type: 'date', label: '出生日期', prop: 'dd', 'value-format': 'yyyy-MM-dd', disabled: true },
{ itemType: 'input', label: '联系电话', prop: 'ee' },
{ itemType: 'input', label: '邮箱', prop: 'ff' },
{ itemType: 'select', label: '籍贯', prop: 'hh', code: 'native_place' },
{ itemType: 'select', label: '学历/文化程度', prop: 'oo', code: 'staff_extension_education' },
{ itemType: 'date-picker', type: 'date', label: '居住证办理日期', prop: 'residentPermitDate', 'value-format': 'yyyy-MM-dd' },
{ itemType: 'input', label: '证件正面照', prop: 'certPositive', isSlot: true },
];
}
}
PS:后续会逐步实现form-item的代码封装,解决最开始提出来的八个问题,后续博客正在路上......
对组件库开发有兴趣的可以进QQ群: 617330944大家一起讨论交流