【手撸低代码工具】二次封装UI库(四)开始封装表单:定义 Interface、controller、验证、多列等
上一篇把 Interface 的命名和分类整理清晰,然后我们可以开始封装表单控件了。
UI库提供的表单组件很强大、灵活,也附带了各种功能,只是用起来稍微有点麻烦,比如我想实现多列的表单,字段的先后顺序需要调整,还有分栏显示、分tabs显示,做一个 step 的步骤提示等。都需要一点一点写代码,这不符合程序员懒惰的特性!
所以,我们开始封装!
定义 Interface
自从会了 Typescript 之后,就喜欢先定义一批 Interface,把需要的参数都定义好,基本可以当做文档来使用了。
表单控件的 props
先给表单控件定义一个 Interface 用于 props:
export interface IFromProps<T extends object> {
model: T,
partModel?: any,
formMeta: IFromMeta,
childPropsList: IFormChildPropsList,
// 标签的后缀,string
labelSuffix?: string,
// 标签的宽度,string
labelWidth?: string
}
- model:表单的 model
- partModel:筛选后的 model
- formMeta:表单控件需要的 meta
- childPropsList:表单里面的 input 这些需要的 props
- 其他:el-form 需要的 props。
表单控件需要的 meta
export interface IFromMeta {
moduleId: number | string,
formId: number | string,
colOrder: Array<number | string>,
columnsNumber: 1 | 2 | 3 | 4 | 6 | 8 | 12 | 24
subMeta?: ISubMeta,
ruleMeta: IRuleMeta,
linkageMeta: ILinkageMeta,
customerControl?: any,
}
- moduleId、formId:模块ID和表单ID,一个模块可以有多个表单。
- colOrder:显示哪些字段,以及字段的先后顺序
- columnsNumber:表单的列数,支持1、2、3、4、6列
- subMeta:分栏的设置
- ruleMeta:验证信息
- linkageMeta:联动筛选用
- customerControl:扩展用的
el-form-item 需要的 props
export interface IFormItemMeta<T extends object> {
colOrder: Array<number | string>,
childProps: IFormChildPropsList,
childMeta: IFormChildMetaList,
ruleMeta: IRuleMeta,
showCol: {[key: number | string]: boolean},
formColSpan: {[key: number | string]: number} ,
model: T
}
- colOrder:字段排序依据,也是显示依据。数组,v-for 的依据
- childProps:input 的 props 集合, 含 meta
- childMeta:验证规则,(直接使用)
- showCol:联动的时候,是否显示的依据。
- formColSpan:el-col 的 span 属性,一个字段使用多少个 span
- model:表单对象
表单验证需要的 meta
UI库的表单验证一般是基于 github.com/yiminghe/as… 实现的,所以我们可以去看看规则,然后定义一套 Interface 先。
// 一条验证规则,一个控件可以有多条验证规则
export interface IRule {
trigger?: "blur" | "change" | "click" | "keyup",
message?: string,
required?: boolean,
type?: IRuleType,
len?: number,
max?: number,
min?: number,
pattern?: string
}
- trigger:验证时机
- message:提示消息
- required:必填
- type:数据类型
- len:长度
- max:最大值
- min:最小值
- pattern:验证用的正则
这是支持的数据类型
type IRuleType =
| 'string' | 'number' | 'boolean' | 'integer' | 'float'
| 'method' | 'regexp'
| 'object' | 'array' | 'enum' | 'date'
| 'hex' | 'email' | 'url'
| 'any'
这是验证信息的集合,key 是字段的ID或者名称,后面是一个数组,里面可以有多个验证规则。
export interface IRuleMeta {
/**
* 控件的ID作为key, 一个控件,可以有多条验证规则
*/
[key: string | number]: Array<IRule>
}
然后我们在json里面设置一段信息,直接对应验证信息。
"ruleMeta": {
"101": [
{ "trigger": "blur", "message": "请输入活动名称", "required": true },
{ "trigger": "blur", "message": "长度在 3 到 5 个字符", "min": 3, "max": 5 }
]
}
又见魔数,这个是字段的ID,也是表单里的input这类组件的ID,这样做的好处是,不怕字段改名了,你愿意咋改就咋改,反正我用编号。
不过 elementPlus 的 el-form 需要的 rule 的key,要求是字段名称,那么怎么办?做个转换吗? 一开始确实做了一个转换函数,不过后来发现 el-form-item 也可以设置 rule,这就不涉及字段名称的问题了,所以,还转换啥,直接给 el-form-item 设置不就行了吗。
关于 elementPlus 的 FormItemRule、 FormRules
以前没看到有这两个接口,这几天整理文档,又去官网查看,才发现提供了,本来想着直接用的,但是引入到项目里之后才想起来,我不仅想封装 element ,还想封装其他UI库,如果封装其他UI库的时候,还需要使用 elementPlus的两个 Interface ,是不是有点。。。
所以,还是决定用自己的。很好,给重复制造轮子找到了一个很好的借口。。。
封装的代码
创建一个管理表单控件的函数,统一进行各种设置和初始化。
创建 controller
/**
* 表单控件的管理类
* @param props 表单控件的 props,包含:表单的meta、child 的props、model等
* @returns
*/
export default function formController<T extends object> (
props: IFromProps<T>
) {
const {
formMeta, //: IFromMeta, 表单的 meta
childPropsList, //: IFormItemList<T>, 表单子组件的 meta
model, //: T, 表单的对象
partModel, // any
} = props
// 关于一个字段占用几个 span 的问题
const {
formColSpan, // 一个字段占几个td的对象
setFormColSpan // 设置的函数
} = getColSpan(formMeta.columnsNumber, childMetaList)
// 监听列数,变化后重新设置
watch(() => formMeta.columnsNumber, (num) => {
setFormColSpan(num)
}, { immediate:true })
// 设置联动筛选
const {
showCol,
setFormColShow
} = setControlShow(formMeta, childMetaList, model, partModel)
// 监听联动的变化,更新字段的显示
watch(formMeta.linkageMeta, () => {
setFormColShow()
})
watch(formMeta.colOrder, () => {
setFormColShow()
setFormColSpan()
})
return {
childMetaList,
formColSpan, // 确定组件占位
// formRules: rules, // 表单的验证规则
showCol, // 是否显示字段的对象
setFormColShow // 设置组件联动
}
}
接收参数,调用需要的函数进行处理,建立各种联动机制,实现基础功能。各个功能分为不同的函数来实现,便于维护和升级功能,Vue 把以前的监听事件,变成了监听响应式的变化,这样写逻辑的时候可以脱离DOM,代码更通用和灵活。
实现多列
el-form 可以纵向排列,也可以横向排列,但是却没有“多列”的方式,所以,给我们留下了一个发挥的空间。
如何实现多列?不知道大家有没有试过 el-row 的这种用法:
<el-row>
<el-col :span="12">a1</el-col>
<el-col :span="12">a2</el-col>
<el-col :span="12">a3</el-col>
<el-col :span="12">a4</el-col>
<el-col :span="12">a5</el-col>
<el-col :span="12">a6</el-col>
</el-row>
猜猜效果是啥?
a1 | a2 |
a3 | a4 |
a5 | a6 |
这样就是多列的效果了,也就是说,我们可以通过调整 span 的数值,指定列数。
还可以实现合并的效果。
于是出现了一道计算题,多列的情况下,span=?
列数 | span | 不能平均分的列 | span |
---|---|---|---|
一列 | 24 | 五列 | 24 / 5 = 4.8 |
两列 | 12 | 七列 | 24 / 7 = 3.43 |
三列 | 8 | 九列 | 24 / 9 = 2.67 |
四列 | 6 | ||
六列 | 4 | ||
八列 | 3 | ||
十二列 | 2 |
好在屏幕不是很宽,一般的表单都是一、两列,三列少见,四列以上那是查询的情况了。
虽然 span 的类型是 number,但不能是小数,这是js的锅,js的数字类型不区分整数、小数。
所以有效列数是:1、2、3、4、6、8、12、24列。不支持 5、7、9、10、11列(不能平分)。不过 1-4 列应该够用了。
然后就是写代码的问题了。
代码要分为两种情况:单列和多列
- 单列:需要考虑字段合并的情况,比如姓名和年龄,两个字段比较短,可以合并在一行显示。
- 多列:一行里面有两个或者多个字段,如果某个字段比较长,那么就需要独占一行,这个需求也应该支持。
所以我们要先建立一个字典和一个判断函数:
// 列数与 span 的字典
const dicColSpan = {
1: 24,
2: 12,
3: 8,
4: 6,
6: 4,
8: 3,
12:2,
24:1
}
/**
* 单列情况下,合并字段的情况计算span
*/
const getSpanForSinger = (colCount: -6 | -4 | -3 | -2 ) => {
switch(colCount) {
case -6:
return 4
case -4:
return 6
case -3:
return 8
case -2:
return 12
default:
console.error(`表单控件不支持【${colCount}】!单列合并请选择 -6、-4、-3、-2`)
break
}
}
- dicColSpan: 直接建立列数和 span 的关系,一列,每个字段是 24 ,两列,每个字段是 12,依此类推。
- getSpanForSinger:需要合并的情况,-2 表示占一行的二分之一(即 12),-3表示占一行的三分之一(即8),依此类推。
然后我们建立一个函数:
// 支持的列数
type IColNumer = 1 | 2 | 3 | 4 | 6 | 8 | 12 | 24
/**
* 处理一个控件占用几个 格子 的需求
* @param columnsNumber 表单控件的列数,支持1、2、3、4、6、8、12、24列
* @param itemMeta input 类的 meta
* @returns
*/
export const getColSpan = (columnsNumber: IColNumer, itemMeta: IFormChildMetaList ) => {
// 确定一个组件占用几个 span
const formColSpan = reactive<FormColSpan>({})
// 根据配置里面的 colCount,设置 formColSpan
const setFormColSpan = (num = columnsNumber) => {
// 表单的列数
const moreColSpan = dicColSpan[num] // 一个控件默认占多少格子
if (!moreColSpan) {
// 不支持的列数
console.error(`表单控件不支持【${num}】列!请选择 1、2、3、4、6、8、12、24列`)
return
}
// 遍历表单子控件属性
for (const key in itemMeta) {
const m = itemMeta[key]
if (typeof m.colCount === 'number') {
// 设置了一个控件占的份数
if (num === 1) {
// 单列
formColSpan[m.columnId] = (m.colCount >= 1) ?
moreColSpan : // 一个控件最大占24格子
getSpanForSinger(m.colCount as (-6 | -4 | -3 | -2 )) // 多控件占一行, 控件占的份数
} else {
// 多列
formColSpan[m.columnId] = (m.colCount < 1) ?
moreColSpan : // 多控件占一行的控件视为占一份
moreColSpan * m.colCount // 格子数 * 份数
}
if (formColSpan[m.columnId] > 24) {
formColSpan[m.columnId] = 24
}
} else {
// 没有设置,默认占一行
formColSpan[m.columnId] = moreColSpan
}
}
}
return {
formColSpan,
setFormColSpan
}
}
这样,多列就搞定了。
- 定义字典建立列数和 span 的对应关系,以前是直接用除法,出现小数都没有注意。
- 先做验证:ts只能验证编写时传入的数据,但是运行时就无能为力了,而我们做的是低代码,运行时才知道要显示几列,所以必要的验证还是需要的。
- 单列情况:主要考虑的是合并字段的情况,这里设定用负数(-2、-3、-4)表示合并后占据的份数,因为一开始是直接做除法的,这样表示比较方便,不过现在改为字典的方式,也就显得不易读了。
template
在template 里面的实现方式:
<el-form
v-bind="$attrs"
:model="model"
:inline="false"
>
<el-row :gutter="15"> // 不需要循环 el-row
<el-col
v-for="(ctrId, index) in colOrder" // 循环 el-col, 通过控制 span 实现多列
:key="'form_' + ctrId + '_' + index"
:span="formColSpan[ctrId]"
v-show="showCol[ctrId]"
>
<el-form-item
:label="childMeta[ctrId].label" // el-form-item 可以放在里面
:prop="childMeta[ctrId].colName"
:label-width="childMeta[ctrId].labelWidth??''"
:rules="ruleMeta[ctrId] ?? []"
v-show="showCol[ctrId]"
>
<component
:is="formItemKey[childMeta[ctrId].controlType]" // 表单里面的组件,比如 el-input 等
:model="model"
:meta="childMeta[ctrId]"
v-bind="childProps[ctrId].props"
>
</component>
</el-form-item>
</el-col>
</el-row>
</el-form>
一般 el-form 里面直接加 el-form-item,但是看了官网一个示例(找不到那个示例了)发现,其实还可以放 el-row,于是可以利用上面说的方式实现多列了。
其他功能后续再说。
转载自:https://juejin.cn/post/7242623149752647738