LoginSignup
48
62

More than 5 years have passed since last update.

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

Posted at

はじめに

  • メニューボタン押すと開く
  • メニューボタンもう一度押すと閉じる
  • メニューの外側押しても閉じる

どこでもよく見るアレを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
    }
})

48
62
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
48
62