LoginSignup
30
19

More than 5 years have passed since last update.

Vue・Vuexでモーダルを管理する - その2

Last updated at Posted at 2018-02-07

Vue・Vuexでモーダルを管理する - その2

以前Vue・Vuexでモーダルを管理するで、動的コンポーネントを用いた実装を記載いたしましたが、
これだけでは実案件で使うために不十分だったため、さらに深掘って実装を追加していきたいと思います。

最終的に出来上がったものをgithubにアップしました。
RikutoYamaguchi/vue-vuex-modal-boilerplate
動作サンプル

そもそもモーダルウィンドウとは

Wikipediaより

モーダルウィンドウ(英: 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オブジェクトのresolverejectを外側の保存しておく必要がありました。(Deferred化と捉えています)

参考:JavaScript Promiseの本 - DeferredとPromise

Deferredを作り、Promiseを返す

/src/store/modal/actions.jsから抜粋
// ...

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;
    }
  },
  // ...
}
// ...
/src/store/modal/index.jsから抜粋
// ...
const mutations = {
  // ...
  [INIT_DEFERRED] (state, { resolve, reject }) {
    state.deferred = { resolve, reject };
  }
};
// ...

関数createDeferredでは、新たに作ったpromiseオブジェクトを変数promiseStoreへ保存します。
引数dfdtrueが入っていた場合、promiseオブジェクト自体を返却します。

mutations[INIT_DEFERRED]ではstate.deferredに対して、先程作成したpromiseオブジェクトのresolverejectを保存します。

Deferredをresolve rejectする

/src/store/modal/actions.jsから抜粋
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');
  },
  // ...
}
/src/store/modal/index.jsから抜粋
const getters = {
  // ...
  deferred: state => state.deferred
};

actions.rejectactions.resolveでは、gettersからstate.deferredを受け取り、
resolveまたはrejectを実行しています。

actions.pushを呼び出す側

/src/components/Home.vueから抜粋
// ...
<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を管理してあげるだけです。

/src/store/modal/index.jsから抜粋
// ...
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させています。
この解決方法がないか模索中。
いい方法があれば教えてください!

30
19
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
19