vue+element大型表单解决方案(8)--数据比对(上)__Vue.js__前端
发布于 2 个月前 作者 banyungong 216 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

theme: qklhk-chocolate highlight: vs2015

代码地址:https://gitee.com/wyh-19/super-form-solution.git
上篇代码分支:essay-7
本篇代码分支:essay-8

系列文章:

前言

表单数据比对并不是常见的需求,无非就是表单操作人员希望看到表单当前数据和变动之前的数据,当数据有差异时,给出提示并能查看之前的数据,很有点类似代码比对的意思。事实上本篇介绍的是简单表单的数据比对,不规则表单的比对确实借鉴了代码比对的一些交互和逻辑,这个要放在不规则表单介绍之后讲了。

准备工作

上篇末尾,演示了数据获取与回显,其中当表单形态是compare时,会先后获取两套数据,分别付给data里的formDataMapoldFormDataMap,其中oldFormDataMap就是老的表单数据。现在给各子表单增加属性传递,将oldFormDataMap传入各子表单,就像formDataMap一样,修改子表单组件的使用代码:

...
<form1 ref="form1" form-key="form1" :data="formDataMap" :old-data="oldFormDataMap"
           @validate="handleValidate" />
...
<form2 ref="form2" form-key="form2" :data="formDataMap" :old-data="oldFormDataMap"
           @validate="handleValidate" />
...

修改子表单组件共用的mixin文件,增加对oldData的数据处理,依次在props、data、computed、watch这四个部分增加对老数据的处理,代码如下(附原有的数据相关代码):

// props:
data: {
  type: Object,
  default: () => ({})
},
oldData: {
  type: Object,
  default: () => ({})
}

// data:
formData: {},
oldFormData: null,

// computed:
partFormData() {
  return this.data[this.formKey]
},
partOldFormData() {
  return this.oldData[this.formKey]
},

// watch:
partFormData: {
  handler(newValue) {
    this.formData = easyClone(newValue) || {}
  },
  immediate: true
},
partOldFormData(newValue) {
  // 当没有老数据时,希望就得到空
  this.oldFormData = easyClone(newValue)
}

要注意的地方是,初始化定义oldFormData为null,并且本地化oldFormData时,并没有强制给空的深拷贝结果转化为空对象,而是希望就得到结果null,目的是判断其为空时则不进行数据比对。

实现思路

显然,最容易想到的是在各个字段项后面加一个组件并用v-if判断,当存在旧数据,且该项的旧数据和新数据不同时,则显示该组件。但是由于我们做的是大型表单,总的表单项会很多,如果每项后面都跟一个v-if,显得特别不友好。幸好vue提供了自定义指令的功能,使得我们可以采用一种非侵入的方式增加dom结构,因此特别适合在这个场景使用。设计好自定义指令后,只需要在各个表单项上增加指令的使用即可,是否显示数据差异标志完全交给指令处理,这样远比在后面追加template代码要高效的多。

具体实现

新增指令

在src目录新增directives目录,并增加index.js和v-compare文件夹,v-compare目录下,新增js文件,并填入vue指令插件的标准代码,如下:

export default {
  install(Vue) {
      Vue.directive('compare', {
          // 钩子函数
      })
  }
}

directives目录下的index.js引入v-compare,并使用Vue.use(VCompare)注册组件,最后在系统入口处引入directives文件夹,即可在系统内完成组件的引用,具体操作不予赘述。

确定使用形式

现在某个表单项的代码如下:

<el-input v-model="formData.name" />

自然的,使用指令的形式应该是下面的样子:

<el-input v-model="formData.name" v-compare="oldFormData.name"/>

这样指令内部取到oldFormData.name和v-model的formData.name进行比较。但是之前我设计oldFormData时,是允许为null的。这里多说一句,因为我考虑到如果不这么设计的话,访问其中不存在的属性时,都是undefined,这样就没法区分这个属性是因为取回来就没有值还是根本就没有oldFormData。所以上面的写法至少要改成下面的形式,避免报错:

<el-input v-model="formData.name" v-compare="oldFormData&&oldFormData.name"/>

这样显得很啰嗦,不是我想要的样子,最终我采用如下的形式:

<el-input v-model="formData.name" v-compare:name="oldFormData"/>

选择生命周期钩子

通过vue官方文档,我们知道vue指令有如下五个生命周期钩子函数

  • bind:只调用一次,指令第一次绑定到元素时调用。
  • inserted:被绑定元素插入父节点时调用。
  • update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。
  • componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
  • unbind:只调用一次,指令与元素解绑时调用。 根据上面的使用形式,显然bind和inserted时,表单还没有拿到数据,是没法进行数据比较的。update和componentUpdated两个,这里我选择了后者,主要是比对的时机并不那么着急。

数据提取

指令钩子函数中能获取如下参数

  • el:指令所绑定的元素,可以用来直接操作 DOM。
  • binding:一个对象,包含以下 property:
    • name:指令名,不包括 v- 前缀。
    • value:指令的绑定值,例如:v-my-directive=“1 + 1” 中,绑定值为 2。
    • oldValue:指令绑定的前一个值,仅在 update 和 componentUpdated 钩子中可用。无论值是否改变都可用。
    • expression:字符串形式的指令表达式。例如 v-my-directive=“1 + 1” 中,表达式为 “1 + 1”。
    • arg:传给指令的参数,可选。例如 v-my-directive:foo 中,参数为 “foo”。
    • modifiers:一个包含修饰符的对象。例如:v-my-directive.foo.bar 中,修饰符对象为 { foo: true, bar: true }。
  • vnode:Vue 编译生成的虚拟节点。
  • oldVnode:上一个虚拟节点,仅在 update 和 componentUpdated 钩子中可用。 以上摘自官方文档,这里要用到如下参数:
  1. el:即挂载指令的表单项组件的dom结构,对其进行dom处理,增加额外的dom结构并给其绑定事件。
  2. value和oldValue:即组件更新前后v-compare指令右侧表达式的值,也就是传入的oldFormData,当oldFormData从无数据到有数据时,才进行一次比对,即!oldValue && value
  3. arg:即表达式v-compare:name中的name,用于表明字段的名字
  4. modifiers:表达式的修饰符,这里暂时未用到,后面会使用。
  5. vnode:vue编译的虚拟节点,可从中访问到v-model里的数据,从而进行数据比较。

编码实现

上面做了大量的基础讲解和准备工作,下面开始具体实现,增加钩子函数:

componentUpdated(el, binding, vnode) {
    const { value, oldValue, arg } = binding
    // oldFormData从无数据到有数据时,才进行比对
    // 避免数据更新过多无效的比对
    if (!oldValue && value) {
      // 进入此if判断时才真正有比对功能
      // 最新的数据,即v-model里现在绑定的值
      const lastModel = vnode.data.model.value
      // 之前的数据,即oldFormData[arg]
      const beforeModel = value[arg]
      // 如果两个数据不相同
      if (lastModel != beforeModel) {
        // 打上标记
        markDiffrent(el, beforeModel)
      }
    }
}

具体的标记方法代码如下:

// 由于样式较多,创建dom结构时,直接使用css,css代码略
import './index.css'

function markDiffrent(el, text) {
  if (text === undefined || text === null || text === '') {
    text = '无数据'
  }
  text = text.toString()
  // 创建标志
  const pop = document.createElement('div')
  pop.className = 'v-compare-pop'
  // 创建标志上的小提示气泡
  const tip = document.createElement('div')
  tip.className = 'v-compare-tip'
  tip.style.display = 'none'
  const tipContent = document.createElement('div')
  tipContent.className = 'v-compare-tip-content'
  tipContent.textContent = text
  tip.appendChild(tipContent)
  // 给标志增加点击事件,切换tip显隐
  pop.addEventListener('click', handleClick, true)
  pop.appendChild(tip)
  el.appendChild(pop)
  // 将pop元素缓存在el中,便于销毁时访问到
  el.__pop__ = pop
}
// 点击控制隐藏显示
function handleClick(e) {
  e.stopPropagation()
  if (e.target.className === 'v-compare-pop') {
    const tip = e.target.childNodes[0]
    if (tip.style.display === 'none') {
      tip.style.display = 'block'
    } else {
      tip.style.display = 'none'
    }
  }
}

markDiffrent方法比较简单,就是指令所在组件的dom元素上在创建标志物dom元素,并给其增加一个tip提示功能。 由于比对指令绑定了点击事件,所以要在销毁指令时销毁这些事件:

unbind(el) {
    // 从el中找出引用的dom元素
    const pop = el.__pop__
    if (pop) {
      pop.removeEventListener('click', handleClick)
    }
}

给所有表单项依次加上比对指令:

form1.vue
...
<el-input v-model="formData.age" v-compare:age="oldFormData" />
...
form2.vue
...
<el-input v-model="formData.company" v-compare:company="oldFormData" />
...

效果如下:

image.png 到这里就初步完成了数据比对指令,但是只是对最简单的文本类表单项做了处理,那么select、radio等表单项怎么回显比对数据呢,这里由于篇幅和时间因素,留到下篇继续。谢谢您的阅读,欢迎提出指正意见!

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: wyh19 原文链接:https://juejin.im/post/6971056138431234056

回到顶部