Vue.jsでいい感じのアニメーションを作りたい

この記事は [クラウドワークス Advent Calendar 2019](https://qiita.com/advent-calendar/2019/crowdworks) の3日目の記事です。

昨日は、[@minamijoyo](https://qiita.com/minamijoyo) さんによる[tfupdateでTerraform本体/プロバイダ/モジュールのバージョンアップを自動化する](https://qiita.com/minamijoyo/items/1350fb28ae82a3dbb354) でした！

## はじめに

こんにちは、最近アルコールに負け続けている新卒エンジニアの [@d4te](https://twitter.com/d4te_kun) です。
Rubyの会社に入社して７ヶ月ほど経ったのですが、この７ヶ月間あまり Ruby は書かずに Vue.js を利用したフロントエンド開発ばかりしていました。

その中でも Vue.js でのアニメーションの実装は経験がなく、一から調べる機会があったので今回はそれについて書いていこうと思います。

## 基本的なこと
Vue.js はデフォルトでトランジション( `transition` )という機能を提供していて、それを利用することでいい感じにアニメーションを実装することができます。

まずここにボタンを押したら `div.hoge` を表示/非表示するだけの [単一ファイルコンポーネント](https://jp.vuejs.org/v2/guide/single-file-components.html) があります。いい感じのアニメーションはありません。

```html
<template>
  <div id="app">
    <button @click="show = !show">
      click!
    </button>
     
    <!--  アニメーションで表示/非表示が切り替わる時にふわっと表示させたい要素 -->
    <div class="hoge" v-if="show">
      hogehoge
    </div>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
    return {
      show: false
    }
  }
}
</script>
```

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="b0ntenmaru" data-slug-hash="VwwJmBq" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="VwwJmBq">
  <span>See the Pen <a href="https://codepen.io/b0ntenmaru/pen/VwwJmBq">
  VwwJmBq</a> by Date (<a href="https://codepen.io/b0ntenmaru">@b0ntenmaru</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

### いい感じのアニメーションを追加する

アニメーションを動作させるためには該当の要素(ここでは `div.hoge` )を `<transition>` タグで囲み、トランジションクラスに対して CSS を指定します。トランジションクラスに関しては後述します。


```html

<transition>
  <div class="hoge" v-if="show">
    hogehoge
  </div>
</transition>
```    
```css

<style>
/* 以下の v-enter, v-enter-to, v-enter-active がトランジションクラス */

/* 表示アニメーションをする前のスタイル */
.v-enter {
  opacity: 0;
}

/* 表示アニメーション後のスタイル */
.v-enter-to {
  opacity: 1;
}

/* 表示アニメーション動作中のスタイル */
.v-enter-active {
  transition: all 500ms;
}
</style>
```

これにより、要素の**表示**に動きを与えることができました。

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="b0ntenmaru" data-slug-hash="dyyxrJr" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="dyyxrJr">
  <span>See the Pen <a href="https://codepen.io/b0ntenmaru/pen/dyyxrJr">
  dyyxrJr</a> by Date (<a href="https://codepen.io/b0ntenmaru">@b0ntenmaru</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## トランジションクラス
トランジションクラスは、 `<transition>` タグで要素を囲むことで使用できるようになるCSSのクラスです。
ちょうど上で登場した `v-enter` ,  `v-enter-to` ,  `v-enter-active` がトランジションクラスにあたります。

トランジションクラスには Enter と Leave の２つのフェーズが存在し、

- `<transition>` タグで囲った要素を表示する時を **Enter**
- `<transition>` タグで囲った要素を非表示にする時を **Leave**

と言います。（上のサンプルでは Enter のみ）

この２つのフェーズにはそれぞれ、アニメーションの動作前・動作中・動作後の３つ状態に対応した3つのクラスがあります。

|クラス|要素の状態|
|---|---|
|v-enter|表示アニメーションの開始時のスタイル|
|v-enter-to|表示アニメーションの終了時のスタイル|
|v-enter-active|表示アニメーション中のスタイル|
|v-leave|非表示アニメーションの開始時のスタイル|
|v-leave-to|非表示アニメーションの終了時のスタイル|
|v-leave-active|非表示アニメーション中のスタイル|

<img width="765" alt="02.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195724/ff8eb5d6-ef72-0f73-b49e-69c059cf8b67.png">

### いい感じの非表示アニメーションを追加する
続いて非表示アニメーションを実装します。
Enter の時とやることは逆ですが、下記のように指定すると非表示アニメーションが動いてくれます。

```css

/* 非表示アニメーション動作前のスタイル */
.v-leave {
  opacity: 1;
}

/* 非表示アニメーション動作後のスタイル */
.v-leave-to {
  opacity: 0;
}

/* 非表示アニメーション動作中のスタイル */
.v-leave-active {
  transition: all 500ms;
}
```

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="b0ntenmaru" data-slug-hash="yLyBLRP" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="yLyBLRP">
  <span>See the Pen <a href="https://codepen.io/b0ntenmaru/pen/yLyBLRP">
  yLyBLRP</a> by Date (<a href="https://codepen.io/b0ntenmaru">@b0ntenmaru</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

このトランジションクラスが Vue でアニメーションを実装するための基本となります。

## 複数要素のトランジション
続いて、`v-for` など同時に描画された複数の要素にアニメーションを適応させるためのリストトランジションについて説明します。

下記の[単一ファイルコンポーネント](https://jp.vuejs.org/v2/guide/single-file-components.html)を例とします。
現状 `hoge` が一覧されており、ADD ボタン押下で新しい `hoge` が一覧に追加・ `hoge` 横の x ボタン押下で一覧から削除できる仕様となっています。


```html

<template>
  <div id="app">
    <button @click="add">ADD</button>
    <div v-for="(todo, index) in todos" :key="todo.key">
      <span>{{ todo.value }}</span><input @click="remove(index)" type="button" value="x" />
    </div>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
    return {
      todos: [
        {key: 0, value: 'hoge'},
        {key: 1, value: 'hoge'},
        {key: 2, value: 'hoge'},
      ],
      nextNum: 2
    }
  },

  methods: {
    add: function() {
      const value = 'hoge'
      const todo = {
        key: this.nextNum += 1,
        value
      }
      this.todos.push(todo)
    },
    remove: function(index) {
      this.todos.splice(index, 1)
    }
  }
}
</script>
```

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="b0ntenmaru" data-slug-hash="LYEPxyJ" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="LYEPxyJ">
  <span>See the Pen <a href="https://codepen.io/b0ntenmaru/pen/LYEPxyJ">
  LYEPxyJ</a> by Date (<a href="https://codepen.io/b0ntenmaru">@b0ntenmaru</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

### いい感じのアニメーションを適応させる
`hoge` 追加時/削除時にアニメーションを適応させます。
やることとしては単一要素のトランジションと同じでですが、今回のような複数の要素にアニメーションを適応させる時は `<transition-group>` タグで囲い、フェーズ( Enter / Leave )ごとにスタイルを書いていきます。

```html

<transition-group>
  <div v-for="(todo, index) in todos" :key="todo.key">
    <span>{{ todo.value }}</span><input @click="remove(index)" type="button" value="x" />
  </div>
</transition-group>
```

```css

<style>
/* 表示・非表示アニメーション中 */
.v-enter-active, .v-leave-active {
  transition: all 500ms;
}

/* 表示アニメーション開始時 ・ 非表示アニメーション後 */
.v-enter, .v-leave-to {
  opacity: 0;
}
</style>
```
<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="b0ntenmaru" data-slug-hash="VwYZPNo" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="VwYZPNo">
  <span>See the Pen <a href="https://codepen.io/b0ntenmaru/pen/VwYZPNo">
  VwYZPNo</a> by Date (<a href="https://codepen.io/b0ntenmaru">@b0ntenmaru</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

一点自分がハマった[ポイント](#配列のインデックスをキーにv-bindしてはダメ)があるのでこちらも是非読んでみてください。


これで複数要素のアニメーションが実装できました。
ですが削除ボタン押下後、要素が移動する時にアニメーションがなく、新しい位置に味気なく移動してしまっています。

## 要素移動のトランジション
`<transition-group>` は Enter / Leave だけでなく、要素の移動のためのトランジションクラスである `v-move` クラスを使うことで上のような味気ない移動にアニメーションを加えることができます。

やることとしては、簡単で以下の２点を追記します。

```css
/* 要素が移動する時に700msで移動するように指定 */
.v-move {
  transition: all 700ms;
}

.v-leave-active {
  /* 移動のトランジションをさせる場合は非表示アニメーション中に position: absoluteを指定しないと正しく動作しない */
  position: absolute;
}
```

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="b0ntenmaru" data-slug-hash="jOENGvN" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="jOENGvN">
  <span>See the Pen <a href="https://codepen.io/b0ntenmaru/pen/jOENGvN">
  jOENGvN</a> by Date (<a href="https://codepen.io/b0ntenmaru">@b0ntenmaru</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

これで複数要素の表示/非表示/移動時のアニメーションが実装できました。

ちなみにこの `v-move` に関しては要素のあらゆる移動の時に適応されるので、複数要素をシャッフルさせる機能を追加してもいい感じに動いてくれます。

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="b0ntenmaru" data-slug-hash="yLyBxZQ" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="yLyBxZQ">
  <span>See the Pen <a href="https://codepen.io/b0ntenmaru/pen/yLyBxZQ">
  yLyBxZQ</a> by Date (<a href="https://codepen.io/b0ntenmaru">@b0ntenmaru</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

## ハマったポイント
### 配列のインデックスをキーにv-bindしてはダメ

>中の要素は、key 属性を持つことが 必須 です。

と [Vue の公式](https://jp.vuejs.org/v2/guide/transitions.html#%E3%83%AA%E3%82%B9%E3%83%88%E3%83%88%E3%83%A9%E3%83%B3%E3%82%B8%E3%82%B7%E3%83%A7%E3%83%B3)にも記述されているように、 `<transition-group>` タグ内部の v-for で指定された要素はそれぞれが key を持つことが必須ですが、ここに配列(ここでは `todos` )の index を渡してはいけません、アニメーションが正常に動作しなくなります。
下記はkeyに配列の index を渡した例です。
削除ボタンを押すと決まって最後の要素が削除されたように見えてしまっています。

```html
<transition-group>
  <div v-for="(todo, index) in todos" :key="index">
    <span>{{ todo.value }}</span><input @click="remove(index)" type="button" value="x" />
  </div>
</transition-group>
```

<p class="codepen" data-height="265" data-theme-id="default" data-default-tab="result" data-user="b0ntenmaru" data-slug-hash="GRgKXaE" style="height: 265px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="GRgKXaE">
  <span>See the Pen <a href="https://codepen.io/b0ntenmaru/pen/GRgKXaE">
  GRgKXaE</a> by Date (<a href="https://codepen.io/b0ntenmaru">@b0ntenmaru</a>)
  on <a href="https://codepen.io">CodePen</a>.</span>
</p>
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>


Vue.js の key はどの値に変更があったのかを追うために使われていて、 index を key に指定した場合、最後より前の要素を消すことによって index の値が更新され、 Vue がどの要素をアニメーションさせれば良いかわからなくなり、一番最後の要素が消えるような挙動となってしまうようです。なので key には配列の index ではない一意な値を渡してあげましょう。

## 終わりに
以上、「Vue.jsでいい感じのアニメーションを作りたい」でした。

アニメーションがいい感じかどうかはさておき、 Vue.js でも簡単にアニメーションを実装できることがお分りいただけたかと思います。
Vue.js のアニメーションには[状態のトランジション](https://jp.vuejs.org/v2/guide/transitioning-state.html)などもあるので是非いろいろ触ってみてください！