Vue・Vuexでモーダルを管理する

# はじめに

本記事ではモーダルの管理について記載しますが、
他の「どこからでもユーザーアクションによって呼び出せる系のUI」は、基本的に同じ考え方で実装が可能だと思います。

本記事で書いたソースは以下にアップしています。
https://github.com/RikutoYamaguchi/vue-vuex-modal

実装は[vue-cli](https://github.com/vuejs/vue-cli)をベースに行っています。

記事の内容とは関係ないので説明は省きますが、scssのコンパイルのために以下をインストールしています。
・"node-sass": "^4.5.3"
・"sass-loader": "^6.0.6"

追記：
さらに発展させたものを書きました。
[Vue・Vuexでモーダルを管理する - その２](https://qiita.com/RikutoYamaguchi/items/e13cd2903ba1b493410a)

# Vuexの導入

vue-cliによるスキャフォールディングにはVuexのインストールは含まれないので、はじめにVuexのインストールを行います。

``` bash
$ yarn add vuex
```

## Vuexのセットアップ

次にVuexをモジュールシステムで利用するための記述と、storeを作成するために`src/store/index.js`を作成します。

``` js:src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

// モジュールシステムで使用するための記述
Vue.use(Vuex)

const state = {
}

const modules = {
}

const store = new Vuex.Store({
  state,
  modules
})

export default store

```

## コンポーネントへ注入

また、すべてのコンポーネントでvuexを利用するために、先ほど作成したstoreを注入します。

```diff:src/main.js
// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
+import store from './store'

Vue.config.productionTip = false

/* eslint-disable no-new */
new Vue({
  el: '#app',
+  store,
  template: '<App/>',
  components: { App }
})
```

（Vuex導入の詳細は公式サイトをご参照ください。https://vuex.vuejs.org/ja/installation.html）


# 最も基本的なモーダル実装

それでは、最も基本的で簡単な実装を行い、モーダル実装の処理の流れを理解します。

## モジュールの作成

簡単なモーダルを管理するためのモジュールを作成します。

```js:src/store/modules/basicModal.js
const state = {
  isShow: false
}

const mutations = {
  show (state) {
    state.isShow = true
  },
  hide (state) {
    state.isShow = false
  }
}

export default {
  namespaced: true,
  state,
  mutations
}
```

stateのプロパティ`isShow`で「モーダルが表示されているかどうか」を管理します。
次に作成したモジュールをstoreに登録します。

``` diff:src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
+import basicModal from './modules/basicModal'

// モジュールシステムで使用するための記述
Vue.use(Vuex)

const state = {
}

const modules = {
+  basicModal
}

const store = new Vuex.Store({
  state,
  modules
})

export default store

```

これでモジュールが使用できるようになりました。

## コンポーネントの作成

必要なコンポーネントの作成を行います。
`src/App.vue`は初期状態から不要なものを全て削除した状態にしてスタートです。

``` html:src/App.vue
<template>
  <div id="app">
    <button @click="basicModalShow">basicModalShow</button>
  </div>
</template>

<script>
  import { mapMutations } from 'vuex'
  export default {
    name: 'app',
    methods: {
      ...mapMutations('basicModal', {
        basicModalShow: 'show'
      })
    }
  }
</script>

<style lang="scss">
  @import "./assets/style";
</style>
```

ここでは`mapMutations`を使い、`basicModal`モジュールの`show`ミューテーションを`basicModalShow`という名前のメソッドとして登録し、basicModalShowボタンをクリックした時に`basicModalShow`メソッドを実行するように記述しています。

（`mapMutations`の詳細はこちら→https://vuex.vuejs.org/ja/mutations.html）

次にモーダルのコンポーネントを作成します。

``` html:src/components/BasicModal.vue
<template>
  <transition name="fade">
    <div class="modal" v-if="isShow">
      <div class="modal__bg"></div>
      <div class="modal__content">
        <p>BasicModal</p>
        <button @click="hide">closeModal</button>
      </div>
    </div>
  </transition>
</template>

<script>
  import { mapState, mapMutations } from 'vuex'
  export default {
    name: 'BasicModal',
    computed: {
      ...mapState('basicModal', ['isShow'])
    },
    methods: {
      ...mapMutations('basicModal', ['hide'])
    }
  }
</script>
```

モーダルのコンポーネントでは、`mapState`と`mapMutations`を使用します。
`mapState`では`basicModal`モジュールの`isShow`ステートを参照できるようにします。
`mapMutations`では`basicModal`モジュールの`hide`ミューテーションをメソッドとして登録し、closeModalボタンをクリック時に実行します。
またvueのアニメーション機能である、transitionラッパーコンポーネントを使用して、モーダル出現のフェードを実装しています。

（`mapState`の詳細はこちら→https://vuex.vuejs.org/ja/state.html）
（transitionラッパーコンポーネントの詳細はこちら→https://jp.vuejs.org/v2/guide/transitions.html）

次に、モーダルコンポーネントを`App.vue`の子コンポーネントとして追加します。

``` diff:src/App.vue
<template>
  <div id="app">
    <button @click="basicModalShow">basicModalShow</button>
+    <basic-modal></basic-modal>
  </div>
</template>

<script>
  import { mapMutations } from 'vuex'
+  import BasicModal from './components/BasicModal.vue'
  export default {
    name: 'App',
+    components: {
+      BasicModal
+    },
    methods: {
      ...mapMutations('basicModal', {
        basicModalShow: 'show'
      })
    }
  }
</script>

<style lang="scss">
  @import "./assets/style";
</style>
```

これで、基本的なモーダル実装は完了です。
basicModalShowボタンを押したらモーダルが出現し、closeModalを押したらモーダルが消えることが確認できます。

処理は以下のような流れができています。

■ モーダル表示
1. basicModalShowボタンクリック
2. `basicModal`モジュールの`show`ミューテーションを実行
3. `isShow`ステートを`true`に変更
4. モーダルの表示

■ モーダル非表示
1. closeModalボタンクリック
2. `basicModal`モジュールの`hide`ミューテーションを実行
3. `isShow`ステートを`false`
4. モーダルの非表示

しかし、この実装には複数モーダルを管理できない欠点があります。
モーダルの種類が一種類しかない場合は良いですが、そんなアプリケーションはなかなか存在しないですね。
この実装のまま複数のモーダルを管理しようとすると、モーダル毎にモジュール、ないしは表示状態のステート、状態を更新するミューテーションを作成する必要があります。

次は、この実装の欠点を克服する、複数種類のモーダルを管理する実装を行います。

# 複数種類のモーダルを管理する実装

さて、最も基本的なモーダル実装では基本的なモーダルのための状態管理を知ることができましたが、単一のモーダルに対してしか有効ではありませんでした。
それは「モーダルが表示されているかどうか」という状態を管理対象としたことで、モーダルが複数あった場合、それぞれにその状態を管理する必要が出てきてしまうからです。

そこで、複数種類のモーダルを管理する場合は、「ページ上で表示すべきモーダルの名称」を状態として捉え、実装を行います。
「モーダルの名称」を管理することで、現在表示すべきモーダルコンポーネントの差し替えを行うことが可能です。

## モジュールの作成

早速、複数種類のモーダルを管理するためのモジュールを作成します。

``` js:src/store/modules/multiModal.js
const state = {
  modalName: ''
}

const mutations = {
  setModal (state, name) {
    state.modalName = name
  },
  hideModal (state) {
    state.modalName = ''
  }
}

const actions = {
  showModalTypeA ({ commit }) {
    commit('setModal', 'ModalTypeA')
  },
  showModalTypeB ({ commit }) {
    commit('setModal', 'ModalTypeB')
  },
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}
```

最も簡単なモーダル実装では、`state`のプロパティが`isShow`でしたが、今回は`modalName`としました。
また、`actions`を追加し、表示したいモーダルを呼び出せるようにしています。
（ミューテーションに直接モーダル名称を渡してもいいですが、コンポーネント側にモーダル名称を記述する必要があり、管理が大変になるのでアクションにモーダル名称を格納しました。）

忘れないうちに、storeへモジュールを登録します。


``` diff:src/store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
import basicModal from './modules/basicModal'
+import multiModal from './modules/multiModal'

// モジュールシステムで使用するための記述
Vue.use(Vuex)

const state = {
}

const modules = {
-  basicModal
+  basicModal,
+  multiModal
}

const store = new Vuex.Store({
  state,
  modules
})

export default store

```


## モーダル切り替えコンポーネントの作成

複数モーダルで一番重要な、モーダルを切り替えるコンポーネントを作成します。

``` html:src/components/MultiModalView.vue
<template>
  <transition name="fade">
    <component v-if="isShow" :is="modalName"></component>
  </transition>
</template>

<script>
  import { mapState } from 'vuex'
  export default {
    name: 'MultiModalView',
    computed: {
      ...mapState('multiModal', ['modalName']),
      isShow () {
        return this.modalName !== ''
      }
    }
  }
</script>
```

ここでは、vueの機能である「動的コンポーネント」を使用しています。
動的コンポーネントは:isに対してコンポーネント名を代入することで、そのコンポーネントを呼び出す機能があるので、そこへモーダルの名称を代入することで、モーダルの切り替えを行います。
（動的コンポーネントの詳細はこちら→https://jp.vuejs.org/v2/guide/components.html#動的コンポーネント）

computedのisShowでは、`multiModal`モジュールの`modalName`ステートの値が空文字かどうかを判断し、動的コンポーネントを表示するかどうかの制御を行っています。

App.vueの子コンポーネントとして追加するのを忘れずに。


``` diff:src/App.vue
<template>
  <div id="app">
    <button @click="basicModalShow">basicModalShow</button>
    <basic-modal></basic-modal>
+    <multi-modal-view></multi-modal-view>
  </div>
</template>

<script>
  import { mapMutations } from 'vuex'
  import BasicModal from './components/BasicModal.vue'
+  import MultiModalView from './components/MultiModalView.vue'
  export default {
    name: 'App',
    components: {
-      BasicModal
+      BasicModal,
+      MultiModalView
    },
    methods: {
      ...mapMutations('basicModal', {
        basicModalShow: 'show'
      })
    }
  }
</script>

<style lang="scss">
  @import "./assets/style";
</style>
```

## モーダルコンポーネントの作成

それではサクッとモーダルコンポーネントを用意しましょう。

``` html:src/components/ModalTypeA.vue
<template>
  <div class="modal">
    <div class="modal__bg"></div>
    <div class="modal__content">
      <p>ModalTypeA</p>
      <button @click="hideModal">closeModal</button>
    </div>
  </div>
</template>

<script>
  import { mapMutations } from 'vuex'
  import MultiModalMixin from '../mixins/MultiModalMixin'
  export default {
    name: 'ModalTypeA',
    mixins: [MultiModalMixin]
  }
</script>
```

``` html:src/components/ModalTypeB.vue
<template>
  <div class="modal">
    <div class="modal__bg"></div>
    <div class="modal__content">
      <p>ModalTypeB</p>
      <button @click="hideModal">closeModal</button>
    </div>
  </div>
</template>

<script>
  import { mapMutations } from 'vuex'
  import MultiModalMixin from '../mixins/MultiModalMixin'
  export default {
    name: 'ModalTypeB',
    mixins: [MultiModalMixin]
  }
</script>
```

``` js:src/mixins/MultiModalMixin.js
import { mapMutations } from 'vuex'
export default {
  methods: {
    ...mapMutations('multiModal', ['hideModal'])
  }
}
```

`multiModal`モジュールで用意した２つのアクションに対応するモーダルコンポーネントを作成しました。
`multiModal`モジュールの`hideModal`ミューテーションは両モーダルとも共通の記述になるのでmixinで登録します。
最も簡単なモーダル実装ではモーダルコンポーネントにtrainsitionでアニメーションを実装していましたが、この実装ではモーダル切り替えコンポーネントでアニメーションさせているので、モーダルコンポーネントには不要となります。

これらをモーダル切り替えコンポーネントの子コンポーネントへ追加します。

``` diff:src/components/MultiModalView.vue
<template>
  <transition name="fade">
    <component v-if="isShow" :is="modalName"></component>
  </transition>
</template>

<script>
  import { mapState } from 'vuex'
+  import ModalTypeA from './ModalTypeA.vue'
+  import ModalTypeB from './ModalTypeB.vue'
  export default {
    name: 'MultiModalView',
+    components: {
+      ModalTypeA,
+      ModalTypeB,
+    },
    computed: {
      ...mapState('multiModal', ['modalName']),
      isShow () {
        return this.modalName !== ''
      }
    }
  }
</script>

```

## 呼び出しの実装

最後にモーダルを呼ば出すボタンを`App.vue`へ追加します。


``` diff:src/App.vue
<template>
  <div id="app">
    <button @click="basicModalShow">basicModalShow</button>
+    <button @click="showModalTypeA">showModalTypeA</button>
+    <button @click="showModalTypeB">showModalTypeB</button>
    <basic-modal></basic-modal>
    <multi-modal-view></multi-modal-view>
  </div>
</template>

<script>
-  import { mapMutations } from 'vuex'
+  import { mapActions, mapMutations } from 'vuex'
  import BasicModal from './components/BasicModal.vue'
  import MultiModalView from './components/MultiModalView.vue'
  export default {
    name: 'App',
    components: {
      BasicModal,
      MultiModalView
    },
    methods: {
      ...mapMutations('basicModal', {
        basicModalShow: 'show'
-      })
+      }),
+      ...mapActions('multiModal', ['showModalTypeA', 'showModalTypeB'])
    }
  }
</script>

<style lang="scss">
  @import "./assets/style";
</style>
```

これで、ModalTypeAボタンとModalTypeBボタンを押したときに、それぞれのモーダルが表示されることが確認できます。

# おわりに

複数種類のモーダルを管理する実装は、冒頭でも延べたように他の「どこからでもユーザーアクションによって呼び出せる系のUI」にも応用ができます。
例えばstateへクリックされた位置を追加して、コンテキストメニューの管理をしたり。
また、モーダルの次のアクションとして他のモーダルを表示するときは、モーダルコンポーネントへ次のモーダルを呼び出すアクションを追加することで可能です。

次はコンテキストメニューの実装か、vue-routerを用いたモーダルの実装を書いてみようと思います。
ということで、最後まで閲覧いただきありがとうございました。