Vue.jsとtransitionを使ってシンプルなスライダーを作ってみる

私は、バックエンド開発業務が中心なので、普段フロントエンドの作業はしていませんが、
Vue.jsが注目されてきているようなので試してみようと思います。

フロントエンドでアニメーションといったら、jQueryで実装することが多いと思いますが、
便利なライブラリを使ったり、また、自作する場合にはanimateメソッドや
easingでエフェクトを使うなどして、いろいろなアニメーションを作ることができました。

ノーマルな横スライダーであれば、スライドさせる複数枚の画像を横一列に並べて
画像の横サイズと移動量を計算して、animateメソッドを使って動かすことで
簡単に実装できました。

SlickとかSwiper、bxSliderなどの有名なライブラリを使うと便利ではあるのですが、
少しだけカスタマイズしたいと思った時には、ソースコードを解析するなど苦労も多いので、
スライダーを自作して自由自在にカスタマイズする方が個人的には楽だと感じてました。

Vue.jsが実際の案件でも少しづつ使い始められてきたようなので、
従来jQueryで実装していた横スライダーを、Vue.jsで実装するにはどうするのか？
ということを知るために試してみました。

そして、自分なりにいろいろと理解できた点を５つのポイントにして
まとめてみました。

## 従来のHTMLの場合
```haml
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script type="text/javascript" src="main.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
  <div id="app">
    <div class="slider-outer">
      <ul class="slider-inner">
        <li class="slide1"><img src="450x300_1.png" width="450" height="300"></li>
        <li class="slide2"><img src="450x300_2.png" width="450" height="300"></li>
        <li class="slide3"><img src="450x300_3.png" width="450" height="300"></li>
        <li class="slide3"><img src="450x300_4.png" width="450" height="300"></li>
      </ul>
    </div>
    </div>
    <div class="btn">
      <button click="prev()">prev</button>
      <button click="next()">next</button>
    </div>
  </div>
</body>
</html>
```


このようにulとli要素を用いてスライドする画像を並べることが多かったです。
汎用的なプラグインを実装にするには、このli要素をJavaScriptで動的に生成してappendすることもありました。


## Vue.js向けに書き換えた場合
```javascript

<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
<script type="text/javascript" src="main.js"></script>
<link rel="stylesheet" href="style.css">
</head>
<body>
  <div id="app">
    <div class="slider-outer">
      <transition :name="trans_name">
          <div class="slider-inner" :key="idx" v-for="(slide, idx) in slides" v-if="current_slide == idx"/>
              <img :key="idx" v-bind:src="slides[idx].img" width="450" height="300">
          </div>
      </transition>
    </div>
    <div class="btn">
      <button @click="prev()">prev</button>
      <button @click="next()">next</button>
    </div>
  </div>
</body>
</html>
```

## スタイルを適用
```css:style.css
html,body{
  height:100%;
  width:100%;
  margin:0;
  padding:0;
}
.slider-outer{
  position:relative;
  width:450px;
  height:300px;
  overflow:hidden;
  margin: 0 auto 20px;
}
.slider-inner{
  position: absolute;
  width:450px;
  height:300px;
}
.btn{
  position:relative;
  margin: 0 auto 20px;
  width:90px;
}
/* -------------------------------- */
/* 以下 Vue.js で使用するクラスの定義 */
/* -------------------------------- */
.next-enter-active, .next-leave-active,
.prev-enter-active, .prev-leave-active  {
  transition: all .8s ease-out;
}
.next-enter {
  transform: translateX(450px);
}
.next-enter-to {
  transform: translateX(0);
}
.next-leave {
  transform: translateX(0);
}
.next-leave-to {
  transform: translateX(-450px);
}
.prev-enter {
  transform: translateX(-450px);
}
.prev-enter-to {
  transform: translateX(0);
}
.prev-leave {
  transform: translateX(0);
}
.prev-leave-to {
  transform: translateX(450px);
}
```

## JavaScriptでスライドを実装
```javascript:main.js

window.onload=function(){
  new Vue({
    el: '#app',
    data: {
      trans_name: 'next',
      current_slide: 0,
      slides: [{
        img: '450x300_1.png'
      },{
        img: '450x300_2.png'
      },{
        img: '450x300_3.png'
      },{
        img: '450x300_4.png'
      }],
    },
    methods: {
      prev(){
        this.trans_name = 'prev';
        this.current_slide--;
        if(this.current_slide == -1){
          this.current_slide = this.slides.length -1;
        }
      },
      next(){
        this.trans_name = 'next';
        this.current_slide++;
        if(this.current_slide == this.slides.length){
          this.current_slide = 0;
        }
      },
    },
  })
}
```


## ■ポイント１ transition
- アニメーションさせたい要素をtransition要素で囲む

```html
<transition>
アニメーションさせたい要素
</transition>
```

同時にアニメーションさせたい要素が複数ある場合には、transition-group要素を使用するようですが、ここではまだ試してはいません。

## ■ポイント２ v-bind
v-bind:srcを使って、Vue.jsのdataプロパティで定義された画像ファイルのリストの中から
idx値で指定された画像ファイル名を引っ張ってきてsrc属性にはめ込む処理を記述する。

```html
<img :key="idx" v-bind:src="slides[idx].img">
```

　　　　　　　　　　　　↑


```javascript

data: {
  slides: [{
    img: '450x300_1.png'
  },{
    img: '450x300_2.png'
  },{
    img: '450x300_3.png'
  },{
    img: '450x300_4.png'
  }],
},
```

例えば、idxの値が1であれば、450x300_2.png の画像ファイルがsrc属性に適用されます。
これによりHTML構文の中に直接画像ファイル名を記述する必要がなくなりシンプルに仕上がります。
実際の属性への適用（はめ込み）は、Vue.jsが自動でやってくれるので、自分でその処理を
書く必要がなくなり楽になりました。

## ■ポイント３ methods
ボタンが押された時のアクション

```html
<button @click="prev()">prev</button>
<button @click="next()">next</button>
```

これは、
prevボタンが押された時はprev()メソッド
nextボタンがおされた時はnext()メソッド
のそれぞれが実行されます。

アットマークは、Vue.jsのv-onディレクティブの省略記法のようです。
（v-bindも省略できるようなので記述量を少し減らすことができます）

```javascript

methods: {
  prev(){
    this.trans_name = 'prev';
    this.current_slide--;
    if(this.current_slide == -1){
      this.current_slide = this.slides.length -1;
    }
  },
  next(){
    this.trans_name = 'next';
    this.current_slide++;
    if(this.current_slide == this.slides.length){
      this.current_slide = 0;
    }
},
```

current_slide変数は、現在表示されている画像の番号（0～3）を格納しておく
ためのものです。

next()の場合は、次の表示画像になるので数値を1をプラスしています。
prev()の場合は、前の表示画像になるので数値を1をマイナスしています。

カルーセルタイプにしているので、3以上になったら0に戻すなど、
補正処理も入れます。

## ■ポイント４ transform
クラスの定義であり、ここがアニメーションさせる部分です。
はじめて見たときは、さっぱり理解できなかったのですが、
よく見てみると簡単な話しでした。

```css
.next-enter-active, .next-leave-active,
.prev-enter-active, .prev-leave-active  {
  transition: all .8s ease-out;
}
.next-enter {
  transform: translateX(450px);
}
.next-enter-to {
  transform: translateX(0);
}
.next-leave {
  transform: translateX(0);
}
.next-leave-to {
  transform: translateX(-450px);
}
.prev-enter {
  transform: translateX(-450px);
}
.prev-enter-to {
  transform: translateX(0);
}
.prev-leave {
  transform: translateX(0);
}
.prev-leave-to {
  transform: translateX(450px);
}
```

transition要素のname属性の値に応じて、適用されるclassが変わってきます。
（:nameとなっていますが、実はv-bind:name をあらわします）

もしtrans_name変数にprevが格納されたとしたら、
.prev-enter, .prev-enter-to など、先頭にprevがついているクラスが適用されます。

ここで、このようにプレフィックスを付加しないかった場合には、Vue.jsが自動で v-
を付加する仕組みになっているようです。

- 消滅する要素は-leave のついているクラス
- 出現する要素は-enter のついているクラス

がVue.jsによって transaction で囲まれた要素に自動で付与されます。

例えば、nextボタンをクリックした場合、スライドを右から左へ動かすために、

消えゆく画像については、

- 開始時 translateX(0) 
- 終了時 translateX(-450) 

のように、X軸の座標上を右(0px)から左(-450px)に向かってスライドするよう
.next-leave　と　.next-leave-to　へ下記のように設定します。

```css
.next-leave {
  transform: translateX(0px);
}
.next-leave-to {
  transform: translateX(-450);
}
```


右側から出現する画像については、
- 開始時 translateX(450) 
- 終了時 translateX(0) 
のように、.next-enter　と　.next-enter-to　へ下記のように設定します。

```css
.next-enter {
  transform: translateX(450px);
}
.next-enter-to {
  transform: translateX(0);
}
```

また、-active とついているクラスは、アニメーション中の状態を定義するので、
ここでは0.8秒かけてease-outでアニメーションするようにしています。

```css
.next-enter-active, .next-leave-active,
.prev-enter-active, .prev-leave-active  {
  transition: all .8s ease-out;
}
```


細かくクラス定義をしましたが、translateXの初期値は0なので、
下記のように簡略化することができることがわかりました。

```css
.next-enter, .prev-leave-to {
  transform: translateX(450px);
}
.next-leave-to, .prev-enter {
  transform: translateX(-450px);
}
```

## ■ポイント５ v-if v-for key
v-if、v-for、key属性やDOMの遷移

```html
<div class="slider-inner" :key="idx" v-for="(slide, idx) in slides" v-if="current_slide == idx"/>
    <img :key="idx" v-bind:src="slides[idx].img">
</div>
```

v-forによって、slidesの（画像ファイル名が入った）配列を展開します。
この時、key属性を与えてやることが必要であり、これをやらないと
アニメーションがうまく動かないようです。

また、展開された画像の中から１つだけを新たに表示させる必要があるため、
v-ifによって、current_slide値とidx値の合致する画像だけが表示されるようにします。
（current_slideは、prevまたはnextボタンによって0～3の範囲で変化します）

もし、アニメーションをさせないなら、このdiv要素は、
prevやnextボタンを押すと、瞬時に切り替わり、
画面表示上も、パチパチと画像が切り替わります。

アニメーションさせることにより、アニメーションしている間は、
div要素が２つ存在することになります。
（nextをクリックした場合、1つ目は左へ消えゆく画像で、
2つ目は右から現れる画像であり、この２つのことです）

たとえば、左へスライドアニメーションの、前、中、後のDOMの状態は
下記のようになります。

- アニメーション開始前

```html
<div class="slider-inner"/>
    <img src="450x300_1.png" width="450" height="300">
</div>
```

- アニメーション中

```html
<div class="slider-inner next-leave-to next-leave-active"/>
    <img src="450x300_1.png" width="450" height="300">
</div>
<div class="slider-inner next-enter-to next-enter-active"/>
    <img src="450x300_2.png" width="450" height="300">
</div>
```

- アニメーション完了後

```html
<div class="slider-inner"/>
    <img src="450x300_2.png" width="450" height="300">
</div>
```

このようになります。アニメーション時間を長めに設定すると、この変化の様子がツールで確認できます。
下記、実際に動作させた際の様子です。

![anim1.gif](https://qiita-image-store.s3.amazonaws.com/0/287208/f48eb520-1be9-b391-b5a0-5386789b3d2e.gif)



## クロスフェードタイプに変更してみる

```css
.next-enter, .next-leave-to {
  opacity: 0;
}
.prev-enter, .prev-leave-to {
  opacity: 0;
}
```

これに書き換えるだけで出来てしまいました。

![anim2.gif](https://qiita-image-store.s3.amazonaws.com/0/287208/d92718a1-5605-6f84-5bc7-12752f37f276.gif)



下記のサイトを参考にさせて頂きました。
https://jp.vuejs.org/v2/guide/transitions.html

## まとめ

Vue.jsを使いこなすには、そのお作法を覚えるのが大変そうですね（まぁなんでもそうですが）
すっきりと記述できて便利になった分、覚えるべきルールもふえるので、
それがわからないと、目的のものを実現するまでは多少苦労するかもしれません。

「いままでのjQueryを使った実装で大変だったこと」を経験している人であれば、
Vue.jsの良さが体感できると思うのですが、その経験がない方がいきなりVue.jsを使っても
その良さはなかなか実感できないかもしれません。

個人的には、Vue.jsをまだ使いこなせてない間は、いろいろと小回りのきくjQueryと
使って便利そうなところはVue.jsを適用するなどの両方をうまく共存させながら
使っていくのが、今のところは良いのかなと。Vue.jsに押されぎみなjQueryですが、
使い慣れていることもあるので、消えてはほしくないなと思ってます。

ただ、jQueryにしてもVue.jsにしても、そのほかのフレームワークにしても、
JavaScriptの基本的なことを理解していることは必要ですね。

また新しい発見がありましたら投稿したいと思います。

※投稿直前にダブルクリック時対策がされていないことに気づきましたが、
今回はこの対策は割愛します。