前言

今天我们来探讨一下 form 表单组件。市面上的开源 form 表单组件写法基本上都类似,一个 form 组件内部需要写一大堆的 form-item 组件,内嵌了 selectradiocheckbox 等等。这些需要写循环,而且在布局时还需要写一堆的 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>

优化重复代码思路

根据重复代码可以优化的原则,我们可以建立以下思路:

  1. 观察form中的n个form-item,发现每一项都被包裹在el-form-item内;
  2. 考虑将整个form-item设置为一个数组,每个数组项对应一个form-item;
  3. 每个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大家一起讨论交流

Logo

前往低代码交流专区

更多推荐