【Vue】モーダルウィンドウを自作してみた話
先日、Vue.jsを用いてSPAのHP制作を行なった。
SPAの中でモーダルウィンドウを用いて、詳細情報を表示することは情報をはっきりと強調して提示でき有効な手段である。今回は、そのモーダルウィンドウを自作したので、実装方法を紹介する。
なぜ、自作したのか?
便利なモジュールはたくさんある。調べていて出てくるのは以下のモジュールだ。
最初はこちらを使って実装していたが、ある問題にぶつかった。 Max-Heightが思うように機能してくれない。。。 モーダルウィンドウの中のコンテンツが縦長い場合に以下のようなデザインになってしまった。
中央でモーダルウィンドウを固定して、画面全体まで広がってほしくなかったが、パラメータを設定してもどうしても実現できず、自作することにした。
Vue.jsの公式にも以下のように実装例がのっていた。
(本来はコードを読んできちんと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内にある変数 isOpened
と v-if
を使うことで、Trueになった時だけModalコンポーネントのDOMを生成する。
アニメーションに関しては、 v-if
と transition
を使うことで実現することができる。今回のモーダルに関しては、ウィンドウの右側からフェードインして、ウィンドウの左側にフェードアウトするアニメーションとした。
そして、最後に、モーダルウィンドウの背景(modal-wrapper)に @click.self="hide"
イベントを設定することで、モーダルウィンドウの外側をタップした場合に、モーダルウィンドウが閉じるように設定を行った。
ここでは、イベント修飾子の self
を使うことで、モーダルウィンドウの内側がクリックされた場合には、イベントが発火されず、外側の背景がクリックされた時にのみ、イベントが発火される。このイベント修飾子はとても便利なので、是非覚えておきたい。
ただ、実際に私は最初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>
上記の実装では、iOSのChromeとSafariでクリックイベントを検出できないことが発覚し、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のように、インスタンスプロパティにモーダル開閉のメソッドは登録して呼び出せるようにしたいと思えた。