Explain in detail how to encapsulate dialog components in the project of vue+element ui

  • 2021-10-13 06:24:14
  • OfStack

Directory 1, Origin of the Problem
2. Problem analysis
3. Design
3.1 Event Handling
3.2 Attribute processing
3.3 Treatment of slots
STEP 4 Apply
4.1 Component invocation
4.2 Using Composition API
Summarize

1. Origin of the problem

Because of the component-based design of Vue, thanks to this idea, we can improve the reusability of code by encapsulating components in Vue projects. According to my current experience, I know that Vue splitting module has at least two advantages:

1. Code reuse.

2. Code splitting

In the project based on element-ui, we may want to write a similar scheduling pop-up function, and it is easy to write the following code:


<template>
  <div>
    <el-dialog :visible.sync="cnMapVisible"> I am the pop-up window of the map of China </el-dialog>
    <el-dialog :visible.sync="usaMapVisible"> I am a pop-up window on the map of America </el-dialog>
    <el-dialog :visible.sync="ukMapVisible"> I am the pop-up window of the map of Britain </el-dialog>
    <el-button @click="openChina"> Open the map of China </el-button>
    <el-button @click="openUSA"> Open the map of the United States </el-button>
    <el-button @click="openUK"> Open the map of Britain </el-button>
  </div>
</template>
<script>
export default {
  name: "View",
  data() {
    return {
      //  Baidu Maps and Google Maps 1 Some business processing codes   Omission 
      cnMapVisible: false,
      usaMapVisible: false,
      ukMapVisible: false,
    };
  },
  methods: {
    //  Baidu Maps and Google Maps 1 Some business processing codes   Omission 
    openChina() {},
    openUSA() {},
    openUK() {},
  },
};
</script>

There are many problems in the above code. First of all, when there are more and more pop-up windows, we will find that more and more variables need to be defined to control the display or hiding of this pop-up window.

Because there is business logic to deal with inside our pop-up window, At this time, there will be quite a lot of business processing codes mixed in one case (for example, I need to use Gaode map or Baidu map when I call China map, while I can only use Google map when I call US and UK maps, which will make the two sets of business logic located in one file respectively, which seriously increases the coupling degree of business)

According to the principle of separating business and reducing coupling, we split the code according to the following ideas:

1. View. vue


<template>
  <div>
    <china-map-dialog ref="china"></china-map-dialog>
    <usa-map-dialog ref="usa"></usa-map-dialog>
    <uk-map-dialog ref="uk"></uk-map-dialog>
    <el-button @click="openChina"> Open the map of China </el-button>
    <el-button @click="openUSA"> Open the map of the United States </el-button>
    <el-button @click="openUK"> Open the map of Britain </el-button>
  </div>
</template>
<script>
export default {
  name: "View",
  data() {
    return {
      /**
        Pull all the map services away to the corresponding dialog Go inside, View Store only dispatching business codes 
      */
    };
  },
  methods: {
    openChina() {
      this.$refs.china && this.$refs.china.openDialog();
    },
    openUSA() {
      this.$refs.usa && this.$refs.usa.openDialog();
    },
    openUK() {
      this.$refs.uk && this.$refs.uk.openDialog();
    },
  },
};
</script>

2. ChinaMapDialog. vue


<template>
  <div>
    <el-dialog :visible.sync="baiduMapVisible"> I am the pop-up window of the map of China </el-dialog>
  </div>
</template>
<script>
export default {
  name: "ChinaMapDialog",
  data() {
    return {
      //  Encapsulation of China Map Business Logic   Omission 
      baiduMapVisible: false,
    };
  },
  methods: {
    //  Baidu Maps and Google Maps 1 Some business processing codes   Omission 
    openDialog() {
      this.baiduMapVisible = true;
    },
    closeDialog() {
      this.baiduMapVisible = false;
    },
  },
};
</script>

3. USAMapDialog. vue and UKMapDialog. vue have been omitted to avoid excessive length, since only pseudo code is shown here and the meaning expressed by ChinaMapDialog. vue is identical to that expressed by ChinaMapDialog. vue

2. Problem analysis

Through the analysis of these pop-up windows, we abstracted the design just now and found that there is a common part in it, that is, our operating codes for dialog are reusable codes. If we can write an abstract pop-up window,
Then combine it with business code at the right time, and you can achieve the effect of 1+1=2.

3. Design

Since Vue does not change the default mixin principle (it is better not to change the default, which may cause confusion to later maintainers), If a naming conflict occurs during blending, By default, methods are merged (data objects are recursively merged internally and component data takes precedence in case of conflicts), Therefore, mixin can't overwrite the original implementation, but what we expect is that the parent class provides a more abstract implementation, and the subclass inherits the parent class. If the subclass needs to change this behavior, the subclass can override the method of the parent class (a polymorphic implementation).

So we decided to use the library vue-class-component to write this abstract pop-up window as a class.


import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {}

3.1 Event Handling

Looking at the official website of Element-UI, we found that ElDialog threw four events, so we need to take over these four events in advance.
Therefore, we need to preset the handler of this 4 events in our abstract pop-up window (because the behavior of components is divided, and the processing of pop-up window should be subordinate to the pop-up window itself, so I did not penetrate the listening method when calling externally through $listeners)


import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {
  open() {
    console.log(" The pop-up window opens and I don't do anything ");
  }

  close() {
    console.log(" The pop-up window is closed, and I won't do anything ");
  }

  opened() {
    console.log(" The pop-up window opens and I don't do anything ");
  }

  closed() {
    console.log(" The pop-up window is closed, and I won't do anything ");
  }
}

3.2 Attribute processing

dialog has many attributes. By default, we only need to pay attention to before-close and title. Because these two attributes are subordinate to the behavior of the pop-up window itself in terms of responsibilities, we will handle the tasks of switches and title in the abstract pop-up window


import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {
  visible = false;

  t = "";

  loading = false;

  // The purpose of defining this attribute is to realize that it can be changed by passing in the attribute dialog Also supports the internal default of the component dialog Properties of 
  attrs = {};

  get title() {
    return this.t;
  }

  setTitle(title) {
    this.t = title;
  }
}

3.3 Treatment of slots

Looking at the official website of Element-UI, we found that ElDialog has three slots, so we need to take over these three slots

1. Treatment of header


import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
class AbstractDialog extends Vue {
  /*
    To build a pop-up window Header
   */
  _createHeader(h) {
    //  Determine whether the outside world passed in when calling header If any, the slot from outside shall prevail 
    var slotHeader = this.$scopedSlots["header"] || this.$slots["header"];
    if (typeof slotHeader === "function") {
      return slotHeader();
    }
    // If the user has no incoming slot, it is judged whether the user wants to overwrite Header
    var renderHeader = this.renderHeader;
    if (typeof renderHeader === "function") {
      return <div slot="header">{renderHeader(h)}</div>;
    }
    // If there are none ,  Return undefined , then dialog Will use our preset title
  }
}

2. Treatment of body


import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
class AbstractDialog extends Vue {
  /**
   *  To build a pop-up window Body Part 
   */
  _createBody(h) {
    //  Determine whether the outside world passed in when calling default If any, the slot from outside shall prevail 
    var slotBody = this.$scopedSlots["default"] || this.$slots["default"];
    if (typeof slotBody === "function") {
      return slotBody();
    }
    // If the user does not have an incoming slot, it is determined that the user wants to insert into the body Part of the content 
    var renderBody = this.renderBody;
    if (typeof renderBody === "function") {
      return renderBody(h);
    }
  }
}

3. Treatment of footer

Because footer of dialog often has a similar business, we need to encapsulate these codes with high repetition rate here. If at some time, users need to rewrite footer, then rewrite it, otherwise use the default behavior


import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "BaseDialog",
})
export default class BaseDialog extends Vue {
  showLoading() {
    this.loading = true;
  }

  closeLoading() {
    this.loading = false;
  }

  onSubmit() {
    this.closeDialog();
  }

  onClose() {
    this.closeDialog();
  }

  /**
   *  To build a pop-up window Footer
   */
  _createFooter(h) {
    var footer = this.$scopedSlots.footer || this.$slots.footer;
    if (typeof footer == "function") {
      return footer();
    }
    var renderFooter = this.renderFooter;
    if (typeof renderFooter === "function") {
      return <div slot="footer">{renderFooter(h)}</div>;
    }

    return this.defaultFooter(h);
  }

  defaultFooter(h) {
    return (
      <div slot="footer">
        <el-button
          type="primary"
          loading={this.loading}
          on-click={() => {
            this.onSubmit();
          }}
        >
           Save 
        </el-button>
        <el-button
          on-click={() => {
            this.onClose();
          }}
        >
           Cancel 
        </el-button>
      </div>
    );
  }
}

Finally, we organized the code we wrote through JSX, and we got the abstract pop-up window we finally wanted
The code is as follows:


import Vue from "vue";
import Component from "vue-class-component";
@Component({
  name: "AbstractDialog",
})
export default class AbstractDialog extends Vue {
  visible = false;

  t = "";

  loading = false;

  attrs = {};

  get title() {
    return this.t;
  }

  setTitle(title) {
    this.t = title;
  }

  open() {
    console.log(" The pop-up window opens and I don't do anything ");
  }

  close() {
    console.log(" The pop-up window is closed, and I won't do anything ");
  }

  opened() {
    console.log(" The pop-up window opens and I don't do anything ");
  }

  closed() {
    console.log(" The pop-up window is closed, and I won't do anything ");
  }

  showLoading() {
    this.loading = true;
  }

  closeLoading() {
    this.loading = false;
  }

  openDialog() {
    this.visible = true;
  }

  closeDialog() {
    if (this.loading) {
      this.$message.warning(" Please wait for the operation to complete !");
      return;
    }
    this.visible = false;
  }

  onSubmit() {
    this.closeDialog();
  }

  onClose() {
    this.closeDialog();
  }

  /*
    To build a pop-up window Header
   */
  _createHeader(h) {
    var slotHeader = this.$scopedSlots["header"] || this.$slots["header"];
    if (typeof slotHeader === "function") {
      return slotHeader();
    }
    var renderHeader = this.renderHeader;
    if (typeof renderHeader === "function") {
      return <div slot="header">{renderHeader(h)}</div>;
    }
  }

  /**
   *  To build a pop-up window Body Part 
   */
  _createBody(h) {
    var slotBody = this.$scopedSlots["default"] || this.$slots["default"];
    if (typeof slotBody === "function") {
      return slotBody();
    }
    var renderBody = this.renderBody;
    if (typeof renderBody === "function") {
      return renderBody(h);
    }
  }

  /**
   *  To build a pop-up window Footer
   */
  _createFooter(h) {
    var footer = this.$scopedSlots.footer || this.$slots.footer;
    if (typeof footer == "function") {
      return footer();
    }
    var renderFooter = this.renderFooter;
    if (typeof renderFooter === "function") {
      return <div slot="footer">{renderFooter(h)}</div>;
    }

    return this.defaultFooter(h);
  }

  defaultFooter(h) {
    return (
      <div slot="footer">
        <el-button
          type="primary"
          loading={this.loading}
          on-click={() => {
            this.onSubmit();
          }}
        >
           Save 
        </el-button>
        <el-button
          on-click={() => {
            this.onClose();
          }}
        >
           Cancel 
        </el-button>
      </div>
    );
  }

  createContainer(h) {
    // To prevent the external misinformation parameters from affecting the original design of the pop-up window, therefore, some parameters need to be filtered out, including title beforeClose ,  visible
    var { title, beforeClose, visible, ...rest } = Object.assign({}, this.$attrs, this.attrs);
    return (
      <el-dialog
        {...{
          props: {
            ...rest,
            visible: this.visible,
            title: this.title || title || " Pop-up window ",
            beforeClose: this.closeDialog,
          },
          on: {
            close: this.close,
            closed: this.closed,
            opened: this.opened,
            open: this.open,
          },
        }}
      >
        {/*  According to JSX Rendering rules of  null ,  undefined ,  false ,  ''  And so on will not be displayed on the page, if createHeader Return undefined Will use the default title */}
        {this._createHeader(h)}
        {this._createBody(h)}
        {this._createFooter(h)}
      </el-dialog>
    );
  }

  render(h) {
    return this.createContainer(h);
  }
}

STEP 4 Apply

4.1 Component invocation

Let's take writing ChinaMapDialog. vue as an example and rewrite it


<template>
  <div>
    <china-map-dialog ref="china"></china-map-dialog>
    <usa-map-dialog ref="usa"></usa-map-dialog>
    <uk-map-dialog ref="uk"></uk-map-dialog>
    <el-button @click="openChina"> Open the map of China </el-button>
    <el-button @click="openUSA"> Open the map of the United States </el-button>
    <el-button @click="openUK"> Open the map of Britain </el-button>
  </div>
</template>
<script>
export default {
  name: "View",
  data() {
    return {
      /**
        Pull all the map services away to the corresponding dialog Go inside, View Store only dispatching business codes 
      */
    };
  },
  methods: {
    openChina() {
      this.$refs.china && this.$refs.china.openDialog();
    },
    openUSA() {
      this.$refs.usa && this.$refs.usa.openDialog();
    },
    openUK() {
      this.$refs.uk && this.$refs.uk.openDialog();
    },
  },
};
</script>
0

4.2 Using Composition API

Since we call the method of the component through an instance of the component, we need to get the properties on the refs of the current component every time, which makes our call extremely long and cumbersome to write.
We can simplify this by using Composition API


<template>
  <div>
    <china-map-dialog ref="china"></china-map-dialog>
    <usa-map-dialog ref="usa"></usa-map-dialog>
    <uk-map-dialog ref="uk"></uk-map-dialog>
    <el-button @click="openChina"> Open the map of China </el-button>
    <el-button @click="openUSA"> Open the map of the United States </el-button>
    <el-button @click="openUK"> Open the map of Britain </el-button>
  </div>
</template>
<script>
export default {
  name: "View",
  data() {
    return {
      /**
        Pull all the map services away to the corresponding dialog Go inside, View Store only dispatching business codes 
      */
    };
  },
  methods: {
    openChina() {
      this.$refs.china && this.$refs.china.openDialog();
    },
    openUSA() {
      this.$refs.usa && this.$refs.usa.openDialog();
    },
    openUK() {
      this.$refs.uk && this.$refs.uk.openDialog();
    },
  },
};
</script>
1

Summarize

Knowledge points used to develop this pop-up window:
1. The application of object-oriented design in front-end development;
2. How to write components based on class style (vue-class-component or vue-property-decorator);
3. The application of JSX in vue;
4. The application of $attrs and $listeners in developing high-order components (personal name);
5. slots slot and its usage in JSX;
6. Use Composition API in Vue2.x;


Related articles: