【低代码】用JSON描述表单控件子组件之间的联动

4,521 阅读10分钟

组件的联动,其实也不难

低代码的表单控件,最大的问题就是如何实现灵活性,而灵活性又包含很多情况,像 el-form 提供了很高的灵活性,但是用起来比较繁琐,所以需要我们找到一个折中点。

首先考虑的是方便性,然后才去考虑灵活性,否则直接用 el-form 好了。

相关文章

数据的联动,比如省市区县级联

数据的联动比较简单,可以直接使用 UI 库提供的组件,比如 el-cascader 。所以本篇介绍的不是这个。

如果想自己控制组件的话,问题也不大,使用 watch 监听组件值,然后根据值加载需要的数据,绑定给下级组件即可。

以前要设置事件,或则监听事件,现在Vue提供了响应性的功能,所以我们可以使用监听的方式来实现,个人感觉用监听根方便一些。当然如果您喜欢使用事件,那么也可以。

组件的联动

组件的联动是什么样子的?还是用一个动图来描述一下:

10组件联动演示.gif

用户选择一个选项后,显示对应的组件,隐藏不需要的组件。

想一想这种情况要如何描述呢?

变量是什么?组件值 —— 需要显示的组件。

好像有办法了。

实现方式

先定义json,然后搞定内部代码。表单控件的实现方式可以看这里:【摸鱼神器】UI库秒变低代码工具——表单篇(一)设计

定义json的描述方式

"linkageMeta": {
  "90": {
    "1": [90, 101, 100, 102, 103, 104, 105, 106, 107, 108],
    "2": [90, 110, 111, 112],
    "3": [90, 120, 121, 122, 123, 124, 125, 126, 127, 128],
    "4": [90, 130, 131, 132],
    "5": [90, 150, 151, 152, 153],
    "6": [90, 160, 161, 162, 163, 165, 166, 164]
  } 
}

  • 90 : 这是 “分类” 组件的编号。
  • 1-6: 这是“分类”组件的六个选项值。当然也可以设置其他的值。
  • 数组:当用户选择某个选项后,依据数组里的编号显示对应的组件。

并不需要手撸

你说啥,全是魔数没法看!不要着急,我也没说要手动处理呀,我们提供了维护工具:

10组件联动的设置.gif

是不是很直观方便?目前支持选项型的组件,都可以依据选项值设置对应的组件。

表单控件内部代码的实现方式

那么这么神奇的功能是如何实现的呢?其实也挺简单的,就是用 watch 监听组件的值,然后根据json里的配置,设置对应的组件的 v-show。

为啥用 v-show?因为想加点动画效果,而动画效果需要使用 v-show 作为判断依据。所以只好用v-show 的方式来控制。

一开始是直接控制 colOrde 数组的。现在要增加 showCol 来记录组件的 v-show 值。

/**
 * 设置备选项和子控件的联动
 * @param formMeta 表单控件的meta
 * @param model 表单完整的 model
 * @param partModel 表单部分 的 model
 * @returns 
 */
export const setControlShow = (
  formMeta: IFromMeta,
  itemMeta: IFormItemList,
  model: any,
  partModel: any
) => {
  // 获取 配置信息
  const {
    linkageMeta, // 联动的描述
    colOrder // 字段的先后顺序
  } = formMeta
  
  // 设置字段的是否可见(为了加上动画效果)
  const showCol = reactive<ShowCol>({})
  // 依据meta设置,默认都可见
  const setShow = () => {
    if (typeof itemMeta === 'object') {
      for (const key in itemMeta) {
        showCol[key] = true
      }
    }
  }

  // 设置联动
  const setFormColShow = () => {
    setShow()
    // 数据变化,联动组件的显示
    if (typeof linkageMeta === 'object' && Object.keys(linkageMeta).length > 0) {
      // 遍历需要联动的组件
      for (const key in linkageMeta) {
        const ctl = linkageMeta[key] // 触发联动的组件
        const colName = itemMeta[key].formItemMeta.colName // 字段名称
        if (typeof model[colName] !== 'undefined') {
          // 监听 组件的值,有变化就重新设置局部 model
          watch(() => model[colName], (v1, v2) => {
            if (v1 === null) {
              // 清空选项,设为全部可见
              Object.keys(itemMeta).forEach(key => {
                showCol[key] = true
              })
            } else if (typeof ctl[v1] !== 'undefined') {
              // 隐藏表单里全部组件
              Object.keys(itemMeta).forEach(key => {
                showCol[key] = false
              })
              // 显示需要的组件
              ctl[v1].forEach(id => {
                showCol[id] = true
              })
              // 设置部分的 model
              createPartModel(model, partModel, itemMeta, showCol)
            }
          },
          { immediate: true })
        }
      }
      // 监听完整 model 的值的变化,同步值
      if (typeof partModel !== 'undefined') {
        watch(model, () => {
          for (const key in model) {
            if (typeof(partModel[key]) !== 'undefined') {
              partModel[key] = model[key]
            }
          }
        })
      }
    }
  }
  setFormColShow()
   
  return {
    showCol,
    setFormColShow
  }
}

这是表单控件里面组件联动部分的代码,简单地说,就是监听组件的值,然后设置其他对应的组件是否可见。

两种 model

一般一个表单只需要一个 model,但是现在有了组件的联动,如果还是一个的话,那就不太方便了,所以我们提供了两种 model,方便大家按需选择。

<nf-form
  v-form-drag="formMeta"
  :model="model" // 完整的 model
  :partModel="model2" // 仅显示的组件的 model
  v-bind="formMeta"
></nf-form>

完整的 model

完整的 model 包含所有的字段,不管显示与否。

model: {
  kind:1,
  text:'文本33',
  area:'多行文本11',
  pwd:'密码222',
  Email'Email',
  Tel'13800000000',
  url:'http://www.naturefw.com',
  Search'Search',
  Autocomplete'选填',
  Color'#3D0B0B',
  number1:2,
  number2:3,
  number3:4,
  date:'2022-5-02',
  dateRange1:[],
  datetime:'2022-5-03',
  dateRange2:[],
  month:'2022 03',
  monthR1:'',
  monthR2:'',
  monthR1_monthR2:[],
  year:2023,
  week:'',
  check2:'',
  其他略
}

仅显示的组件的 model

按照显示的组件组合的而成的 model。

有的时候,我们只需要记录显示的组件对应的值,隐藏起来的组件就不需要了,所以设置了第二种 model。和吧,其实是做维护 json 工具的时候,需要这种功能。

model2: {
  kind:1,
  text:'文本33',
  area:'多行文本11',
  pwd:'密码222',
  Email'Email',
  Tel'13800000000',
  url:'http://www.naturefw.com',
  Search'Search',
  Autocomplete'选填',
  Color'#3D0B0B'
}

维护json工具的组件联动

联动还用一个比较常见的需求,那就是维护JSON工具里面,给组件设置属性的时候。

UI库的组件一般提供了很多的属性,但是组件里的一些属性,并不是同时有效,比如 el-input 的 show-word-limit 只有在 “只在 type = "text"type = "textarea" 时有效”。

这时候需要有联动的功能,比如这样:

10组件联动演示2.gif

当我们选择组件类型的时候,只显示需要属性,避免混乱。

使用方式

使用的时候非常简单的,使用工具设置好json文件,然后引入json,绑定属性即可。

比如上面的例子,只需要设置这样的 json 即可:

{
  "formMeta": {
    "moduleId": 1001,
    "formId": 100110,
    "columnsNumber": 1,
    "colOrder": [
      90,
      10103, 10101,10105,
      10106, 10107,
      10001, 10002, 10006,
      10201,
      10801, 10802, 10804, 10803,
      500
    ],
    "linkageMeta": {
      "90": {
        "101": [ 90, 10103,10101,10105, 10106, 10107],
        "100": [ 90, 10103,10101,10105,  10001, 10002, 10006],
        "102": [ 90, 10103,10101, 10201],
        "103": [ 90, 10103,10101,10106, 10107],
        "104": [ 90, 10103,10101,10106, 10107],
        "105": [ 90, 10103,10101],
        "106": [ 90, 10103,10101,10106, 10107],
        "107": [ 90, 10103,10101, 500],
        "108": [ 90, 10801, 10802, 10804]
      }
    },
    "ruleMeta": {}
  },
  "itemMeta": {
    "90": {  
      "formItemMeta": {
        "columnId": 90,
        "colName": "controlType",
        "label": "类型",
        "controlType": 160,
        "defValue": 0,
        "colCount": 1
      },
      "placeholder": "类型",
      "title": "类型",
      "optionList": [
        { "value": 101, "label": "单行文本" },
        { "value": 100, "label": "多行文本" },
        { "value": 102, "label": "密码" },
        { "value": 105, "label": "URL" },
        { "value": 107, "label": "可选" },
        { "value": 108, "label": "颜色" },
        { "value": 106, "label": "查询" },
        { "value": 103, "label": "邮编" },
        { "value": 104, "label": "电话" }
      ]
    },
    "10101": {  
      "formItemMeta": {
        "columnId": 10101,
        "colName": "maxlength",
        "label": "最大输入长度",
        "controlType": 101,
        "defValue": "",
        "colCount": 1
      },
      "title": "最大输入长度",
      "placeholder": "最大输入长度"
    },
    "10103": {  
      "formItemMeta": {
        "columnId": 10103,
        "colName": "readonly",
        "label": "是否只读",
        "controlType": 151,
        "defValue": false,
        "colCount": 1
      },
      "title": "是否只读",
      "placeholder": "是否只读"
    },
    "10105": {
      "formItemMeta": {
        "columnId": 10105,
        "colName": "show-word-limit",
        "label": "显示输入字数",
        "controlType": 151,
        "defValue": false,
        "colCount": 1
      },
      "title": "显示输入字数",
      "placeholder": "显示输入字数"
    },
    "10106": {  
      "formItemMeta": {
        "columnId": 10106,
        "colName": "prefix-icon",
        "label": "前缀",
        "controlType": 107,
        "defValue": "",
        "colCount": 1
      },
      "placeholder": "自定义前缀图标",
      "title": "自定义前缀图标",
      "optionList": [
        { "value": "CloseBold", "label": "" },
        { "value": "Plus", "label": "" },
        { "value": "Star", "label": "" },
        { "value": "UserFilled", "label": "" },
        { "value": "Loading", "label": "" },
        { "value": "Connection", "label": "" },
        { "value": "Edit", "label": "" },
        { "value": "FolderOpened", "label": "" }
      ]
    },
    "10107": {  
      "formItemMeta": {
        "columnId": 10107,
        "colName": "suffix-icon",
        "label": "后缀",
        "controlType": 107,
        "defValue": "",
        "colCount": 1
      },
      "placeholder": "自定义后缀图标",
      "title": "自定义后缀图标",
      "optionList": [
        { "value": "CloseBold", "label": "" },
        { "value": "Plus", "label": "" },
        { "value": "Star", "label": "" },
        { "value": "UserFilled", "label": "" },
        { "value": "Loading", "label": "" },
        { "value": "Connection", "label": "" },
        { "value": "Edit", "label": "" },
        { "value": "FolderOpened", "label": "" }
      ]
    },
    "10201": {
      "formItemMeta": {
        "columnId": 10201,
        "colName": "show-password",
        "label": "显示密码图标",
        "controlType": 151,
        "defValue": "",
        "colCount": 1
      },
      "title": "显示切换密码图标",
      "placeholder": "显示切换密码图标"
    },
    "10001": {
      "formItemMeta": {
        "columnId": 10001,
        "colName": "rows",
        "label": "行数",
        "controlType": 110,
        "defValue": 0,
        "colCount": 1
      },
      "title": "多行文本的行数",
      "placeholder": "多行文本的行数"
    },
    "10002": {  
      "formItemMeta": {
        "columnId": 10002,
        "colName": "autosize",
        "label": "是否自适应",
        "controlType": 151,
        "defValue": false,
        "colCount": 1
      },
      "title": "是否自适应",
      "placeholder": "是否自适应"
    },
    "10003": {  
      "formItemMeta": {
        "columnId": 10003,
        "colName": "autosize",
        "label": "是否自适应",
        "controlType": 101,
        "defValue": "",
        "colCount": 1
      },
      "title": "是否自适应",
      "placeholder": "是否自适应"
    },
    "10006": {  
      "formItemMeta": {
        "columnId": 10006,
        "colName": "resize",
        "label": "缩放",
        "controlType": 107,
        "defValue": "",
        "colCount": 1
      },
      "placeholder": "控制是否能被用户缩放",
      "title": "控制是否能被用户缩放",
      "optionList": [
        { "value": "none", "label": "不行" },
        { "value": "both", "label": "都可" },
        { "value": "horizontal", "label": "横向" },
        { "value": "vertical", "label": "纵向" }
      ]
    },
    "10801": {  
      "formItemMeta": {
        "columnId": 10801,
        "colName": "show-alpha",
        "label": " 支持透明度",
        "controlType": 151,
        "isClear": false,
        "defValue": false,
        "colCount": 1
      },
      "title": " 支持透明度",
      "placeholder": "支持透明度"
    },
    "10802": {  
      "formItemMeta": {
        "columnId": 10802,
        "colName": "color-format",
        "label": " 颜色的格式",
        "controlType": 160,
        "isClear": false,
        "defValue": "",
        "colCount": 1
      },
      "placeholder": "颜色的格式",
      "title": " 颜色的格式",
      "optionList": [
        { "value": "hsl", "label": "hsl" },
        { "value": "hsv", "label": "hsv" },
        { "value": "hex", "label": "hex" },
        { "value": "rgb", "label": "rgb" }
      ]
    },
    "10803": {  
      "formItemMeta": {
        "columnId": 10803,
        "colName": "popper-class",
        "label": " 自定义图标",
        "controlType": 107,
        "isClear": false,
        "defValue": "",
        "colCount": 1
      },
      "placeholder": "自定义图标",
      "title": " 自定义图标",
      "optionList": [
        { "value": "CloseBold", "label": "" },
        { "value": "Plus", "label": "" },
        { "value": "Star", "label": "" },
        { "value": "UserFilled", "label": "" },
        { "value": "Loading", "label": "" },
        { "value": "Connection", "label": "" },
        { "value": "Edit", "label": "" },
        { "value": "FolderOpened", "label": "" }
      ]
    },
    "10804": {  
      "formItemMeta": {
        "columnId": 10804,
        "colName": "predefine",
        "label": " 预定义颜色",
        "controlType": 161,
        "isClear": false,
        "defValue": [],
        "colCount": 1
      },
      "placeholder": "预定义颜色",
      "title": " 预定义颜色",
      "optionList": [
        { "value": "#ff4500", "label": "" },
        { "value": "#ff8c00", "label": "" },
        { "value": "rgba(255, 69, 0, 0.68)", "label": "" },
        { "value": "rgb(255, 120, 0)", "label": "" },
        { "value": "hsv(51, 100, 98)", "label": "" },
        { "value": "hsva(120, 40, 94, 0.5)", "label": "" },
        { "value": "hsl(181, 100%, 37%)", "label": "" },
        { "value": "hsla(209, 100%, 56%, 0.73)", "label": "" }
      ],
      "multiple": true
    },
    "500": {
      "formItemMeta": {
        "columnId": 500,
        "colName": "optionList",
        "label": "备选项",
        "controlType": 170,
        "defValue": [],
        "colCount": 1
      },
      "title": "备选项",
      "placeholder": "备选项"
    }
  }
}

内部组件:

<template>
  <nf-form
    :model="model"
    :partModel="partModel"
    v-bind="extendFormProps"
  >
  </nf-form>
</template>

<script>
  import { defineComponent, reactive } from 'vue'
  import { getItemState } from '../state-item'
  import myWatch from './controller'

  import item_text from '../json/item-text.json'
 
  export default defineComponent({
    name: 'nf-meta-help-item-extend-test',
    inheritAttrs: false,
    setup(props) {
      // 文本类扩展属性表单,需要的 meta
      const extendFormProps = reactive(item_text)

      // 内部 model
      const state = getItemState()

      const {
        setup,
        model,
        partModel,
      } = myWatch(props)

      setup(model)

      return {
        state,
        model,
        partModel,
        extendFormProps
      }
    }
  })

</script>

把变化的部分放在 json 里面,不变的抽象为 hook,最后在 setup 里面组合即可。

我正在参与掘金技术社区创作者签约计划招募活动,点击链接报名投稿