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
- Advantage:
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 titletemplate[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 theonclose[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 start
ondragstart
- Drag and drop the
ondraging
- Drag the end
ondragend
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 in
document
If 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 used
transform: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).