Vueでコンファームダイアログコンポーネントを実装する

多くのプロジェクトではモーダルダイアログが必要になるが、最近のプロジェクトではアラートとコンファームのデザインが全く異なるため、それぞれ別のコンポーネントとして分離した。本記事ではコンファームダイアログの実装について説明する。

コンポーネント構造

<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

タグ: vue.js コンポーネント モーダルダイアログ Promise イベント処理

7月7日 16:43 投稿