基礎からメモ: Vue.js CH6-p205 リストトランジション

リストの要素をグループ化して、追加、削除、移動のアニメーションを行う。

<transition-group>タグを使いtag属性でタグ名を指定する。tag属性を省略するとspan要素でラップされる。リストトランジションではキーの設定が必須。

<transition-group name="list" tag="ul">
  <li v-for="item in list" v-bind:key="item.id"></li>
</transition-group>

リストへの追加や削除、条件変更で描画状態が変化したときに、Enter系とLeave系のトランジションクラスが付与されるのは単一トランジションと同じ。リストトランジションではこれらに加えて、リストの順番が変わった時にMove系のトランジションクラスとして.v-moveとが付与される。MoveCSStransformプロパティでシームレスな移動アニメーションを行う。

.v-move {
  transition: transform 1s;
}

<transition-group>に名前を付けてプレフィックスv-以外のものに変更できるのも単一トランジションと同じ。

移動トランジションの例

ボタンクリックでリストの順番を逆にする。

<div id="app">
  <button v-on:click="order=!order">切り替え</button>
  <transition-group tag="ul" class="list" >
    <li v-for="item in sortedList" v-bind:key="item.id">
      {{ item.name }} : {{ item.price }}円
    </li>
  </transition-group>
</div>

orderの値によってリスト順を反転する算出プロパティを定義

var app = new Vue({
  el: '#app',
  data: {
    order: false,
    list: [
      { id: 1, name: 'りんご', price: 100 },
      { id: 2, name: 'ばなな', price: 200 },
      { id: 3, name: 'いちご', price: 300 }
    ]
  },
  computed: {
    // order値に応じてリストの順を反転する算出プロパティ
    sortedList: function() {
      // Lodash の orderBy を使用
      return _.orderBy(this.list, 'price', this.order ? 'desc' : 'asc')
    }
  }
});

CSSv-moveクラスにトランジションを定義

.v-move {
  transition: transform 1s;
}

ボタンを押すと順序がヌルっと反転する。

Leave と Move は同時発生することがある

例えば次のようなHTMLがある。リストアイテムは自身のshow属性がtrueの時だけ表示される。

<div id="app">
  <button v-on:click="changeList">リストの1だけにする</button>
  <transition-group tag="ul" class="list" >
    <li v-for="item in list" v-if="item.show" v-bind:key="item.id">
      {{ item.name }} : {{ item.price }}円
    </li>
  </transition-group>
</div>

ボタンをクリックするとメソッドが呼ばれ、リスト2と3のshow属性が反転するので、消えたり表示されたりする。

var app = new Vue({
  el: '#app',
  data: {
    list: [
      { id: 1, name: 'りんご', price: 100, show:true },
      { id: 2, name: 'ばなな', price: 200, show:true },
      { id: 3, name: 'いちご', price: 300, show:true }
    ]
  },
  methods: {
    // リストの2と3の表示を反転
    changeList: function() {
      this.list[1].show = !(this.list[1].show);
      this.list[2].show = !(this.list[2].show);
    }
  }
});

要素が最初に消えるアイテム2(ばなな)はLeave系クラスだけが追加されるが、アイテム3(いちご)はアイテム2が消えた後に上へ移動してから消えるのでLeave系とMove系の両方のクラスが追加される。消える時のアイテム2と3に追加されるクラスはこんな感じになる。

<ul class="list">
  <li>りんご : 100円</li>
  <li class="v-leave-active v-leave-to">ばなな : 200円</li>
  <li class="v-leave-active v-move v-leave-to">いちご : 300円</li>
</ul>

今まで通りのCSSだと、アイテム3はtransitionプロパティが上書きされ、即座にopacity:0となりゆっくりと移動して消える感じではなくなる。消える際にゆっくり消える2よりも先に3が消えるはずだ。

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

.v-leave-active {
  position: absolute;
}

.v-move {
  transition: transform 1s; /* これが優先され、 v-leave-activeに指定したtransitionは上書きされる */
}

.v-enter, .v-leave-to {
  opacity: 0;
}

そういう場合は次のようなスタイルの一括指定か

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

または、:not()を利用することでうまくいく。

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

inline-flexなリストでの適用例(サポートサイトに載っている例)

Lodashを使用

<div id="app">
  <p>
    <button v-on:click="doShuffle">シャッフル</button>
    <button v-on:click="doAdd">追加</button>
  </p>
  <transition-group tag="ul" class="list">
    <li v-for="(item, index) in list" 
      v-bind:key="item" 
      class="item" 
      v-on:click="doRemove(index)">
      {{ item }}
    </li>
  </transition-group>
</div>
// 移動トランジション X&Y座標
var app = new Vue({
  el: '#app',
  data: {
    list: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
  },
  methods: {
    doShuffle: function () {
      this.list = _.shuffle(this.list)
    },
    doAdd: function() {
      var newNumber = Math.max.apply(null, this.list) + 1
      var index = Math.floor(Math.random() * (this.list.length + 1))
      this.list.splice(index, 0, newNumber)
    },
    doRemove: function (index) {
      this.list.splice(index, 1)
    }
  }
});
.list {
  width: 240px;
  padding: 0;
}

.item {
  display: inline-flex;
  justify-content: center;
  align-items: center;
  margin: 4px;
  width: 40px;
  height: 40px;
  background: #f5f5f5;
}

/* トランジション用スタイル */
.v-enter-active, .v-leave-active, .v-move {
  transition: all 1s;
}
.v-leave-active {
  position: absolute;
}
.v-enter, .v-leave-to {
  opacity: 0;
  background: #f9a3b1;
  transform: translateY(-30px);
}