Posted at

[Vue.js]外側をクリックすると閉じるドロップダウンメニュー

More than 3 years have passed since last update.


はじめに


  • メニューボタン押すと開く

  • メニューボタンもう一度押すと閉じる

  • メニューの外側押しても閉じる

どこでもよく見るアレをvue.jsで


デモ

デモ(jsfiddle)


説明


親コンポーネント


html

  <div style="position:relative">

<button v-on:click.stop="showMenu = !showMenu">Menu</button>
<dropdown-menu
v-if="showMenu"
v-on:close="showMenu = false"></dropdown-menu>
</div>

単純に親コンポーネントのv-if="showMenu"だけで表示の切り替えをしています。

子コンポーネント内で$emit('close')と発火させるとv-on:closeが拾って親がメニューを閉じてくれます。

v-bind:show.sync="showMenu"という構文はVue2.0で滅びるようです。

メニューを開く時のボタンクリックはメニューの外側で行うので、バブリングの停止が必要です。

v-on:click.stop="showMenu = !showMenu".stopmodifierでやっています。


子コンポーネント


js

Vue.component('dropdown-menu', {

mixins:[listener],
template: '#menu-template',
created:function(){
this.listen(window, 'click', function(e){
if (!this.$el.contains(e.target)){
this.$emit('close');
}
}.bind(this));
}
});

windowをクリックしたとき、自分自身と子孫がターゲットでないならcloseイベントをemitしています。

this.$elで自身のDOMが取得できます。


イベントリスナー


js

var listener = {

methods:{
listen :function(target, eventType, callback) {
if (!this._eventRemovers){
this._eventRemovers = [];
}
target.addEventListener(eventType, callback);
this._eventRemovers.push( {
remove :function() {
target.removeEventListener(eventType, callback)
}
})
}
},
destroyed:function(){
if (this._eventRemovers){
this._eventRemovers.forEach(function(eventRemover){
eventRemover.remove();
});
}
}
}

コンポーネントを破棄したときに自動的にイベントリスナーをremoveしてくれるmixinです。

このデモひとつだけならまったく余計ですが、コンポーネントが幾つもある場合、

addEventListenerremoveEventListenerに目を光らせずに済むので、少しコーディングが気楽になります。


コード全文


html

<div id="app">

<div style="position:relative">
<button v-on:click.stop="showMenu = !showMenu">Menu</button>
<dropdown-menu
v-if="showMenu"
v-on:close="showMenu = false"></dropdown-menu>
</div>
<p>
ボタンをクリックするとドロップダウンします
</p>
</div>

<script type="text/x-template" id="menu-template">
<div class="menu" style="position:absolute">
<div>AAA</div>
<div>BBB</div>
<div>CCC</div>
</div>
</script>



js

var listener = {

methods:{
listen :function(target, eventType, callback) {
if (!this._eventRemovers){
this._eventRemovers = [];
}
target.addEventListener(eventType, callback);
this._eventRemovers.push( {
remove :function() {
target.removeEventListener(eventType, callback)
}
})
}
},
destroyed:function(){
if (this._eventRemovers){
this._eventRemovers.forEach(function(eventRemover){
eventRemover.remove();
});
}
}
}

Vue.component('dropdown-menu', {
mixins:[listener],
template: '#menu-template',
created:function(){
this.listen(window, 'click', function(e){
if (!this.$el.contains(e.target)){
this.$emit('close');
}
}.bind(this));
}
});

new Vue({
el: '#app',
data: {
showMenu: false
}
})