前面说了列表的低代码化的方法,本篇介绍一下表单的低代码化。
内容摘要
- 需求分析。
- 定义 interface。
- 定义表单控件的 props。
- 定义 json 文件。
- 基于 el-form 封装,实现依赖 json 渲染。
- 实现多列、验证、分栏等功能。
- 使用 slot 实现自定义扩展。
- 自定义子控件。(下篇介绍)
- 表单子控件的设计与实现。(下篇介绍)
- 做个工具维护 json 文件。(下下篇介绍)
需求分析
表单是很常见的需求,各种网页、平台、后台管理等,都需要表单,有简单的、也有复杂的,但是目的一致:收集用户的数据,然后提交给后端。
表单控件的基础需求:
- 可以依赖 JSON 渲染。
- 依赖 JSON 创建 model。
- 便于用户输入数据。
- 验证用户输入的数据。
- 便于程序员实现功能。
- 可以多列。
- 可以分栏。
- 可以自定义扩展。
- 其他。
el-form 实现了数据验证、自定义扩展等功能(还有漂亮的UI),我们可以直接拿过来封装,然后再补充点代码,实现多列、分栏、依赖 JSON 渲染等功能。
设计 interface
首先把表单控件需要的属性分为两大类:el-form 的属性、低代码需要的数据。
表单控件需要的属性的分类
整理一下做个脑图:
表单控件的接口
我们转换为接口的形式,再做个脑图:
然后我们定义具体的 interface
IFromProps:表单控件的接口 (包含所有属性,对应 json 文件)
/** * 表单控件的属性 */ export interface IFromProps { /** * 表单的 model,对象,包含多个字段。 */ model: any, /** * 根据选项过滤后的 model,any */ partModel?: any, /** * 表单控件需要的 meta */ formMeta: IFromMeta, /** * 表单子控件的属性,IFormItem */ itemMeta: IFormItemList, /** * 标签的后缀,string */ labelSuffix: string, /** * 标签的宽度,string */ labelWidth: string, /** * 控件的规格,ESize */ size: ESize, /** * 其他扩展属性 */ [propName: string]: any }
- model:表单数据,可以依据 JSON 创建。
- partModel:组件联动后,只保留可见组件对应的数据。
- formMeta:低代码需要的属性集合。
- itemMeta:表单子控件需要的属性集合。
- 其他:el-table 组件需要的属性,可以使用 $attrs 进行扩展。
本来想用这个接口约束组件的 props,但是有点小问题:
- 如果用 Option API 的话,不支持这种形式的接口。
- 如果使用 Composition API 的话,虽然支持,但是只能在组件内部定义 interface,暂时不支持从外部文件引入。
接口文件应该可以在外部定义,然后引入组件。如果不能的话,那就尴尬了。
所以只好暂时放弃对组件的 props 进行整体约束。
IFromMeta:低代码需要的属性接口
/** * 低代码的表单需要的 meta */ export interface IFromMeta { /** * 模块编号,综合使用的时候需要 */ moduleId: number | string, /** * 表单编号,一个模块可以有多个表单 */ formId: number | string, /** * 表单字段的排序、显示依据,Array<number | string>, */ colOrder: Array<number | string>, /** * 表单的列数,分为几列 number, */ columnsNumber: number /** * 分栏的设置,ISubMeta */ subMeta: ISubMeta, /** * 验证信息,IRuleMeta */ ruleMeta: IRuleMeta, /** * 子控件的联动关系,ILinkageMeta */ linkageMeta: ILinkageMeta }
- moduleId 模块编号,以后使用
- formId 表单编号,一个模块可以有多个表单
- colOrder 数组形式,表单里包含哪些字段?字段的先后顺序如何确定?就用这个数组。
- columnsNumber 表单控件的列数,表单只能单列?太单调,支持多列才是王道。
ISubMeta:分栏的接口
/** * 分栏表单的设置 */ export interface ISubMeta { type: ESubType, // 分栏类型:card、tab、step、"" (不分栏) cols: Array<{ // 栏目信息 title: string, // 栏目名称 colIds: Array<number> // 栏目里有哪些控件ID }> }
UI库提供了 el-card、el-tab、el-step等组件,我们可以使用这几个组件来实现多种分栏的形式。
IRule、IRuleMeta、:数据验证的接口
el-form 采用 async-validator
实现数据验证,所以我们可以去官网(https://github.com/yiminghe/async-validator)看看可以有哪些属性,针对这些属性指定一个接口(IRule),然后定义一个【字段编号-验证数组】的接口(IRuleMeta)
/** * 一条验证规则,一个控件可以有多条验证规则 */ export interface IRule { /** * 验证时机:blur、change、click、keyup */ trigger?: "blur" | "change" | "click" | "keyup", /** * 提示消息 */ message?: string, /** * 必填 */ required?: boolean, /** * 数据类型:any、date、url等 */ type?: string, /** * 长度 */ len?: number, // 长度 /** * 最大值 */ max?: number, /** * 最小值 */ min?: number, /** * 正则 */ pattern?: string } /** * 表单的验证规则集合 */ export interface IRuleMeta { /** * 控件的ID作为key, 一个控件,可以有多条验证规则 */ [key: string | number]: Array<IRule> }
ILinkageMeta:组件联动的接口
有时候需要根据用户的选择显示对应的一组组件,那么如何实现呢?其实也比较简单,还是做一个key-value ,字段值作为key,需要显示的字段ID集合作为value。这样就可以了。
/** * 显示控件的联动设置 */ export interface ILinkageMeta { /** * 控件的ID作为key,每个控件值对应一个数组,数组里面是需要显示的控件ID。 */ [key: string | number]: { /** * 控件的值作为key,后面的数组里存放需要显示的控件ID */ [id: string | number]: Array<number> } }
- 根据选项,显示对应的组件
定义表单控件的 props。
interface 都定义好了,我们来定义组件的 props(实现接口)。
这里采用 Option API 的方式,因为可以从外部文件引入接口,也就是说,可以实现复用。
import type { PropType } from 'vue' import type { IFromMeta // 表单控件需要的 meta } from '../types/30-form' import type { IFormItem, IFormItemList } from '../types/20-form-item' import type { ESize } from '../types/enum' import { ESize as size } from '../types/enum' /** * 表单控件需要的属性 */ export const formProps = { /** * 表单的完整的 model */ model: { type: Object as PropType<any>, required: true }, /** * 根据选项过滤后的 model */ partModel: { type: Object as PropType<any>, default: () => { return {}} }, /** * 表单控件的 meta */ formMeta: { type: Object as PropType<IFromMeta>, default: () => { return {}} }, /** * 表单控件的子控件的 meta 集合 */ itemMeta: { type: Object as PropType<IFormItemList>, default: () => { return {}} }, /** * 标签的后缀 */ labelSuffix: { type: String, default: ':' }, /** * 标签的宽度 */ labelWidth: { type: String, default: '130px' }, /** * 控件的规格 */ size: { type: Object as PropType<ESize>, default: size.small } }
在组件里的使用方式
那么如何使用呢?很简单,用 import 导入,然后解构即可。
// 表单控件的属性 import { formProps, formController } from '../map' export default defineComponent({ name: 'nf-el-from-div', props: { ...formProps // 还可以设置其他属性 }, setup (props, context) { 略。。。 } })
这样组件里的代码看起来也会很简洁。
定义 json 文件
我们做一个简单的 json 文件:
{ "formMeta": { "moduleId": 142, "formId": 14210, "columnsNumber": 2, "colOrder": [ 90, 101, 100, 110, 111 ], "linkageMeta": { "90": { "1": [90, 101, 100], "2": [90, 110, 111] } }, "ruleMeta": { "101": [ { "trigger": "blur", "message": "请输入活动名称", "required": true }, { "trigger": "blur", "message": "长度在 3 到 5 个字符", "min": 3, "max": 5 } ] } }, "itemMeta": { "90": { "columnId": 90, "colName": "kind", "label": "分类", "controlType": 153, "isClear": false, "defValue": 0, "extend": { "placeholder": "分类", "title": "编号" }, "optionList": [ {"value": 1, "label": "文本类"}, {"value": 2, "label": "数字类"} ], "colCount": 2 }, "100": { "columnId": 100, "colName": "area", "label": "多行文本", "controlType": 100, "isClear": false, "defValue": 1000, "extend": { "placeholder": "多行文本", "title": "多行文本" }, "colCount": 1 }, "101": { "columnId": 101, "colName": "text", "label": "文本", "controlType": 101, "isClear": false, "defValue": "", "extend": { "placeholder": "文本", "title": "文本" }, "colCount": 1 }, "110": { "columnId": 110, "colName": "number1", "label": "数字", "controlType": 110, "isClear": false, "defValue": "", "extend": { "placeholder": "数字", "title": "数字" }, "colCount": 1 }, "111": { "columnId": 111, "colName": "number2", "label": "滑块", "controlType": 111, "isClear": false, "defValue": "", "extend": { "placeholder": "滑块", "title": "滑块" }, "colCount": 1 } } }
温馨提示:JSON 文件不需要手撸哦。
基于 el-form 封装,实现依赖 json 渲染。
准备工作完毕,我们来二次封装 el-table 组件。
<el-form :model="model" ref="formControl" :inline="false" class="demo-form-inline" :label-suffix="labelSuffix" :label-width="labelWidth" :size="size" v-bind="$attrs" > <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].label" :prop="itemMeta[ctrId].colName" :rules="ruleMeta[ctrId] ?? []" :label-width="itemMeta[ctrId].labelWidth??''" :size="size" v-show="showCol[ctrId]" > <component :is="formItemKey[itemMeta[ctrId].controlType]" :model="model" v-bind="itemMeta[ctrId]" > </component> </el-form-item> </transition> </el-col> </el-row> </el-form>
-
通过 props 绑定 el-table 的属性
props 里面定义的属性,直接绑定即可,比如:label-suffix="labelSuffix"
。 -
通过 $attrs 绑定 el-table 的属性
props 里面没有定义的属性,会保存在 $attrs 里面,可以通过v-bind="$attrs"
的方式绑定,既方便又支持扩展。 -
使用动态组件(component)加载表单子组件。
-
实现数据验证,设置 rules 属性即可,
:rules="ruleMeta[ctrId] ?? []"
。
实现多列
使用 el-row、el-col 实现多列的效果。
el-col 分为了24个格子,通过一个字段占用多少个格子的方式实现多列,也就是说,最多支持 24列。当然肯定用不了这么多。
所以,我们通过各种参数计算好 span 即可。篇幅有限,具体代码不介绍了,感兴趣的话可以看源码。
- 单列表单
- 双列表单
- 三列表单
-
多列表单
因为 el-col 的 span 最大是 24,所以最多支持24列。 -
支持调整布局
三列表单里面 URL组件就占用了一整行,这类的调整都是很方便实现的。
分栏
这里分为多个表单控件,以便于实现多种分栏方式,并不是在一个组件内部通过 v-if 来做各种判断,这也是我需要把 interface 写在单独文件里的原因。
<el-form :model="model" ref="formControl" :inline="false" class="demo-form-inline" :label-suffix="labelSuffix" :label-width="labelWidth" :size="size" v-bind="$attrs" > <el-tabs v-model="tabIndex" type="border-card" > <el-tab-pane v-for="(item, index) in cardOrder" :key="'tabs_' + index" :label="item.title" :name="item.title" > <el-row :gutter="15"> <el-col v-for="(ctrId, index) in item.colIds" :key="'form_' + ctrId + index" :span="formColSpan[ctrId]" v-show="showCol[ctrId]" > <transition name="el-zoom-in-top"> <el-form-item :label="itemMeta[ctrId].label" :prop="itemMeta[ctrId].colName" v-show="showCol[ctrId]" > <component :is="formItemKey[itemMeta[ctrId].controlType]" :model="model" v-bind="itemMeta[ctrId]" > </component> </el-form-item> </transition> </el-col> </el-row> </el-tab-pane> </el-tabs> </el-form>
- 分栏的表单(el-card)
- 分标签的表单(el-tabs)
- 分步骤的表单(el-steps)
使用 slot 实现自定义扩展。
虽然表单控件可以预设一些表单子控件,比如文本、数字、日期、选择等,但是客户的需求是千变万化的,固定的子控件肯定无法满足客户所有的需求,所以必须支持自定义扩展。
比较简单的扩展就是使用 slot 插槽,el-table 里面的 el-form-item 其实就是以 slot 的形式加入到 el-table 内部的。
所以我们也可以通过 slot 实现自定义的扩展:
<nf-form v-form-drag="formMeta" :model="model" :partModel="model2" v-bind="formMeta" > <template v-slot:text> <h1>外部插槽 </h1> <input v-model="model.text"/> </template> </nf-form>
nf-form 就是封装后的表单控件,设置属性和 model 后就可使用了,很方便。
如果想扩展的话,可以使用 <template v-slot:text>
的方式,里面的 【text】 是字段名称(model 的属性)。
也就是说,我们是依据字段名称来区分 slot 的。
实现 interface 扩展子组件
虽然使用 slot 可以扩展子组件,但是对于子组件的结构复杂的情况,每次都使用 slot 的话,明显不方便复用。
既然都定义 interface 了,那么为何不实现接口制作组件,然后变成新的表单子组件呢?
当然可以了,具体方法下次再介绍。
关于 TypeScript
-
为什么要定义 interface ?
定义 interface 可以比较清晰的表明结构和意图,然后实现接口即可。避免过段时间自己都忘记含义。 -
JSON 文件导入后会自动解析为 js 的对象,那么还用 interface 做什么?
这就比较尴尬了,也是我一直没有采用 TS 的原因之一。
TS只能在编写代码、打包时做检查,但是在运行时就帮不上忙了,所以对于低代码的帮助有限。
源码和演示
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/