背景
之前已经在项目中实现过类似的功能,就是要通过后端接口返回的 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/