このモーダルはHTMLで作ってください
削除ボタン押したときに**「削除してよろしいですか?」**ってモーダル1をHTMLでデザインして作ってください…なんて言われたことはありませんか?
正直、モーダル回りではwindow.alert()
やwindow.confirm()
の方が勝手が良いことが多いのが作る側の印象です。
これらのWEB APIはブラウザ側で完全に同期として動いてくれるので効率が良いです(特にconfirm)。
if (window.confirm('削除してよろしいですか?')){
// OKを選択した時のロジック
}
これで削除確認のモーダルが書けます。
見た目もスマホのブラウザならシステムとの親和性も高い見た目で、ユーザーの誤認も防ぐことが出来ると思います。
しかしながらデザイナーサイドの声が強い場合、作らざるを得ないのですがモーダルをコンポーネント化すると、いたるところにモーダルコンポーネントが潜む形になり、管理コストが上がってしまう事が多いです。
さらに、DOM階層の深いところからコンポーネントとして表示すると、重なり合わせの条件が複雑になりz-indexでのコントロールが辛くなります。
最終的に、異常に大きい数値のz-indexが指定されるという…。
自分としては、あまりモーダルをコンポーネント化して、各所で読み込ませるような作りはお勧めできないのではないのかと思っています。
DOM階層側からの考察
モーダル等を作る際に、DOM構造から最も良い場所を考えます。
<template>
<div id="app" class="page">
<section class="content">
<router-view />
</section>
<Modal class="modal" />
</div>
</template>
<style>
.content {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: auto;
}
.modal {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>
メインコンテンツとモーダルのDOMを完全に並列の位置関係にします。
cssでbody
要素のスクロールはしない様にし、.content
でスクロールをするようにします。
こうすることで、.modal
領域のスクロールイベントに.content
領域が引っ張られるようなことはなくなります。
つまり、モーダル出現時にスクロールをロックするようなscriptを書く必要がなくなります。
(ただスマホの場合はURLバーが消えなかったりするようになるので、ケースバイケースで行います。)
また、.content
よりも.modal
が後に来ているので、表示順としては最終的に.modal
側が優先されるようになります。
使い勝手の良いモーダルにするには
自分の見解としてはwindow.confirm()
などの使い方が一番好きなので、それに近い使用感を持った構造にします。
仕組みとしては以下のような仕立てを考えました。
- Vuexでモーダル全体の表示と文字列をコントロールする
- if分の代わりに
promise
で制御するようにする。
この計画に合わせて実装していきます。
実際に動く状態のコードに落とし込んでみる
Vuexで実装した場合mutationの種類は、
- モーダル開く
- モーダル閉じる→OKボタンなどの「ポジティブ要素」
- モーダル閉じる→キャンセルなどの「ネガティブ要素」
大体3種類くらは必要になります。
promiseでこの仕組みを組むには少し難しかったので、jQueryなどでおなじみのDeferred
を使います。
jQueryと言うとjQuery殺すマンが押し寄せてきますので、MDNにあるヘルパー関数を利用します。
参考: https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/Promise.jsm/Deferred
こんな感じのjsファイルを作ります。
export default function Deferred() {
if (typeof Promise != 'undefined' && Promise.defer) {
return Promise.defer()
} else if (typeof PromiseUtils != 'undefined' && PromiseUtils.defer) {
return PromiseUtils.defer()
} else {
this.resolve = null
this.reject = null
this.promise = new Promise(
function(resolve, reject) {
this.resolve = resolve
this.reject = reject
}.bind(this)
)
Object.freeze(this)
}
}
store.jsはこんな感じ
import Vue from 'vue'
import Vuex from 'vuex'
import Deferred from '@/deferred' // Deferredのヘルパー
Vue.use(Vuex)
let deferred = null
export default new Vuex.Store({
state: {
modal: {
isOpen: false,
head: '',
body: '',
data: {}
}
},
mutations: {
changeModalState(state, object) {
state.modal = object
},
resetModalState(state) {
state.modal = {
isOpen: false,
name: '',
message: '',
data: {}
}
}
},
actions: {
modalOpen(context, options) {
deferred = new Deferred()
const option = {
isOpen: true,
head: '',
body: '',
data: {},
...options
}
context.commit('changeModalState', option)
deferred.promise.then(
() => { deferred = null },
() => { deferred = null }
)
return deferred.promise
},
modalResolve(context, result = '') {
deferred.resolve(result)
context.commit('resetModalState')
},
modalReject(context, result = '') {
deferred.reject(result)
context.commit('resetModalState')
}
},
})
モーダル部分の構造はこうなります。(万が一のdataは使ってないです)
<template>
<section v-if="modal.isOpen" class="modal">
<div class="background">
<div class="outline">
<div class="head">{{ modal.head }}</div>
<div class="body">{{ modal.body }}</div>
<div class="button-area">
<button @click="method.negativeAction()">CANCEL</button>
<button @click="method.positiveAction()">OK</button>
</div>
</div>
</div>
</section>
</template>
<script>
import { defineComponent, computed } from '@vue/composition-api'
import store from '@/store/index'
const method = {
positiveAction() {
store.dispatch('modalResolve', 'OKボタンを選択')
},
negativeAction() {
store.dispatch('modalReject', 'CANCELボタンを選択')
}
}
export default defineComponent({
setup() {
// templateでは$store.state.modalでも呼べるけどcomputedを使って省略する
const modal = computed(() => store.state.modal)
return {
modal,
method
}
}
})
</script>
モーダルを呼び出す場合は下記のようになります。
store.dispatch('modalOpen', {
head: 'モーダルテスト',
body: 'ボタンを選択後、選択結果をcosole.logで確認できます。'
}).then((result) => {
console.log(result)
}).catch((result) => {
console.log(result)
})
dispatch
にDeferred
を使ってPromise
を受け取ることが出来ます。
ifほどでは無いですがOKとキャンセルの様な要素を受け取れるので、(そこまで)分岐も複雑にはなりません。
様々なモーダルパターンが必要な場合は、制御用のIDなどを設定し、IDもdispatchで引き渡して合致するものだけ表示するようにすれば良いかと思います。
個人的には、モーダルのボタンを押した後の処理が、then
とcatch
にかけるので、モーダル表示した瞬間に削除APIが呼ばれてしまう…なんて事は起きにくいのかなぁと思って使っています。
-
厳密にはダイアログですが、便宜上モーダルと言う名称で統一しています ↩