zoukankan      html  css  js  c++  java
  • vue实现一个简易Popover组件

    概述

    之前写vue的时候,对于下拉框,我是通过在组件内设置标记来控制是否弹出的,但是这样有一个问题,就是点击组件外部的时候,怎么也控制不了下拉框的关闭,用户体验非常差。

    当时想到的解决方法是:给根实例创建一个标记来控制,然后一级一级的把这个标记传进来。但是这样每次配置都要改根组件,非常不灵活

    最近看museUI库,发现它的下拉框Select实现的非常灵活,点击组件外也能控制下拉框关闭,于是想探究一番,借此机会也深入学习一下vue。

    museUI源码

    首先去看Select的源码:

    directives: [{
        name: 'click-outside',
        value: (e) => {
            if (this.open && this.$refs.popover.$el.contains(e.target)) return;
            this.blur();
        }
     }],
    

    可以看到,有个click-outsidepopover,然后它是通过用自定义指令directives实现的。然后去museUI搜popover,果然这是一个弹出组件,并且能够在组件外部控制弹窗关闭。于是开始看popover的源码

    close (reason) {
        if (!this.open) return;
        this.$emit('update:open', false);
        this.$emit('close', reason);
    },
    clickOutSide (e) {
        if (this.trigger && this.trigger.contains(e.target)) return;
        this.close('clickOutSide');
    },
    

    可以看到,它也是通过click-outside来实现的,click-outside字面意思是点击外面,应该就是这个了。然后看click-outside的源码

    name: 'click-outside',
    bind (el, binding, vnode) {
      const documentHandler = function (e) {
        if (!vnode.context || el.contains(e.target)) return;
        if (binding.expression) {
          vnode.context[el[clickoutsideContext].methodName](e);
        } else {
          el[clickoutsideContext].bindingFn(e);
        }
      };
      el[clickoutsideContext] = {
        documentHandler,
        methodName: binding.expression,
        bindingFn: binding.value
      };
      setTimeout(() => {
        document.addEventListener('click', documentHandler);
      }, 0);
    },
    

    原来它是通过自定义指令,在组件创建的时候,给document绑定一个全局click事件,当点击document的时候,通过判断点击节点来控制弹窗关闭的。这差不多就是事件代理

    所以总结一下,要实现组件外部控制组件弹窗的关闭,主要利用directives,bind,document就行了。

    自己实现

    既然知道原理就有点跃跃欲试了,通过查阅官方文档得知,directives可以用于局部组件,这样就变成了局部指令。于是写代码如下:

    <template>
        <div class="pop-over">
            <a @click="toggleOpen" class="pop-button" href="javascript: void(0);">
                {{ 按钮1 }}
            </a>
            <ul v-clickoutside="close" v-show="open" class="pop-list">
                <li>选项1</li>
                <li>选项2</li>
                <li>选项3</li>
                <li>选项4</li>
            </ul>
        </div>
    </template>
    
    <script>
    export default {
        name: 'PopOver',
        data() {
            return {
                open: false
            }
        },
        methods: {
            toggleOpen: function() {
                this.open = !this.open;
            },
            close: function(e) {
                if(this.$el.contains(e.target)) return;
                this.open = false;
            }
        },
        directives: {
            clickoutside: {
                bind: function (el, binding, vnode) {
                    const documentHandler = function (e) {
                        if (!vnode.context || el.contains(e.target)) return;
                        binding.value(e);
                    };
    
                    setTimeout(() => {
                        document.addEventListener('click', documentHandler);
                    }, 0);
                }
            }
        }
    }
    </script>
    

    注意,在我们close方法里面,我们通过判断点击节点是否被组件包含,如果包含的话,不执行关闭行为。

    但是上面的组件不通用,正好官方文档学习了slot,于是用slot改写如下:

    <template>
        <div class="pop-over">
            <a @click="toggleOpen" class="pop-button" href="javascript: void(0);">
                {{ buttonText }}
            </a>
            <ul v-clickoutside="close" v-show="open" class="pop-list">
                <slot></slot>
            </ul>
        </div>
    </template>
    
    <script>
    export default {
        name: 'PopOver',
        props: ['buttonText'],
        data() {
            return {
                open: false
            }
        },
        methods: {
            toggleOpen: function() {
                this.open = !this.open;
            },
            close: function(e) {
                if(this.$el.contains(e.target)) return;
                this.open = false;
            }
        },
        directives: {
            clickoutside: {
                bind: function (el, binding, vnode) {
                    const documentHandler = function (e) {
                        if (!vnode.context || el.contains(e.target)) return;
                        binding.value(e);
                    };
    
                    setTimeout(() => {
                        document.addEventListener('click', documentHandler);
                    }, 0);
                }
            }
        }
    }
    </script>
    
    <style scoped>
    .pop-over {
        position: relative;
         100%;
        height: 100%;
    }
    .pop-button {
        position: relative;
         100%;
        height: 100%;
        text-decoration:none;
        color: inherit;
    }
    .pop-list {
        position: absolute;
        left: 0;
        top: 0;
    }
    .pop-list li {
         100%;
        height: 100%;
        padding: 8px 3px;
        list-style:none;
    }
    </style>
    

    利用props自定义按钮文字,slot自定义弹窗文字,这样一个简易的Popover组件就完成了。

    我学到了什么

    1. directives自定义指定,事件代理,slot练手一番,感觉很爽。
    2. 在看源码的过程中,也看到了render方法的使用,以及museUI的组件化思想
    3. 对于组件外控制组件的行为有了新的思路。
  • 相关阅读:
    WSGI学习系列WSME
    Murano Weekly Meeting 2015.08.11
    Trace Logging Level
    OpenStack Weekly Rank 2015.08.10
    markdown语法测试集合
    css-定位
    html图像、绝对路径和相对路径,链接
    html块、含样式的标签
    html标题、段落、换行与字符实体
    html概述和基本结构
  • 原文地址:https://www.cnblogs.com/yangzhou33/p/10023410.html
Copyright © 2011-2022 走看看