VueJS 速習チュートリアル

Vue v-for アニメーション

Vue に組み込まれている <TransitionGroup> コンポーネントを使用すると、v-for を使ってページに追加された要素に対してアニメーションを適用することができます。

1.  コンポーネント

<TransitionGroup> コンポーネントは、v-for で作成された要素を囲むために使用されます。これにより、要素が追加または削除される際に、それぞれの要素に対して個別にアニメーションを付与できます。

<TransitionGroup> の内部で v-for によって作成されるタグには、必ず key 属性を定義しなければなりません。

また、<TransitionGroup>tag プロップ(prop)を使用して特定のタグを指定した場合のみ、実際の HTML タグとしてレンダリングされます。

<TransitionGroup tag="ol">
  <li v-for="x in products" :key="x">
    {{ x }}
  </li>
</TransitionGroup>

上記のコードが Vue によってレンダリングされると、以下のような結果になります。

<ol>
  <li>リンゴ</li>
  <li>ピザ</li>
  <li>ご飯</li>
</ol>

これで、リストに新しいアイテムが追加された際のアニメーションを CSS で定義できるようになります。

<style>
  .v-enter-from {
    opacity: 0;
    rotate: 180deg;
  }
  .v-enter-to {
    opacity: 1;
    rotate: 0deg;
  }
  .v-enter-active {
    transition: all 0.7s;
  }
</style>

この例では、products 配列に新しいアイテムを追加するだけで、そのアイテムに対して自動的にアニメーションが適用されます。

1.1 実装例

App.vue:

<template>
  <h3><TransitionGroup> コンポーネント</h3>
  <p><TransitionGroup> を使用して、新しい商品にアニメーションを適用します。</p>
  <input type="text" v-model="inpName"> 
  <button @click="addEl">追加</button>
  <TransitionGroup tag="ol">
    <li v-for="x in products" :key="x">
      {{ x }}
    </li>
  </TransitionGroup>
</template>

<script>
  export default {
    data() {
      return {
        products: ['リンゴ','ピザ','ご飯'],
        inpName: ''
      }
    },
    methods: {
      addEl() {
        const el = this.inpName;
        this.products.push(el);
        this.inpName = null;
      }
    }
  }
</script>

<style>
  .v-enter-from {
    opacity: 0;
    rotate: 180deg;
  }
  .v-enter-to {
    opacity: 1;
    rotate: 0deg;
  }
  .v-enter-active {
    transition: all 0.7s;
  }
</style>

2. 要素の追加と削除

要素の間に割り込んで削除を行うと、他の要素が削除された場所を埋めるように移動します。リストの他のアイテムが移動する様子をアニメーション化するには、自動的に生成される v-move クラスを使用します。

まずは、要素が削除されたときに他のアイテムが瞬時に移動してしまい、アニメーションされない例を見てみましょう。

2.1 移動のアニメーションがない例

App.vue:

<template>
  <h3><TransitionGroup> コンポーネント</h3>
  <p>要素が削除された際の、他の要素の移動に注目してください。</p>
  <button @click="addDie">サイコロを振る</button>
  <button @click="removeDie">ランダムに削除</button><br>
  <TransitionGroup>
    <div v-for="x in dice" :key="x" class="diceDiv" :style="{ backgroundColor: 'hsl('+x*40+',85%,85%)' }">
      {{ x }}
    </div>
  </TransitionGroup>
</template>

<script>
  export default {
    data() {
      return {
        dice: []
      }
    },
    methods: {
      addDie() {
        const newDie = Math.ceil(Math.random()*6);
        this.dice.push(newDie);
      },
      removeDie() {
        if(this.dice.length > 0){
          this.dice.splice(Math.floor(Math.random()*this.dice.length), 1);
        }
      }
    },
    mounted() {
      this.addDie();
      this.addDie();
      this.addDie();
    }
  }
</script>

<style>
.v-enter-from {
  opacity: 0;
  translate: 200px 0;
  rotate: 360deg;
}
.v-enter-to {
  opacity: 1;
  translate: 0 0;
  rotate: 0deg;
}
.v-enter-active,
.v-leave-active {
  transition: all 0.7s;
}
.v-leave-from { opacity: 1; }
.v-leave-to { opacity: 0; }

.diceDiv {
  margin: 10px;
  width: 30px;
  height: 30px;
  line-height: 30px;
  vertical-align: middle;
  text-align: center;
  border: solid black 1px;
  border-radius: 5px;
  display: inline-block;
}
</style>

上記の例では、アイテムが削除された際、後続のアイテムが即座に新しい位置へジャンプしてしまいます。これを解消するために、自動生成される v-move クラスを使用します。

v-move クラスは他の要素が移動する際のアニメーションを制御しますが、1 つ問題があります。それは「削除されるアイテムが、消え去るまでその場所に居座り続けてしまう」ことです。その結果、他のアイテムがスムーズに移動できず v-move の効果が発揮されません。

これを解決するには、v-leave-active クラスに position: absolute; を設定します。これにより、削除されるアイテムはフェードアウト中も表示はされますが、レイアウト上のスペースを占有しなくなります。

2.2 スムーズな移動アニメーションの実装例

前述の例に、2 つの新しい CSS クラス指定(14行目と17行目付近)を加えたものです。

App.vue (styleセクション):

<style>
.v-enter-from {
  opacity: 0;
  translate: 200px 0;
  rotate: 360deg;
}
.v-enter-to {
  opacity: 1;
  translate: 0 0;
  rotate: 0deg;
}
/* v-move を追加 */
.v-enter-active,
.v-leave-active,
.v-move {
  transition: all 0.7s;
}
/* 削除中の要素を絶対配置にすることで、他の要素をスムーズに移動させる */
.v-leave-active { position: absolute; }

.v-leave-from { opacity: 1; }
.v-leave-to { opacity: 0; }

.diceDiv {
  margin: 10px;
  width: 30px;
  height: 30px;
  line-height: 30px;
  vertical-align: middle;
  text-align: center;
  border: solid black 1px;
  border-radius: 5px;
  display: inline-block;
}
</style>

3. 実践的な大規模構成例

さらに高度な例として、リスト全体の追加・削除・ソート(並び替え)がどのようにアニメーションされるかを確認しましょう。

この例では以下の操作が可能です:

  • アイテムをクリックして個別に削除
  • アイテムのソート(昇順)
  • シャッフル(ランダムな並べ替え)
  • リスト内のランダムな位置に新しいアイテムを追加

3.1 実装例

App.vue:

<template>
  <h3><TransitionGroup> コンポーネント</h3>
  <p><TransitionGroup> 内のアイテムは、作成、削除、並び替えの際にアニメーションされます。</p>
  <button @click="addDie">サイコロを1個振る</button>
  <button @click="addDie10">10個振る</button>
  <button @click="dice.sort(compareFunc)">ソート</button>
  <button @click="dice.sort(shuffleFunc)">シャッフル</button><br>
  <TransitionGroup>
    <div 
    v-for="x in dice" 
    :key="x.keyNmbr" 
    class="diceDiv" 
    :style="{ backgroundColor: 'hsl('+x.dieNmbr*60+',85%,85%)' }"
    @click="removeDie(x.keyNmbr)">
      {{ x.dieNmbr }}
    </div>
  </TransitionGroup>
</template>

<script>
  export default {
    data() {
      return {
        dice: [],
        keyNumber: 0
      }
    },
    methods: {
      addDie() {
        const newDie = {
          dieNmbr: Math.ceil(Math.random()*6),
          keyNmbr: this.keyNumber
        };
        // ランダムな位置に挿入
        this.dice.splice(Math.floor(Math.random()*this.dice.length), 0, newDie);
        this.keyNumber++;
      },
      addDie10() {
        for(let i=0; i<10; i++) {
          this.addDie();
        }
      },
      compareFunc(a, b){
        return a.dieNmbr - b.dieNmbr;
      },
      shuffleFunc(){
        return Math.random() - 0.5;
      },
      removeDie(key) {
        const pos = this.dice.map(e => e.keyNmbr).indexOf(key);
        this.dice.splice(pos, 1);
      }
    },
    mounted() {
      this.addDie10();
    }
  }
</script>

<style>
.v-enter-from {
  opacity: 0;
  scale: 0;
  rotate: 360deg;
}
.v-enter-to {
  opacity: 1;
  scale: 1;
  rotate: 0deg;
}
.v-enter-active,
.v-leave-active,
.v-move {
  transition: all 0.7s;
}
.v-leave-active { position: absolute; }
.v-leave-from { opacity: 1; }
.v-leave-to { opacity: 0; }

.diceDiv {
  margin: 10px;
  width: 30px;
  height: 30px;
  line-height: 30px;
  vertical-align: middle;
  text-align: center;
  border: solid black 1px;
  border-radius: 5px;
  display: inline-block;
}
.diceDiv:hover {
  cursor: pointer;
  box-shadow: 0 4px 8px 0 rgba(0, 0, 0, 0.2), 0 6px 20px 0 rgba(0, 0, 0, 0.19);
}
#app {
  position: relative;
}
</style>