【Nuxt.js】1ページで複数のモーダルウィンドウを実装する

#■目的
**勉強のためNuxt.jsのサンプル集を作る**

##目標3
**モーダルウィンドウを実装する**

##ざっくり仕様
* 配列からコンテンツを読み込んでタップすると該当の内容がモーダルウィンドウで表示される
* PCではモーダルウィンドウの機能を解除する（ただ並べる）
* モーダルウィンドウ展開時、背景の黒いところをタップで閉じる
* モーダルウィンドウ展開時、後ろでスクロールさせない
* モーダルウィンドウのロジックはstoreで共通管理したい

##default.vue
ベースファイルは[ここ](https://qiita.com/MasayukiAbe/items/c4ed2d788fbd80b85506 "【Nuxt.js】ハンバーガーボタンとグロナビを実装する")で書いたものと同じ。
`components/template/SampleList.vue`内で処理をしていく。

##SampleList.js
まずは読み込むための配列データを用意する。

```javascript:assets/js/SampleList.js
const sampleList = [
  {
    id: '1',
    path: 'hamburger-menu',
    img: '/images/sample/hamburger-menu.jpg',
    name: 'Hamburger Menu',
    nameJa: 'ハンバーガーメニュー',
    comment: '言わずと知れた「三本線」のボタンクリックでメニューの開閉を実行。ヘッダ右上のボタンがそれ。（PC版では非表示）'
  },
  {
    id: '2',
    path: 'modal-window',
    img: '/images/sample/modal-window.jpg',
    name: 'Modal Window',
    nameJa: 'モーダルウィンドウ',
    comment: 'ウィンドウ内で、「子ウィンドウ」を展開。1ページ複数のモーダルウィンドウに対応'
  },
  {
    id: '3',
    path: 'accordion',
    img: '/images/sample/accordion.jpg',
    name: 'Accordion',
    nameJa: 'アコーディオン',
    comment: 'ボタンクリックで下に展開するアコーディオンメニュー（トグルメニュー）。ベタ書きと配列処理の2パタン'
  },
  {
    id: '4',
    path: 'tabs-menu',
    img: '/images/sample/tabs-menu.jpg',
    name: 'Tabs Menu',
    nameJa: 'タブメニュー',
    comment: 'タブ切り替え機能。PC・スマホともにタブメニューと、PCではタブ、スマホではアコーディオンの2パタン。どちらも配列処理'
  }
]

export default sampleList
```

たとえば「タブメニュー」をタップするとその内容がモーダルウィンドウで開くように実装する。

##Modal.js
次に、storeディレクトリに置くモーダルウィンドウのスクリプト。
シンプルにフラグを切り替えるのみにした。
（正直、Storeについてまだ全然理解していないので、むしろこれしか書けなかった）

```javascript:store/Modal.js
export const state = () => ({
  modalFlag: false
})
export const mutations = {
  openModal (state) {
    state.modalFlag = true
  },
  closeModal (state) {
    state.modalFlag = false
  }
}
```

`state`で`modalFlag`のデフォルト値を管理して、
`mutations`の中で関数を書いて`modalFlag`の真偽値を変更する。

##SampleList.vue
さて書いていく。

```vue:components/template/SampleList.vue
<template>
  <ul class="c-list_sample">
    <li v-for="sample in limitCount" :key="sample.id" class="c-modal" :class="modalItem.path">
      <template v-if="$window.width < 1024">
        <AtomsButtonsTextBtn
          btn-style="modal"
          :link-text="sample.name"
          color="white"
          @open-modal="openModal(sample)"
        />
        <MoleculesEtcModal
          v-if="isModalState"
          :top-position="$window.pageYOffset"
          @close-modal="$store.commit('Modal/closeModal')"
        >
          <dl>
            <dt>
              <span>{{ modalItem.name }}</span><br>
              {{ modalItem.nameJa }}
            </dt>
            <dd>
              <p class="comment f-txt">
                {{ modalItem.comment }}
              </p>
              <AtomsButtonsTextBtn
                btn-style="ghost"
                color="white"
                :link-path="modalItem.path"
                :link-text="modalItem.name"
              />
            </dd>
          </dl>
        </MoleculesEtcModal>
      </template>
      <template v-else>
        <figure><img :src="sample.img" width="960" height="960"></figure>
        <dl>
          <dt>
            <span>{{ sample.name }}</span><br>
            {{ sample.nameJa }}
          </dt>
          <dd>
            <p class="comment f-txt">
              {{ sample.comment }}
            </p>
            <AtomsButtonsTextBtn
              btn-style="ghost"
              color="white"
              :link-path="sample.path"
              :link-text="sample.name"
            />
          </dd>
        </dl>
      </template>
    </li>
  </ul>
</template>

<script>
import sampleList from 'assets/js/SampleList'

export default {
  data () {
    return {
      modalItem: ''
    }
  },
  head () {
    return {
      bodyAttrs: {
        class: this.isModalState ? 'modal-on' : ''
      }
    }
  },
  computed: {
    limitCount () {
      if (this.$route.name === 'index') {
        return sampleList.slice(0, 3)
      } else {
        return sampleList
      }
    },
    isModalState () {
      return this.$store.state.Modal.modalFlag
    }
  },
  methods: {
    openModal (sample) {
      this.$store.commit('Modal/openModal')
      this.modalItem = sample
    }
  }
}
</script>
```

**Windowサイズを取得して条件分岐**
windowサイズの取得は前回同様、[こちらの記事](https://qiita.com/allein-s/items/967a525e862a913bc3b1 "nuxt.jsで現在のウインドウサイズをどのコンポーネントからでもリアクティブなオブジェクトとして取得できるプラグインのつくりかた")のコードを拝借した。

**モーダルウィンドウを開くメソッド**
`@open-modal="openModal(sample)"`は子コンポネントのボタンから`@click`を`$emit`で受け取り、`sample`を引数に

**MoleculesEtcModal**

* `v-if="isModalState"`で`isModalState`が`true`のときに
* `:top-position="$window.pageYOffset"`で先にWindowサイズ取得コードでスクロール量を子コンポネントに渡す
* `@close-modal="$store.commit('Modal/closeModal')"`でstoreに設定した`スクリプトを読み込む
* `MoleculesEtcModal`の中はコンポネント側で`<slot />`を指定しているので自由に記述

`<template v-else>`はただ読み込んでいるだけなので割愛して……

**SampleList.jsの読み込み**
`import sampleList from 'assets/js/SampleList'`で読み込む

**modalItem**
`data`の中で`return`している`modalItem`は、モーダルウィンドウを開いたときに、該当する配列のデータを格納するための変数。これがないと一番最後の配列の値が一番上に来てしまう（自分はこの方法しか解決方法を見つけられなかった）

**head()のbodyAttr**
`class: this.isModalState ? 'modal-on' : ''`これを記述することで、`isModalState`が`true`のとき、`<body>`に`modal-on`というclassを付与する。

**limitCount（）**
これはなにかというと、いわゆるトップページのときは配列を3個だけ取得して、そうでないときは全部取得しなさいよということで、別になくてもいい。

**isModalState()**
`return this.$store.state.Modal.modalFlag`でstoreで設定した真偽値を返している。

**openModal()**

* `this.$store.commit('Modal/openModal')`でstoreで設定したスクリプトを読み出し
* `this.modalItem = sample`で`sample`を`modalItem`に格納する。

##Modal.vue
`SampleList.vue`で読み込んでいる子コンポネント

```vue:components/molecules/etc/Modal.vue
<template>
  <transition name="modal" appear>
    <div
      class="c-modal_base"
      :style="modalTopPosition"
      @click.self="closeModal"
    >
      <div class="c-modal_wrap">
        <div class="c-modal_content">
          <slot />
        </div>
        <AtomsIconsTimeIcn
          @click.native="closeModal"
        />
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  props: {
    topPosition: {
      type: Number,
      default: 0
    }
  },
  computed: {
    modalTopPosition () {
      return {
        '--top': this.topPosition + 'px'
      }
    }
  },
  methods: {
    closeModal () {
      this.$emit('close-modal')
    }
  }
}
</script>

<style lang="scss">
.c-modal{
  &_base{
    position: absolute;
    left: 0;
    top: var(--top);
    z-index: 1000;
    width: 100%;
    height: 100%;
    background-color: rgba($dark, 0.7);
  }

  &_wrap{
    position: absolute;
    left: 50%;
    top: 50%;
    z-index: 1100;
    width: 90%;
    height: 80%;
    transform: translate(-50%, -50%);

    .icon.time{
      right: 0;
      top: -1.5em;
      width: 2em;
      height: 2em;
    }
  }

  &_content{
    overflow-y: scroll;
    width: 100%;
    height: 100%;
    padding: 1em;
    background-color: rgba($white, 0.9);
    border: 2px $black solid;
  }
}

//transition
.modal{
  &-enter-active,
  &-leave-active{
    transition: opacity 0.6s ease;

    .c-modal_content{
      transition: opacity 0.4s ease, transform 0.8s ease;
    }
  }
  &-leave-active{
    transition: opacity 0.6s ease 0.1s;
  }

  &-enter,
  &-leave-to{
    opacity: 0;

    .c-modal_content{
      opacity: 0;
      transform: translateY(-20px);
    }
  }
}
</style>
```

* `@click.self="closeModal"`の`.self`を指定することで、子要素をタップしても発火しないように設定する
* `props`で`topPosition`を受け取り、`computed`の`modalTopPosition()`でcssの値をreturnする。それを`c-modal_base`の`top`に指定することで、どこでモーダルウィンドウを開いても画面の上部から始まるようにする

##body.modal-on
cssで以下を設定する。
```overflow: hidden;
width: 100%;
height: 100%;```

##結果
求めていた通りのモーダルウィンドウができた。
でもコードをもっとスリムにしたい。まだまだ研究の余地がありそうだ。