Vue 中 使用 json 定义生成表单(支持 ElementUi / iview / Ant Design Vue)

背景

之前已经在项目中实现过类似的功能,就是要通过后端接口返回的 json 动态输出相应的表单。但之前的实现方式是通过条件判断来根据 json 定义的类型输出相应的组件组成表单。本次遇到类似的需求,实现方式想通过 Vue 中的 component :is 的方式来实现每个动态组件的输出。

实现思路

因为都是使用现成的 ElementUi / iview / Ant Design Vue 的 Form 来按需进行对组件二次封装。那么,我们只需使用 Form 中的 Form Item 按需包住相应的控如 input、select、picker 等组件进行组件的二次封装即可。然后使用 Form Item 独立的 rules 单独编写的校验规则,非常方便。

本次设计的 Form Item 通用组件 Json Schema 如下(用于规范以后所有动态组件的定义):

      {
        "type": "object",
        "required": [
          "widget",
          "config",
          "uid"
        ],
        "properties": {
          "widget": {
            "type": "string"
          },
          "config": {
            "type": "object",
            "required": ["label"],
            "properties": {
              "label": {
                "type": "string"
              },
              "required": {
                "type": "boolean"
              },
              "options": {
                "type": "array"
              },
              "regExp": {
                "type": "string"
              }
            }
          },
          "value": {
            "type": [
              "null",
              "string",
              "object",
              "array",
              "number",
              "boolean"
            ]
          },
          "uid": {
            "type": "string"
          },
        }
      }

用 json 定义三个组件

1、一个输入框组件

        {
          widget: 'form-item-input',
          config: {
            label: 'My 名称',
            required: true,
            options: null,
            regExp: null
          },
          value: '',
          uid: '00001'
        }

2、一个下拉框组件

        {
          widget: 'form-item-select',
          config: {
            label: 'My 性别',
            required: true,
            options: [
              {
                label: '男',
                value: '1',
              },
              {
                label: '女',
                value: '2',
              }
            ],
            regExp: null
          },
          value: '',
          uid: '00002'
        }

3、一个级联下拉框组件

        {
          widget: 'form-item-cascade-select',
          config: {
            label: 'My 省市',
            required: true,
            options: [
              {
                id: 0,
                label: 'xx1省',
                value: '0',
                children: [
                  {
                    id: 1,
                    label: 'xx1市',
                    value: '1',
                  },
                  {
                    id: 2,
                    label: 'xx2市',
                    value: '2',
                  },
                  {
                    id: 3,
                    label: 'xx3市',
                    value: '3',
                  }
                ]
              },
              {
                id: 4,
                label: 'xx4省',
                value: '4',
                children: [
                  {
                    id: 5,
                    label: 'xx5市',
                    value: '5',
                  },
                  {
                    id: 6,
                    label: 'xx6市',
                    value: '6',
                  },
                  {
                    id: 7,
                    label: 'xx7市',
                    value: '7',
                  }
                ]
              }
            ],
            regExp: null
          },
          value: '',
          uid: '00003'
        }

整体组件封装设计思路(这里以 iview 的 Form 组件来作 Demo)

1、动态 Form 父组件
Vue Template 代码的设计

            <div class="dynamic-form">
              <Form :model="dynamicForm" label-position="right" :label-width="180">
                <Row type='flex' :gutter="0">
                  <Col span="18">
                  <Row type='flex' :gutter="0">
                    <Col v-for="(item, index) in dynamicFormUIArr" span="12" :key="index">
                      <component :ref="`dynamic_form_item_${index}`" :is="item.widget" :config="item.config" :sharedData="sharedData" v-model="item.value" :prop="`dynamic_form_prop_${index}`" @on-change="(val) => onChange(val, item, index)" />
                    </Col>
                  </Row>
                  </Col>
                  <Col span="6">
                  <div class="dynamic-form__btns">
                    <Button type="primary">查 询</Button><Button @click="resetDynamicForm">重 置</Button>
                  </div>
                  </Col>
                </Row>
              </Form>
            </div>

Vue js 关键代码的设计

export default {
  data () {
    return {
      dynamicForm: {},
      dynamicFormUiList: [
        {
          widget: 'form-item-input',
          config: {
            label: 'My 名称',
            required: true,
            options: null,
            regExp: null
          },
          value: '',
          uid: '00001'
        },
        {
          widget: 'form-item-select',
          config: {
            label: 'My 性别',
            required: true,
            options: [
              {
                label: '男',
                value: '1',
              },
              {
                label: '女',
                value: '2',
              }
            ],
            regExp: null
          },
          value: '',
          uid: '00002'
        },
        {
          widget: 'form-item-cascade-select',
          config: {
            label: 'My 省市',
            required: true,
            options: [
              {
                id: 0,
                label: 'xx1省',
                value: '0',
                children: [
                  {
                    id: 1,
                    label: 'xx1市',
                    value: '1',
                  },
                  {
                    id: 2,
                    label: 'xx2市',
                    value: '2',
                  },
                  {
                    id: 3,
                    label: 'xx3市',
                    value: '3',
                  }
                ]
              },
              {
                id: 4,
                label: 'xx4省',
                value: '4',
                children: [
                  {
                    id: 5,
                    label: 'xx5市',
                    value: '5',
                  },
                  {
                    id: 6,
                    label: 'xx6市',
                    value: '6',
                  },
                  {
                    id: 7,
                    label: 'xx7市',
                    value: '7',
                  }
                ]
              }
            ],
            regExp: null
          },
          value: '',
          uid: '00003'
        }
      ]
    }
  },

  computed: {
    sharedData () {
      const { dynamicFormUiList, dynamicForm } = this
      return {
        dynamicFormUiList,
        dynamicForm
      }
    }
  },

  created () {
    this.dynamicFormUiList.forEach((_, index) => {
      this.dynamicForm[`dynamic_form_prop_${index}`] = _.value
    })
  },

  methods: {
    onChange (val, item, index) {
      this.dynamicForm[`dynamic_form_prop_${index}`] = val
    },
    resetDynamicForm () { // 重置所有动态组件
      const arr = [...this.dynamicFormUiList]
      this.dynamicFormUiList = arr.map((_) => {
        return {
          ..._,
          value: null
        }
      })
      this.$nextTick(() => { // 注意要等数据更新完的 nextTick 再执行每个 form item 子组件的 reset
        dynamicFormUiList.forEach((_, index) => {
          this.$refs[`dynamic_form_item_${index}`][0].resetView()
        })
      })
    },
  }
}

2、Form Item 子组件(组件需 install 到全局中上面才能直接使用 component :is 直接使用,即 Vue.component(FormItemInput.name, FormItemInput)

<template>
  <FormItem :label="config.label" :prop="prop" :rules="rules" ref="formItem">
    <Input v-model="model" placeholder="请输入" @input="onInput" />
  </FormItem>
</template>

<script>
export default {
  name: 'FormItemInput',
  props: {
    prop: String,
    config: {
      type: Object,
      default: () => {
        return {
          label: '',
          required: false,
          options: null, // 其他下拉组件会用到
          regExp: null // 自定义正则校验
        }
      }
    },
    value: String
  },

  data () {
    return {
      model: '',
      rules: {
        required: this.required,
        validator: this.validate
      }
    }
  },

  model: { // 定义好即可使用 v-model
    prop: 'value',
    event: 'updateVal'
  },

  created () {
    this.reset()
  },

  methods: {
    validate (rule, val, callback) {
      if (this.config.required) {
        const value = this.model
        if (!value) {
          callback(new Error(`${this.label}不能为空`))
          return
        }
        if (this.regExp && !new RegExp(this.regExp).test(value)) {
          callback(new Error('输入不符合要求'))
          return
        }
        callback()
      } else {
        callback()
      }
    },
    onInput (val) {
      if (val !== this.value) {
        this.$emit('updateVal', val)
        this.$emit('on-change', val)
      }
    },
    // api for outer use
    resetView () {
      this.model = this.value
    }
  }
}
</script>

2、其他子组件开发
与输入框组件开发类似,主要也是定义好使用 v-model 后非常方便,但可能其他的组件使用时要注意避免重复给父组件 emit change,主要是 UI 框架的原因,可以定义一个 timer 来防抖动处理;其次,触发校验的事件也要自己定义,主要使用 this.$refs['formItem'].validate() 手动触发 Form Item 强行校验;接着是要处理视图更新时要使用 this.$set 来改变数组的值强制触发视图更新;最后,最好定义一个 resetView 方法用于初始化时给 model 赋值,也可以暴露给外面 Form 父组件来调用,用于 reset form

总结

这里与其他 Github 上的一些动态表单(如: form-createvue-form-making等等)实现思路有所不同。这里的实现方式是把所有表单 Form Item 组件实现用统一的 JSON 格式来定义,按功能需求通过对流行的 ElementUi / iview / Ant Design Vue 框架中的组件使用 Form Item 包起来进行组件二次封装,然后使用统一的 JSON 来定义。

    1. 本文作者:Nelson Kuang,欢迎大家留言及多多指教
    2. 版权声明:欢迎转载学习 => 请标注信息来源于 http://www.a4z.cn/fe/2020/06/09/vue-json-dynamic-form/

作者: 博主

Talk is cheap, show me the code!

发表评论

邮箱地址不会被公开。

Captcha Code