vue+element大型表单解决方案(6)--自动标识

1,845 阅读4分钟

随着文章的更新和代码的增长,为了方便阅读和实践,从这一篇开始我将代码发布到了gitee上,代码地址如下。如果要看最终效果,可以切换到本文对应的分支上;如果要顺着文章一步步实现,则切换到上一篇的分支上,然后对着文章操作即可。(第5篇之前没有分支,以后会陆续补上)

代码地址:gitee.com/wyh-19/supe…
上篇代码分支:essay-5
本篇代码分支:essay-6

系列文章:

前言

上一篇实现了保存时校验表单,并把未通过的子表单标识在锚点中。美中不足的是,标记不会随表单填写自动标记,这给用户带来一定的困扰,这一篇将实现在锚点中自动标识表单的状态。

实现思路

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()
}

逻辑也很简单,通过子表单的名字找到相应的章节,并根据子表单传递的结果打上或者去除标记,最后更新锚点。测试下效果,当表单项校验时,会自动更新右侧锚点状态,如下图所示:

image.png

抽取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也增加自动标识的能力。效果如下:

image.png 通过mixin,可以快速赋予同类组件相同的能力,后面添加新功能或者代码修改都将直接修改mixin文件。

代码优化

做到这里,代码需要做下面几点优化:

  1. 主表单中已多次出现this.$refs['anchor'].reRender()代码,需优化成本地的函数调用,这样可以更好的控制重绘(比如页面在某种状态下不需要锚点组件显示,此时函数体内可以做更细节的判断),增加本地的reRenderAnchor函数,代码如下:
reRenderAnchor() {
  if (this.$refs['anchor']) {
    this.$refs['anchor'].reRender()
  }
}

并修改this.$refs['anchor'].reRender()this.reRenderAnchor()

  1. 当表单项的校验rule是change时,每次输入变化都会触发子表单中el-form组件的validate事件,进而触发主表单中子表单的的validate事件,最终转化成dom操作以及锚点重绘的代码执行,因此要使用防抖节省性能。在mixin中,对handleValidate函数进行改造,代码如下:
handleValidate: debounce(function(rule, isValid) {
  // 记录表单项结果,用于判断是否所有项都通过
  this.ruleResults[rule] = isValid
  ...代码未变动,省略
}, 200)
  1. 现在主表单中对子表单的使用如下:
<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" />

这样子就尽可能减少了form1form2这样的书写次数,避免写错导致难以发现的错误。 为了支持上面的组件传参,需要修改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)
  }
}

最终测试下效果,完全正常,代码也变成了我想要的样子,为后续的表单传参定了型。谢谢您的阅读,欢迎提出指正意见!