Processing of a complex form in Vue (sub form nesting)

Keywords: Vue Vue.js

This demand is a complex data entry form (as shown in the figure below), which is divided into five steps. The sub forms of steps 1, 2, 4 and 5 are relatively simple input and selection. The most complex step is step 3: it is divided into six organization types. Each type can add (up to 10) and delete its organizations (companies), and each organization (company) can add, delete and modify contacts (up to 20), and the form needs to be verified in real time, including required fields, organization (company) name weight judgment, contact weight judgment

Before development, consider the following questions:

  1. Is the value passing props of parent-child components a one-way data flow?

  2. What is the difference between light copy and deep copy?

  3. How to regress the validation of multi-level sub components to the parent level

  4. How does the data of multi-level sub components regress to the parent

  5. What is the function of promise.all?

Concrete implementation

First, split the whole form. According to the above figure, it can be divided into multiple sub components: 5 sub form components + organization type components + Organization (company) components + contact components + other function module components.

For the organization (company), the company component is circularly nested in the organization type, and the contact component is circularly nested in the company... It sounds complex, but it's not. Imagine that we only need to consider a simple form component every time, whether it's adding, deleting and modifying, obtaining form data, or form verification. Here are two points to consider:

  1. How does the parent component get the latest value of the child component (how does the child component return the value to the parent component)
  2. If the parent component obtains the verification result of the child component

The implementation of these two points is shown in the code posted below.

We can think that the nested components in this large form are almost the same. Except for the business logic module code for each form, the rest is the transmission of values and verification results.

Answer the questions thrown at the beginning

  1. Is the value passing props of parent-child components a one-way data flow?

    Vue officials clearly stated that the delivery of props is a one-way data flow,

    All props form a one-way downstream binding between their parent and child props: the updates of the parent props will flow down to the child components, but not vice versa. This will prevent accidental changes in the state of the parent component from the child components, resulting in the incomprehensible data flow of your application.

    After reading the code, you will find that each sub component changes the value in the parent component form, but no error is reported. Does this violate Vue's regulations on props single data flow? Continue to read the official documents

    Note that in JavaScript, objects and arrays are passed in by reference, so for an array or object type prop, changing the object or array itself in the child component will affect the state of the parent component.

    Using prop as an object type, we imitate and implement "two-way data flow" here, which provides great convenience for the development of this complex form. Although we can fully follow the provisions of one-way data volume and do not directly modify the value of prop in sub components, it will increase some complexity. Here, we can continue to look at the next problem.

  2. What is the difference between light copy and deep copy?

    The child component directly obtains the prop is a shallow copy, so we modify the value of the child component form, and the parent component will change at the same time. If we want the prop not to be modified by the child component, we need to use the deep copy. Here, we will flexibly use the shallow copy or the deep copy according to the actual needs.

  3. How does the verification of multi-level sub components return to the parent?

    The verification of each component is written separately in the component. If you need to return to the parent component when submitting, the verification method in the child component should return promise. In this way, you can call the method in the child component in this.$refs.step1.validateForm() and return layer by layer.

    Here we can use async and await to simplify our code.

  4. How does the data of multi-level sub components regress to the parent?

    If we use the value of deep copy prop, we need to write an additional method to let the parent component obtain the data of the child component after calling. The principle is similar to the regression of verification.

  5. What is the function of promise.all?

    Promise.all can package multiple promise instances into a new promise instance. At the same time, the return values of success and failure are different. When successful, a result array is returned, and when failed, the value of the first reject ed failure state is returned.

    The parent component will call asynchronous verification methods of multiple child components at the same time. We need to ensure that all child components are verified before they can be handed over to the parent component. This requires promise.all.

    Mention promise.race in the sequence

    As the name suggests, Promse.race means running a race, which means that whichever result in Promise.race([p1, p2, p3]) gets faster will return that result, regardless of whether the result itself is in a successful state or a failed state.

There are too many codes in the whole module, many of which are business logic codes, so only some codes are posted here to demonstrate the value transfer between components

Parent form form.vue
<template>
    <div>
    <!--Omitted here-->
    <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" />
     <!--Omitted here-->
      <el-button type="primary" :loading="btnLoading" @click="submitForm">{{ btnLoading ?'Submitting':'Direct arraignment' }} </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... Used to receive subform data
      res1: {}, res2: {}, res3: {}, res4: {}, res5: {},
      validResult: [], // Validation results of the entire form
      // ... 
    }
},
created(){
    // ... initialize the value of the form and distribute the echoed data to the postForm of the sub form
    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]
      }
    },
    // ... 
    // Get data of subform
    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)
      })
    },

    // Form submission
    submitForm() {
      const v0 = this.$refs.form.validate()
      const v1 = this.$refs.step1.validateForm()  // Call the validateForm method in the subform
      const v2 = this.$refs.step2.validateForm()
      const v3 = this.$refs.step3.validateForm()
      const v4 = this.$refs.step4.validateForm()
      const v5 = this.$refs.step5.validateForm()
		
      // Use the validation results of the subform
      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: 'Operation succeeded',
                  type: 'success'
                })
              }
            }).catch(e => {
              this.btnLoading = false
            })
          })
        } else {
          this.$message({
            message: 'Please fill in and submit again!',
            type: 'error'
          })
        }
      })
    }
    
    // ... 
}

Parts 1, 2, 4 and 5 are similar. The complete code of form2.vue is shown here
<template>
  <el-form ref="postForm" :model="postForm" :rules="rules" size="small" label-width="120px" class="demo-ruleForm">
    <el-form-item label="details" 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="Progress" 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: 'Please fill in', trigger: 'blur' }
        ],
        progress: [
          { required: true, message: 'Please fill in', trigger: 'blur' }
        ],        
      }
    }
  },

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

  methods: {
    // The validation of form2 is called by the parent component through this.$refs.step2.validateForm()
    async validateForm() {
      return this.$refs['postForm'].validate().then(valid => {
        return valid
      }).catch(e => {
        return false
      })
    }
  }
}
</script>

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

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

// ... 
data(){
    return {
        postForm: {
            companyGroups: [
              { key: 1, label: 'Organization type 1', units: [] },
              { key: 2, label: 'Organization type 2', units: [] },
              { key: 3, label: 'Organization type 3', units: [] },
              { key: 4, label: 'Organization type 4', units: [] },
              { key: 5, label: 'Organization type 5', units: [] },
              { key: 6, label: 'Organization type 6', units: [] }
            ]
     	},
    }
},
methods:{
    // Because the data conforming to the back-end interface needs to be returned, some processing is done here
    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 }
    },
	// Verification results of collection sub components
    async validateForm() {
      if (!(this.postForm.companyGroups[0].units && this.postForm.companyGroups[0].units.length)) {
        this.errMsg = 'Please add at least one unit'
        this.$message({
          message: 'Please add at least one primary company',
          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>

The contents of ContactCompany.vue, ContactLinkman.vue and ContactUnit.vue are similar, so the code is no longer posted.

Posted by irish21 on Sun, 28 Nov 2021 14:26:46 -0800