7839

雑草魂エンジニアブログ

【Vue】モーダルウィンドウを自作してみた話

先日、Vue.jsを用いてSPAのHP制作を行なった。

SPAの中でモーダルウィンドウを用いて、詳細情報を表示することは情報をはっきりと強調して提示でき有効な手段である。今回は、そのモーダルウィンドウを自作したので、実装方法を紹介する。

なぜ、自作したのか?

便利なモジュールはたくさんある。調べていて出てくるのは以下のモジュールだ。

github.com

最初はこちらを使って実装していたが、ある問題にぶつかった。 Max-Heightが思うように機能してくれない。。。 モーダルウィンドウの中のコンテンツが縦長い場合に以下のようなデザインになってしまった。

f:id:serip39:20200604182738j:plain

中央でモーダルウィンドウを固定して、画面全体まで広がってほしくなかったが、パラメータを設定してもどうしても実現できず、自作することにした。

Vue.jsの公式にも以下のように実装例がのっていた。

jp.vuejs.org

(本来はコードを読んできちんとvue-js-modalの問題解決をすればよかったが、今回はモーダルウィンドウを自作したことはなかったので、こちらにチャレンジすることにした。実際に、実装してみて、親と子のデータの受け渡しやtransionなどを使い、Vueの勉強には本当にはいい題材であると思えた。)

実装方法

色んな実装方法があるが、参考としてご紹介。 今回は最初に完成したコードを紹介して、つまづいた点を共有したいと思う。 以下のコードはModalComponentとして作成し、pages/index.vueで呼び出す。

<script>
export default {
  head () {
    return {
      bodyAttrs: {
        class: this.isOpened ? 'modal-opened' : ''
      }
    }
  },

  data () {
    return {
      isOpened: false,
    }
  },

  methods: {
    show () {
      this.isOpened = true
    },

    hide () {
      this.isOpened = false
    },
  }
}
</script>

<template>
  <transition name="r2l">
    <div
      v-if="isOpened"
      class="modal-wrapper"
      @click.self="hide"
    >
      <div id="modal" class="modal-container">
        <!-- Modal Contents -->
      </div>
    </div>
  </transition>
</template>

<style lang="scss" scoped>
.modal {
  &-wrapper {
    position: fixed;
    top: 0;
    bottom: 0;
    right: 0;
    left: 0;
    background: rgba(0,0,0,.75);
    z-index: 999;
    display: flex;
    justify-content: center;
    align-items: center;
  }
  &-container {
    background-color: white;
    width: 85%;
    max-width: 900px;
    height: auto;
    max-height: 90%;
    border-radius: 8px;
    overflow: auto;
  }
}

.r2l-enter-active,
.r2l-leave-active {
  transition: opacity 500ms;
}
.r2l-enter,
.r2l-leave-to {
  opacity: 0;
}
.r2l-enter-active > .modal-container,
.r2l-leave-active > .modal-container {
  transition: all 500ms;
}
.r2l-enter > .modal-container {
  transform: translateX(100%);
}
.r2l-leave-to > .modal-container {
  transform: translateX(-100%);
}
</style>

まずは、モーダルウィンドウが開いたときに、背景がスクロールしてしまう問題が発生するので、headメソッド内で bodyAttrs を使って、bodyにクラス名を付与する。bodyに modal-openedが付与された場合には、背景がスクロールされないようなcssを付与しておく。

bodyAttrs: {
  class: this.isOpened ? 'modal-opened' : ''
}
body {
  &.modal-opened {
    overflow: hidden;
    height: 100%;
  }
}

モーダルウィンドウの開閉に関しては、data内にある変数 isOpenedv-if を使うことで、Trueになった時だけModalコンポーネントのDOMを生成する。

アニメーションに関しては、 v-iftransitionを使うことで実現することができる。今回のモーダルに関しては、ウィンドウの右側からフェードインして、ウィンドウの左側にフェードアウトするアニメーションとした。

jp.vuejs.org

そして、最後に、モーダルウィンドウの背景(modal-wrapper)に @click.self="hide"イベントを設定することで、モーダルウィンドウの外側をタップした場合に、モーダルウィンドウが閉じるように設定を行った。

ここでは、イベント修飾子の self を使うことで、モーダルウィンドウの内側がクリックされた場合には、イベントが発火されず、外側の背景がクリックされた時にのみ、イベントが発火される。このイベント修飾子はとても便利なので、是非覚えておきたい。

jp.vuejs.org

ただ、実際に私は最初windowに対して、イベントリスナーを追加して以下のように実装していた。 transitionにもイベントハンドラーが設定してあり、Enter/Leaveの前後でメソッドを実行することができるのは非常に便利であると感じた。

<script>
export default {
  methods: {
    show () {
      this.isOpened = true
    },

    hide (e) {
      if (e && e.target.closest('#modal')) return
      this.isOpened = false
    },

    afterEnter () {
      window.addEventListener('click', this.hide, false)
    },

    beforeLeave () {
      window.removeEventListener('click', this.hide, false)
    }
  }
}
</script>

<template>
  <transition
    @after-enter="afterEnter"
    @before-leave="beforeLeave"
  >
    <div
      v-if="isOpened"
      class="modal-wrapper"
    >
      <div id="modal" class="modal-container">
        <!-- Modal内容 -->
      </div>
    </div>
  </transition>
</template>

上記の実装では、iOSChromeSafariでクリックイベントを検出できないことが発覚し、DOMのクリックイベントで発火するように変更を行った。

親(pages/index.vue)から子(modal.vue)のメソッドを使う場合は、 $refsを用いて実行することができる。(参考までに、子から親のメソッドを使う場合は、 $emitを用いて実行することができる。)

<script>
import Modal from '~/components/modal.vue'

export default {
  components: { Modal }
}
</script>

<template>
  <section>
    <button @click="$refs.modal.show()">Open Modal</button>
    <Modal ref="modal" />
  </section>
</template>

モーダルウィンドウを今回自作してみたが、様々なコンポーネントで呼び出す場合に毎回各コンポーネントref を定義して呼び出す必要があり、スマートな実装とは言えない。vue-modal-jsのように、インスタンスプロパティにモーダル開閉のメソッドは登録して呼び出せるようにしたいと思えた。

jp.vuejs.org