多くのプロジェクトではモーダルダイアログが必要になるが、最近のプロジェクトではアラートとコンファームのデザインが全く異なるため、それぞれ別のコンポーネントとして分離した。本記事ではコンファームダイアログの実装について説明する。
コンポーネント構造
<template>
<div class="modal" v-show="isVisible" transition="fade">
<div class="modal-dialog">
<div class="modal-content">
<!-- ヘッダー -->
<div class="modal-header">
<slot name="header">
<p class="title">{{config.title}}</p>
</slot>
<a v-touch:tap="handleClose(0)" class="close" href="javascript:void(0)"></a>
</div>
<!-- 本文 -->
<div class="modal-body">
<slot name="body">
<p class="notice">{{config.text}}</p>
</slot>
</div>
<!-- フッター -->
<div class="modal-footer">
<slot name="button">
<a v-if="config.showCancelButton" href="javascript:void(0)" class="button {{config.cancelButtonClass}}" v-touch:tap="handleClose(1)">{{config.cancelButtonText}}</a>
<a v-if="config.showConfirmButton" href="javascript:void(0)" class="button {{config.confirmButtonClass}}" v-touch:tap="handleConfirm">{{config.confirmButtonText}}</a>
</slot>
</div>
</div>
</div>
</div>
<div v-show="isVisible" class="modal-backup" transition="fade"></div>
</template>
モーダルはヘッダー、ボディ、フッターの3つの部分に分けられ、それぞれにslotが用意されており、カスタマイズ可能である。
スタイル
.modal {
position: fixed;
left: 0;
top: 0;
right: 0;
bottom: 0;
z-index: 1001;
-webkit-overflow-scrolling: touch;
outline: 0;
overflow: scroll;
margin: 30/@rate auto;
}
.modal-dialog {
position: absolute;
left: 50%;
top: 0;
transform: translate(-50%,0);
width: 690/@rate;
padding: 50/@rate 40/@rate;
background: #fff;
}
.modal-backup {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 1000;
background: rgba(0, 0, 0, 0.5);
}
基本的なスタイルのみを示す。モバイル向けのプロジェクトであり、タオバオのレスポンシブ設計を使用しており、@rateはデザイン時の比率である。
インターフェース定義
/**
* モーダルダイアログのパラメータ定義
* @param {string} config.title タイトル
* @param {string} config.text 内容
* @param {boolean} config.showCancelButton キャンセルボタン表示
* @param {string} config.cancelButtonClass キャンセルボタンスタイル
* @param {string} config.cancelButtonText キャンセルボタンテキスト
* @param {boolean} config.showConfirmButton 確定ボタン表示
* @param {string} config.confirmButtonClass 確定ボタンスタイル
* @param {string} config.confirmButtonText 確定ボタンテキスト
*/
props: ['modalConfig'],
computed: {
/**
* propsから渡されたパラメータを処理し、デフォルト値を設定
*/
config: {
get() {
let modal = this.modalConfig;
modal = {
title: modal.title || '確認',
text: modal.text,
showCancelButton: typeof modal.showCancelButton === 'undefined' ? true : modal.showCancelButton,
cancelButtonClass: modal.cancelButtonClass ? modal.showCancelButton : 'btn-default',
cancelButtonText: modal.cancelButtonText ? modal.cancelButtonText : 'キャンセル',
showConfirmButton: typeof modal.showConfirmButton === 'undefined' ? true : modal.cancelButtonClass,
confirmButtonClass: modal.confirmButtonClass ? modal.confirmButtonClass : 'btn-active',
confirmButtonText: modal.confirmButtonText ? modal.confirmButtonText : '確定',
};
return modal;
},
},
},
パラメータにはタイトル、内容、ボタン表示・非表示、ボタンスタイルを指定可能で、computedでデフォルト値を管理している。
内部メソッド
data() {
return {
isVisible: false, // 表示状態
resolve: '',
reject: '',
promise: '', // Promiseオブジェクトの保持
};
},
methods: {
/**
* 確定処理、Promiseを解決状態にする
*/
handleConfirm() {
this.resolve('submit');
},
/**
* 閉じる処理、Promiseを拒否状態にする
* @param type {number} 閉じる方法 0=閉じるボタン、1=キャンセルボタン
*/
handleClose(type) {
this.isVisible = false;
this.reject(type);
},
/**
* コンファームダイアログを表示し、Promiseオブジェクトを作成
* @returns {Promise}
*/
confirm() {
this.isVisible = true;
this.promise = new Promise((resolve, reject) => {
this.resolve = resolve;
this.reject = reject;
});
return this.promise;
},
},
コンポーネント内に3つのメソッドがあり、最も重要なのはconfirmメソッドで、親コンポーネントから呼び出される。このメソッドはPromiseオブジェクトを返し、resolveとrejectをコンポーネントのデータに格納する。キャンセルボタンクリック時はreject状態にし、閉じる。確定ボタンクリック時はresolve状態にし、閉じる処理は親コンポーネントに任せる。
呼び出し
<!-- template -->
<confirm v-ref:dialog :modal-config.sync="modal"></confirm>
<!-- methods -->
this.$refs.dialog.confirm().then(() => {
// 確定ボタンクリック時の処理
callback();
this.$refs.dialog.isVisible = false;
}).catch(() => {
// キャンセルボタンクリック時の処理
callback();
});
v-refを使ってコンポーネントのインスタンスを取得し、簡単な操作ができる。これでコンポーネントの実装は完了である。
他の実装方法
モーダル内の確定・キャンセルボタンクリック時に親コンポーネントにイベントを伝えるのが難しい。いくつかの実装方法を参考にした。
イベント転送を使う
この方法は同僚が実装したもので、$dispatchと$broadcastを使用してイベントを送信する。
まずルートコンポーネントでtransmitイベントを受け取り、それをbroadcastで他のコンポーネントに送信する。
events: {
/**
* イベントの転送
* @param {string} eventName イベント名
* @param {object} arg 引数
* @return {null}
*/
'transmit': function (eventName, arg) {
this.$broadcast(eventName, arg);
}
},
次にモーダルコンポーネントでイベント名を受け取り、ボタンクリック時に発火させる。
// イベント受信
events: {
'tip': function(obj) {
this.events = {
cancel: obj.events.cancel,
confirm: obj.events.confirm
}
}
}
// キャンセルボタン
cancel:function() {
this.$dispatch('transmit',this.events.cancel);
}
// 確定ボタン
submit: function() {
this.$dispatch('transmit',this.events.submit);
}
親コンポーネントでの呼び出しは以下の通り:
this.$dispatch('transmit','tip',{
events: {
confirm: 'confirmEvent'
}
});
this.$once('confirmEvent',function() {
callback();
}
最初にtipイベントを送信し、イベント名をモーダルに渡し、$onceでイベントを監視する。しかし、Vue 2.0では$dispatchと$broadcastが廃止されたため、今後の移行を考慮して使用しない方が良い。
emitを使用する
vue-bootstrap-modalの実装方法に倣い、ボタンクリック時にemitイベントを発生させ、コンポーネント側で直接監視する。
// 確定ボタン
ok () {
this.$emit('ok');
if (this.closeWhenOK) {
this.isVisible = false;
}
},
// キャンセルボタン
cancel () {
this.$emit('cancel');
this.isVisible = false;
},
呼び出しは:
<modal title="Modal Title" :show.sync="show" @ok="ok" @cancel="cancel">
Modal Text
</modal>
ただし、複数のモーダルが必要な場合、各モーダルごとにテンプレートを記述する必要があり、煩雑になる。
参考資料
- vue.js dynamic create nest modal
- es6 Promiseオブジェクト
- vue-bootstrap-modal