# リストのトランジション
ここまでで、私たちが管理しているトランジションは以下です:
- 個別のノード
- 一度だけレンダリングされる複数のノード
その上で、例えば v-for
のように、全てをまとめてレンダリングしたいリスト全体がある場合はどうしましょうか?このような場合では、 <transition-group>
コンポーネントを利用します。ですが具体的な例を見る前に、コンポーネントについていくつかの重要なことを知っておく必要があります:
- デフォルトでは、ラッパー要素はレンダリングされませんが、
tag
属性でレンダリングする要素を指定することができます。 - 2つの排他の要素が切り替わっているわけではないため、トランジションモード は利用できません。
- 要素の内部では、 常に 固有の
key
属性を 持つ必要 があります。 - CSS トランジションクラスは内部の要素に適用され、グループやコンテナには適用されません。
# リストの Enter/Leave トランジション
早速例に飛び込んでみましょう。以前利用したものと同じ CSS クラスを利用して、 enter/leave を行います:
<div id="list-demo">
<button @click="add">Add</button>
<button @click="remove">Remove</button>
<transition-group name="list" tag="p">
<span v-for="item in items" :key="item" class="list-item">
{{ item }}
</span>
</transition-group>
</div>
2
3
4
5
6
7
8
9
const Demo = {
data() {
return {
items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
nextNum: 10
}
},
methods: {
randomIndex() {
return Math.floor(Math.random() * this.items.length)
},
add() {
this.items.splice(this.randomIndex(), 0, this.nextNum++)
},
remove() {
this.items.splice(this.randomIndex(), 1)
}
}
}
Vue.createApp(Demo).mount('#list-demo')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.list-item {
display: inline-block;
margin-right: 10px;
}
.list-enter-active,
.list-leave-active {
transition: all 1s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateY(30px);
}
2
3
4
5
6
7
8
9
10
11
12
13
この例には一つの問題があります。item を追加や削除を行うと、その item の周りのものがスムーズに遷移せず、すぐに移動してしまいます。これは後で修正します。
# List Move Transitions
<transition-group>
コンポーネントはもう一つの機能を持っています。それは、 enter/leave にだけでなく、位置の変更もアニメーションできることです。この機能を利用するための概念として、要素が位置を変更するときに v-move
クラス が追加されることを知る必要があります。これはその他のクラスと同様、接頭辞はトランジションの name
属性と一致しているほか、 move-class
属性で手動でクラスを指定できます。
このクラスは、後述のようにトランジションのタイミングや easing カーブを設定する際に便利です。
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.17.15/lodash.min.js"></script>
<div id="flip-list-demo">
<button @click="shuffle">Shuffle</button>
<transition-group name="flip-list" tag="ul">
<li v-for="item in items" :key="item">
{{ item }}
</li>
</transition-group>
</div>
2
3
4
5
6
7
8
9
10
const Demo = {
data() {
return {
items: [1, 2, 3, 4, 5, 6, 7, 8, 9]
}
},
methods: {
shuffle() {
this.items = _.shuffle(this.items)
}
}
}
Vue.createApp(Demo).mount('#flip-list-demo')
2
3
4
5
6
7
8
9
10
11
12
13
14
.flip-list-move {
transition: transform 0.8s ease;
}
2
3
これは魔法のように見えますが、Vue は FLIP (opens new window) と呼ばれるアニメーションのテクニックによって、transform を利用した要素の位置をスムーズに遷移させています。
このテクニックと前の実装を組み合わせることで、リストへの変更をアニメーションさせることができます!
<script src="https://cdnjs.cloudflare.com/ajax/libs/lodash.js/4.14.1/lodash.min.js"></script>
<div id="list-complete-demo" class="demo">
<button @click="shuffle">Shuffle</button>
<button @click="add">Add</button>
<button @click="remove">Remove</button>
<transition-group name="list-complete" tag="p">
<span v-for="item in items" :key="item" class="list-complete-item">
{{ item }}
</span>
</transition-group>
</div>
2
3
4
5
6
7
8
9
10
11
12
const Demo = {
data() {
return {
items: [1, 2, 3, 4, 5, 6, 7, 8, 9],
nextNum: 10
}
},
methods: {
randomIndex() {
return Math.floor(Math.random() * this.items.length)
},
add() {
this.items.splice(this.randomIndex(), 0, this.nextNum++)
},
remove() {
this.items.splice(this.randomIndex(), 1)
},
shuffle() {
this.items = _.shuffle(this.items)
}
}
}
Vue.createApp(Demo).mount('#list-complete-demo')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.list-complete-item {
transition: all 0.8s ease;
display: inline-block;
margin-right: 10px;
}
.list-complete-enter-from,
.list-complete-leave-to {
opacity: 0;
transform: translateY(30px);
}
.list-complete-leave-active {
position: absolute;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
TIP
重大な注意点として、これらの FLIP トランジションは、 display: inline
が設定された要素では動作しません。代わりに、 display: inline-block
を利用するか、または flex コンテキスト内に要素を配置することとなります。
これらの FLIP アニメーションは、一つの軸の動作に限定されません。多次元のグリッドに対しても、同様に (opens new window)操作できます:
TODO: example
# 時間差でのリストトランジション
data 属性によって JavaScript によるトランジションと通信することで、時間差でのリストトランジションが可能となります:
<script src="https://cdnjs.cloudflare.com/ajax/libs/gsap/3.3.4/gsap.min.js"></script>
<div id="demo">
<input v-model="query" />
<transition-group
name="staggered-fade"
tag="ul"
:css="false"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<li
v-for="(item, index) in computedList"
:key="item.msg"
:data-index="index"
>
{{ item.msg }}
</li>
</transition-group>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const Demo = {
data() {
return {
query: '',
list: [
{ msg: 'Bruce Lee' },
{ msg: 'Jackie Chan' },
{ msg: 'Chuck Norris' },
{ msg: 'Jet Li' },
{ msg: 'Kung Fury' }
]
}
},
computed: {
computedList() {
var vm = this
return this.list.filter(item => {
return item.msg.toLowerCase().indexOf(vm.query.toLowerCase()) !== -1
})
}
},
methods: {
beforeEnter(el) {
el.style.opacity = 0
el.style.height = 0
},
enter(el, done) {
gsap.to(el, {
opacity: 1,
height: '1.6em',
delay: el.dataset.index * 0.15,
onComplete: done
})
},
leave(el, done) {
gsap.to(el, {
opacity: 0,
height: 0,
delay: el.dataset.index * 0.15,
onComplete: done
})
}
}
}
Vue.createApp(Demo).mount('#demo')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
# 再利用可能なトランジション
トランジション群は、 Vue のコンポーネントシステムによって再利用できます。再利用可能なトランジションを作るためには、コンポーネントのルートに <transition>
または <transition-group>
を配置し、その children をトランジションコンポーネントへと渡すだけです。
TODO: refactor to Vue 3
これらは、template コンポーネントを利用した例です:
Vue.component('my-special-transition', {
template: '\
<transition\
name="very-special-transition"\
mode="out-in"\
@before-enter="beforeEnter"\
@after-enter="afterEnter"\
>\
<slot></slot>\
</transition>\
',
methods: {
beforeEnter(el) {
// ...
},
afterEnter(el) {
// ...
}
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
そして関数型コンポーネントは、このようなタスクに時に適しています:
Vue.component('my-special-transition', {
functional: true,
render: function(createElement, context) {
var data = {
props: {
name: 'very-special-transition',
mode: 'out-in'
},
on: {
beforeEnter(el) {
// ...
},
afterEnter(el) {
// ...
}
}
}
return createElement('transition', data, context.children)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 動的なトランジション
そうです!Vue では、トランジションの設定さえもデータ駆動となります。動的なトランジション設定の最も基本的な例は、 name
属性を動的なプロパティに紐付けるものです。
<transition :name="transitionName">
<!-- ... -->
</transition>
2
3
これは Vue のトランジションクラスの規約によって、 CSS トランジション/アニメーションを定義し、切り替えたい場合に便利です。
そしてこれは本当にあらゆるトランジション属性を動的に紐付けることができ、その対象は属性だけではありません。イベントフックはメソッドであるため、コンテキストにおけるあらゆるデータへとアクセスすることができます。それはコンポーネントの状態に依存して、 JavaScript トランジションが異なる挙動する可能性があるということです。
<script src="https://cdnjs.cloudflare.com/ajax/libs/velocity/1.2.3/velocity.min.js"></script>
<div id="dynamic-fade-demo" class="demo">
Fade In:
<input type="range" v-model="fadeInDuration" min="0" :max="maxFadeDuration" />
Fade Out:
<input
type="range"
v-model="fadeOutDuration"
min="0"
:max="maxFadeDuration"
/>
<transition
:css="false"
@before-enter="beforeEnter"
@enter="enter"
@leave="leave"
>
<p v-if="show">hello</p>
</transition>
<button v-if="stop" @click="stop = false; show = false">
Start animating
</button>
<button v-else @click="stop = true">Stop it!</button>
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const app = Vue.createApp({
data() {
return {
show: true,
fadeInDuration: 1000,
fadeOutDuration: 1000,
maxFadeDuration: 1500,
stop: true
}
},
mounted() {
this.show = false
},
methods: {
beforeEnter(el) {
el.style.opacity = 0
},
enter(el, done) {
var vm = this
Velocity(
el,
{ opacity: 1 },
{
duration: this.fadeInDuration,
complete: function() {
done()
if (!vm.stop) vm.show = false
}
}
)
},
leave(el, done) {
var vm = this
Velocity(
el,
{ opacity: 0 },
{
duration: this.fadeOutDuration,
complete: function() {
done()
vm.show = true
}
}
)
}
}
})
app.mount('#dynamic-fade-demo')
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
TODO: example
最後に、究極的には、プロップを受け付けるコンポーネントを利用し、性質を変化させる、動的なトランジションを実現することができます。安っぽい表現に聞こえるかもしれませんが、限界はあなたの想像力次第です。