Vue.js でたのしいトランジション リスト編

リストトランジションを使ってフィルタリングやソートに動きを付ける!

リストトランジションとは

複数の要素を扱うトランジションです。使い方とこんな感じのギャラリー向けトランジションを作る方法を紹介します。トランジションの基本は前回のトランジション通常編の記事を読んで下さいね!

このリストの作り方は最後の方で説明してます。

リストトランジションで使うクラス

基本は通常トランジションと一緒で表示/非表示が切り替わった場合 enter/leave の状態になります。どちらでもなく位置が変わるだけの場合は move という状態になり v-move というクラスが使えるようになります。

CSS トランジションの設定

ホバーのような非トランジション時のアニメー��ョンに影響しないように要素ではなく .v-enter-active, .v-leave-active.v-movetransition を設定するといいです。

.v-enter-active, .v-leave-active {
  transition: opacity 1s;
}
.v-move {
  transition: transform 1s;
}

v-move は要素の位置が変わる時に追加されるクラスで位置を動かすために transform プロパティは必須です。これも忘れないように設定します。付くタイミングは v-enter-active/v-leave-active と一緒でトランジションの始まりから終わりまで通して付きます。

リストトランジションを作る

表示・非表示の enter/leave については通常トランジションと一緒なので省略します。通常トランジションの記事でやった enter/leave のクラスの付くタイミングをしっかり掴んでおくとリストも同じ要領でスタイルを付けることが出来ると思います。

サンプルはあらかじめ、リストに追加、クリックで削除、倍数のIDだけ表示するボタン、ソートの機能を組んでいます。

new Vue({
  el: '#app',
  data: {
    current: 1,
    order: -1,
    list: [],
    lastId: 3
  },
  computed: {
    filteredList() {
      var result = this.list.filter(v => {
        return v.id % this.current === 0
      })
      result.sort((a, b) => {
        return (a.id === b.id ? 0 : (a.id > b.id ? -1 : 1) * this.order)
      })
      return result
    }
  },
  methods: {
    add() {
      this.lastId += 1
      this.list.push({ id: this.lastId })
    },
    remove(id) {
      this.list = this.list.filter(v => { return v.id !== id })
    }
  },
  created() {
    for (var i = 0; i < this.lastId; i++) {
      this.list.push({ id: i + 1 })
    }
  }
})

基本的なリストトランジション

move の動きのおかげでアイテムが消えてもカクっとならないでスムーズに位置が変わってくれます。

<transition-group name="demo" tag="ul" class="list" appear>
  <li class="item" v-for="(item,idx) in filteredList" :key="item.id" @click="remove(item.id)">item {{ item.id }}</li>
</transition-group>

リストトランジションの場合はキーの設定が必須で必ず 不変なユニーク なキーを設定します。ラップ用のタグ tag="ul" はトランジションタグに直接設定します。ちなみに通常トランジションと一緒で初期表示の時もトランジションする appear オプションも使用できます。

.demo-enter-active, .demo-leave-active {
  transition: transform .5s, opacity .5s;
}
.demo-move:not(.demo-leave-active) {
  transition: transform .5s;
}
/* 表示される時は上からスライド */
.demo-enter {
  opacity: 0;
  transform: translateY(-20px);
}
/* 消える時は縮小される */
.demo-leave-to {
  opacity: 0;
  transform: scale(0.8);
}
.demo-leave-active {
  position: absolute;
}

表示する時と消える時の動きを別々にしてます。

.demo-move:not(.demo-leave-active)

ここで :not() を指定してる理由ですが下の方で詳しく説明しているんですけど、leavemove は同時に発生する事がありこれが無いと leave の時に move の CSS がマージされて opacity のトランジションが消されてしまうためです。動きが安定しない場合はこれが原因になってる事もあります。

時間差を付けてずらして表示する

公式のスタッガリングトランジションのサンプルは完全な JavaScript 処理をしてますが、できれば CSS でアニメーションしたいのでフックで直接要素のディレイだけ操作するようにしました。

Vue はトランジションをする時CSSのデュレーションやディレイ処理時間を計算してるようなので処理がズレるかなぁ~と思ったんですけど問題ないみたいです。

<transition-group name="demo" tag="ul" class="list" appear
    @before-enter="beforeEnter"
    @after-enter="afterEnter"
    @enter-cancelled="afterEnter">
  <li class="item" v-for="(item,idx) in filteredList" :key="item.id" @click="remove(item.id)" :data-index="idx">item {{ item.id }}</li>
</transition-group>

これだけです。

methods: {
  // enter の初めにインデックス×100でディレイを付ける
  beforeEnter(el) {
    el.style.transitionDelay = 100 * el.dataset.index + 'ms'
  },
  // enter が終わるか中止されたらディレイを消す
  afterEnter(el) {
    el.style.transitionDelay = ''
  }
}

:data-index="idx" でインデックスを要素に持たせておき before-enter フックでインデックス数値をかけたディレイを付けて、 after-enterenter-cancelled (重複発生でキャンセルされた時) にディレイを削除してます。

既にあるリストをフィルタリングやソートするだけならこれで良いんですが、新しくリストに追加する時にリストの最後の方の長いディレイが付いてしまうのでその場合はちょっとフラグを付けて回避します。

methods: {
  beforeEnter(el) {
    Vue.nextTick(() => {
      // フラグが無ければディレイを付けて、フラグがあればフラグを消すだけ
      if (!this.addEnter) {
        el.style.transitionDelay = 100 * el.dataset.index + 'ms'
      } else {
        this.addEnter = false
      }
    })
  },
  add() {
    // 新しく追加する時フラグをたてる
    this.addEnter = true
    this.lastId += 1
    this.list.push({ id: this.lastId })
  },
}

これで新規で追加した時はディレイなしに enter の処理がされます。

これ終わったあとクラスが消えなかったりして結構ハマってたんですけど下に書いてる move&leave の上書きの問題だったみたいで、最終的にはわりとあっさりでした。

ギャラリー向けの印象的なトランジション

一番最初のデモに使ったリストトランジションの方法ですが基本的には上の時間差を付けるやつと殆ど一緒で CSS を盛ってるぐらいです。(スマホだと激重い!)

.demo-enter-active, .demo-leave-active {
  transition: transform 1s, opacity 1s, filter 1s;
}
.demo-move:not(.demo-leave-active) {
  transition: transform 1s;
}
.demo-enter {
  opacity: 0;
  filter: blur(5px);
}
.demo-leave-to {
  opacity: 0;
  transform: translateY(-20px);
}
.demo-leave-active {
  position: absolute;
}

それから、FLIPの動きでアイテムにマージンが付いてると枠外の変な方向に move&leave されてしまう事があるんですけど、���らかじめ位置とサイズを補正するといいみたいです。

<transition-group name="demo" tag="ul" class="list" appear
  @before-enter="beforeEnter"
  @after-enter="afterEnter"
  @enter-cancelled="afterEnter"
  @before-leave="beforeLeave">
  <li class="item" v-for="(item,idx) in filteredList" :key="item.id" :data-index="idx">item {{ item.id }}</li>
</transition-group>

ひとつ前のデモのコードに before-leave のフックを追加して位置補正をしてます。

beforeLeave(el) {
  var { marginLeft, marginTop, width, height } = window.getComputedStyle(el)
  el.style.left = el.offsetLeft - parseFloat(marginLeft, 10) + 'px'
  el.style.top = el.offsetTop - parseFloat(marginTop, 10) + 'px'
  el.style.width = width
  el.style.height = height
}

動かないでその場で消えるパターンは v-leave-to で移動にディレイを付けたら出来ました。

.demo-move {
  transition: transform 1s;
}
/* move のあとに */
.demo-leave-to {
  transition: transform 0s ease 1s, opacity 1s;
}

.v-move 中にもアニメーションを設定したい

enter/leave と違って細かい事はできませんが v-move と要素自体のクラスに CSS の transition プロパティを設定しておけば動いているときにもアニメーションさせる事ができます。

.item {
  transition: opacity 1s;
}
.v-move {
  opacity: 0.5;
  transition: transform 1s, opacity 1s;
}

これは動き始めたら透明度を 0.5 にして終わったら戻すアニメーションです。

transition-group のフック

通常トランジションと同じ leave/enter 周りのフックできます。ただ各要素毎のフックだけで全てが始まる前&全て終わった後というフックが無くて不便なので検討中っぽいです。 move のフックもまだ無いようです。この2つが出来るようになったら捗りそう。

ハマりポイント

アイテムに不変のユニークなキーを設定する

よく間違えやすいのがキーに index を設定する例で、リストを更新したり算出プロパティを使ってフィルタリングするとインデックスが変わってしまうので正しく動作しません。ハマらないようにするため配列の中にユニークなIDを持っておくと良いです。

leave と move は同時に発生するのでクラス指定に注意

フィルターなどで要素が消えて v-leave が発生するとき position:absolute で即座に位置が変わる場合には v-leave/v-leave-to/v-leave-activeleave 系統と v-move の両方が付きます。

例えば item1, item2, item3 という3個の要素があって item2, item3 が消える場合、最初に消える item2 は leave 系統だけですが item3 は前の要素が無くなるので move が付きます。1フレーム進んだトランジションの始まりは以下のようなクラスになります。

<ul>
  <li class="item">item1</li>
  <li class="item v-leave-active">item2</li>
  <li class="item v-leave-active v-move v-leave-to">item3</li>
</ul>

スタイルを記述してる順番によってスタイルがマージ・上書きされてしまって意図しない動きになる事があるので注意。

対策1 :not() を指定する

.v-move:not(.v-leave-active) {
  transition: transform 1s;
}

対策2 v-move を先に記述する

なお v-leave-active にも transform が無いと move が動かなくなる。

.v-move {
  transition: transform 1s;
}
.v-enter-active, .v-leave-active {
  transition: transform 1s, opacity 1s;
}

対策3 全部まとめて指定する

最初からまとめて設定しておけばいいじゃんという過激派。数値が同じならこれでも良いと思います。

.v-enter-active, .v-leave-active .v-move {
  transition: transform 1s, opacity 1s;
}

表示・非表示の切り替えがある時 v-show は使用できない

場合によりますが v-show で条件分岐して enter/leave がされると出現位置がおかしくなったりクラス操作がされない事があります。その場合は v-if か算出プロパティを使います。将来的に出来るようになるのかもしれないけど今のところ仕様みたいです。表示の切り替えはしないでソートや追加と削除だけなら使えるようです。

まとめ

基本的に CSS のスタイル次第で色んなトランジションアニメーションが出来ます。楽しいですね!一番上のサンプルはちょっとやり過ぎた感ありますが、ギャラリーサイトやプロモーションサイトなんかではインパクトが出そうですよね。

ちなみに transition-group で使用してる FLIP という手法は、例えば高さ可変の要素のリストでその中の要素が変更されて高さが変わった時とかでもアニメーションで移動してくれるんですよ…すごーい(⁰▿⁰)/