Help us understand the problem. What is going on with this article?

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

More than 1 year has passed since last update.

はじめに

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

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

実装はvue-cliをベースに行っています。

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

追記:
さらに発展させたものを書きました。
Vue・Vuexでモーダルを管理する - その2

Vuexの導入

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

$ yarn add vuex

Vuexのセットアップ

次にVuexをモジュールシステムで利用するための記述と、storeを作成するためにsrc/store/index.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を注入します。

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)

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

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

モジュールの作成

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

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に登録します。

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は初期状態から不要なものを全て削除した状態にしてスタートです。

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)

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

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>

モーダルのコンポーネントでは、mapStatemapMutationsを使用します。
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の子コンポーネントとして追加します。

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. モーダルの非表示

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

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

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

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

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

モジュールの作成

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

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へモジュールを登録します。

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

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

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

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の子コンポーネントとして追加するのを忘れずに。

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>

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

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

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>
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>
src/mixins/MultiModalMixin.js
import { mapMutations } from 'vuex'
export default {
  methods: {
    ...mapMutations('multiModal', ['hideModal'])
  }
}

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

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

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へ追加します。

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を用いたモーダルの実装を書いてみようと思います。
ということで、最後まで閲覧いただきありがとうございました。

ozvision
購買プラットフォーム「ハピタス」を開発・運営するベンチャー企業
https://www.oz-vision.co.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away