Implement a production-ready modal box plug-in using native JS

For all code in this article -github, each code commit in this section has a corresponding COMMIT.

The examples are in the example folder of the code repository. Click here to see the example -gitPage

Considerations for plug-in encapsulation

The principle of

Some principles of encapsulation:

  • Ease of use

    • Call simple
    • Don’t need too many dependencies (preferably zero)
    • Various fault tolerant handling and perfect error prompt
    • Detailed documentation and reference demos for various cases
  • strong

    • Powerful function, often present in the project effect, basic can support
    • Adapt to more needs
    • More user – defined extensions (styles/features)
  • Upgrade and backward compatibility

    • Reduce the cost of learning
    • High performance (performance optimization, lightweight (less code, small size))
    • Maintainability (application of various design patterns)

Style controls

Several ways to control styles in plug-ins

  • Inside the plug-in, all styles are assigned to elements on an “in-line” basis
    • Advantage: no separate import style, as long as the import plug-in JS, in THE JS internal given the corresponding style, easy to call
    • Disadvantages: No common extraction of styles; Implementation and later maintenance, because JS and CSS are not separated, so it will be chaotic; It is not convenient for users to customize the modification style;
  • Write all the styles in the style sheet
    • Advantage:
      • CSS and JS separation is good for code maintenance;
      • Styles can implement common extraction;
      • Users can customize the style they want according to the style class of the viewing element and the default style of the plug-in..drag_modal{xxx:xxx ! important}
      • This pattern is now the classic approach to UI component/plug-in development (ANTD/Element)
    • Disadvantages: Import a separate stylesheet when you need the user to use the plug-in

Note:

  • Try not to import external images in the stylesheet, because later use of the plug-in will need to import images and specify the structure order. If you really need an image, you can use a font icon or convert the image to BASE64.
  • All written plug-in content needs to be compiled in WebPack

Webpack automatically merges all JS

When developing plug-ins, we need to use engineering deployment tools such as WebPack. Try not to write plug-ins with too many dependencies (zero is best), but often when developing a complex plug-in, you need some other method (e.g. utility class methods, etc.). However, we can’t let users download their dependencies and import them separately, which is too troublesome for users to use. We need to merge all dependencies and those developed by ourselves and pack them together (and compress them). Finally, users only need to import the files we merge and pack.

For example: overall project structure and Webpack configuration

//index.js

import utils from './lib/utils';
import Sub from './lib/sub';
function ModalPlugin() {
  
}
ModalPlugin.prototype = {
    version: '1.0.0'.constructor: ModalPlugin,
    / /...
}
/ /...
if (typeof window! = ="undefined") {
    window.M = window.ModalPlugin = ModalPlugin;
}
if (typeof module= = ="object" && typeof module.exports === "object") {
    module.exports = ModalPlugin;
}
Copy the code
//webpack.config.js
const path = require('path');
module.exports = {
    mode: 'production'.// entry: write the plugin code JS
    entry: './src/index.js'./ / packaging
    output: {
        filename: 'modalplugin.min.js'.path: path.resolve(__dirname, 'dist')}};Copy the code

API design

The configuration information supported by the plug-in (based on different configuration information, to achieve different functions) :

  • title[string]The title
  • template[string]Custom content or templates (es6-based template strings, concatenated with richer content structure)
  • buttons[array]Custom buttons (Groups)[{text: 'sure', click: [callback]},...
  • modal[boolean]Controls whether the mask layer is displayed the default istrue
  • drag[boolean]Whether drag is allowed The default is yestrue
  • onopen[function]Open the
  • onclose[function]Shut down

Drag-and-drop lifecycle functions (things that are allowed to be handled by users on a node of the current operation) (publish subscribe)

  • Drag the startondragstart
  • Drag and drop theondraging
  • Drag the endondragend

Main function code implementation

For all code in this article -github, each code commit in this section has a corresponding COMMIT.

Examples are in the repository’s example folder, [click here to see examples -gitPage](mtt3366. Github).

The overall structure

import utils from './lib/utils';
function ModalPlugin(config) {
  
}
ModalPlugin.prototype = {
    version: '1.0.0'.constructor: ModalPlugin,
    / /...
};

const proxyModal = function proxyModal(options) {
   / /...
    return new ModalPlugin(config);
};
if (typeof window! = ="undefined") {
    window.M = window.ModalPlugin = proxyModal;
}
if (typeof module= = ="object" && typeof module.exports === "object") {
    module.exports = proxyModal;
}
Copy the code

This allows us to use the plugin in M({}) or ModalPlugin({}) mode, directly executing the function without using new to return an instance object

Parameter rule processing scheme

First we need to define the rules for each parameter (interface) and fix the data type. The passed parameter traversal is then validated and merged with the default value of the parameter

The logic is as follows:

Check whether the manually passed configuration appears in the rule:

  • If the configuration is not transmitted: Verify whether it is mandatory && take the default value of the parameters
  • If configuration is passed: validate the format of the value && take the value passed in (merge with the default value using object deep merge)
function ModalPlugin(config) {
    
}
ModalPlugin.prototype = {
    version: '1.0.0'.constructor: ModalPlugin
};

// Define rules for each interface
const props = {
    title: {
        type: 'string'.default: 'System Warm Tips'
    },
    template: {
        type: 'string'.required: true
    },
    buttons: {
        type: 'array'.default: []},modal: {
        type: 'boolean'.default: true
    },
    drag: {
        type: 'boolean'.default: true
    },
    onopen: {
        type: 'function'.default: () = >{}},onclose: {
        type: 'function'.default: () = >{}}};const proxyModal = function proxyModal(options) {
    // Check if the options passed in is an object.! options ||typeofoptions ! = ='object' ? options = {} : null;
    // init params
    let config = {};

    // Iterate through the rule and process the parameters
    for (const key in props) {
        const rule = props[key]
        let optValue = options[key], // The argument passed in
            {
                type,
                default: defaultValue,
                required
            } = rule;
        // options does not pass the key: verify that it is mandatory && takes the default parameter value
        if (typeof optValue === "undefined") {
            if (required) throw new TypeError( ` ${key} must be required! ` );
            config[key] = defaultValue;
        }else{
            // Options have a pass key: verify the format of the value && take the passed value "extension: object deep merge"
            if(utils.toType(optValue) ! == type)throw new TypeError( ` ${key} must be an ${type}! `); config[key] = utils.merge(defaultValue, optValue); }}return new ModalPlugin(config);
};
Copy the code

Main function realization

Create the DOM structure you need

Since you’ll need to manipulate the window DOM later, mount it to the instance for ease of operation

/ /...
function ModalPlugin(config) {
    let self = this;
    self.config = config;
    self.$drag_modal = null;
    self.$drag_content = null;
    self.init();
}
ModalPlugin.prototype = {
    / /...
    init() {
        let self = this;
        self.create();
        
    },
    // Dynamically create Modal DOM structures
    create() {
        let self = this,
            config = self.config,
            fragment = document.createDocumentFragment();
        // Create mask layer
        if (config.modal) {
            self.$drag_modal = document.createElement('div');
            self.$drag_modal.className = 'drag_modal';
            fragment.appendChild(self.$drag_modal);
        }
        // Create content
        self.$drag_content = document.createElement('div');
        self.$drag_content.className = 'drag_content';
        self.$drag_content.innerHTML =  ` 
            <div class="drag_head">
                ${config.title}
                <a href="javascript:;" class="drag_close"></a>
            </div>
            <div class="drag_main">${config.template}</div>
            ${config.buttons.length>0? `<div class="drag_foot">
                ${config.buttons.map((item,index)=>{
                    return  `<a href="javascript:;" class="drag_button" index="${index}"> ${item.text} </a>` ;
                }).join(' ')}
            </div>` : ` ` }
        ` ;
        fragment.appendChild(self.$drag_content);

        // Add dynamically created elements to the page
        document.body.appendChild(fragment);
        fragment = null;

        // Control element display: Opacity set to 1 "Transition animation"
        self.$drag_content.offsetHeight; // Refresh the render queue (get the styles so that the above elements are added to the page and changed in two separate renderings so that the transition effect appears)
        self.$drag_modal ? self.$drag_modal.style.opacity = 1 : null;
        self.$drag_content.style.opacity = 1; }};Copy the code

The CSS default initial transparency value is 0, and the box is placed on the page, but tests show that the box has no transition effect. The reason has to do with the browser’s rendering mechanism. In order to reduce backflow and reversion, browsers will add DOM to the page and modify THE CSS to merge into a single processing, only render once, all without transparency from 0 to 1. In order to give it a transition effect, put the box in the page and change the element styles separately. So write a code to get the box style and refresh the render queue to animate the transition.

Bind events in a container based on event delegates

Mainly do the following processing

  • Custom buttons trigger functions that are passed in as arguments, for example

    buttons: [{
        text: 'cancle'.click(self){ self.close(); }}]Copy the code
  • After closing the popover, let its transparency gradually decrease to 0 before removing the DOM to free memory, using the onTransitionEnd method

ModalPlugin.prototype = {
    version: '1.0.0'.constructor: ModalPlugin,
    init() {
        let self = this;
        self.create();

        // Implement the transaction related to the element click in the container based on the event delegate: close button && custom button
        if (self.$drag_content) {
            self.$drag_content.addEventListener('click'.function (ev) {
                let target = ev.target,
                    targetTag = target.tagName,
                    targetClass = target.className;
                // Close the button
                if (targetTag === 'A' && targetClass === 'drag_close') {
                    self.close();
                    return;
                }
                // Customize the button
                if (targetTag === 'A' && targetClass === 'drag_button') {
                    let index = +target.getAttribute('index'),
                    // Use index to find which button is clicked
                        item = self.config.buttons[index];
                    if (item && utils.isFunction(item.click)) {
                        item.click.call(self, self);
                    }
                    return; }}); }},// Dynamically create Modal DOM structures
    create() {
       / /...
    },
    // Turn off Modal (remove in page)
    close() {
        let self = this,
            body = document.body;
        if (self.$drag_modal) {
            self.$drag_modal.style.opacity = 0;
            self.$drag_modal.ontransitionend = () = > {
                // After the transition animation of the element
                body.removeChild(self.$drag_modal);
                self.$drag_modal = null;
            };
        }
        if (self.$drag_content) {
            self.$drag_content.style.opacity = 0;
            self.$drag_content.ontransitionend = () = > {
                body.removeChild(self.$drag_content);
                self.$drag_content = null; }; }}};Copy the code

Example:

M({
    title: 'Log in to the management System'.template:  '
      
`
, buttons: [{ text: 'cancle'.click(self){ self.close(); }}, {text: 'confirm'.click(self) { let username = document.querySelector('#username'), userpass = document.querySelector('#userpass'); console.log(username.value, userpass.value); self.close(); }}});Copy the code

At this point, the basic functionality is complete.

Advanced function code implementation

Drag function implementation

The principle is to calculate the position of the window according to the mouse position, and pay attention to the judgment of the boundary.

Pay attention to

  • Drag move events and mouse up events are bound indocumentIf it is bound to the pop-up window, the mouse will fall out of the box if it moves too fast.
  • Why change the center? Because the default CSS is usedtransform:translate(-50%,-50%);It shifts the box 50% up and 50% to the left. But in the drag calculation, it is still calculated according to the previous position, so there will be problems in the drag.
function ModalPlugin(config) {
    / /...
    self.startX = 0;
    self.startY = 0;
    self.startL = 0;
    self.startT = 0;
    self._MOVE = null;
    self._UP = null;
    
    self.init();
}

ModalPlugin.prototype = {
    version: '1.0.0'.constructor: ModalPlugin,
    init() {
        let self = this;
        self.create();
        / /...
        if (self.config.drag) {
            // Enable multidrop
            let $drag_head = self.$drag_content.querySelector('.drag_head');
            $drag_head.style.cursor = 'move';
            $drag_head.addEventListener('mousedown', self.down.bind(self)); }},/ /...
    // Drag and drop methods
    down(ev) {
        let self = this;
        self.startX = ev.pageX;
        self.startY = ev.pageY;
        self.startL = self.$drag_content.offsetLeft;
        self.startT = self.$drag_content.offsetTop;
        self._MOVE = self.move.bind(self);
        self._UP = self.up.bind(self);
        document.addEventListener('mousemove', self._MOVE);
        document.addEventListener('mouseup', self._UP);
    },
    move(ev) {
        let self = this,
            curL = ev.pageX - self.startX + self.startL,
            curT = ev.pageY - self.startY + self.startT;
        // boundary judgment
        let HTML = document.documentElement,
            minL = 0,
            minT = 0,
            maxL = HTML.clientWidth - self.$drag_content.offsetWidth,
            maxT = HTML.clientHeight - self.$drag_content.offsetHeight;
        curL = curL < minL ? minL : (curL > maxL ? maxL : curL);
        curT = curT < minT ? minT : (curT > maxT ? maxT : curT);
        self.$drag_content.style.left = curL + 'px';
        self.$drag_content.style.top = curT + 'px';
    },
    up() {
        let self = this;
        document.removeEventListener('mousemove', self._MOVE);
        document.removeEventListener('mouseup', self._UP); }};Copy the code

Life cycle function

Plug-in life cycle: Support for user customization at some stage of the current plug-in processing

There are two ways to implement the lifecycle:

Use the callback function

In the parameter configuration item, we pass in the callback function what the user needs to do for himself, and execute the callback function at each stage

Use a publish and subscribe model

Lifecycle functions are implemented using the publish-subscribe model, which gives you the flexibility to subscribe to an event as many methods as you want to execute, or in other locations

implementation

Onopen and onclose are implemented using callbacks, and the three drag-and-drop events are implemented using publish-subscribe to compare the two

The code is as follows:

import Sub from './lib/sub';
/ /...
function ModalPlugin(config) {
    let self = this;
    / /...
    // If drag is enabled, we need to create three event pools
    if (self.config.drag) {
        self.ondragstart = new Sub;
        self.ondraging = new Sub;
        self.ondragend = new Sub;
    }
    self.init();
}
ModalPlugin.prototype = {
    / /...
    // Dynamically create Modal DOM structures
    create() {
       / /...
        // Trigger the open periodic function
        self.config.onopen.call(self, self);
    },
    // Turn off Modal (remove in page)
    close() {
        let self = this,
            body = document.body;
            
        / /...
        if (self.$drag_content) {
            self.$drag_content.style.opacity = 0;
            self.$drag_content.ontransitionend = () = > {
                body.removeChild(self.$drag_content);
                self.$drag_content = null;
                // Trigger the closed periodic functionself.config.onclose.call(self, self); }; }},// Drag and drop methods
    down(ev) {
         / /...
        // Notifies method execution in the event pool
        self.ondragstart.fire(self);
    },
    move(ev) {
          / /...
        // Notifies method execution in the event pool
        self.ondraging.fire(self);
    },
    up() {
        / /...
        // Notifies method execution in the event poolself.ondragend.fire(self); }};Copy the code

use

let m2 = M({
    template:  We implemented plug-in component encapsulation today ,
    buttons: [{
        text: 'sure'.click(self){ self.close(); }}].// Periodic function "callback function: everything the user needs to do is written in two callback functions"
    onopen: self= > {
        console.log('Already open', self);
    },
    onclose() {
        console.log('Closed'.this); }});// Periodic function "publish subscribe: can give a particular event to subscribe to the method it wants to execute, can be multiple, can be in other places"
m2.ondragstart.on(self= > {
    console.log('Drag start 1', self);
});
m2.ondragstart.on(self= > {
    console.log('Drag start 2', self);
});
m2.ondraging.on(self= > {
    console.log('In drag... ');
});
m2.ondragend.on(self= > {
    console.log('End of drag');
});
Copy the code

For all code in this article -github, each code commit in this section has a corresponding COMMIT.

Examples are in the repository’s example folder, [click here to see examples -gitPage](mtt3366. Github).