2017年10月20日

デモとコード例で学ぶ Vue.js のトランジション

フロントエンド開発においてトランジション、アニメーションの実装は比較的難しいものだと思いますが、心地の良い UI を作るためには必要不可欠です。Vue.js にはその実装を簡単にしてくれる機能があるので、デモを交えながらその使い方と、強力さを紹介します。

<transition> コンポーネント

要素が挿入、削除される時のアニメーションは Vue.js デフォルトで用意されている <transition> コンポーネントを使用することで簡単に書くことができます。<transition> コンポーネントはただ一つの子要素を持ち、子要素の表示状態に応じてアニメーションに関する処理を行います。例えば、以下のようなコードになります。

<transition>
  <p v-if="isAppear">この要素はアニメーションの対象になります。</p>
</transition>

上記の例では <transition> に囲まれている <p> 要素がアニメーションの対象になります。アニメーションが発生するタイミングは v-if="isAppear" の値が変わり、<p> 要素が挿入、削除される瞬間です。以降の節で、具体的なアニメーションの処理をどのように実装するか見ていきましょう。

トランジションクラス

<transition> に囲まれた要素がアニメーションする際、以下のような特別なクラスがそれぞれ別々のタイミングで付与されます。これらのクラスは name 属性の値によって変更することができます。例えば、<transition name="fade"> と指定すると、v-enter ではなく fade-enter になります。

  • v-enter: 要素が挿入される前に付与され、アニメーション開始時に削除されるクラス。挿入アニメーションの初期スタイルをあてるために使用します。
  • v-enter-to: 要素のアニメーション開始時に付与され、アニメーション終了後に削除されるクラス。挿入アニメーションの終了時のスタイルをあてるために使用します。
  • v-enter-active: 要素の挿入前からアニメーション終了まで付与されるクラス。トランジションの設定を書くために使用できます。
  • v-leave: 要素のアニメーション開始前に付与され、アニメーション開始時に削除されるクラス。削除アニメーションの初期スタイルをあてるために使用します。
  • v-leave-to: 要素のアニメーション開始時に付与され、アニメーション終了後に削除されるクラス。削除アニメーションの終了時のスタイルをあてるために使用します。
  • v-leave-active: アニメーション開始前から終了後まで付与されるクラス。トランジションの設定を書くために使用できます。

それぞれのクラスの関係は Vue.js 公式ドキュメントにある図を見ると分かりやすいでしょう。

トランジションクラスの関係図

例えば単純なフェードイン・フェードアウトのアニメーションは以下の CSS を書くだけで動作します。

.v-enter-active,
.v-leave-active {
  /* アニメーションの時間、イージングなどを設定 */
  transition: opacity 300ms ease-out;
}

/* フェードイン */
.v-enter {
  /* フェードインの初期状態 */
  opacity: 0;
}

.v-enter-to {
  /* フェードインの終了状態 */
  opacity: 1;
}

/* フェードアウト */
.v-leave {
  /* フェードアウトの初期状態 */
  opacity: 1;
}

.v-leave-to {
  /* フェードアウトの終了状態 */
  opacity: 0;
}

もう少し複雑な例だと、以下のようなアニメーションも書くことができます。以下の例では is 属性を使ってコンポーネントを入れ替えている要素を <transition> で囲い、コンポーネント間の切り替えをアニメーションさせています。また、子要素だけではなく、それよりネストした要素に対しても子孫セレクタで CSS トランジションを指定して、別々のトランジションを実行させています (.page-enter-active .enter-1 など)。さらに、<transition> 要素には duration 属性でトランジションにかかる時間を与え、子孫要素のアニメーションを待つようにしています。

See the Pen Simple Page Transition by ktsn (@ktsn) on CodePen.

この例から、<transition> と CSS トランジションだけでも多くの表現ができることがわかるかと思います。

JavaScript フック

<transition> コンポーネントと CSS トランジション (アニメーション) だけでも大抵のアニメーションは作れるのですが、時には JavaScript による処理が必要となる場合もあります。例えば、アコーディオンの開閉など、要素の大きさや位置を取得する必要のあるアニメーションが主な例でしょう。

<transition> にはこのような場合のために JavaScript による処理を挟むことのできる機能もあります。具体的には以下のイベントがそれぞれのアニメーションの進捗に応じて発生します。イベントが発生するタイミングはそれぞれ以下のとおりです。

  • before-enter: 要素が挿入される前
  • enter: 挿入されてアニメーションされる前
  • after-enter: 挿入アニメーション後
  • enter-cancelled: 挿入キャンセル時
  • before-leave: 削除アニメーションが実行される前
  • leave: 削除アニメーションが実行される前で before-leave の後
  • after-leave: 要素が削除された後
  • leave-cancelled: 削除キャンセル時

それぞれのイベントは v-on を使うことで監視することができ、コンポーネントのメソッドをコールバックとすることができます。例えば、以下の例では before-enter イベントが発生したときに beforeEnter メソッドが呼び出されます。

<transition
  v-on:before-enter="beforeEnter"
  v-on:enter="enter"
  v-on:after-enter="afterEnter"
  v-on:enter-cancelled="enterCancelled"

  v-on:before-leave="beforeLeave"
  v-on:leave="leave"
  v-on:after-leave="afterLeave"
  v-on:leave-cancelled="leaveCancelled"
>
  <p v-if="isAppear">JavaScript フックを使ったトランジション。</p>
</transition>

それぞれの JavaScript フックには第一引数にトランジションの対象となる DOM 要素が渡されるため、先に挙げた要素の大きさや位置の取得なども通常の DOM API を使用して行うことができます。以下はアコーディオンを開くアニメーションの実装の例です。要素の高さを CSS トランジションをするには height に具体的な値を指定しなければならないため、enter フック内で要素の高さを取得し、CSS に指定しています。

export default {
  // ...

  methods: {
    beforeEnter(el) {
      // アコーディオンを閉じた状態にする
      el.style.height = '0'
    },

    enter(el) {
      // アコーディオンのコンテンツの高さを指定して開く
      el.style.height = el.scrollHeight + 'px'
    }
  }
}

また、enter, leave のフックは第二引数にコールバックを受け取り、それを実行することでアニメーションの終わりを Vue に教えることができます。Vue はデフォルトで CSS トランジション/アニメーションの終了を検知しますが、JavaScript だけでアニメーションをする時はコールバックを使うと良いでしょう。ちなみに、:css="false" を属性として渡すと、CSS トランジション/アニメーションの終了検知を無効化することができます。これを使うと、jQuery のアニメーションを Vue の中で使うことができたりします。

export default {
  // ...

  methods: {
    enter(el, done) {
      // jQuery の slideDown 関数で要素を開く
      // 第二引数にコールバックを渡し、
      // Vue にアニメーションの終了を伝える
      $(el).slideDown(150, done)
    }
  }
}

もっと複雑な例だと、以下のような画像のサムネイルをクリックしたらそれが浮き上がるような、ネイティブアプリっぽいアニメーションを書くことができます。サムネイルをクリックすると、サムネイル画像の位置と大きさ、フルサイズ画像の位置と大きさを JavaScript フックの中で設定し、ズームされているようなトランジションを実行しています。また、ズーム後の画像を上か下に一定以上の距離をドラッグすると、同様のロジックでズームアウトされているようなトランジションを実行します。

See the Pen Swipe to Zoom Out by ktsn (@ktsn) on CodePen.

また、<transition> とは関係ないですが、ドラッグ時に画像の座標やモーダルの背景色を変える処理は Vue インスタンスのデータの値を変えるだけで実装できています。データバインディングのおかげでユーザーの操作に応じて、リアルタイムに要素を動かすということもやりやすいと感じます。

<transition-group> コンポーネント

<transition> コンポーネントは一つの要素アニメーションさせるときに使いますが、v-for で繰り返している要素など、リストのアニメーションを実装する時は <transition-group> コンポーネントを使用します。<transition-group> のほとんどの API は <transition> と同じなので、先に学んだ知識を使うとリストのアニメーションも実装できると思います。

<transition-group> の特徴としては移動のトランジションを簡単に書くことができるという点が挙げられます。<transition-group> の対象となる要素の数や内容が変更された時、Vue がその要素をスムーズに移動させてくれます。この移動は transform の変更による CSS トランジションで行われているので、以下のように移動用のクラスを付与する必要があります。

.v-move {
  transition: transform 500ms ease-out;
}

この挙動がわかるデモが以下です。Add, Remove ボタンを押したときに、要素の追加、削除だけでなく、それにともなって場所の変わる要素がスムーズに移動しているのがわかると思います。Shuffle ボタンを連打するとこの機能の強力さが伝わるのではないかなと思います (実際のアプリではこんなことすることはないでしょうが……)。

See the Pen Vue list transition example by ktsn (@ktsn) on CodePen.

まとめ

Vue.js にはデフォルトで <transition><transition-group> というコンポーネントが提供されており、それらを使用することで容易にアニメーションを実装することができます。単純なアニメーションであれば、トランジションの際に付与される CSS クラスを使用して、CSS トランジション、アニメーションをするのが良いでしょう。複雑なアニメーションに対しては JavaScript フックを使うことで柔軟な処理を行うことができます。また、<transition-group> には要素の移動をスムーズにしてくれる機能がついており、数行 CSS を書くだけでいい感じの見た目にできるので、とりあえず使ってみることをおすすめします!

RELATED POSTS