form-item组件封装细讲上(组件库系列~四)
今天我们来聊一聊form表单组件。市面上开源的form表单组件,写法基本上千篇一律,一个form组件内部跟上一大堆的form-item组件,内嵌了select、radio、checkbox之类的这些就不停的需要去写遍历,需要布局的时候写上一堆的class.一个form组件稍微大一点的就朝100-200行甚至更多去了。不方便维护而且看起来头晕,不符合vue的数据驱动视图的概念。基于以上问题遂对for
前言
今天我们来探讨一下
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。
- 当需要使用四列等宽排列布局时,需要在各个位置编写相应的col类。
- 当label字段长度发生变化时,需要手动调整label-width的宽度。
- 为了保持嵌入元素的一致宽度和样式,需要大量使用CSS样式。
- 配置表单验证规则过程繁琐。
- 当整个form内部需要全禁用或全带有clearable属性时,编写较为麻烦。
- 实现动态显示/隐藏部分form-item的成本较高,而且代码可读性差。
解决思路
先说一下为什么抽离form-item组件而不是抽离form组件。
优化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设置为一个数组,每个数组项对应一个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 config"
: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 config"
: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
在业务开发中,表单项常常出现各种奇葩情况。如果我们的子组件无法满足这些需求,该怎么办呢?这时候,就需要使用强业务性的解决方案——Slot。
使用 Slot 时,我们首先要明确:
- 哪些组件需要插槽
- 如何确保插槽被正确插入到组件中
- 如何获取插槽中对应的数据
在解决以上问题的基础上,我们可以高效、灵活地处理表单项不符合的情况。值得注意的是,我们的基础组件库要尽可能不包含业务逻辑。
思路清晰,问题得到解决,让我们一起开心地Coding吧!
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 config"
: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 config"
: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进行代码封装优化,以解决之前提出的八个问题。同时,相关内容的博客正在制作中,敬请期待。
组件库代码地址
https://gitee.com/yangxiongasin/hc-basic
组件库文档代码地址
https://gitee.com/yangxiongasin/hc-docs
对组件库开发有兴趣的可以进QQ群: 617330944大家一起讨论交流
更多推荐
所有评论(0)