likes
comments
collection
share

【手撸低代码工具】二次封装UI库(四)开始封装表单:定义 Interface、controller、验证、多列等

作者站长头像
站长
· 阅读数 44

上一篇把 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>

猜猜效果是啥?

a1a2
a3a4
a5a6

这样就是多列的效果了,也就是说,我们可以通过调整 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
评论
请登录