form-item组件封装细讲上(组件库系列~四)

664 阅读4分钟

前言

今天我们来聊一聊form表单组件。市面上开源的form表单组件,写法基本上千篇一律,一个form组件内部跟上一大堆的form-item组件,内嵌了select、radio、checkbox之类的这些就不停的需要去写遍历,需要布局的时候写上一堆的class.一个form组件稍微大一点的就朝100-200行甚至更多去了。不方便维护而且看起来头晕,不符合vue的数据驱动视图的概念。基于以上问题遂对form-item组件进行了二次封装,至于为什么不是对form表单进行二次封装,请往下看

问题剖析

  1. form-item页面元素多的时候代码量大,不便于维护
  2. form-item子项数据类型为list时,写法太不方便,需要在html中各种v-for + key
  3. 需要一行四个这样排列布局的时候需要各处写class col
  4. label字段长度变化就需要去调整label-width的宽度
  5. 嵌入的元素宽度或者样式上保持一致需要大量的css样式支撑
  6. rules规则配置麻烦
  7. form内部全禁用或者全带clearable属性这种情况编写十分麻烦
  8. 动态显示隐藏部分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的形式进行不同组件的渲染,代码和结构图片如下: ![在这里插入图片描述](img-blog.csdnimg.cn/03451f19214… =300x) ![在这里插入图片描述](img-blog.csdnimg.cn/758c1f5a56a… =600x)

<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大家一起讨论交流