Vue.jsで画面を開いた際にtransition-groupでアニメーションさせる

2021.04.20
Vue.jsで画面を開いた際にtransition-groupでアニメーションさせる

画面を開いたタイミングで配列の要素をtransition-group を使ってアニメーションさせる方法を記載します。



最後はanime.jsと組み合わせて、

↑の動画のような少し複雑なアニメーションをさせる方法についても記載します。

検証した環境

1 nuxt 2.14.6
2 animejs 3.2.1
3 @types/animejs 3.1.3

アニメーションをする前の状態を作る

TypeScriptで極力型を指定したいので nuxt-property-decorator を使います。

Nuxtらしい記述はしないので vue-property-decorator で考えてもらって差異はありません。



まず data で用意した文字列をただリスト表示させてみます

<template>
  <ul>
    <li v-for="item in items">
      {{ item }}
    </li>
  </ul>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class IndexPage extends Vue {
  items = ['JavaScript', 'Swift', 'Kotlin', 'GO', 'PHP', 'Ruby']
}
</script>

ここでは後ほどのwarningをあえて表示するためにv-forで回している<li>要素に対してkeyを記載していませんが、

実際の場合は公式にも書かれている通りkeyを設定するのが理想です。


繰り返される DOM の内容が単純な場合や、性能向上のために標準の動作に意図的に頼る場合を除いて、可能なときはいつでも v-for に key 属性を与えることが推奨されます。

リストレンダリング — Vue.jsより引用

アニメーションさせる

ここから徐々にtransition-groupを使っていきます

下準備

<template>
  <ul>
    <transition-group name="list">
      <li v-for="item in items">
        {{ item }}
      </li>
    </transition-group>
  </ul>
</template>
・・・

まずv-forで回している順番に表示させたい要素を<transition-group>で囲みます。



ただし、これで起動してみると ERROR [Vue warn]: <transition-group> children must be keyed: <li> と怒られます


transition-groupを使う場合は keyが必須なんですね、

アニメーションさせる要素を確実に判断するためかな




そのため、likeyを設定します、keyには適宜一意になる値を設定します

<template>
  <ul>
    <transition-group name="list">
      <li v-for="item in items" :key="item">
        {{ item }}
      </li>
    </transition-group>
  </ul>
</template>
・・・



Buttonタップでまずアニメーションさせる

まずよくある「ボタンを押したらアニメーションする」ところから確認します。

<template>
  <div>
    <ul>
      <!-- nameで付けた名前がlist-enter-activeなどtransition-group特有のクラスとして付与される -->
      <transition-group name="list">
        <li v-for="(item, index) in items" :key="item">
          {{ item }}
        </li>
      </transition-group>
    </ul>
    <!--  ボタンを押したら add() を呼び出す -->
    <button @click="add">追加</button>
  </div>
</template>

<script lang="ts">
・・・
export default class IndexPage extends Vue {
  items = ['JavaScript', 'Swift', 'Kotlin', 'GO', 'PHP', 'Ruby']

  add() {
    const langs = ['C#', 'Rust']
    // スプレッド構文を使って items と langs をマージする
    this.items = [...this.items, ...langs]
  }
・・・
</script>

<style>
/* アイテムが追加されアニメーションしている間付与される */
.list-enter-active {
  transition: all 0.3s ease;
}
/* アイテムが追加されアニメーションが始まる最初だけ付与される */
.list-enter {
  transform: translateX(10px);
  opacity: 0;
}
</style>

要素が追加されるタイミングでアニメーションされるようになりました!


しかし追加した2つの要素どちらも同時に表示されてしまう 😅

その点については後ほど。



まず簡単に流れについて書きます。

  1. ボタンを押すとaddメソッドが呼ばれitemsが更新される
  2. keyを元に新たに追加された要素に対してtransition-group特有のクラスを付与
    .list-enter-active.list-enter
  3. 付与されたクラスを元にアニメーションする



transition-groupのクラスについては

公式のドキュメントがイメージも合わせてかなり分かりやすく書かれています


Enter/Leave とトランジション一覧 — Vue.js




さていったん雑にですが、

2つ同時に表示されてしまう問題について<li>要素に対しtransition-delayを付与する事で対応します。

<template>
・・・
<transition-group name="list">
  <li
    v-for="(item, index) in items"
    :key="item"
    :style="{ 'transition-delay': `${index * 0.3}s` }"
  >
    {{ item }}
  </li>
</transition-group>
</template>

ロジックとしては新たに追加された<li>要素に設定されたtransition-delayに応じてアニメーションが実行されます。


その際に配列のindex番号を使っているので、


↑のように最初から表示されている言語に対してもtransition-delayが設定され、

今回追加した要素だとtransition-delay1.8s;2.1s;とかなり時間がかかるため動画のように中々表示がされない、という問題点があります 😅


画面描画時にアニメーションさせる

ここまでを踏まえて、画面描画時に要素をアニメーションさせてみます

<template>
  <ul>
    <transition-group name="list">
      <li
        v-for="(item, index) in items"
        :key="item"
        :style="{ 'transition-delay': `${index * 0.3}s` }"
      >
        {{ item }}
      </li>
    </transition-group>
  </ul>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'

@Component
export default class IndexPage extends Vue {
  // items = [] のみだと何の型の配列か型推論が出来ないため string[] を明記
  items: string[] = []

  mounted() {
    this.items = ['JavaScript', 'Swift', 'Kotlin', 'GO', 'PHP', 'Ruby']
  }
}
</script>

<style>
.list-enter-active {
  transition: all 0.3s ease;
}
.list-enter {
  transform: translateX(10px);
  opacity: 0;
}
</style>



画面を開くと同時にアニメーションされました!



ロジックは画面を開いた際にmountedが呼ばれitemsが更新されます。


すると先ほどと同じでtransition-delay<li>要素に順に設定されつつ、

.list-enter-active.list-enterクラスが付与されるため順番に表示がされます。


注意点として、Nuxt.jsを用いる場合はcreated()だと反応しません!

理由はcreatedはSSR(サーバ側)でも呼び出されるメソッドのため、

サーバ側でレンダリングされてしまうためです。


そのため、Nuxt.jsの場合はcreatedではなくmountedを使いましょう



jsでアニメーション

↑の場合だと、

<template><style>にCSSが分かれてしまいます。


出来れば同じ要素のアニメーションに対してはcssはひとまとめにしたいところです。


そこでここではアニメーションに対するcssを全てjsに持たせてみます

anime.jsの導入

jsを用いて様々なアニメーションをさせる事が出来るライブラリのanime.js


anime.js • JavaScript animation engine

↑トップページのアニメーション見るだけでワクワクするのでぜひ見てみて下さい!😍



最近簡単なアニメーションさせるのはほぼcssで行う事が多くなった印象ですが、

jsを使う分、よりプログラマブルに複雑で色々な事が出来ます。




必要なパッケージを追加します

# anime.jsの導入
$ yarn add animejs

# TypeScriptの場合は@typesを追加
$ yarn add -D @types/animejs



nuxt.configなどに対する設定は必要ありません。

anime.jsを使用するコンポーネントでimportするのを忘れないようにして下さい

// anime.jsを使うコンポーネントではimportが必須
import anime from 'animejs'



anime.jsを使ってアニメーションさせる

transition-groupはcssのクラスを自動付与してくれる機能とは別に、

jsのメソッドを呼び出す事が出来ます。

<template>
  <transition-group
        name="list"
        @before-enter="beforeEnter /* 要素が追加されアニメーションを実行する直前に呼ばれる */" 
        @enter="enter /* 要素が追加されアニメーションを実行する時に呼ぶ */"
        @before-leave="beforeLeave /* 要素が削除されアニメーションを実行する直前に呼ばれる */"
        @leave="leave /* 要素が削除されアニメーションを実行する時に呼ぶ */"
        >
・・・
</template>

after-enterなどその他のタイミングで呼ばれるメソッドが複数あります

詳しくは公式を参照して下さい

JavaScript フック — Vue.js




ここではbefore-enterenterでanime.jsを用いたメソッドを実行しアニメーションさせてみます

<template>
  <ul>
    <transition-group name="list" @before-enter="beforeEnter" @enter="enter">
      <li v-for="(item, index) in items" :key="item" :data-index="index">
        {{ item }}
      </li>
    </transition-group>
  </ul>
</template>

<script lang="ts">
import { Component, Vue } from 'nuxt-property-decorator'
import anime from 'animejs'

@Component
export default class IndexPage extends Vue {
  items: string[] = []

  mounted() {
    this.items = ['JavaScript', 'Swift', 'Kotlin', 'GO', 'PHP', 'Ruby']
  }

  // 要素が追加されアニメーションが始まる前に呼ばれる
  beforeEnter(el: HTMLElement) {
    anime({ targets: el, translateY: 50, opacity: 0 })
  }

  // 要素が追加されアニメーションが始まる時に呼ばれる
  enter(el: HTMLElement, done: () => void) {
    // data属性として設定した index の値を取得
    const index: number = (el.dataset as any).index as number
    if (!index) return

    const delay = index * 420
    setTimeout(() => {
      anime({
        targets: el,
        translateY: [50, 0],
        opacity: [0, 1],
        easing: 'easeInOutSine',
        complete: done
      })
    }, delay)
  }
}
</script>

いい感じにヌルヌル動きますね!




まずbeforeEnterでアニメーションする前の状態を設定しておきます

beforeEnter(el: HTMLElement) {
  anime({ targets: el, translateY: 50, opacity: 0 })
}



そして enter() でアニメーションに関する設定をします。

enter(el: HTMLElement, done: () => void) {
  // data属性として設定した index の値を取得
  const index: number = (el.dataset as any).index as number
  if (!index) return

  const delay = index * 420
  setTimeout(() => {
    anime({
      targets: el,
      translateY: [50, 0],
      opacity: [0, 1],
      easing: 'easeInOutSine',
      complete: done
    })
  }, delay)
}
<li v-for="(item, index) in items" :key="item" :data-index="index">
  {{ item }}
</li>

data属性を用いて配列の添字を持たせ(:data-index="index"の部分)

その値とsetTimeoutを組み合わせて各要素に対しそれぞれどれくらい遅れてアニメーションをするかを設定しています。


data属性の取得がTypeScript使うと複雑ですね 🤔




anime.jsを複雑にしてみる

上記コードのanime({})をサンプルコードを見て少しいじってみていたところ、面白い動きが出来ました 😃


anime.jsのサンプルコードは以下に公式がまとめてくれていまして、

こちらも見ているだけでワクワク出来ます!

Documentation | anime.js


enter(el: HTMLElement, done: () => void) {
  const index: number = (el.dataset as any).index as number
  if (!index) return

  const delay = index * 420
  setTimeout(() => {
    anime({
      targets: el,
      translateY: [50, 0],
      translateX: [10, 0],
      scale: [1.3, 1],
      opacity: [0, 1],
      delay: anime.stagger(200, { direction: 'reverse' }),
      complete: done
    })
  }, delay)
}




anime.random()というメソッドがあって使ってみると予想外の動きをして面白いです😎

enter(el: HTMLElement, done: () => void) {
  const index: number = (el.dataset as any).index as number
  if (!index) return

  const delay = index * 420
  setTimeout(() => {
    anime({
      targets: el,
      translateY: [anime.random(-200, 200), 0],
      translateX: [anime.random(-300, 300), 0],
      rotate: [anime.random(-360, 360), 0],
      color: ['#00c4ff', '#0f0', '#000'],
      opacity: [0, 1],
      easing: 'easeOutBack',
      complete: done
    })
  }, delay)
}

おすすめ