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-outside和popover,然後它是通過用自定義指令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組件就完成了。
我學到了什麽
- 對directives自定義指定,事件代理,slot練手一番,感覺很爽。
- 在看源碼的過程中,也看到了render方法的使用,以及museUI的組件化思想。
- 對於組件外控制組件的行為有了新的思路。
vue實現一個簡易Popover組件