1. 程式人生 > >vue實現一個簡易Popover組件

vue實現一個簡易Popover組件

方法 height 自定義 它的 direct val code button update

概述

之前寫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. 對於組件外控制組件的行為有了新的思路。

vue實現一個簡易Popover組件