【vue动态加载组件】 用render写一个popup.js组件

定义方法调用格式

我发现在写项目的时候,经常会用到 alertconfirmtoast之类的弹出框。但是原生alert对话框的样式已经定型,和UI设计又不一样,再加上弹出面板真的出现频率太多了,于是自己动手写了一个动态弹窗组件。

首先,我先定义了一下,假设已有此组件,我要怎么调用:

    popup({
          type:'alert',
          content: '这是一个警告框!'
    })

于是就有了大概想法,首先这个组件是通过一个方法调用的,其次,这个方法得有一个对象作为自己的参数,对象中的type用来定义当前面板的类型。

经过一系列测试后,最终定下来以下类型,并列出此popup应该有的功能

/**
* alert  confirm  prompt  toast slot
*/
  1. 默认居中、有根元素传入时,按照根元素的位置进行定位
  2. 可以设置弹出面板大小及其他样式
  3. 可以设置面板css类,修改蒙版的不透明度
  4. 具有confirm和close函数回调
  5. 面板内容可以自定义,slot模式时, 可以选择内嵌 组件或者html元素
  6. 当面板的自定义内容为html元素时,支持绑定事件
调用confirm
popup({
    type: 'confirm',
    content: '你确定要删除我吗?',
    confirm: function() {
        // TODO
    },
    close: function() {
        // TODO
    }
})
调用prompt
popup({
    type: 'prompt',
    content: '请输入内容:',
    confirm: function(str) {
        // TODO
        console.log(str);
    },
    close: function() {
        // TODO
    }
})
调用toast
popup({
    type: 'toast',
    content: '我是一个定时提示框!',
    timeout: 3000  // 不传 timeout时,默认时长2秒
})
调用slot 自定义面板
popup({
    type: 'slot',  // 自定义组件模式
    slot: {
        name: 'myComponent',
        url: 'views/components/myComponent.vue'   //此项可选,有时将自动加载为全局组件
    }
})

popup({
    type: 'slot', // html模式
    html: true,
    innerHTML : `
    

这是我定义的html内容·

`, methods: { logMsg: function(){ console.log('this is a event') } } })

大概定的调用方式就是上面这样了,但是除此外还需要几个属性来修饰一下:

popup({
    type: 'alert',
    content: '这是一个警告框'
    root: document.querySelector('.msg'),  // 面板按照此元素在页面中的位置进行定位
    opacity: 0.1,  // 蒙版的不透明度,当为0 时 完全透明,为1时背景全黑
    style: {  // 此属性的值将作用于面板上
        'width': '200px',
        'height': '100px'
    },
    css: ['change1','change2']   //此属性接收一个数组,数组值作为class类绑定到面板上,这个是为了写复杂的样式时,避免写过长的style属性准备的
})

莽代码

先来定义一个大概代码框架


(function(global, factory){
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global = global || self, global.popup = factory());
})(this,function(){
    /* 以下我们定义了一个封闭作用域用来编写我们的popup函数需要用到的其他变量 判断和 处理的函数 */   
    let [template, popupData] = ['', {}];
    const [_toString, doc] = [Object.prototype.toString, document.documentElement];

    // 定义工具函数,留作判断备用
    function _isObj(obj){
        return _toString.call(obj).slice(8,-1) === 'Object';
    };
    
    function _isArr(obj){
        return _toString.call(obj).slice(8,-1) === 'Array';
    };
    
    function _isEle(ele){
        return _toString.call(ele).slice(8,12) === 'HTML';
    };
    
    function _isFun(fun){
        return typeof fun === 'function';
    };
    
    function _isErr(err){
        return _toString.call(err).slice(8,-1) === 'Error';
    };
      
    let methods = {
      location: function(ele) {
        // 计算传入element元素在页面上的位置
        ...
      },
      initpopupData: function(options){
        ...
      },
      template: function(type,options){
        // 通过type 创建对应的 template div
        if(type){
            ...
        }
        template = `
...
` }, rootOffset: function(obj,ele){ // 计算 面板位置偏移量 ... }, judgeData: function(options){ // 判断popupData 合法性 }, complieEvents: function(ele,options){ // 编译元素 查询绑定事件的DOM ... }, addComponent: function(options){ // 添加Vue popup全局组件 ... } } const popup = (options) => { /* 初始化data */ methods.initpopupData(options); /* 判断是否有data 并查看data中是否有禁止项*/ methods.judgeData(options); /* 创建popup组件 */ methods.addComponent(options); return popup; })

上源码!

(function(global, factory){
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global = global || self, global.popup = factory());
})(this,function(){
    
    let [template, popupData] = ['' , {}];
    
    const [_toString, doc] = [ Object.prototype.toString, document.documentElement];

    function _isObj(obj){
        return _toString.call(obj).slice(8,-1) === 'Object';
    };
    
    function _isArr(obj){
        return _toString.call(obj).slice(8,-1) === 'Array';
    };
    
    function _isEle(ele){
        return _toString.call(ele).slice(8,12) === 'HTML';
    };
    
    function _isFun(fun){
        return _toString.call(fun).slice(8,-1) === 'Function';
    };
    
    
    function _isErr(err){
        return _toString.call(err).slice(8,-1) === 'Error';
    };
    
    let methods = {
        location: function(t){
            let [ele,left,top] = [t,0,0];
            left = ele.offsetLeft;
            top = ele.offsetTop;
            
            while(ele.offsetParent !== null){
                ele = ele.offsetParent;
                left += ele.offsetLeft;
                top += ele.offsetTop;
            }
            left += t.offsetWidth;
            top += t.offsetHeight;
            return {
                root: t,
                left: left,
                top: top
            };
        },
        initpopupData: function(options){
            popupData = {
                type: options.type,
                promptValue: '',
                res: {},
                html: ('html' in options) ? options['html'] : false,
                innerHTML: ('html' in options)&&('innerHTML' in options) ? 
                            options['innerHTML'] : 
                            '请设置一个innerHTML属性,指向自定义容器内容'
                            
            };
        },
        template: function(type,options){
            if(type !== 'slot') {
                if(!('content' in options)) return new Error('不存在content属性,请添加此字段,字段的值为显示内容!');
            }
            template = `
            
            `
        },
        rootOffset: function(obj,ele){
            let [bl,bt] = [doc.clientWidth,doc.clientHeight];
            ele.style.position = 'absolute';
            if(obj.top + ele.clientHeight > bt ){
                // 面板底部溢出
                ele.style.bottom = bt - obj.top + obj.root.clientHeight + 'px';
                //doc.scrollTop = doc.clientHeight;
            }else{
                ele.style.top = obj.top + 'px';
                //doc.scrollTop = 0;
            }
            
            if(obj.left + ele.clientWidth > bl){
                // 面板右侧溢出
                ele.style.right = bl - obj.left + obj.root.clientWidth + 'px';
            }else{
                ele.style.left = obj.left + 'px';
            }
        },
        judgeData: function(options){
            if(('data' in options) && _isObj(options['data'])){
                let state = true;
                for(let item in options['data']){
                    for(let i in popupData){
                        if(item === i){
                            console.error('data子属性:'+item+'为初始化data属性值,请更换名字后重试!');
                            state = false;
                            break;
                        }
                    }
                }
                
                if(!state) return false;
            }
            
            return true;
        },
        complieEvents: function(ele,options){

            let eleArr = [];
            
            push(ele);
            
            function push(fragment){
                Array.from(fragment.childNodes).forEach((node)=>{
                    
                    if(node.nodeType === 1 && node.getAttribute('@click')){
                         eleArr.push(node);
                    }
                    
                    if(node.childNodes){
                        push(node);
                    }
                })
            }
            
            if(eleArr.length === 0) return true;
            
            if(!('methods' in options) || !(_isObj(options['methods']))) return false;
            
            for(let item in options['methods']){
                if(!(_isFun(options['methods'][item]))){
                     return false;
                }
            }

            eleArr.forEach(item=>{
                item.addEventListener('click', function(){
                     options['methods'][item.getAttribute('@click')]();
                })
            })
            
            return true;
        },
        addComponent: function(options){
            Vue.component('popup',{
                template:template,
                methods: {
                    close: function(){
                        var popupa = this.$refs.popup;
                        document.body.removeChild(popupa);
                        // callback close
                        if(('close' in options) && _isFun(options.close)){
                            options.close(false);
                        }
                        return false;
                    },
                    confirm: function(){
                        var popupa = this.$refs.popup;
                        document.body.removeChild(popupa);
                        // callback confirm
                        if(('confirm' in options) && _isFun(options.confirm)){
                            if(options.type === 'prompt'){
                                options.confirm(this.promptValue,this.res);
                            }else{
                                options.confirm(true,this.res);
                                
                            }
                        }
                        
                        return true;
                    },
                    __initOptions: function(){
                        // type toast
                        
                        if(options.type === 'toast'){
                            let timeout = 1500;
                            if(('timeout' in options) && !isNaN(Number(options.timeout))){
                                timeout = options.timeout;
                            }
                            
                            let keyframs = [
                                {
                                    opacity: 1,
                                },{
                                    opacity: 0,
                                },
                            ];
                            
                            setTimeout(()=>{
                                this.$refs.popup.animate(keyframs,500);
                                setTimeout(()=>{
                                    this.close();
                                },500);
                            },timeout)
                        }
                        
                        //mask css --- opacity
                        if(('opacity' in options) && !isNaN(options.opacity) && options.opacity >=0 && options.opacity <=1){
                            this.$refs.popup.style.background = 'rgba(0,0,0,'+options.opacity+')';
                        }
                        
                        // content style and css
                        if(('style' in options) && _isObj(options.style)){
                            for(var i in options.style){
                                this.$refs.popupcon.style[i] = options.style[i];
                            }
                        }
                        
                        if(('css' in options) && _isArr(options.css)){
                            options.css.forEach(item=>{
                                this.$refs.popupcon.classList.add(item);
                            })
                        }
                        
                        // content location
                        if(('root' in options) !== null && _isEle(options.root)){
                            let l = methods.location(options.root);
                        
                            methods.rootOffset(l,this.$refs.popupcon);
                        }
                        
                        
                        //content container
                        if(('html' in options) && options['html']){
                            
                        }
                        
                        //bindEvents
                        this.htmlEvent();
                        methods.close = this.close;
                        methods.confirm = this.confirm;
                    },
                    
                    htmlEvent: function(){
                        
                        if(!this.$refs.chtml) return;
                        let state = methods.complieEvents(this.$refs.chtml,options);
                        
                        if(!state){
                            console.error('您未传入methods对象,或methods对象的值有误');
                        }
                    }
                },
                data: function(){
                    let obj = popupData;
                    
                    if(('data' in options) && _isObj(options['data'])){
                        Object.assign(obj, options['data']);
                    }
                    
                    return obj;
                },
                mounted: function(){
                    this.__initOptions();
                }
            })
        }
    };
    
    
    const popup = (options) => {
        let body = document.body;
        let div = document.createElement('div');
        div.setAttribute('id','popup');
        body.appendChild(div);
        
        /* 初始化data */
        methods.initpopupData(options);
        
        /* 判断类型 并创建template-html模板 */
        var ee = methods.template(options.type, options);
        if(_isErr(ee)){
            console.error(ee.message);
            return;
        }
        
        /* 判断是否有data 并查看data中是否有禁止项*/
        methods.judgeData(options);

        /* 创建popup组件 */
        methods.addComponent(options);
        
        if(('slot' in options) && _isObj(options.slot) && options.slot['name'] !== undefined && options.slot['url'] !== undefined){
            
            Vue.component(options.slot['name'], httpVueLoader(options.slot['url']))
            
        }
        
        return new Vue({
            render: function(h){
                return h('popup',{
                    scopedSlots: {
                        content: function(){
                            return h('div',{
                                attrs:{
                                    class: 'popup-section'
                                }
                            },[h(options.slot.name)])
                        }
                    }
                })
            },
            methods: {
                close: function(){
                    methods.close();
                },
                confirm: function(){
                    methods.confirm();
                }
            }
        }).$mount('#popup');
    };
    
    return popup;
})

有点智障的是,throw new Error ()语法 在写的时候写成了 return new Error()半天没反应。后来跑控制台试了下才发现自己搞错了,原来用的console.error(),感觉不太聪明的样子,哈哈哈。当做新手练手项了,其实写完了才发现可以改的地方还很多。我唯一担心的是,每次调用slot模式时, 都会执行 Vue.component挂载全局popup方法, 还有 return了一个 new Vue。但是我暂时又不知道怎么改比较好,如果有人看到这篇文章, 能提出一些改进意见就更好啦!最后附上写的 css代码:

.popup-mask {
    position: fixed;
    top: 0;
    left: 0;
    right: 0;
    bottom: 0;
    background-color: rgba(0,0,0,.2);
    display: flex;
    justify-content: center;
    align-items: center;
    z-index: 999;
}

.popup-content {
    border-radius: 10px;
    border: 1px solid #ccc;
    background-color: #eee;
    box-shadow: 0 0 4px #555;
    color: #333;
    padding: 10px;
    box-sizing: border-box;
}


.popup-alert,
.popup-confirm,
.popup-prompt,
.popup-toast {
    width: 360px;
    height: 200px;
}

.popup-prompt-input {
    width: 100%;
    height: 32px;
    line-height: 32px;
    padding: 0 4px;
    box-sizing: border-box;
    border: 1px solid #ccc;
    outline: none;
    border: 4px;
    margin: 6px 0;
}

.popup-slot {
    width: 60%;
    height: 60%;
}

.popup-tips {
    height: calc(100% - 36px);
    padding: 10px;
    box-sizing: border-box;
}

.popup-options {
    display: flex;
    justify-content: flex-end;
    align-items: center;
    height: 36px;
    padding: 0 10px;
}

.popup-options .popup-btn-confirm,
.popup-options .popup-btn-close {
    padding: 4px 8px;
    border-radius: 4px;
    border: 1px solid #ccc;
    outline: none;
    background-color: #eee;
    color: #fff;
    cursor: pointer;
}

.popup-options .popup-btn-confirm {
    background-color: #5CB85C;
    margin-right: 10px;
    color: #fff;
}

.popup-options .popup-btn-close {
    color: #333;
}

你可能感兴趣的:(【vue动态加载组件】 用render写一个popup.js组件)