随着文章的更新和代码的增长,为了方便阅读和实践,从这一篇开始我将代码发布到了gitee上,代码地址如下。如果要看最终效果,可以切换到本文对应的分支上;如果要顺着文章一步步实现,则切换到上一篇的分支上,然后对着文章操作即可。(第5篇之前没有分支,以后会陆续补上)
代码地址:gitee.com/wyh-19/supe…
上篇代码分支:essay-5
本篇代码分支:essay-6
系列文章:
- vue+element大型表单解决方案(1)–概览
- vue+element大型表单解决方案(2)–表单拆分
- vue+element大型表单解决方案(3)–锚点组件(上)
- vue+element大型表单解决方案(4)–锚点组件(下)
- vue+element大型表单解决方案(5)–校验标识
前言
上一篇实现了保存时校验表单,并把未通过的子表单标识在锚点中。美中不足的是,标记不会随表单填写自动标记,这给用户带来一定的困扰,这一篇将实现在锚点中自动标识表单的状态。
实现思路
Element的form提供了validate
事件,当任一表单项被校验后触发,可得到该表单项的校验结果。利用该事件提供的能力,当有任一表单项校验结果为false时,通知主表单该子表单校验失败;当所有表单项校验通过后,通知主表单该子表单校验通过。
实现过程
子表单增加validate事件
进入form1.vue文件,给el-form
增加validate事件,代码如下:
<el-form ref="form" :model="formData" :rules="rules" @validate="handleValidate" label-width="80px" size="small">
复制代码
相应的事件处理函数如下:
//!!!先给data增加:ruleResults: {},用于缓存结果
handleValidate(rule, isValid) {
// 记录表单项结果,用于判断是否所有项都通过
this.ruleResults[rule] = isValid
if (!isValid) {
// 只要本项失败,则通知主表单校验失败
this.$emit('validate', false)
} else {
let result = true
// 如果存在未通过的项,则本表单校验未通过
for (const key in this.ruleResults) {
if (!this.ruleResults[key]) {
result = false
break
}
}
// 当不存在未通过的项时,通知主表单校验通过
if (result) {
this.$emit('validate', true)
}
}
}
复制代码
当任何一项校验时,缓存于ruleResults
中,并检测如果ruleResults
所有项校验通过,则通过validate
事件传递true
值通知主表单成功,反之则传递false
通知失败。
回到主表单中,给form1组件增加validate
事件处理函数:
<form1 ref="form1" :data="formDataMap.form1" @validate="handleValidate('form1',$event)" />
复制代码
相应的处理函数如下:
handleValidate(formKey, isValid) {
// 通过formKey查找章节
const section = this.pageBlock.querySelector(`[data-for=${formKey}]`)
if (!isValid) {
section?.setAttribute('data-tip', '')
} else {
section?.removeAttribute('data-tip')
}
this.$refs['anchor'].reRender()
}
复制代码
逻辑也很简单,通过子表单的名字找到相应的章节,并根据子表单传递的结果打上或者去除标记,最后更新锚点。测试下效果,当表单项校验时,会自动更新右侧锚点状态,如下图所示:
抽取mixin
由于子表单form2中也要增加validate的事件处理函数,因此将form1中增加的代码移植到mixin中,迁移后的mixin代码如下:
import { easyClone } from '@/utils'
export default {
props: {
data: {
type: Object,
default: () => ({})
}
},
data() {
return {
formData: {},
ruleResults: {}
}
},
watch: {
data: {
handler(newValue) {
this.formData = easyClone(newValue) || {}
},
immediate: true
}
},
methods: {
validForm() {
let result = false
this.$refs['form'].validate((valid) => { result = valid })
return result
},
handleValidate(rule, isValid) {
// 记录表单项结果,用于判断是否所有项都通过
this.ruleResults[rule] = isValid
if (!isValid) {
// 只要本项失败,则通知主表单校验失败
this.$emit('validate', false)
} else {
let result = true
// 如果存在未通过的项,则本表单校验未通过
for (const key in this.ruleResults) {
if (!this.ruleResults[key]) {
result = false
break
}
}
// 当不存在未通过的项时,通知主表单校验通过
if (result) {
this.$emit('validate', true)
}
}
}
}
}
复制代码
在form2.vue中,只需要在el-form上添加@validate="handleValidate"
,并给主表单的form2组件增加@validate="handleValidate('form2',$event)"
,即可给子表单2也增加自动标识的能力。效果如下:
通过mixin,可以快速赋予同类组件相同的能力,后面添加新功能或者代码修改都将直接修改mixin文件。
代码优化
做到这里,代码需要做下面几点优化:
- 主表单中已多次出现
this.$refs['anchor'].reRender()
代码,需优化成本地的函数调用,这样可以更好的控制重绘(比如页面在某种状态下不需要锚点组件显示,此时函数体内可以做更细节的判断),增加本地的reRenderAnchor
函数,代码如下:
reRenderAnchor() {
if (this.$refs['anchor']) {
this.$refs['anchor'].reRender()
}
}
复制代码
并修改this.$refs['anchor'].reRender()
为this.reRenderAnchor()
。
- 当表单项的校验rule是change时,每次输入变化都会触发子表单中
el-form
组件的validate
事件,进而触发主表单中子表单的的validate
事件,最终转化成dom操作以及锚点重绘的代码执行,因此要使用防抖节省性能。在mixin中,对handleValidate函数进行改造,代码如下:
handleValidate: debounce(function(rule, isValid) {
// 记录表单项结果,用于判断是否所有项都通过
this.ruleResults[rule] = isValid
...代码未变动,省略
}, 200)
复制代码
- 现在主表单中对子表单的使用如下:
<form1 ref="form1" :data="formDataMap.form1" @validate="handleValidate('form1',$event)" />
...
<form2 ref="form2" :data="formDataMap.form2" @validate="handleValidate('form2',$event)" />
复制代码
其中ref使用表单key值暂时没有可优化办法,但是后面属性或者事件中又多次使用了form的Key值,随着业务的扩展,说不定还要更写多的formKey,比如:
<form1 ref="form1" :data="formDataMap.form1" :old-data="oldFormDataMap.form1" @validate="handleValidate('form1',$event)" />
复制代码
这样是不利于后期维护的,所以我想只指定一次formKey,因此将html修改成如下的样子:
<form1 ref="form1" form-key="form1" :data="formDataMap" @validate="handleValidate" />
...
<form2 ref="form2" form-key="form2" :data="formDataMap" @validate="handleValidate" />
复制代码
这样子就尽可能减少了form1
、form2
这样的书写次数,避免写错导致难以发现的错误。
为了支持上面的组件传参,需要修改mixin,在props中增加formKey,代码如下:
formKey: {
type: String,
default: ''
},
复制代码
由于父表单传入的data是完整的formDataMap
,因此需要在子表单中根据formKey拆解成自身需要的表单数据,增加partFormData
计算属性,代码如下:
computed: {
partFormData() {
return this.data[this.formKey]
}
}
复制代码
并将partFormData
本地化成formData
,代码如下:
watch: {
// data: {
// handler(newValue) {
// this.formData = easyClone(newValue) || {}
// },
// immediate: true
// },
// 之前是直接将data属性本地化成formData的
partFormData: {
handler(newValue) {
this.formData = easyClone(newValue) || {}
},
immediate: true
}
}
复制代码
最后修改handleValidate
中的两处this.$emit('validate', true/false)
为this.$emit('validate', this.formKey, true/false)
。通过这一系列的修改,完整的mixin代码如下:
import debounce from 'lodash.debounce'
import { easyClone } from '@/utils'
export default {
props: {
formKey: {
type: String,
default: ''
},
data: {
type: Object,
default: () => ({})
}
},
data() {
return {
formData: {},
ruleResults: {}
}
},
computed: {
partFormData() {
return this.data[this.formKey]
}
},
watch: {
partFormData: {
handler(n) {
this.formData = easyClone(n) || {}
},
immediate: true
}
},
methods: {
validForm() {
let result = false
this.$refs['form'].validate((valid) => { result = valid })
return result
},
handleValidate: debounce(function(rule, isValid) {
// 记录表单项结果,用于判断是否所有项都通过
this.ruleResults[rule] = isValid
if (!isValid) {
// 只要本项失败,则通知主表单校验失败
this.$emit('validate', this.formKey, false)
} else {
let result = true
// 如果存在未通过的项,则本表单校验未通过
for (const key in this.ruleResults) {
if (!this.ruleResults[key]) {
result = false
break
}
}
// 当不存在未通过的项时,通知主表单校验通过
if (result) {
this.$emit('validate', this.formKey, true)
}
}
}, 200)
}
}
复制代码
最终测试下效果,完全正常,代码也变成了我想要的样子,为后续的表单传参定了型。谢谢您的阅读,欢迎提出指正意见!