自作Vue.jsコンポーネントでv-transitionによるアニメーション


この記事はVue.js Advent Calendar 23日目の記事です。

前回の記事「Vue.jsでpopup componentを作ってみる」の続きです。

Vue.jsではvueifyvue-loader等を利用すると、自作コンポーネントが比較的簡単に作れます。
前回の記事ではHTMLを以下のように記述してpopupするコンポーネントを作成しました。
(※Vue.jsのバージョンはv0.11.4です)

<body id="app">
    <vue-popup v-show="popupOpened" title="{{item.title}}">
        <vue-sample v-with="item"></vue-sample>
    </vue-popup>
</body>

前回と同様のデモ(ボタンぽちってみてください)

今回は、このデモで使用しているv-transitionによるアニメーションについて簡単に紹介します。

Vue.jsでのtransition定義

Vue.jsではv-transitionというディレクティブがあり、それを使用するとDOMの変化時(例えば挿入、削除)のタイミングに適用されるCSSクラスもしくはJSのhookポイントが提供されます。

公式ドキュメントによると以下の3つのディレクティブによりDOMの変化があった場合、もしくは、vm.$appendToなどのvmインスタンスのDOMを操作するメソッドが呼ばれた場合にtransitionが適用されます。

  • v-show
  • v-if
  • v-repeat

DOMで削除・非表示になる時の処理の流れ

  1. v-leaveクラスを対象のエレメントに適用
  2. transitionが終わるイベント(transitionend)を待つ
  3. DOMから削除し、v-leaveクラスも削除

DOMで挿入・表示される時の処理の流れ

  1. v-enterクラスを対象のエレメントに追加
  2. DOMに挿入
  3. CSS(v-enter)が実際に有効化
  4. オリジナルの状態に戻すためにv-enterクラスを削除

v-componentでは、transitionが指定された場合にその処理の流れをコントロールすることができます。

transition-mode
デフォルトではコンポーネントの表示と非表示が同時に起こりますが、以下のパラメーター(in-out, out-in)を指定するとその順序をコントロールすることが可能です。
in-out: 新しいコンポーネントのtransitionが先で、そのtransitionが終了後に、現在のコンポーネントの消えていくtransitionsが始まる
out-in: 現在のコンポーネントの消えていくtransitionが先で、そのtransitionが終了後に、新しいコンポーネントのtransitionが始まる

transition-modeを指定する例(公式ドキュメントから抜粋)

<!-- fade out first, then fade in -->
<div v-component="hoge"
v-transition="fade"
transition-mode="out-in">
</div>

今回のデモのpopupが開いていく例では、heightとalphaを表示・非表示でv-transitionを指定して、CSSでアニメーションを実現しています。

自作コンポーネントにv-transitionを追加

今回の場合はpopupする外側のcomponentにexpandという名前でheightとalphaをanimationして表示・非表示するようにします。
HTML側はvueのcomponentを記述している部分にv-transition=”expand”を指定するだけです。

    <vue-popup v-show="popupOpened" v-transition="expand" title="{{item.title}}">
        <vue-sample v-with="item"></vue-sample>
    </vue-popup>

CSSでenter leaveを指定

cssでtransitionを実装する例

        vue-popup {
            transition: all 0.8s ease;
            max-height: 442px;
            overflow: hidden;
            display: block;
        }

        vue-popup.expand-enter, vue-popup.expand-leave {
            max-height: 0;
            opacity: 0;
        }

enterとleaveが適用されるクラスはv-transtionで指定したパラメーターがprefixとして付きます。今回の例ではexpandという名前でv-transitionを指定していたので、expand-enterとexpand-leaveクラスがtransition時に適用されます。

注意点は、自作のカスタムコンポーネントはブラウザにとってunknownなタグであるため、displayのstyleを意図的に指定しないとwidth&heightが認識されない点です。

display: block;を設定することで、通常のblock系のエレメントと同様に扱われます。今回はheightをアニメーションで変更しているため、この指定が必要でした。
したがって、vue-popupタグではなくdivエレメントにv-component=”vue-popup”を指定すれば、divは元々blockなので、display: blockを外しても期待通り動きます。

ちなみに、transitionの実装はCSSで定義せずにjsでハンドリングすることもできます。

JSでenter leaveを実装

JSでtransitionを実装する例

    Vue.transition('expand', {
        beforeEnter: function (el) {
            $(el).css({
                transition: "all 0.8s ease",
                overflow: "hidden",
                display: "block",
                maxHeight: 0,
                opacity: 0
            });       
        },
        enter: function (el, done) {
            $(el).animate({
                maxHeight: "442px",
                opacity: 1
            });
        },
        leave: function (el, done) {
            $(el).animate({
                maxHeight: 0,
                opacity: 0
            });
        }
    });

JSではVue.transitionの引数に、v-transitionで指定したパラメータとそれぞれのhookに対するコールバックを定義したオプションを渡します。
それぞれのhookポイントは以下のようになっています。

  • beforeEnter: elementがDOM挿入される直前に呼ばれる
  • enter: elementはDOMに挿入された後に呼ばれる
  • leave: elementが削除される直前に呼ばれる

この例ではjQueryで同様のCSSの処理をしています。こちらでもやはり、vue-popupという自作タグだとぺちゃんこな要素として扱われてしまうため、divにv-componentを指定した方が良さそうです。
ということで、なんとなくWeb Componentsっぽい記述ができてカッコいいという理由のためだけでは少しだけ弊害が生じることが判明しましたw

enter/leaveのfunctionの引数のdoneというコールバックは、ドキュメントにはアニメーションが終わった時に呼ぶと書いてあったのですが、今回のケースでは必要ありませんでした。
(今回は実際にDOMから削除しないv-showディレクティブを使っていたからなのか、公式ドキュメントがup-to-dateでないのかは、ちょいとわかりません。)

こんなかんじで自作コンポーネントに上記で解説した処理を加えると、冒頭のデモで紹介したようなアニメーションが実現できます。

Vue.jsではアニメーションをディレクティブの付与で実現できるため、既存のコンポーネント自身のロジックとアニメーションのロジックを分離できるのが良いですねぇ。

次のVue.jsに関する続き記事は、このcomponentを使用してアプリケーションを作っていく部分を紹介しようかなーっと思っています(いつ書くか分かりませんが)。