Vue・Vuexでモーダルを管理する - その2
以前Vue・Vuexでモーダルを管理するで、動的コンポーネントを用いた実装を記載いたしましたが、
これだけでは実案件で使うために不十分だったため、さらに深掘って実装を追加していきたいと思います。
最終的に出来上がったものをgithubにアップしました。
RikutoYamaguchi/vue-vuex-modal-boilerplate
動作サンプル
そもそもモーダルウィンドウとは
モーダルウィンドウ(英: modal window)は、コンピュータアプリケーションソフトウェアのユーザインタフェース設計において、何らかのウィンドウの子ウィンドウとして生成されるサブ要素のうち、ユーザーがそれに対して適切に応答しない限り、制御を親ウィンドウに戻さないもの。モーダルウィンドウはGUIシステムで、ユーザーに注意を促したり、選択肢を提示したり、緊急の状態を知らせたりする目的でよく使われる。モーダルダイアログやポップアップと呼ばれることもある。
ユースケース
モーダルウィンドウは以下のような状況でよく使われる。
- 特に重要な情報への注意を喚起する。この用法は効果的ではないという批判もある。
- 必要な入力がなされるまで、アプリケーションの実行をブロックする。
- アプリケーションの設定オプションを1つのダイアログに集約する。
- 今実行しようとしている操作が取り消すことができないことを警告する。ファイルの書き込みや削除でよく見られるが、その意図する効果(間違った操作を防ぐ)は疑問視する向きもあり、代替策もある。
とありますね。
この中で注目したいのは「ユースケース」の中の必要な入力がなされるまで、アプリケーションの実行をブロックする。
という部分です。
これ自分が作ってきたアプリケーションだと殆どの場合が当てはまるんですよね。
モーダルウィンドウからの処理実行パターン
- 中断した処理の継続
- 中断した処理の破棄
- 中断した処理へ新たな値を渡し継続
- 中断した処理とは別の処理へ移動(破棄&新規処理追加)
など、いろんなパターンがありそうです。
実装したいこと
今回実装していきたい項目を出します。
callback実装
モーダルウィンドウを管理するVuexモジュールへcallbackも一緒に渡してあげることで、
モーダルウィンドウを呼び出した側から後の実行を制御できそうです。
Promise(Deferred)実装
モーダルウィンドウを出力すること自体をPromiseで実装し、then/catchを用いて後処理の実装を制御します。
モーダルウィンドウを呼び出した側では、呼び出した時に戻ってくるDeferredにthen/catchを追加します。
履歴処理
vue-routerのようなメソッドでモーダルウィンドウの呼び出し、遷移を行うようにします。
※ ついでにtransitionの制御も実装!
実装したもの
RikutoYamaguchi/vue-vuex-modal-boilerplate
実装ポイント解説
全体の実装を解説すると、長くなってしまうので詳細はソースをご覧いただければと思います。
ここでは要点だけに絞って解説します。
actionからPromise(Deferred)を返す
公式ドキュメントにて、Promiseを返す方法やasync/awaitで記述する方法が紹介されています。
今回は「モーダルウィンドウの表示が終わったらPromiseを解決する」実装をしたいので、モーダルウィンドウを呼び出したときのアクション内ではPromiseの解決が起こりません。
なので、一旦promiseオブジェクトのresolve
とreject
を外側の保存しておく必要がありました。(Deferred化と捉えています)
参考:JavaScript Promiseの本 - DeferredとPromise
Deferredを作り、Promiseを返す
// ...
let promiseStore = null;
const createDeferred = (commit) => {
let _resolve = null;
let _reject = null;
promiseStore = new Promise((resolve, reject) => {
_resolve = resolve;
_reject = reject;
});
commit(INIT_DEFERRED, {
resolve: _resolve,
reject: _reject
});
};
// ...
export default {
push({ commit, getters, dispatch }, { name, params, callback = null, dfd }) {
// ...
if (deferred === null) {
createDeferred(commit);
}
if (dfd) {
return promiseStore;
}
},
// ...
}
// ...
// ...
const mutations = {
// ...
[INIT_DEFERRED] (state, { resolve, reject }) {
state.deferred = { resolve, reject };
}
};
// ...
関数createDeferred
では、新たに作ったpromiseオブジェクトを変数promiseStore
へ保存します。
引数dfd
にtrue
が入っていた場合、promiseオブジェクト自体を返却します。
mutations[INIT_DEFERRED]
ではstate.deferred
に対して、先程作成したpromiseオブジェクトのresolve
とreject
を保存します。
Deferredをresolve
reject
する
export default {
// ...
reject({ dispatch, getters }, err = null) {
const { callbacks, deferred } = getters;
execCallbacks(callbacks, { err, data: null });
if (deferred !== null) {
deferred.reject(err);
}
dispatch('close');
},
resolve({ dispatch, getters }, data) {
const { callbacks, deferred } = getters;
execCallbacks(callbacks, { err: null, data });
if (deferred !== null) {
deferred.resolve(data);
}
dispatch('close');
},
// ...
}
const getters = {
// ...
deferred: state => state.deferred
};
actions.reject
とactions.resolve
では、getters
からstate.deferred
を受け取り、
resolve
またはreject
を実行しています。
actions.push
を呼び出す側
// ...
<script>
import { mapActions } from 'vuex'
export default {
name: 'Home',
methods: {
...mapActions('modal', ['push']),
onClickPush () {
const dfd = this.push({
name: 'ModalSample1',
params: {
message: `This message was passed by 'params'.`
},
dfd: true, // If you need feedback from modal module.
callback: (err, data) => {
console.log('callback');
console.log(data);
}
});
// If you need feedback from modal module.
dfd.then(data => {
console.log('then');
console.log(data);
}).catch(err => {
console.log('err');
console.log(err);
})
}
}
}
</script>
// ...
modasl/push
をmapしたコンポーネントにて、
this.push
を実行する際dfd:true
を指定することでpromiseオブジェクトが返ってきます。
履歴の管理
actions.forward
actions.go(n)
actions.back()
などで前のモーダル、後のモーダルへ移動できるようにします。
基本的には配列での情報と現在のindexを管理してあげるだけです。
// ...
const state = {
modalNames: [],
modalParams: [],
currentIndex: 0,
// ...
}
// ...
const mutations = {
[PUSH] (state, { name, params = null, callback }) {
state.modalNames.push(name);
state.modalParams.push(params);
state.callbacks.push(callback);
},
[REPLACE] (state, { name, params = null, callback, index }) {
state.modalNames[index] = name;
state.modalParams[index] = params;
state.callbacks[index] = callback;
state.modalNames.splice(index + 1, state.modalNames.length - 1);
state.modalParams.splice(index + 1, state.modalParams.length - 1);
state.callbacks.splice(index + 1, state.callbacks.length - 1);
},
[CLOSE] (state) {
state.modalNames = [];
state.modalParams = [];
state.transitionName = TRANSITION_NAMES.default;
state.currentIndex = 0;
state.callbacks = [];
state.deferred = null;
},
[ADD_INDEX] (state, n) {
state.currentIndex += n;
},
[CHANGE_INDEX] (state, n) {
state.currentIndex = n;
},
};
const getters = {
// ...
currentIndex: state => state.currentIndex,
currentModalName: state => state.modalNames[state.currentIndex] || null,
currentModalParams: state => state.modalParams[state.currentIndex] || null,
// ...
};
// ...
state
には履歴と同等に管理したい内容を配列で管理します。
mutations
では常に配列に対して操作をしていく感じになりますね。
getters
では現在の情報をstate.currentIndex
を使って返してあげます。
困ったこと
操作に応じてtransitionの挙動を変更しているのですが、
state.transitionName
の変更後、state.currentIndex
をすぐに変更してしまうと、transitionが思ったように動かない(前のtransitionNameが採用される)ので、
現状はdelayさせています。
この解決方法がないか模索中。
いい方法があれば教えてください!