vue-youtubeを使用して、複数のモーダル上で動画を再生させる

# 実装結果

## git映像

![動画.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214546/2fd1f9f8-10cb-b008-7ef9-de542b16ec5e.gif)

## 実装サイトURL

https://miwa-vue-youtube.netlify.app/

# 実装コード

- ディレクトリ

```console

src
├── App.vue
├── assets
│   └── scss
│       └── main.scss
├── components
│   ├── MovieButton.vue
│   └── MovieModal.vue
├── main.js
└── plugins
    └── vue-youtube.js
```

- バージョン

```
"@vue/cli": "4.4.6",
"vue": "^2.6.11",
"vue-youtube": "^1.4.0"
```

<details><summary>コードの内容を見る</summary><div>

- src/App.vue

```vuejs
<template>
  <div id="app">
    <movie-button
      :btn-num="1"
      :movie-id="movieData[0]"
      @modal-open="modalOpen($event)"
    />
    <movie-button
      :btn-num="2"
      :movie-id="movieData[1]"
      @modal-open="modalOpen($event)"
    />
    <movie-button
      :btn-num="3"
      :movie-id="movieData[2]"
      @modal-open="modalOpen($event)"
    />
    <movie-modal
      :modal-content="modalContent"
      :is-open="isOpen"
      @modal-close="modalClose()"
    />
  </div>
</template>

<script>
import MovieButton from "./components/MovieButton";
import MovieModal from "./components/MovieModal";
export default {
  name: "App",
  components: {
    MovieButton,
    MovieModal
  },
  data() {
    return {
      isOpen: false,
      modalContent: {},
      movieData: ["r8bECyGsw6Q", "2MqjzMeD3Uo", "4Vsi174LRgg"]
    };
  },
  methods: {
    modalOpen(event) {
      console.log(event);
      this.modalContent = event;
      this.isOpen = true;
    },
    modalClose() {
      this.isOpen = false;
    }
  }
};
</script>

<style lang="scss">
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 60px;
}
</style>
```

- src/components/MovieButton.vue

```vuejs
<template>
  <transition name="fade" mode="out-in">
    <div class="modal" v-if="isOpen">
      <div class="modal__overlay" @click="onClick()"></div>
      <div class="modal__body">
        <p>モーダル</p>
        <div class="youtube__wrapper">
          <youtube :video-id="modalContent.movieId" ref="youtube"></youtube>
        </div>
        <button type="button" @click="onClick()">ボタン閉じる</button>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  props: {
    modalContent: {
      type: Object
    },
    isOpen: {
      type: Boolean
    }
  },
  methods: {
    onClick() {
      this.$emit("modal-close");
    }
  }
};
</script>

<style lang="scss" scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
.modal__overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #0008;
}
.modal__body {
  position: relative;
  top: 50%;
  right: 0;
  bottom: 0;
  left: 0;
  max-width: 1000px;
  margin: auto;
  background: #fff;
  transform: translateY(-50%);
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>
```

- src/components/MovieModal.vue

```vuejs
<template>
  <button type="button" @click="onClick()">動画{{ btnNum }}</button>
</template>

<script>
export default {
  props: {
    btnNum: {
      default: 0,
      type: Number
    },
    movieId: {
      type: String
    }
  },
  methods: {
    onClick() {
      this.$emit("modal-open", {
        movieId: this.movieId
      });
    }
  }
};
</script>

<style lang="scss" scoped></style>

```

- src/plugins/vue-youtube.js

```javascript
import Vue from "vue";
import VueYoutube from "vue-youtube";

Vue.use(VueYoutube);
```

- src/main.js

```javascript
import Vue from "vue";
import App from "./App.vue";

import "./plugins/vue-youtube.js";

Vue.config.productionTip = false;

require("@/assets/scss/main.scss");

new Vue({
  render: h => h(App)
}).$mount("#app");
```

- src/assets/main.scss

```scss
.youtube__wrapper {
  position: relative;
  width: 100%;
  margin: 0 auto;
  height: 0;
  padding-bottom: 56.25%;
  overflow: hidden;
  background: #aaa;

  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
}
```

</div></details>

# 実装手順

## 1. vue-youtubeインストール

[vue-youtube - npm](https://www.npmjs.com/package/vue-youtube)

```shell
$ npm install vue-youtube // or yarn add vue-youtube
```

## 2. vue-youtube記載

- src/plugins/vue-youtube.js

```javascript
import Vue from "vue";
import VueYoutube from "vue-youtube";

Vue.use(VueYoutube);
```

- src/main.js

```javascript
import "./plugins/vue-youtube.js";
```

※Nuxt.jsの場合、pluginsに記載

```javascript
export default{
  plugins: ["~plugins/vue-youtube.js"]
}
```

### 参考リンク

- [プラグイン — Vue.js](https://jp.vuejs.org/v2/guide/plugins.html)

## 3. ボタン追加

- src/components/MovieButton.vue

```vuejs
<template>
  <button type="button" @click="onClick()">動画{{ btnNum }}</button>
</template>

<script>
export default {
  props: {
    btnNum: {
      default: 0,
      type: Number
    },
    movieId: {
      type: String
    }
  },
  methods: {
    onClick() {
      this.$emit("modal-open", {
        movieId: this.movieId
      });
    }
  }
};
</script>

<style lang="scss" scoped></style>
```

- src/App.vue

```vuejs
<template>
  <div id="app">
    <movie-button
      :btn-num="1" 
      :movie-id="movieData[0]"
      @modal-open="modalOpen($event)"
    />
    <movie-button
      :btn-num="2"
      :movie-id="movieData[1]"
      @modal-open="modalOpen($event)"
    />
    <movie-button
      :btn-num="3"
      :movie-id="movieData[2]"
      @modal-open="modalOpen($event)"
    />
  </div>
</template>

<script>
import MovieButton from "./components/MovieButton";
export default {
  components: {
    MovieButton
  },
  data() {
    return {
      isOpen: false,
      modalContent: {},
      movieData: ["r8bECyGsw6Q", "2MqjzMeD3Uo", "4Vsi174LRgg"]
    };
  },
  methods: {
    modalOpen(event) {
      this.modalContent = event;
      this.isOpen = true;
    }
  }
};
</script>
```

### `movie-button`について


- `btn-num`  
ボタンの番号を識別させるためのもの(今回の実装では必要なし)
- `movie-id`  
動画のidで動画の内容を識別させるためのもの(管理しやすくするため、data内のmovieDataに登録)
- `@modal-open="modalOpen($event)"`  
子コンポーネントのクリックイベントを検知して、実行(モーダルを開くためのもの)

参考

- [プロパティ — Vue.js](https://jp.vuejs.org/v2/guide/components-props.html)
- [【Vue.js】【図解】emit、propsを使ったコンポーネント間のデータのやり取り | CCT blog](https://blog2.cct-inc.co.jp/blog/vue-js/vue_emit-props/)

## 4. モーダル実装

- src/components/MovieModal.vue

```vuejs
<template>
  <transition name="fade" mode="out-in">
    <div class="modal" v-if="isOpen">
      <div class="modal__overlay" @click="onClick()"></div>
      <div class="modal__body">
        <p>モーダル</p>
        <div class="youtube__wrapper">
          <youtube :video-id="modalContent.movieId" ref="youtube"></youtube>
        </div>
        <button type="button" @click="onClick()">ボタン閉じる</button>
      </div>
    </div>
  </transition>
</template>

<script>
export default {
  props: {
    modalContent: {
      type: Object
    },
    isOpen: {
      type: Boolean
    }
  },
  methods: {
    onClick() {
      this.$emit("modal-close");
    }
  }
};
</script>

<style lang="scss" scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
}
.modal__overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: #0008;
}
.modal__body {
  position: relative;
  top: 50%;
  right: 0;
  bottom: 0;
  left: 0;
  max-width: 1000px;
  margin: auto;
  background: #fff;
  transform: translateY(-50%);
}

.fade-enter-active,
.fade-leave-active {
  transition: opacity 0.3s;
}
.fade-enter,
.fade-leave-to {
  opacity: 0;
}
</style>
```

- src/App.vue

```vuejs
<template>
  <div id="app">
    <movie-modal
      :modal-content="modalContent"
      :is-open="isOpen"
      @modal-close="modalClose()"
    />
  </div>
</template>

<script>
import MovieModal from "./components/MovieModal";
export default {
  name: "App",
  components: {
    MovieModal
  },
  data() {
    return {
      isOpen: false,
      modalContent: {},
      movieData: ["r8bECyGsw6Q", "2MqjzMeD3Uo", "4Vsi174LRgg"]
    };
  },
  methods: {
    modalClose() {
      this.isOpen = false;
    }
  }
};
</script>
```

- src/assets/main.scss

```scss
.youtube__wrapper {
  position: relative;
  width: 100%;
  margin: 0 auto;
  height: 0;
  padding-bottom: 56.25%;
  overflow: hidden;
  background: #aaa;

  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
}
```

### `movie-modal`について

- `modal-content`  
モーダルに送る情報(今回は`movieId`のみ)
- `is-open`  
モーダルが開いているか判定させるもの
- `@modal-close="modalClose()"`  
子コンポーネントのクリックイベントを検知して、実行(モーダルを閉じるためのもの)

参考リンク

-  [Enter/Leave とトランジション一覧 — Vue.js](https://jp.vuejs.org/v2/guide/transitions.html)
-  [vue-youtube - npm](https://www.npmjs.com/package/vue-youtube)

# +α

## 1. モーダルを開いたときに自動で再生を行う

PCのみ自動再生を行いたい場合 `autoplay = 1`を追加

```vuejs
<template>
  <youtube
    :video-id="modalContent.movieId"
    ref="youtube"
    :player-vars="playerVars"
  ></youtube>
</template>
<script>
  export default {
    data() {
      return {
        playerVars: {
          autoplay: 1
        }
      };
    },
  }
</script>
```

参考リンク

-  [example - vue-youtube - npm](https://www.npmjs.com/package/vue-youtube#example)

`autoplay = 1`のみだとSPなどのデバイスで対応することができないため、SPでも対応させたい場合、無音でインライン再生に変更を行うことで自動再生を行うことができる。

```vuejs
<template>
  <youtube
    :video-id="modalContent.movieId"
    ref="youtube"
    :player-vars="playerVars"
    @ready="ready"
  ></youtube>
</template>
<script>
export default {
  data() {
    return {
      playerVars: {
        playsinline: 1
      }
    };
  },
  methods: {
    async fetchYoutube() {
      await (this.isOpen = ture);
      this.$refs.youtube.fetchData();
    },
    ready() {
      const youtubePlayer = this.$refs.youtube.player
      youtubePlayer.mute()
      youtubePlayer.playVideo()
    }
  }
};
</script>
```

参考リンク

- [events - vue-youtube - npm](https://www.npmjs.com/package/vue-youtube#events)
- [iframe 組み込みの YouTube Player API リファレンス  |  YouTube IFrame Player API](https://developers.google.com/youtube/iframe_api_reference#Events)
- [パラメータ一覧 - iframe 組み込みの YouTube Player API リファレンス](https://developers.google.com/youtube/player_parameters?hl=ja#Parameters)
-  [スマホ（iOS）でも動画を自動再生させよう！（iFrame Player API、videoタグ対応） | 株式会社 エヴォワークス -EVOWORX-](https://www.evoworx.co.jp/blog/video-autoplay/)

## 2. 字幕を自動で追加を行う

`cc_lang_pref = 1`に設定を行うことで字幕を追加できる

<img width="825" alt="スクリーンショット 2020-08-09 17.52.34.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214546/58f683a3-f26a-f691-71d8-0497628c7c9b.png">

```javascript
export default {
  data() {
    return {
      playerVars: {
        cc_lang_pref: 1 // cc_lang_prefを1に行う
      }
    };
  },
}
```

参考リンク

- [パラメータ一覧 - iframe 組み込みの YouTube Player API リファレンス](https://developers.google.com/youtube/player_parameters?hl=ja#Parameters)

# 詰まった部分について

## 1. youtubeのスタイルが一部ずれる

- vueコンポーネント内に

```scss
.youtube__wrapper {
  position: relative;
  width: 100%;
  margin: 0 auto;
  height: 0;
  padding-bottom: 56.25%;
  overflow: hidden;
  background: #aaa;

  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
}
```

のような記載を行うと `iframe`内のコンポーネントにスタイルが当たらないため以下のような状態になる。
<img width="1135" alt="スクリーンショット 2020-08-09 16.05.30.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214546/d9864f16-de53-5fe7-3e5c-bbe0f58507ee.png">


また、スタイルを当てていないとモーダルを閉じる時、動画の高さがなくなり、以下のような状態になる。

<img width="1095" alt="スクリーンショット 2020-08-09 16.06.07.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214546/07de3f6a-3a9a-1b62-4e5e-eff11ced999c.png">


なので、グローバルcssの場所に以下を記載

```scss
.youtube__wrapper {
  position: relative;
  width: 100%;
  margin: 0 auto;
  height: 0;
  padding-bottom: 56.25%;
  overflow: hidden;
  background: #aaa;

  iframe {
    position: absolute;
    top: 0;
    left: 0;
    width: 100%;
    height: 100%;
  }
}
```

参考リンク

- [Youtube 動画の埋め込みをCSSでレスポンシブ対応する方法 - HAM MEDIA MEMO](https://h2ham.net/youtube-responsive/)

## 2. v-ifを用いると`this.$refs`が取得できない

`v-if`を用いるとコンポーネントが描画されていない状態でthis.$refsの内容を取得しようとするので、取得できないエラーになる。
なので、`async/await`を用いることになり、コンポーネント描画 → $refs取得を行うことができるようにする

```javascript
async fetchYoutube() {
  await (this.isOpen = ture); // isOpenフラグがtrueになったら
  this.$refs.youtube.fetchData(); // this.$refs.youtubeの取得を行う
},
```

参考リンク

- [【Vue.js】v-ifと$refsによるTypeErrorの対処法 - Qiita](https://qiita.com/aiandrox/items/cb4c4ff5a8834b5a5533)

## 3. モーダルが上下中央によらない

```scss
.modal__body {
  position: absolute;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  max-width: 1000px;
  margin: auto;
  background: #fff;
}
```
のような状態で配置した場合、

<img width="1326" alt="スクリーンショット 2020-08-09 17.42.06.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214546/7dcb4899-8f90-82c4-9d58-6d633676c9f7.png">

のような状態で、高さが取得されないため、 `position: relative`と `top: 50%`などで変更を行う

```scss
.modal__body {
  position: relative; /* relativeに変更 */
  top: 50%; /* 50%に変更 */
  right: 0;
  bottom: 0;
  left: 0;
  max-width: 1000px;
  margin: auto;
  background: #fff;
  transform: translateY(-50%); /* -50%を追加 */
}
```