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

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

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

最終的に出来上がったものをgithubにアップしました。
[RikutoYamaguchi/vue-vuex-modal-boilerplate](https://github.com/RikutoYamaguchi/vue-vuex-modal-boilerplate)
[動作サンプル](https://rikutoyamaguchi.github.io/vue-vuex-modal-boilerplate/index.html)

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

[Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%A2%E3%83%BC%E3%83%80%E3%83%AB%E3%82%A6%E3%82%A3%E3%83%B3%E3%83%89%E3%82%A6)より

> モーダルウィンドウ（英: modal window）は、コンピュータアプリケーションソフトウェアのユーザインタフェース設計において、何らかのウィンドウの子ウィンドウとして生成されるサブ要素のうち、ユーザーがそれに対して適切に応答しない限り、制御を親ウィンドウに戻さないもの。モーダルウィンドウはGUIシステムで、ユーザーに注意を促したり、選択肢を提示したり、緊急の状態を知らせたりする目的でよく使われる。モーダルダイアログやポップアップと呼ばれることもある。

> # ユースケース
> モーダルウィンドウは以下のような状況でよく使われる。
>
> - 特に重要な情報への注意を喚起する。この用法は効果的ではないという批判もある。
> - 必要な入力がなされるまで、アプリケーションの実行をブロックする。
> - アプリケーションの設定オプションを1つのダイアログに集約する。
> - 今実行しようとしている操作が取り消すことができないことを警告する。ファイルの書き込みや削除でよく見られるが、その意図する効果（間違った操作を防ぐ）は疑問視する向きもあり、代替策もある。

とありますね。

この中で注目したいのは「ユースケース」の中の`必要な入力がなされるまで、アプリケーションの実行をブロックする。`という部分です。
これ自分が作ってきたアプリケーションだと殆どの場合が当てはまるんですよね。

# モーダルウィンドウからの処理実行パターン

- 中断した処理の継続
- 中断した処理の破棄
- 中断した処理へ新たな値を渡し継続
- 中断した処理とは別の処理へ移動（破棄＆新規処理追加）

など、いろんなパターンがありそうです。

# 実装したいこと

今回実装していきたい項目を出します。

## callback実装

モーダルウィンドウを管理するVuexモジュールへcallbackも一緒に渡してあげることで、
モーダルウィンドウを呼び出した側から後の実行を制御できそうです。

## Promise(Deferred)実装

モーダルウィンドウを出力すること自体をPromiseで実装し、then/catchを用いて後処理の実装を制御します。
モーダルウィンドウを呼び出した側では、呼び出した時に戻ってくるDeferredにthen/catchを追加します。

## 履歴処理

[vue-router](https://router.vuejs.org/ja/)のようなメソッドでモーダルウィンドウの呼び出し、遷移を行うようにします。
※ ついでにtransitionの制御も実装！

# 実装したもの

[RikutoYamaguchi/vue-vuex-modal-boilerplate](https://github.com/RikutoYamaguchi/vue-vuex-modal-boilerplate)

# 実装ポイント解説

全体の実装を解説すると、長くなってしまうので詳細はソースをご覧いただければと思います。
ここでは要点だけに絞って解説します。

## actionからPromise(Deferred)を返す


[公式ドキュメント](https://vuex.vuejs.org/ja/actions.html#%E3%82%A2%E3%82%AF%E3%82%B7%E3%83%A7%E3%83%B3%E3%82%92%E6%A7%8B%E6%88%90%E3%81%99%E3%82%8B)にて、Promiseを返す方法やasync/awaitで記述する方法が紹介されています。

今回は「モーダルウィンドウの表示が終わったらPromiseを解決する」実装をしたいので、モーダルウィンドウを呼び出したときのアクション内ではPromiseの解決が起こりません。
なので、一旦promiseオブジェクトの`resolve`と`reject`を外側の保存しておく必要がありました。(Deferred化と捉えています)

参考:[JavaScript Promiseの本 - DeferredとPromise](http://azu.github.io/promises-book/#deferred-and-promise)

### Deferredを作り、Promiseを返す

```javascript:/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;
    }
  },
  // ...
}
// ...
```

```javascript:/src/store/modal/index.jsから抜粋
// ...
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`する


```javascript:/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');
  },
  // ...
}
```

```javascript:/src/store/modal/index.jsから抜粋
const getters = {
  // ...
  deferred: state => state.deferred
};
```

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


### `actions.push`を呼び出す側

```html:/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を管理してあげるだけです。

```javascript:/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させています。
この解決方法がないか模索中。
いい方法があれば教えてください！