上一篇介绍了表单控件,这一篇介绍一下表单里面的各种子控件的封装方式。
主要内容
- 需求分析
- 子控件的分类
- 子控件属性的分类
- 定义 interface。
- 定义子控件的的 props。
- 定义 json 文件。
- 基于 UI库 进行二次封装,实现依赖 json 渲染。
- 通过 slot 、 “字典”,实现自定义子控件。
- 做个工具维护 json 文件。(下篇介绍)
需求分析
表单里面需要各种各样的子控件,像文本、数字、选择、日期等常见的需求,可以由内部提供组件解决,但是其他各种“奇奇怪怪”的需求怎么办呢?
如果还是由“内部”提供组件的话,那肯定是行不通的,因为以往的经验教训告诉我们,内部不断扩充子控件的结果,必然会导致内部代码越来越臃肿,以至后期无法维护,最终崩盘!
所以必须支持自定义扩展!感谢 Vue 和 UI库,提供基础的技术支持,让扩展变得非常容易。
我们先对表单子控件进行一下分类,然后为其设计一套接口,即定义一套规则,这样才好方便做长期维护。
子控件的分类
我们对常见的组件进行分析,得到了下面的分类:
上图涵盖了一些常用控件,但是很显然并不全面,比如没有金额类的控件,输入金额也是需要一些辅助的,比如金额的大小写的切换等,不过这些应该用扩展的方式实现。
属性的分类
组件的分类可以做的“规整”一些,但是组件的属性的分类,就比较有难度了,我们可以把组件需要的属性分为三个主要部分:代码里需要的、共用的、扩展的。
-
低代码需要的属性
需要在代码里面使用的属性,比如字段名称、控件类型、默认值、防抖延迟等,集中在一起,通过 props 的方式传递。 -
共用属性
各个组件(或者大部分组件)都需要的属性,比如浮动提示、size、是否显示清空按钮等,作为一级属性,通过 props 的方式传递。 -
扩展属性
某个组件需要的属性,比如数字组件需要 max、min、step等。通过 $attrs 的方式传递。
其中扩展属性最为复杂,如果按照面向对象的方式来设计的话,结构就会非常复杂,会复杂到什么程度呢?可以参考当初 asp.net 里面 webform 的继承结构:
(controll是控件(组件)的意思,下面分出来WebControll和 repeater 两个子类,然后又,,,算了不说了,是不是看着就很累的样子?)
定义接口
现在是 JS 环境,我们没有必要生搬硬套,而是可以利用JS的灵活性来做简洁设计:
我们给表单子控件的 props 定义一个interface:(虽然暂时用不上)
- IFormItemProps
/** * 表单控件的子控件的 props。 */ export interface IFormItemProps { /** * 低代码需要的数据 */ formItemMeta: IFormItemMeta, /** * 子控件备选项,一级或者多级 */ optionList: Array<IOptionList | IOptionTree>, /** * 表单的 model,含义多个属性 */ model: any, /** * 是否显示清空的按钮 */ clearable: boolean, /** * 浮动提示信息 */ title: string, /** * 子控件的扩展属性 */ [key: string]: any }
- IFormItemMeta 的定义:
/** * 子控件的低代码需要的数据 */ export interface IFormItemMeta { /** * -- 字段ID、控件ID */ columnId?: number | string, /** * -- 字段名称 */ colName: string, /** * -- 字段的中文名称,标签 */ label?: string, /** * -- 子控件类型,number,EControlType */ controlType: EControlType | number, /** * 子控件的默认值 */ defValue: any, /** * -- 一个控件占据的空间份数。 */ colCount?: number, /** * 访问后端API的配置信息,有备选项的控件需要 */ webapi?: IWebAPI, /** * -- 防抖延迟时间,0:不延迟 */ delay: number, /** * 防抖相关的事件 */ events?: IEventDebounce, }
规则定义之后呢,总会发现有特例的属性,比如 select 的 option。代码里面需要使用 option 去绑定组件,应该放在“低代码需要的属性”里面。
但是实际使用的时候发现,放在“共用属性”里面会更方便。
然后在做“维护JSON的小工具”的时候,发现需要放在“扩展属性”里面维护,这样维护代码更容易实现。
综合考虑之后,就出现了一个不符合规则的属性 —— optionList。
定义组件的 props。
按照接口实现一下 props 的定义。
import type { PropType } from 'vue' import type { IOptionList, IOptionTree, IFormItemProps } from '../types/20-form-item' /** * 基础控件的共用属性,即表单子控件的基础属性 */ export const itemProps = { formItemMeta: { type: Object as PropType<IFormItemProps>, default: () => {return {}} }, /** * optionList:IOptionList | IOptionTree,控件的备选项,单选、多选、等控件需要 */ optionList: { type: Object as PropType<Array<IOptionList | IOptionTree>>, default: () => {return []} }, /** * 表单的 model,整体传入,便于子控件维护字段值。 */ model: { type: Object }, /** * 是否显示可清空的按钮,默认显示 */ clearable: { type: Boolean, default: true }, /** * 浮动的提示信息,部分控件支持 */ title: { type: String, default: '' } }
其他属性以及扩展属性,可以通过 $attrs 传递和绑定,这样可以方便各种扩展。
定义 json 文件。
我们来定义一个示例用的 json文件。
{ "formItemMeta": { "columnId": 90, "colName": "kind", "label": "分类", "controlType": 107, "isClear": false, "defValue": 0, "colCount": 7 }, "placeholder": "分类", "title": "编号", "optionList": [ {"value": 1, "label": "文本类"}, {"value": 2, "label": "数字类"}, {"value": 3, "label": "日期类"}, {"value": 4, "label": "时间类"}, {"value": 5, "label": "选择类"}, {"value": 6, "label": "下拉类"} ] }
基于 UI库 封装,实现依赖 json 渲染。
首先要感谢强大的UI库,实现了大部分的功能,我们只需要再稍微封装一下即可,只有少数几个组件需要我们补充点代码。
文本类
- template
<el-input v-model="value" v-bind="$attrs" :id="'c' + formItemMeta.columnId" :name="'c' + formItemMeta.columnId" :title="title" :clearable="clearable" @blur="run" @change="run" @clear="run" @keydown="clear" > </el-input>
使用 v-bind="$attrs"
绑定扩展属性
- ts
import { defineComponent } from 'vue' import { ElInput } from 'element-plus' // 引入组件需要的属性、控制类 import { itemProps, itemController } from '@naturefw/ui-elp' export default defineComponent({ name: 'nf-el-form-item-text', inheritAttrs: false, components: { ElInput }, props: { modelValue: [String, Number], ...itemProps // 基础属性 }, emits: ['update:modelValue'], setup (props, context) { const { value, run, clear } = itemController(props, context.emit) return { value, run, clear } } })
使用 ...itemProps
定义属性。
是不是很简单。
可能你会问了,这不是封装了个寂寞吗,你看看里面空荡荡的,完全没有封装的必要嘛。
确实,对于文本这类简单的组件,确实没有封装的必要,直接使用UI库提供的组件即可。
那么为啥好要封装一下呢?
首先为了统一风格,不管是简单的,还是复杂的,都按照统一方式封装一下,这样便于维护和扩展。
日期类
- template
<el-date-picker ref="domDate" v-model="value" v-bind="$attrs" :type="dateType" :name="'c' + formItemMeta.columnId" :format="format" :value-format="valueFormat" :title="title" :clearable="clearable" > </el-date-picker>
- ts
import { defineComponent } from 'vue' // 引入组件需要的属性 引入表单子控件的管理类 import { itemProps, itemController } from '@naturefw/ui-elp' /** * 日期 */ export default defineComponent({ name: 'nf-el-from-item-date', inheritAttrs: false, props: { ...itemProps, // 基础属性 format: { type: String, default: 'YYYY-MM-DD' }, 'value-format': { type: String, default: 'YYYY-MM-DD' }, modelValue: [String, Date, Number, Array] }, emits: ['update:modelValue'], setup (props, context) { const { value } = itemController(props, context.emit) // 根据类型判断是否为数组,判断是否 使用范围。 let dateType = 'date' if (props.formItemMeta.controlType == '125' ) { dateType = 'daterange' if (!Array.isArray(value.value)) { value.value = [] } } else { if (Array.isArray(value.value)) { value.value = '' } } return { dateType, // 控件类型 value // 控件值 } } })
可以增设属性,然后根据需求设置默认值,这样方便统一风格。
选择类
- template
<el-select v-model="value" v-bind="$attrs" :id="'c' + formItemMeta.columnId" :name="'c' + formItemMeta.columnId" :clearable="clearable" :multiple="multiple" :collapse-tags="collapseTags" :collapse-tags-tooltip="collapseTagsTooltip" > <el-option v-for="item in optionList" :key="'select' + item.value" :label="item.label" :value="item.value" :disabled="item.disabled" > </el-option> </el-select>
- ts
import { defineComponent, computed } from 'vue' // 引入组件需要的属性 引入表单子控件的管理类 import { itemProps, itemController } from '@naturefw/ui-elp' export default defineComponent({ name: 'nf-el-from-select', inheritAttrs: false, props: { ...itemProps, // 基础属性 'collapse-tags': { type: Boolean, default: true }, 'collapse-tags-tooltip': { type: Boolean, default: true }, modelValue: [String, Number, Array] }, emits: ['update:modelValue'], setup (props, context) { const multiple = computed (() => props.formItemMeta.controlType === 161) return { ...itemController(props, context.emit) } } })
template 里面增加了 el-option 部分,通过对 optionList 的遍历,实现了选项的渲染。
其他组件也是一样的方式进行封装,就不一一介绍了。
封装 el-form-item
el-table 通过 el-form-item 来加载子组件,所以我们也可以封装一下:
<el-row :gutter="15"> <el-col v-for="(ctrId, index) in colOrder" :key="'form_' + ctrId + '_' + index" :span="formColSpan[ctrId]" v-show="showCol[ctrId]" > <transition name="el-zoom-in-top"> <el-form-item :label="itemMeta[ctrId].formItemMeta.label" :prop="itemMeta[ctrId].formItemMeta.colName" :rules="ruleMeta[ctrId] ?? []" :label-width="itemMeta[ctrId].formItemMeta.labelWidth??''" :size="size" v-show="showCol[ctrId]" > <component :is="formItemKey[itemMeta[ctrId].formItemMeta.controlType]" :model="model" v-bind="itemMeta[ctrId]" > </component> </el-form-item> </transition> </el-col> </el-row>
- el-row、el-col:实现多列
- transition:组件联动的时候的动画效果
- component:动态加载子控件
- formItemKey 子控件的字典,key-value形式,key就是控件编号,value是组件。这样就可以根据控件的编号加载对应的子控件了。
使用 slot 和 字典 实现扩展自定义子控件。
这里要感谢强大的 vue3,提供了插槽这种很灵活的扩展方式。以及组件的形成管理代码。
说到扩展,想必大家想到的是插槽,我们也支持使用插槽的扩展方式,不过我觉得,既然定义了接口,那么不用的话,是不是有点浪费。
我们可以定义组件实现接口,然后并入字典(formItemKey),这样表单控件就可以从字典里面加载我们自己定义的组件了,更便于管理和扩展。
源码和演示
core:https://gitee.com/naturefw-code/nf-rollup-ui-controller
二次封装: https://gitee.com/naturefw-code/nf-rollup-ui-element-plus
演示: https://naturefw-code.gitee.io/nf-rollup-ui-element-plus/