引言
在工作中我经常需要处理一些复杂、动态表单,但是随着需求不断迭代,我们也许会发现曾经两三百行的.vue
文件现在不知不觉到了两千行,三千行,甚至更多...
这对于一个需要长期维护的项目,无疑是增加了很多难度。
因此,为了减小文件大小,优化表单组织的结构,我在日常的开发中实践出一种基于组件的表单拆分方法,同时还能保证所有的表单项是处于同一个el-form
中。
这对于一个一开始就没有做好文件组织,组件化的项目,有以下几个优点:
- 改动小!后续新增表单项基本不会改动以前的代码
- 基于组件!在逻辑上对表单项做出拆分,并在任何地方嵌入
- 易维护!化单个大组件为多个小组件,每个组件只专注于一部分表单。
表单拆分
接下来我们会通过完成一个实际表单的方式来介绍如何实践这种表单组织方式。
以element-ui
文档中的这个表单为例,接下来尝试用我们的方式来实现
首先假设我们当前有一个vue
文件 ./form/myForm.vue
html
<template>
<el-form ref="form" :model="form" label-width="140px">
...
</el-form>
<template>
<script>
export default {
name: 'myForm',
data() {
return {
form: {}
}
}
}
</script>
如果我们直接按照element-ui
的表单文档来写,那么我们的myForm.vue
文件可能就会变成这样:
html
<el-form ref="form" :model="form" label-width="80px">
<el-form-item label="活动名称">
<el-input v-model="form.name"></el-input>
</el-form-item>
<el-form-item label="活动区域">
<el-select v-model="form.region" placeholder="请选择活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
<el-form-item label="活动时间">
<el-col :span="11">
<el-date-picker type="date" placeholder="选择日期" v-model="form.date1" style="width: 100%;"></el-date-picker>
</el-col>
<el-col class="line" :span="2">-</el-col>
<el-col :span="11">
<el-time-picker placeholder="选择时间" v-model="form.date2" style="width: 100%;"></el-time-picker>
</el-col>
</el-form-item>
<el-form-item label="即时配送">
<el-switch v-model="form.delivery"></el-switch>
</el-form-item>
<el-form-item label="活动性质">
<el-checkbox-group v-model="form.type">
<el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>
<el-checkbox label="地推活动" name="type"></el-checkbox>
<el-checkbox label="线下主题活动" name="type"></el-checkbox>
<el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
</el-checkbox-group>
</el-form-item>
<el-form-item label="特殊资源">
<el-radio-group v-model="form.resource">
<el-radio label="线上品牌商赞助"></el-radio>
<el-radio label="线下场地免费"></el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="活动形式">
<el-input type="textarea" v-model="form.desc"></el-input>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="onSubmit">立即创建</el-button>
<el-button>取消</el-button>
</el-form-item>
</el-form>
<script>
export default {
data() {
return {
form: {
name: '',
region: '',
date1: '',
date2: '',
delivery: false,
type: [],
resource: '',
desc: ''
}
}
},
methods: {
onSubmit() {
console.log('submit!');
}
}
}
</script>
假设我们还需要为这个表单增加审批流程,例如文档中的这个表单
在加入新的表单项后,可能目前看着还好,但是随着表单项越来越多,这个文件会变得越来越大,越来越难以维护。所以我们尝试将这个表单项拆分为单个组件,模拟我们维护一个超大表单的场景。
新增子表单项组件
我习惯在当前表单的目录下,创建一个components
目录,然后在components
目录下创建一个audit
目录,并在audit
目录下创建一个index.vue
文件,用于存放审批流程相关的组件。如果后续有一些只有audit/index.vue
文件中才用到的组件,我也会放在audit
目录下。保持目录结构清晰。
html
<template>
<div class="audit-form-item">
<el-form-item label="审批人" :prop="`${propPrefix}.user`">
<el-input v-model="form.user" placeholder="审批人"></el-input>
</el-form-item>
<el-form-item label="活动区域" :prop="`${propPrefix}.region`">
<el-select v-model="form.region" placeholder="活动区域">
<el-option label="区域一" value="shanghai"></el-option>
<el-option label="区域二" value="beijing"></el-option>
</el-select>
</el-form-item>
</div>
</template>
<script>
export const auditFormData = () => ({
user: '',
region: ''
})
export default {
name: 'auditFormItem',
props: {
value: {
type: Object,
default: () => auditFormData()
},
propPrefix: {
type: String,
default: ''
}
},
data() {
return {
form: this.value
}
},
watch: {
value(newVal) {
this.form = newVal
},
form(newVal) {
this.$emit('input', newVal)
}
}
}
</script>
因为element-ui
在对表单进行校验时,实际上是对model
上绑定的数据进行校验,所以为了能够对数据正确执行校验,我们需要在auditFormItem
组件中实现v-model
指令。
auditFormItem
组件的propPrefix
属性用于指定表单项的前缀,便于我们在嵌入到el-form
中时,能够正确绑定表单项的prop
属性。
auditFormData
函数返回了当前表单项的默认数据。父组件通过执行该函数,可以对子表单执行正确的初始化。不仅如此,通过这种方式,我们将每个子表单项的数据和组件绑定在一起,避免了父组件data中出现大量表单项数据,导致难以维护的问题。每个子表单维护各自的数据,互不干扰。
如何嵌入已有项目
接下来我们尝试将auditFormItem
组件嵌入到myForm.vue
文件中
html
<template>
<el-form ref="form" :model="form" label-width="140px">
<!-- 其他表单项 -->
<!-- ... -->
<audit-form-item v-model="form.audit" propPrefix="audit"></audit-form-item>
</el-form>
</template>
<script>
import auditFormItem, { auditFormData } from './components/audit/index.vue'
export default {
components: {
auditFormItem
},
data() {
return {
form: {
audit: auditFormData()
}
}
}
}
</script>
如何进行校验
经过上面的操作,我们实现了将一个表单拆分为多个子表单项,那么如何进行表单校验呢?
我们知道在element-ui
中,要对一个表单项进行校验有两种方式:
一种是在el-form
上绑定rules
属性,它会通过prop
进行索引,自动对绑定的表单项进行校验。
另一种是在el-form-item
上绑定rules
属性,这会对单条表单项进行校验。
出于我们拆分表单项的场景,我们选择第二种方式,在el-form-item
上绑定rules
属性,然后在各个子组件中维护rules
。如果有一些通用的校验规则,我们也可以在audit/validate.js
文件中进行维护,然后通过import
的方式引入。
如何处理联动校验
在复杂表单中,我们可能需要对多个表单项进行联动校验,例如:实时校验表单项的合法性,当form.region
为shanghai
时,form.user
不能为空,当form.region
为beijing
时,form.user
必须为空。
那么如何处理这种联动校验呢?
我们可以在el-form
所在的组件中,定义一个validate方法,通过element-ui
提供的validateField
方法,对特定进行校验。
js
<audit-form-item v-model="form.audit" propPrefix="audit" @validate="validate"></audit-form-item>
methods: {
validate(fields) {
this.$refs.form.validateField(fields)
}
}
然后在audit/index.vue
文件中,我们可以通过$emit('validate', fields)
方法,对当前表单项进行校验。
如何处理跨组件的联动校验
在我们拆分表单为多个子组件后,还可能会出现不同子组件之间的联动校验,例如:当子组件1中的form.region
为shanghai
时,子组件2中的form.user
不能为空。
对于这个问题,其实我目前还没有想到很好的解决方案,当前是在el-form
所在的组件中,定义额外的校验规则,然后绑定到一个空白的el-form-item
上。
js
<el-form-item label="" label-width="0" prop="_form_validate_"></el-form-item>
rules: {
_form_validate_: {
validator: (rule, value, callback) => {
// 联动校验逻辑
}
}
}
也可以考虑用vuex来维护需要跨组件共享的数据。
js
const crossCmpConfig = {
state: {
region: '',
},
mutations: {
UPDATE: (state, { key, val }) => {
state[key] = val
},
},
}
export default crossCmpConfig
多层嵌套
基于上面的方式,很容易就能想到,我们甚至可以继续在audit/index.vue
文件中,继续嵌入别的子组件,例如:audit/audit-info/index.vue
, 然后通过相同的方式,继续嵌入到audit/index.vue
文件中。
结语
上面就是我日常开发中,处理复杂表单的一些经验总结,希望对大家有所帮助。