本次需求是一个比较复杂的数据录入表单(如下图所示),分成5个步骤,其中1,2,4,5步骤的子表单是比较简单的输入和选择,最复杂的是步骤3:分为6种组织类型,每种类型可以增(上限10个)删改其下的组织(公司),每个组织(公司)下可以增删改联系人(上限20个),同时需要对表单做即时验证,包括必填字段,组织(公司)名称判重,联系人判重…

在这里插入图片描述


开发之前,思考一下几个问题:
  1. 父子组件传值props是单向数据流吗?

  2. 浅拷贝和深拷贝的区别?

  3. 多级子组件的验证如何往父级回归

  4. 多级子组件的数据如何往父级回归

  5. promise.all 的作用是什么?


具体实现

首先,对整个表单进行拆分,按上图,可以拆成多个子组件:5个子表单组件 + 组织类型组件 + 组织(公司)组件 + 联系人组件 + 其它功能模块组件。

组织(公司)这一块,组织类型中循环嵌套公司组件,公司中再循环嵌套联系人组件… 听起来比较复杂,其实不然~ 想象我们每次只用考虑一个简单的表单组件,无论是增删改,还是获取表单数据,表单验证,都是很普通的代码。此处要考虑的就是两点:

  1. 父组件如何拿到子组件最新的值(子组件如何把值回传给父组件)
  2. 父组件如果获得子组件的验证结果

在下方贴出的代码中展示了这两点的实现方式。

我们可以这样认为,这一个大表单中层层嵌套的组件都差不多,除去针对各个表单自己的业务逻辑模块代码,剩下得就是值的传递和验证结果的传递。


回答开头抛出的几个问题
  1. 父子组件传值props是单向数据流吗?

    Vue官方明确说明props的传递是单向数据流,

    所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。

    往下阅读代码,你会发现,每个子组件都在改变父组件form中的值,但是并么有报错,这是不是违反了Vue关于props单项数据流的规定?继续阅读官方文档

    注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变变更这个对象或数组本身将会影响到父组件的状态。

    利用prop是对象类型,我们在此模仿实现出“双向数据流”,这为本次复杂表单的开发提供了很大的便利。虽然我们可以完全遵循单向数据量的规定,不直接在子组件修改prop的值,但这样就会增加一些复杂度。说到这里,可以继续看下一个问题。

  2. 浅拷贝和深拷贝的区别?

    子组件直接获取prop是浅拷贝,所以我们修改子组件form的值,父组件会同时变更。如果我们想让prop不被子组件修改,就需要用深拷贝。在此我们会根据实际需要,灵活使用浅拷贝或者深拷贝。

  3. 多级子组件的验证如何往父级回归?

    每个组件的验证单独写在组件中,如果需要在提交时回归到父组件,子组件中的验证方法应返回promise,这样在this.$refs.step1.validateForm() 调用子组件中的方法,一层一层往上返回即可。

    在此可以使用async,await简化我们的代码。

  4. 多级子组件的数据如何往父级回归?

    如果我们使用深拷贝prop的值,就要额外的写一个方法,让父组件调用后获取子组件的数据,原理和验证的回归类似。

  5. promise.all 的作用是什么?

    Promise.all可以将多个Promise实例包装成一个新的Promise实例。同时,成功和失败的返回值是不同的,成功的时候返回的是一个结果数组,而失败的时候则返回最先被reject失败状态的值。

    父组件会同时调用多个子组件的异步验证方法,我们需要保证所有子组件验证都完成才能交给父组件,这是就需要用promise.all。

    顺表提一下promise.race

    顾名思义,Promse.race就是赛跑的意思,意思就是说,Promise.race([p1, p2, p3])里面哪个结果获得的快,就返回那个结果,不管结果本身是成功状态还是失败状态。

整个模块代码太多,有不少是业务逻辑代码,故此处只贴出部分代码演示组件间传值

父表单 form.vue
<template>
    <div>
    <!--此处有省略-->
    <step-form1 v-if="form1.id" v-show="active==0" ref="step1" :options="options" :form="form1" />
      <step-form2 v-if="form2.id" v-show="active==1" ref="step2" :form="form2" />
      <step-form3 v-if="form3.id" v-show="active==2" ref="step3" :options="options" :form="form3" />
      <step-form4 v-if="form4.id" v-show="active==3" ref="step4" :options="options" :form="form4" />
      <step-form5 v-if="form5.id" v-show="active==4" ref="step5" :options="options" :form="form5" />
     <!--此处有省略-->
      <el-button type="primary" :loading="btnLoading" @click="submitForm">{{ btnLoading ?'正在提交':'直接提审' }} </el-button>
     </div>
</template>
// ...
data(){
    return {
      // ...
      postForm: {},
      form: { webid: '', tracking: '', arraigned: 0, pid: '', exStatus: '' },
     
      form1: { id: '', title:'',postTime:'', description:'' },
      form2: { id: '', content: '', progress: '' },
      form3: { id: '', companys: [] },
      form4: { id: '', tag: '', type:'' },
      form5: { id: '', editor:'', status:'' },
      // res1,res2。。。用于接收子表单数据
      res1: {}, res2: {}, res3: {}, res4: {}, res5: {},
      validResult: [], // 整个表单的验证结果
      // ... 
    }
},
created(){
    // ... 初始化表单的值,把回显的数据分给子表单的postForm
    this.setFormData(this.postForm, this.form1)
    this.setFormData(this.postForm, this.form2)
    this.setFormData(this.postForm, this.form3)
    this.setFormData(this.postForm, this.form4)
    this.setFormData(this.postForm, this.form5)
    // ...
},
methods:{
    // ... 
    setFormData(res, form) {
      for (const key in form) {
        form[key] = res[key]
      }
    },
    // ... 
    // 获取子表单的数据
    async getPostData() {
      const res1 = this.$refs.step1.postForm
      const res2 = this.$refs.step2.postForm
      const res4 = this.$refs.step4.postForm
      const res5 = this.$refs.step5.postForm
      const companys = await this.$refs.step3.getData()

      await this.$nextTick(() => {
        this.postForm = Object.assign(res1, res2, companys, res4, res5)
      })
    },

    // 表单提交
    submitForm() {
      const v0 = this.$refs.form.validate()
      const v1 = this.$refs.step1.validateForm()  // 调用子表单中的validateForm方法
      const v2 = this.$refs.step2.validateForm()
      const v3 = this.$refs.step3.validateForm()
      const v4 = this.$refs.step4.validateForm()
      const v5 = this.$refs.step5.validateForm()
		
      // 使用子表单的验证结果
      Promise.all([v1, v2, v3, v4, v5, v0]).then((res) => {
        this.validResult = res       
        if (res.every(val => val)) {
          this.btnLoading = true
          this.getPostData().then(res => {           
            updateProject({ ...this.postForm, ...this.form }).then(res => {
              this.btnLoading = false
              if (res.code === 1) {
                this.$message({
                  message: '操作成功',
                  type: 'success'
                })
              }
            }).catch(e => {
              this.btnLoading = false
            })
          })
        } else {
          this.$message({
            message: '请填写完整后再提交!',
            type: 'error'
          })
        }
      })
    }
    
    // ... 
}

表单1,2,4,5部分类似,此处展示form2.vue完整代码
<template>
  <el-form ref="postForm" :model="postForm" :rules="rules" size="small" label-width="120px" class="demo-ruleForm">
    <el-form-item label="详情" prop="content">
      <el-input v-model="postForm.content" type="textarea" :rows="5" show-word-limit maxlength="1000" />
    </el-form-item>
    <el-form-item class="item-notice" label="进展情况" prop="progress">
      <el-input v-model="postForm.progress" type="textarea" :rows="5" show-word-limit maxlength="1000" />
  </el-form>
</template>

<script>
export default {
  name: 'StepForm2',
  props: {
    form: {
      type: Object,
      default: () => {}
    }

  },
  data() {
    return {
      postForm: { },
      rules: {
        content: [
          { required: true, message: '请填写', trigger: 'blur' }
        ],
        progress: [
          { required: true, message: '请填写', trigger: 'blur' }
        ],        
      }
    }
  },

  created() {
    this.postForm = this.form
  },

  methods: {
    // form2的验证,通过 this.$refs.step2.validateForm() 被父组件调用
    async validateForm() {
      return this.$refs['postForm'].validate().then(valid => {
        return valid
      }).catch(e => {
        return false
      })
    }
  }
}
</script>

form3.vue
<template>
    <!--此处有省略-->
    <div v-for="group in postForm.companyGroups" :key="group.key">
          <contact-unit v-if="group.units.length" ref="group" :data="group" :options="options" />
        </div>
	<!--此处有省略-->
</template>

// ... 
import ContactUnit from './ContactUnit'

// ... 
data(){
    return {
        postForm: {
            companyGroups: [
              { key: 1, label: '组织类型1', units: [] },
              { key: 2, label: '组织类型2', units: [] },
              { key: 3, label: '组织类型3', units: [] },
              { key: 4, label: '组织类型4', units: [] },
              { key: 5, label: '组织类型5', units: [] },
              { key: 6, label: '组织类型6', units: [] }
            ]
     	},
    }
},
methods:{
    // 因为要传回符合后端接口的数据,这里对数据做一些处理
    getData() {
      const temp = this.postForm.companyGroups
      const res = []
      temp.forEach((val, key) => {
        if (val.units && val.units.length) {
          val.units.forEach(company => {
            res.push({
              type: key,
              ...company
            })
          })
        }
      })
      return { companys: res }
    },
	// 收集子组件的验证结果
    async validateForm() {
      if (!(this.postForm.companyGroups[0].units && this.postForm.companyGroups[0].units.length)) {
        this.errMsg = '请至少添加一个单位'
        this.$message({
          message: '请至少添加一个主单位',
          type: 'danger'
        })
        return false
      } else {
        this.errMsg = ''
        const result = []
        this.postForm.companyGroups.forEach((val, key) => {
          const el = this.$refs['group'][key]
          el && result.push(el.validate())
        })
        return Promise.all(result).then((res) => {
          return !res.some(val => !val)
        })
      }
    },
}
// ...

ContactUnit.vue
<template>
  <div>
    <el-collapse-item :title="`${item.label} (${item.units.length})`" :name="item.key">
      <contact-company
        v-for="(unit, uIndex) in item.units"
        :key="uIndex"
        ref="company"
        :data="unit"
        :type="item.type"
        :index="uIndex"
        :options="options"
        @remove="removeCompany"
      />
    </el-collapse-item>
  </div>
</template>

<script>
import ContactCompany from './ContactCompany'
export default {
  name: 'ContactUnit',
  components: { ContactCompany },
  props: {
    data: {
      type: Object,
      default: () => {}
    },
    options: {
      type: Object,
      default: () => []
    }
  },
  data() {
    return {
      item: null
    }
  },

  created() {
    this.item = this.data
  },

  methods: {
    removeCompany(index) {
      this.item.units.splice(index, 1)
    },  

    async validate() {
      const result = []
      this.item.units.forEach((val, key) => {
        const el = this.$refs['company'][key]
        el && result.push(el.validate())
      })

      return Promise.all(result).then((res) => {
        return !res.some(val => !val)
      })
    }
  }
}
</script>

ContactCompany.vue, ContactLinkman.vue和ContactUnit.vue内容差不多,就不再贴代码了。

Logo

前往低代码交流专区

更多推荐