vue+element大型表单解决方案(10)--表单通信和动态表单

6,202 阅读3分钟

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

系列文章:

前言

这个大型表单解决方案的本质在于将大表单拆分成多个子表单,这个在表单拆分篇中已经完整演示说明过;后面大量篇幅都是我在实践中遇到的附加需求,比如辅助锚点工具、数据比对等。实践中,不仅仅有演示中那些简单的表单,还有复杂的一些场景,比如有些字段存在联动交互,当字段分散在不同子表单中,就涉及子表单通信问题;还有些子表单可能是动态变化的。这一篇就结合字段联动和动态表单进行复杂表单实践。

子表单通信

子表单通信本质就是组件之间通信,只是在这个表单拆分的设计方案里,会有一些可优化可规范的地方,进而达到简化和减少样板代码的目的。比如,form1.vue中选择学历这个字段,需要把选择结果通知给form2.vue,正常做法就是将学历change后的值传递给总表单index.vue,再通过index.vue传递给form2.vue中供其使用。总表单index.vue成为各个子表单之间的数据传递通道,如果子表单之间联动的字段比较多,在index.vue中逐个字段维护会变得比较散乱。实践中,整个通信流程不变,只是我对这些字段进行了收纳管理,完整的代码流程如下:

  1. 给form1.vue中学历字段增加change事件,向上传递数据
<el-select v-model="formData.education"
           v-compare:education.map="{oldFormData,map:composeOptions(educationList)}"
           @change="$emit('field-change','education',$event)">
    <el-option v-for="item in educationList" :key="item.value" :value="item.value"
           :label="item.label"></el-option>
</el-select>
  1. index.vue中,给form1组件增加field-change的事件处理函数,并在data中增加linkChannel: {},该字段用于收纳管理所有的联动字段,并将linkChannel传给所有子表单组件
...
<!--增加field-change事件和link-channel属性-->
<form1 ref="form1" form-key="form1" :data="formDataMap" :old-data="oldFormDataMap"
       :link-channel="linkChannel" @validate="handleValidate"
       @field-change="handleFieldChange" />
...    
<form2 ref="form2" form-key="form2" :data="formDataMap" :old-data="oldFormDataMap"
       :link-channel="linkChannel" @validate="handleValidate" />
methods: {
    handleFieldChange(key, value) {
        this.$set(this.linkChannel, key, value)
    },
    ...
}
  1. 在super-form-mixin中增加linkChannel这个props,使得所有子表单都可以接受该prop
props:{
    ...
    linkChannel: {
      type: Object,
      default: () => ({})
    }
}
  1. 在form2.vue中,增加对目标字段的监听,实现需要的逻辑,这里只是简单打印一下
 watch: {
    'linkChannel.education': {
      handler(v) {
        console.log(v)
      }
    }
}

实践中,所有的字段联动都维护在linkChannel中,正如其名,是子表单之间联动通信的通道。

动态表单

有时候会遇到表单动态增删的情况,比如增加如下业务场景:增加一个子表单用于填写用户的工作履历,可动态添加过往的工作信息,并有相关的字段校验,其中公司名是必填的,并且该司司龄小于10个月时,离职原因必填,下面实现这些需求。

  1. index.vue中设计字段,并处理表单分组
// data中初始化数据
 formDataMap: this.resolveDataToMap({
    name: '',
    age: undefined,
    education: undefined,
    gender: undefined,
    hobby: [],
    company: '',
    // 增加工作履历字段
    workList: [{}]
  }),
  
// methods中给resolveDataToMap函数增加form3分组
resolveDataToMap(data) {
  const form1 = {
    name: data.name,
    age: data.age,
    education: data.education,
    gender: data.gender,
    hobby: data.hobby
  }
  const form2 = {
    company: data.company
  }
  // 增加form3
  const form3 = {
    workList: data.workList
  }
  return {
    form1,
    form2,
    form3
  }
},
  1. 添加form3.vue
<template>
  <el-form ref="form" :model="formData" :disabled="formDisabled" :rules="rules" label-width="80px"
           size="small" @validate="handleValidate">
    <el-button type="primary" size="small" @click="handleAdd">增加</el-button>
    <div v-for="(item,index) in formData.workList" :key="index">
      <el-button size="mini" @click="handleRemove(index)">删除</el-button>
      <el-form-item label="公司" prop="company" class="field-wrapper">
        <el-input v-model="item.company" />
      </el-form-item>
      <el-form-item label="司龄" class="field-wrapper">
        <el-input-number v-model="item.month" />
        <span>(个月)</span>
      </el-form-item>
      <el-form-item label="离职原因" prop="reason" class="field-wrapper">
        <el-input v-model="item.reason" />
      </el-form-item>
    </div>
  </el-form>
</template>
<script>
import SuperFormMixin from '@/mixins/super-form-mixin'
export default {
  name: 'Form3',
  mixins: [SuperFormMixin],
  data() {
    return {
      rules: {
        company: [
          { required: true, message: '请输入公司名称', trigger: ['change', 'blur'] }
        ],
        reason: [
          { required: true, message: '请输入离职原因', trigger: ['change', 'blur'] }
        ]
      }
    }
  },
  methods: {
    handleAdd() {
      this.formData.workList.push({})
    },
    handleRemove(i) {
      this.formData.workList.splice(i, 1)
    }
  }
}
</script>

以上代码实现了基本的页面结构需求,效果如下:

image.png 此时,新增和删除都是正常的,唯独校验存在问题。首先公司输入了名字,依然提示请输入;其次离职原因是根据司龄动态决定是否需要校验的。下面来分析并解决这两个问题。
当普通表单(比如form2)的formItem指定prop后,form组件会根据prop找到model中改字段的值,并和rules中与prop匹配的规则进行校验;而这里不再是普通表单,而是循环遍历的动态表单,要让form组件找到某一项的值,需要给其完整的路径,并单独指定校验规则,修改公司项代码如下:

<el-form-item label="公司" :prop="`workList.${index}.company`" :rules="rules.company" class="field-wrapper">
    <el-input v-model="item.company" />
</el-form-item>

上述代码中,指定了prop为workList.${index}.company,即该表单项在model中的完整路径,form组件可以根据路径找到其值,并指定了rules,这样就可以实现校验。同理,离职原因校验规则是动态的,修改代码如下:

<el-form-item label="离职原因" :prop="`workList.${index}.reason`"
              :rules="item.month <10 ?rules.reason:{}" class="field-wrapper">
    <el-input v-model="item.reason" />
</el-form-item>

此时动态表单的校验逻辑已正常实现,效果如下图,当司龄小于10个月时,离职原因为必填项:

image.png 当所有校验通过时,输出如下数据:

image.png

后记

本来还计划写一下动态表单的数据比对,奈何实践中确实过于复杂,涉及的准备工作就很多,一直想写又感觉无从下手(可能哪天会补上...)。后面由于工作变动的原因,将以react为主要技术栈,但是这一年对vue的学习使用,是我最充实的一年。通过本系列文章的编写,我也积累了不少经验,其中最大的一条就是这种手把手实践类的系列文章真的很容易越写越水,以后将主要写归纳总结类和原理分析类的文章。到这里,这个系列就已经写完了。