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>