Vue.jsを使ってスライダーを作ったときの備忘録

# はじめに
Vue.jsの勉強がてら、スライダーを作成したので、その際の作成方法等を備忘録としてまとめます。

# 仕様
以下仕様を満たすものを作成しました。

- レスポンシブ対応
- 1画面内に複数のスライダーを表示できる
- 画像の高さが変わるとスライダーの高さも変わる

# デモ

<p data-height="500" data-theme-id="dark" data-slug-hash="XBLmoa" data-default-tab="js,result" data-user="inumberx" data-pen-title="VueSlider" data-preview="true" class="codepen">See the Pen <a href="https://codepen.io/inumberx/pen/XBLmoa/">VueSlider</a> by N/NE (<a href="https://codepen.io/inumberx">@inumberx</a>) on <a href="https://codepen.io">CodePen</a>.</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

※上手く表示できていない場合は「RERUN」ボタンを押してください。

# JavaScript

```javascript
var mixin = {
  vueSlider: {
    data: {
      winWidth: window.innerWidth
    },
    methods: {
      // スライダーを戻す処理
      sliderPrev() {
        this.transitionName = 'show-prev';
        if(this.visibleContent === 0) {
          this.visibleContent = this.contents.length - 1;
        } else {
          this.visibleContent--;
        }
      },
      // スライダーを進める処理
      sliderNext() {
        this.transitionName = 'show-next';
        if(this.visibleContent === this.contents.length - 1) {
          this.visibleContent = 0;
        } else {
          this.visibleContent++;
        }
      },
      // ウィンドウサイズを更新する処理
      setWindowWidth() {
        this.winWidth = window.innerWidth;
      },
      // スライダーの高さを更新する処理
      setVueSliderHeight() {
        this.contentHeight = this.$refs.vueSliderContentBox.querySelector('.vue-slider-content').clientHeight + 'px';
      }
    },
    // インスタンスが作成された後に実行する処理
    created: function() {
      // リサイズ時にウィンドウサイズを更新する処理を設定
      window.addEventListener('resize', this.setWindowWidth, false);
    },
    // インスタンスがマウントされた後に実行する処理
    // DOM要素にアクセスできる
    mounted: function() {
      // スライダーの高さを更新
      this.setVueSliderHeight();
    },
    // インスタンスが破棄される前に実行する処理
    beforeDestroy: function () {
      // リサイズ時にウィンドウサイズを更新する処理を削除
      window.removeEventListener('resize', this.setWindowWidth, false);
    },
    watch: {
      // winWidthが更新された時に実行する処理
      winWidth: function() {
        // スライダーの高さを更新
        this.setVueSliderHeight();
      }
    }
  }
}

var vueSlider01 = new Vue({
  el: '#VUE_SLIDER_01',
  mixins: [mixin.vueSlider],
  data: {
    visibleContent: 0,
    transitionName: 'show-next',
    contentHeight: '',
    contents: [
      {
        title: 'タイトル',
        imgUrl: '画像パス'
      },{
        title: 'タイトル',
        imgUrl: '画像パス'
      },{
        title: 'タイトル',
        imgUrl: '画像パス'
      },{
        title: 'タイトル',
        imgUrl: '画像パス'
      }
    ]
  }
})
```

## var mixin = { ～
1ページ内に複数のスライダーを表示できるように、スライダーを切り替える処理等は[ミックスイン](https://jp.vuejs.org/v2/guide/mixins.html)としてまとめ、再利用可能にしました。

### data

#### winWidth
ウィンドウサイズが格納される変数です。
ウィンドウサイズがresizeするたびに値が更新されます。

### methods
各メソッドを定義します。

#### sliderPrev
スライダーを戻す処理です。
スライダーの左側のボタンを押したときに実行される処理になります。
「transitionName」を「show-prev」にすることにより、CSSで設定したアニメーションが実行されます。
visibleContentが0（一番最初の画像が表示されている）の場合はvisibleContentをcontents.length - 1（一番最後の画像）に設定し、それ以外の場合はデクリメントします。

#### sliderNext
スライダーを進める処理です。
スライダーの右側のボタンを押したときに実行される処理になります。
基本的にsliderPrevの逆の処理をやっています。

#### setWindowWidth
ウィンドウサイズを更新する処理です。
ウィンドウサイズを取得し、winWidthに代入します。

#### setVueSliderHeight
スライダーの高さを更新する処理です。
$refsでHTML内でref="～"と記述したDOM要素にアクセス出来るようになります。
this.$refs.vueSliderContentBoxと記述することで、ref="vueSliderContentBox"が指定されたDOM要素を取得できます。
ref="vueSliderContentBox"が指定されたDOM要素の子孫要素でclass="vue-slider-content"が指定されたDOM要素のheightを取得し、contentHeightに代入しています。

今回はCSSの設定上、明示的にheightを設定する必要がありました（スライダーが表示されないため）。
また、画像の高さがバラバラでもスライダーが適切な高さになるように、こちらの処理を実装しました。
この処理は、初期表示時、ウィンドウサイズresize時、スライダー切り替え時に実行されます。

### created
インスタンスが作成された後に実行する処理です。
ウィンドウサイズをrisizeした時にsetWindowWidthを実行する設定を行います。

### mounted
インスタンスがマウントされた後に実行する処理です。
DOM要素へのアクセスが可能です。
setVueSliderHeightを実行し、初期表示時にスライダーのheightを設定します。

### beforeDestroy
インスタンスが破棄される前に実行する処理です。
createdで設定した、「ウィンドウサイズをrisizeした時にsetWindowWidthを実行する設定」を破棄します。
今回はインスタンスを破棄しないので、この設定は不要かもしれません。。。

### watch
指定した変数が更新されるたびに実行される処理です。
ウィンドウサイズが変わり、winWidthが更新されるたびにsetVueSliderHeightを実行しスライダーの高さを更新します。
こちらの処理を行うことでレスポンシブ対応を実現させました。

## var vueSlider01 = new Vue({ ～

スライダーに表示するコンテンツ等を記述します。
スライダーを複数表示させたい場合は、このインスタンスを複数記述します。

### el
HTML内のIDを指定します。

### mixins
ミックスインで定義した、スライダーを切り替える処理等を設定します。

### data

#### visibleContent
現在表示されているスライダーの番号です。

#### transitionName
スライダーを切り替える際のアニメーションの名前です。

#### contentHeight
スライダーの高さです。

#### contents
スライダーに表示するコンテンツです。
今回はサンプルで、画像のタイトルとパスを設定しました。
こちらは、表示させたいコンテンツに合わせて適宜変更します。

# HTML

```html
<div class="vue-slider-box" id="VUE_SLIDER_01" v-cloak>
<div class="vue-slider-main">
<a href="javascript:void(0);" class="vue-slider-prev-btn" v-on:click="sliderPrev"><i></i></a>
<div class="vue-slider-content-box"
     v-bind:style="{height: contentHeight}"
     ref="vueSliderContentBox"
>
<transition v-bind:name="transitionName"
            v-on:after-leave="setVueSliderHeight"
>
<div class="vue-slider-content"
     v-for="(item, index) in contents"
     v-if="visibleContent == index"
     v-bind:key="index"
>
<img v-bind:src="item.imgUrl"
     v-bind:alt="item.title"
>
</div>
</transition>
</div><!-- /.vue-slider-content-box -->
<a href="javascript:void(0);" class="vue-slider-next-btn" v-on:click="sliderNext"><i></i></a>
</div><!-- /.vue-slider-main -->
<div class="vue-slider-footer">
<ul>
<li class="vue-slider-footer-dot"
    v-for="(item, index) in contents"
    v-bind:class="{'is-visible' : visibleContent == index}"
></li>
</ul>
</div><!-- /.vue-slider-footer -->
</div><!-- /.vue-slider-box -->
<!-- js -->
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.min.js"></script>
```

## スライドを戻すボタン

```html
<a href="javascript:void(0);" class="vue-slider-prev-btn" v-on:click="sliderPrev"><i></i></a>
```

v-on:clickでこの要素がクリックされた時に実行したい処理を設定します。

## コンテンツ部分

```html
<div class="vue-slider-content-box"
     v-bind:style="{height: contentHeight}"
     ref="vueSliderContentBox"
>
<transition v-bind:name="transitionName"
            v-on:after-leave="setVueSliderHeight"
>
<div class="vue-slider-content"
     v-for="(item, index) in contents"
     v-if="visibleContent == index"
     v-bind:key="index"
>
<img v-bind:src="item.imgUrl"
     v-bind:alt="item.title"
>
</div>
</transition>
</div><!-- /.vue-slider-content-box -->
```

### transition

#### name

transitionはDOM要素の追加・削除などを開始してから終了するまでに以下のようなclassが自動的に付与されます。

1. v-enter
2. v-enter-active
3. v-enter-to
4. v-leave
5. v-leave-active
6. v-leave-to

[詳細は公式サイトを参照ください。](https://jp.vuejs.org/v2/guide/transitions.html#トランジションクラス)

各classは、transitionの名前が先頭に付きます。
nameが未指定の場合は「v」、指定した場合はその文字列が「v」に置き換わり設定されます。
今回のようにv-bindを指定することで、JavaScriptで定義した変数をnameに設定することが可能です。
スライドを戻した場合は「show-prev」、進めた場合は「show-next」という文字列がtransitionNameに代入されます。
なので、スライドを戻した場合は

1. show-prev-enter
2. show-prev-enter-active
3. show-prev-enter-to
4. show-prev-leave
5. show-prev-leave-active
6. show-prev-leave-to

スライドを進めた場合は

1. show-next-enter
2. show-next-enter-active
3. show-next-enter-to
4. show-next-leave
5. show-next-leave-active
6. show-next-leave-to

というclassが状態に応じて付与されます。
CSSに各classのスタイルを記述することで、スライドを切り替える際のアニメーションなどを表現できます。

#### after-leave

transitionには以下のようなJavaScriptフックを定義することができます。

1. before-enter
2. enter
3. after-enter
4. enter-cancelled
5. before-leave
6. leave
7. after-leave
8. leave-cancelled

[詳細は公式サイトを参照ください。](https://jp.vuejs.org/v2/guide/transitions.html#JavaScript-フック)

今回のようにafter-leaveでJavaScriptのメソッドを指定することで、DOM要素が削除された後に指定したメソッドが実行されるようになります。
つまり、スライドの切り替えが終了した段階でsetVueSliderHeightが実行され、スライダーの高さが更新されます。

## スライドを進めるボタン

```html
<a href="javascript:void(0);" class="vue-slider-next-btn" v-on:click="sliderNext"><i></i></a>
```

v-on:clickでこの要素がクリックされた時に実行したい処理を設定します。


## スライダー下部のドット部分

```html
<div class="vue-slider-footer">
<ul>
<li class="vue-slider-footer-dot"
    v-for="(item, index) in contents"
    v-bind:class="{'is-visible' : visibleContent == index}"
></li>
</ul>
</div><!-- /.vue-slider-footer -->
```

v-forでスライドのコンテンツの数だけ`<li>`が作られます。
現在表示されている番号の`<li>`の場合はclass="is-visible"が付与されます。

# CSS

```css
[v-cloak] {
display: none;
}

.vue-slider-box {
width: 100%;
margin: 24px auto;
}
.vue-slider-main {
width: 80%;
position: relative;
margin: 0 auto;
z-index: 1;
overflow: hidden;
}
.vue-slider-prev-btn,
.vue-slider-next-btn {
display: block;
position: absolute;
width: 60px;
height: 60px;
top: 50%;
margin-top: -30px;
z-index: 2;
}
.vue-slider-prev-btn {
left: 8px;
}
.vue-slider-next-btn {
right: 8px;
}
.vue-slider-prev-btn i,
.vue-slider-next-btn i {
display: block;
width: 100%;
height: 100%;
border: 1px solid #fff;
border-radius: 50%;
position: relative;
transition: all .4s;
}
.vue-slider-prev-btn:hover i,
.vue-slider-next-btn:hover i {
background-color: rgba(255, 255, 255, 0.8);
}
.vue-slider-prev-btn i:before,
.vue-slider-next-btn i:before {
content: '';
width: 20px;
height: 20px;
border: 0px;
border-top: 1px solid #fff;
border-right: 1px solid #fff;
position: absolute;
top: 50%;
margin-top: -9px;
}
.vue-slider-prev-btn i:before {
-ms-transform: rotate(225deg);
-webkit-transform: rotate(225deg);
transform: rotate(225deg);
right: 15px;
}
.vue-slider-next-btn i:before {
-ms-transform: rotate(45deg);
-webkit-transform: rotate(45deg);
transform: rotate(45deg);
left: 15px;
}
.vue-slider-prev-btn:hover i:before,
.vue-slider-next-btn:hover i:before {
border-top: 1px solid #333;
border-right: 1px solid #333;
}

.vue-slider-content {
color: #fff;
left: 0;
position: absolute;
text-align: center;
top: 0;
width: 100%;    
}
.vue-slider-content-box {
transition: all .2s;
}
.vue-slider-content img {
width: 100%;
}

.vue-slider-footer ul {
align-items: center;
display: flex;
justify-content: center;
width: 100%;
margin: 0;
padding: 8px 0;
}
.vue-slider-footer ul li {
margin: 8px;
}
.vue-slider-footer-dot {
background-color: #ccc;
border-radius: 50%;
height: 8px;
width: 8px;
}
.vue-slider-footer-dot.is-visible {
background-color: #666;
}

.show-next-enter-active,
.show-next-leave-active,
.show-prev-enter-active,
.show-prev-leave-active {
transition: all .4s;
}
.show-next-enter,
.show-prev-leave-to {
transform: translateX(100%);
}
.show-next-leave-to,
.show-prev-enter {
transform: translateX(-100%);
}
```
   
# 関連リンク
作成にあたり、以下サイトを参考にさせていただきました。
- [Enter/Leave とトランジション一覧 - Vue.js](https://jp.vuejs.org/v2/guide/transitions.html)
- [Vue.jsでカルーセルをつくる - Qiita](https://qiita.com/Wave7KN/items/5a18c9a6ed7d6fac940f)
- [Vue.jsで画像スライドコンポーネントをつくる - 豆腐とコンソメ](http://www.tohuandkonsome.site/entry/2018/02/18/222834)