1. The origin of the problem

Because Vue is based on componentized design, we can improve code reuse by encapsulating components in our Vue projects thanks to this idea. Based on my experience so far, I know that Vue split components have at least two advantages: 1. Code reuse. In a project based on Element-UI development, we might want to write a similar scheduling popover. It would be easy to write the following code:

<template> <div> <el-dialog :visible. Sync ="cnMapVisible"> I am a Chinese map popup </el-dialog> <el-dialog < EL-dialog :visible. Sync ="ukMapVisible"> I am a popover for the UK map </el-dialog> <el-button @click="openChina"> Open map of China </el-button> <el-button @click="openUSA"> Open map of The United States </el-button> <el-button </el-button> </div> </template> <script> export default {name: "View", data() {return {cnMapVisible: false, usaMapVisible: false, ukMapVisible: false,}; OpenChina () {}, openUSA() {}, openUK() {},},}; </script>Copy the code

There are many problems in the above code. First, as more and more popovers are created, we will find that more and more variables need to be defined to control the display or hiding of the popover. Because when we need to deal with the interior of the popup window and business logic, so at this time there will be mixed together quite a lot of business processing code (such as a map of China I call I need to use gold or baidu map, and calls to the United States, Britain, I can only use Google map, it will lead to the two sets of business logic in a file, severe increases the coupling of the business)

In accordance with the principle of separating business and reducing coupling degree, 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"> </el-button> <el-button @click="openUSA"> </el-button> <el-button @click="openUK"> </el-button> </div> </template> <script> export Default {name: "View", data() {return {/** select * from map */; }, 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>Copy the code

2, ChinaMapDialog. Vue

<template> <div> <el-dialog :visible. Sync ="baiduMapVisible"> </el-dialog> </div> </template> <script> export Default {name: "ChinaMapDialog", data() {return {// baiduMapVisible: false,}; }, methods: {openDialog() {this.baidumapvisible = true; }, closeDialog() { this.baiduMapVisible = false; ,}}}; </script>Copy the code

3. As only pseudo-code is shown here and the meaning is the same as chinamapdialog. vue, usamapdialog. vue and ukmapdialog. vue have been omitted to avoid excessive length

2. Problem analysis

Through the analysis of these pop-ups, we abstract the design and find that there is a common part, that is, the operation code of our Dialog is reusable code. If we can write an abstract popover and combine it with business code at the right time, I can achieve 1+1=2.

3, design,

Due to the Vue without changing the default mixin principle (the default is best not to change, could lead to confusion to the later maintenance personnel), if the naming conflicts happened in the process of mixing, the default method will be combined (department of data objects, recursive merger and in conflict with the component data is preferred), therefore, Mixins can’t override the original implementation. Instead, we expect a more abstract implementation from the parent class. The subclass inherits the parent class, and if the subclass needs to change the table behavior, the subclass can override the parent class’s methods (an implementation of polymorphism). So we decided to use the vue-class-Component library to write the abstract popover as a class.

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

3.1 Event Handling

Looking at the official Element-UI website, we see that the ElDialog throws four events, so we need to pre-take these four events. Therefore, we need to preset the handler of the four events in our abstract pop-ups (because the component’s behavior is divided, and the handling of the pop-ups should be subject to the pop-ups themselves, so I haven’t used $Listeners to see through the external call methods).

import Vue from "vue";
import Component from "vue-class-component";
@Component({
    name: "AbstractDialog",})export default class AbstractDialog extends Vue {
    open() {
        console.log("Popover opens, I'm not doing anything.");
    }

    close() {
        console.log("Popover closes, I'm not doing anything.");
    }

    opened() {
        console.log("Popover opens, I'm not doing anything.");
    }

    closed() {
        console.log("Popover closes, I'm not doing anything."); }}Copy the code

3.2 Attribute Processing

Dialog has many properties. By default, we only need to focus on before-close and title, because these two properties are responsible for the behavior of the popover itself, so we will handle the switch and title tasks in the abstract popover

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

    t = "";

    loading = false;

    // This property is defined so that the properties of the dialog can be changed by passing in properties from the outside world, as well as by default properties of the component inside the Dialog
    attrs = {};

    get title() {
        return this.t;
    }

    setTitle(title) {
        this.t = title; }}Copy the code

3.3 Slots processing

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

1. Header processing

import Vue from "vue";
import Component from "vue-class-component";
@Component({
    name: "AbstractDialog",})class AbstractDialog extends Vue {
    /* Build the popover's Header */
    _createHeader(h) {
        // Determine whether the header slot is passed in when the call is made. If so, the slot is used
        var slotHeader = this.$scopedSlots["header"] | |this.$slots["header"];
        if (typeof slotHeader === "function") {
            return slotHeader();
        }
        // If the user does not pass in the slot, it determines whether the user wants to overwrite the Header
        var renderHeader = this.renderHeader;
        if (typeof renderHeader === "function") {
            return <div slot="header">{renderHeader(h)}</div>;
        }
        // If none is available, return undefined, and the dialog will use our preset title}}Copy the code

2. Processing of body

import Vue from "vue";
import Component from "vue-class-component";
@Component({
    name: "AbstractDialog",})class AbstractDialog extends Vue {
    /** * Build the Body part of the popover */
    _createBody(h) {
        // Determine whether the socket of default is passed in when the call is made. If so, the socket of default is passed in
        var slotBody = this.$scopedSlots["default"] | |this.$slots["default"];
        if (typeof slotBody === "function") {
            return slotBody();
        }
        // If the user has no incoming slot, determine what the user wants to insert into the body section
        var renderBody = this.renderBody;
        if (typeof renderBody === "function") {
            returnrenderBody(h); }}}Copy the code

Since dialog footers often have similar business, we need to encapsulate the code with high repetition rates. If at some point, the user needs to rewrite the footer, then rewrite it. Otherwise, the default behavior is used

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();
    }

    /** * Build popover 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(); }} > cancelled</el-button>
            </div>); }}Copy the code

Finally, we can organize the code we wrote through JSX to get the abstract popover code we want:

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("Popover opens, I'm not doing anything.");
    }

    close() {
        console.log("Popover closes, I'm not doing anything.");
    }

    opened() {
        console.log("Popover opens, I'm not doing anything.");
    }

    closed() {
        console.log("Popover closes, I'm not doing 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();
    }

    /* Build the popover's 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>; }}/** * Build the Body part of the popover */
    _createBody(h) {
        var slotBody = this.$scopedSlots["default"] | |this.$slots["default"];
        if (typeof slotBody === "function") {
            return slotBody();
        }
        var renderBody = this.renderBody;
        if (typeof renderBody === "function") {
            returnrenderBody(h); }}/** * Build popover 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(); }} > cancelled</el-button>
            </div>
        );
    }

    createContainer(h) {
        // To prevent the error of external parameters affecting the original popover design, therefore, some parameters need to filter out, such as 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",beforeClose: this.closeDialog,},on: {
                        close: this.close.closed: this.closed.opened: this.opened.open: this.open,}}} >{/* According to JSX render rules null, undefined, false, "" will not be displayed on the page, if createHeader returns undefined, The default title */} {this._createHeader(h)} {this._createBody(h)} {this._createFooter(h)} will be used</el-dialog>
        );
    }

    render(h) {
        return this.createContainer(h); }}Copy the code

4. The application

4.1 Component Invocation

Let’s take writing chinamapdialog.vue as an example and rewrite it

<script> import Vue from "vue"; import AbstractDialog from "@/components/AbstractDialog.vue"; import Component from "vue-class-component"; @component ({name: "ChinaMapDialog",}) class ChinaMapDialog extends AbstractDialog {get title() {return "ChinaMapDialog"; } attrs = {width: "600px",} attrs = {width: "600px",} renderBody(h) {return <div> } } </script>Copy the code

4.2 Use Composition API

Since we are calling the component’s methods from an instance of the component, we need to get the properties above the current component’s refs each time, which makes our call extremely long and cumbersome to write.

We can simplify this by using the 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"> </el-button> <el-button @click="openUSA"> </el-button> <el-button @click="openUK"> </el-button> </div> </template> <script> import { ref } from "@vue/composition-api"; export default { name: "View", setup() { const china = ref(null); const usa = ref(null); const uk = ref(null); return { china, usa, uk, }; }, data() {return (** *) {return (** *);}, data() {return (** *); }, methods: {openChina() {this.china && this.china.openDialog();}, methods: {openChina() {this.china && this.China. }, openUSA() { this.usa && this.usa.openDialog(); }, openUK() { this.uk && this.uk.openDialog(); ,}}}; </script>Copy the code

conclusion

To develop this popover:

1. Application of object-oriented design in front-end development;

How to write a component based on a class style (vue-class-component or vue-property-decorator)

3. JSX application in VUE;

How $Listeners and $attrs can be used to develop advanced components

Slots, and the use of slots in JSX.

6. Use Composition API in Vue2. X;

This is my first time in nuggets published in the article, because my level is limited, talent and learning shallow, the article will inevitably have flaws, if there is a mistake, ask you big guy light spray, thank you.

If you feel helpful to the development of small partners, I hope you quality three even oh, ha ha ha.