背景
之前已经在项目中实现过类似的功能,就是要通过后端接口返回的 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-create、vue-form-making等等)实现思路有所不同。这里的实现方式是把所有表单 Form Item 组件实现用统一的 JSON 格式来定义,按功能需求通过对流行的 ElementUi / iview / Ant Design Vue 框架中的组件使用 Form Item 包起来进行组件二次封装,然后使用统一的 JSON 来定义。
-
- 本文作者:Nelson Kuang,欢迎大家留言及多多指教
- 版权声明:欢迎转载学习 => 请标注信息来源于 http://www.a4z.cn/fe/2020/06/09/vue-json-dynamic-form/