Fork me on GitHub

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;
    width: 100%;
    height: 100%;
}
.pop-button {
    position: relative;
    width: 100%;
    height: 100%;
    text-decoration:none;
    color: inherit;
}
.pop-list {
    position: absolute;
    left: 0;
    top: 0;
}
.pop-list li {
    width: 100%;
    height: 100%;
    padding: 8px 3px;
    list-style:none;
}
</style>

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

我学到了什么

  1. directives自定义指定,事件代理,slot练手一番,感觉很爽。
  2. 在看源码的过程中,也看到了render方法的使用,以及museUI的组件化思想
  3. 对于组件外控制组件的行为有了新的思路。
posted @ 2018-11-26 22:17  馒头加梨子  阅读(7693)  评论(0编辑  收藏  举报