Writing in the front

The recent work is all about some background management projects, involving the use of forms. There are a large number of forms with the same content, which can be divided into new forms and edit forms according to the use scenario. In the process of daily brick moving, this kind of more scenes to do some thinking.

The following will be analyzed and realized step by step through a small case combined with several common scenarios.

Take a chestnut

Implement a staff management function, including staff list, new staff, modify staff information function.

  • Employees list
  • New Employee Form

  • Edit employee Form

As can be seen from the above figure, the employee’s edit and delete forms are basically the same.

implementation

The code below uses the Element component, but there is not much difference in thinking whether you use iView, Element, or any other Vue component library.

The worst implementation

There is no encapsulation. After one function is implemented, another function code is copied directly. I’ve seen a lot of people develop this way, and it’s probably the fastest way to get features done, but it’s also the most problematic.

  1. Not easy to maintain, if there is a bug in the copied code, it is equivalent to a copy of the bug. Fixing bugs later, or adding new features, requires two code changes. For example, the form needs to add a date of birth form entry, which needs to be modified in both the added and edited code.
  2. High code redundancy and repetition rate, common code quality management tools will include a repetition rate.
  3. … In summary, copy-and-paste code violates the DRY principle: every function of a system should have a unique implementation. If you encounter the same problem multiple times, you should abstract out a common solution rather than redevelop the same functionality.

Encapsulate the component and extract the entire popover

The entire pop-up layer and form is packaged as a component, and the new and edit functions are invoked directly. When I look at my colleagues’ code, I find that this is also the way my colleagues use it more often.

<template>
  <ElDialog
      :title="title"
      :visible="visible"
      @update:visible="handleVisibleChange"
  >
    <ElForm ref="EditForm" :model="form" :rules="rules">
      <ElFormItem label="Name" prop="name">
        <ElInput v-model="form.name"/>
      </ElFormItem>
      <ElFormItem label="Id Card No." prop="cid">
        <ElInput v-model="form.cid"/>
      </ElFormItem>
      <ElFormItem label="Contact Address" prop="address">
        <ElInput v-model="form.address"/>
      </ElFormItem>
    </ElForm>
    
    <template #footer>
      <ElButton @click="handleVisibleChange(false)"</ElButton> <ElButtontype="primary" @click="handleSave"</ElButton> </template> </ElDialog> </template> <script>export default {
    name: "EditForm", props: {// Whether to display the form visible: {type: Boolean,
        default: false}, // Popup title title: String, // Output data model: {type: Object,
        default: null
      }
    },
    data() {
      return {
        form: {
          cid: ' ',
          name: ' ',
          address: ' '
        },
        rules: {
          name: {required: true, message: 'Please enter your name', trigger: 'blur'},
          cid: {required: true, message: 'Please enter your ID card number', trigger: 'blur'},
          address: {required: true, message: 'Please enter contact address', trigger: 'blur'}},}}, watch: {model(employeeInfo) {this.form = {... EmployeeInfo} // Simple shallow clone}}, methods: {handleSave() {// Form validation returns data this.$refs.EditForm.validate((valid) => {
          if (valid) {
            this.$emit('save', this.form)
          }
        })
      },
      handleVisibleChange(value) {
        this.$emit('update:visible', value)
      }
    }
  }
</script>
Copy the code

In the parent component, all you need to do is pass some necessary data to the form component via props, which implements new and edit functions.

The form can be edited by passing in props named Model, and the form component can implement the echo of the form by changing the Model of Watch.

<template>
  <div>
    <EditForm
        title="New employee"
        :visible.sync="showAddForm"
        @save="handleAddEmployee"
    />
    <EditForm
        :model="editFormData"
        title="Editorial staff"
        :visible.sync="showEditForm"
        @save="handleEditEmployee"
    />
  </div>
</template>
<script>
  import EditForm from "./EditForm";
  export default {
    components: {
      EditForm
    },
    data() {
      return {
        showEditForm: false// Whether to display the edit form showAddForm:falseEditFormData: {}}}, methods: {// Add an employee handleAddEmployee(employeeInfo) {//doSomething}, // Edit a handleEditEmployee(employeeInfo) {//do something
      }
    }
  }
</script>
Copy the code

Edit and create now reuse the same component. If the requirements change: add a date of birth form entry, then you only need to modify this one file to complete the modification of the two functions.

This implementation has been able to meet our current needs, but there are still some problems:

  1. Not consistent with the single responsibility principle: The form component now encapsulates both the data and functionality of the form, as well as the action buttons, and some of the data and actions of the Dialog component (e.g. Visible state), the form component makes a transit to the props needed by the Dialog by defining title, visible, and so on, generating some additional code. Not only that, but suppose you now need to differentiate the save buttons as well: save button text changed to edit when editing an employee, or add temporary buttons and functions when creating a new employee. We need to do this by adding props to the form component, or by making judgments within the component. Here we can do this by adding buttons or modifying copywriting examples. In fact, we can do this through slot slots or other means. The purpose is to show that this does not involve changes to the form functionality, but we still need to modify the same component.
  2. There are few scenarios covered by components. In the case, our additions and modifications are in the form of popovers, but in a more complex real business, employees may need to be added through a page, and editing may be realized through popovers. Or we need to implement functions such as batch adding employees, because our component also integrates dialogs internally, which can’t be done very well.

Take it a step further and strip out Dialog and buttons

Considering the problems of the second approach, we can further abstract the form component by stripping it from the Dialog and action button.

<template>
  <ElForm ref="EditForm" :model="form" :rules="rules">
    <ElFormItem label="Name" prop="name">
      <ElInput v-model="form.name"/>
    </ElFormItem>
    <ElFormItem label="Id Card No." prop="cid">
      <ElInput v-model="form.cid"/>
    </ElFormItem>
    <ElFormItem label="Contact Address" prop="address">
      <ElInput v-model="form.address"/>
    </ElFormItem>
  </ElForm>
</template>
<script>
  export default {
    name: "EditForm", props: {// Output data model: {type: Object,
        default: null
      }
    },
    data() {
      return {
        form: {
          cid: ' ',
          name: ' ',
          address: ' '
        },
        rules: {
          name: {required: true, message: 'Please enter your name', trigger: 'blur'},
          cid: {required: true, message: 'Please enter your ID card number', trigger: 'blur'},
          address: {required: true, message: 'Please enter contact address', trigger: 'blur'}},}}, watch: {model(employeeInfo) {this.form = {... EmployeeInfo} // Simple shallow clone}}, methods: {// The method to obtain data is exposed externally, and the form verification is performed internally. Refs is invoked in the parent componentgetValue() {
        return new Promise((resolve, reject) => {
          this.$refs.EditForm.validate((valid) => {
            if(valid) { resolve({... this.form}) }else {
              reject('Form verification failed, can throw an exception')
            }
          })
        })
      }
    }
  }
</script>
Copy the code

After the Dialog and action buttons were stripped, the logic for the associated props was stripped out of the component.

Since the save button is not in the form component now, there is no way to expose the form data to the parent component through emit. Therefore, a getValue method is registered in Methods, which performs form validation and returns form data or exceptions by returning a Promise. When the parent uses the new form component, it registers a reference to the form component through ref and calls the getValue method in the form component to get the data inside the component.

Specific operations are as follows:

<template> <div> <! --> <Dialog :visible.sync="showAddForm">
      <NewEditForm ref="EditForm"/>
      
      <template #footer>
        <ElButton @click="showAddForm = false"</ElButton> <ElButtontype="primary" @click="handleAddEmployee"> Save </ElButton> </template> </Dialog> <! <ElDialog :visible.sync="showEditForm">
      <NewEditForm ref="AddForm" :model="editFormData"/>
      
      <template #footer>
        <ElButton @click="showEditForm = false"</ElButton> <ElButtontype="primary" @click="handleEditEmployee"> Save </ElButton> </template> </ElDialog> </div> </template> <script> import NewEditForm from"./NewEditForm";
  export default {
    components: {
      NewEditForm
    },
    data() {
      return {
        showEditForm: false// Whether to display the edit form showAddForm:false// display the new form}, methods: {... // Edit an employeehandleEditEmployee() {
        this.$refs.neweditForm.getValue ().then((employeeInfo) => {// call edit interface}).catch((error) => {// process form validation failure})}, // Add an employeehandleAddEmployee() {
        this.$refs.addForm.getValue ().then((employeeInfo) => {// call add interface}).catch((error) => {// failed to process form validation})}} </script>Copy the code

With this encapsulation, when we need to modify the Dialog or action button, we can modify it directly in the parent component. Form components are smaller and more flexible, and can cover more usage scenarios, making it much easier to implement an add employees page or batch add employees.

Add employees in batches

Through the V-for circular form component to achieve batch add staff form function.

The code is as follows:

<template>
  <div>
    <ElDialog title="Add employees in batches" :visible="showForm">
      <div v-for="(symble, index) in employeeList" :key="symble">
        <div class="form-header"{{index + 1}} <ElButtontype="text" @click="handleRemoveForm(symble)"</ElButton> </div> <NewEditForm ref="EmployeeForm"/>
        <hr/>
      </div>

      <ElButton
          type="primary"
          style="display: block"
          @click="handleAddForm"> Add staff </ElButton> <template#footer>
        <ElButton @click="showForm = false"</ElButton> <ElButtontype="primary" @click="handleSave"> Save </ElButton> </template> </ElDialog> </div> </template> <script> import NewEditForm from"./NewEditForm";

  export default {
    components: {
      NewEditForm
    },

    data() {
      return {
        showForm: true, employeeList: [Symbol()] // Delete requires a unique key value to ensure rigor}}, methods: {// Add a formhandleAddForm() { this.employeeList.push(Symbol()) }, / / remove form handleRemoveForm (symble) {this. EmployeeList. Splice (this) employeeList) indexOf (symble), 1)}, // the refs.getValue method is used with the promise.all methodhandleSave() {
        Promise.all(
          this.$refs.EmployeeForm.map(formRef => {
            return formRef.getValue()
          })
        )
          .then(formData => {
            // doSomething}). Catch (error => {// handle validation failure exception})}}} </script>Copy the code

To allow for the deletion of individual employee forms, the V-for loop contains an array of Symbols as the unique key for each form.

When you click Save, you loop through the getValue method in each form component, put the returned Promise into the array, and finally get the form data or handle exceptions through the promise.all method.

(to the end.

The last

This is my first article.

Usually work also encountered many problems, but also learned a lot of excellent code, because there is no record of the habit, always forget, I hope to develop a regular summary, thinking habit 💪.