Vue-Router と Transition 離脱時にアニメーションさせたい

Vue-Router でのページ遷移(実際はコンポーネント切り替え)の時に Transition を利用したアニメーションを行いたい時があります。

一般的な Transition とは動作の差異があったので、まとめておき対応方法を記録しておきます。

最終的には、以下のようになります

目次

参考

Transition 動作の確認

おそらく一番シンプルな使いかた

Item.vue
1
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
<template>
<div>
<button @click="show = !show">ボタン</button>
<transition>
<h1 v-if="show">Item</h1>
</transition>
</div>
</template>

<script>
export default {
data: function () {
return {
show: false,
};
},
};
</script>

<style lang="scss" scoped>
/* enter-active -> enter-to の順番で書くのが大事*/
.v-enter-active {
transition: opacity 3s;
opacity: 0;
}
.v-enter-to {
opacity: 1;
}

/* leave-active -> leave-to の順番で書くのが大事*/
.v-leave-active {
transition: opacity 3s;
opacity: 1;
}
.v-leave-to {
opacity: 0;
}
</style>

こちらを動作させると、以下のように動作します。

transition にnameを付与すると、transition の class の名称がvの部分を変更できる。
変更したものが以下の通りです。

Item.vue
1
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
<template>
<div>
<button @click="show = !show">ボタン</button>
<transition name="fade">
<h1 v-if="show">Item</h1>
</transition>
</div>
</template>

<script>
export default {
data: function () {
return {
show: false,
};
},
};
</script>

<style lang="scss" scoped>
/* transition に与えたnameに基づいて v -> fade */
.fade-enter-active {
transition: opacity 3s;
opacity: 0;
}
.fade-enter-to {
opacity: 1;
}

.fade-leave-active {
transition: opacity 3s;
opacity: 1;
}
.fade-leave-to {
opacity: 0;
}
</style>

複数要素の出し入れをアニメーションさせる

ここでは単独の要素の出し入れではなく、複数の要素の出し入れを試してみます。

Item.vue
1
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
<template>
<div>
<button @click="show = !show">ボタン</button>
<transition name="fade">
<!-- show の値に基づいて表示を出し入れする-->
<h1 v-if="show">Item</h1>
<h1 v-else>NoItem</h1>
</transition>
</div>
</template>

<script>
export default {
data: function () {
return {
show: false,
};
},
};
</script>

<style lang="scss" scoped>
.fade-enter-active {
transition: opacity 3s;
opacity: 0;
}
.fade-enter-to {
opacity: 1;
}

.fade-leave-active {
transition: opacity 3s;
opacity: 1;
}
.fade-leave-to {
opacity: 0;
}
</style>

こちらを動作させると次のようになります。

ちがう ちがう、そうじゃない。

新しいアイテムの描画がされてから削除されてしまいます。
こういう時には、transition に mode="out-in"を付与します。
この mode のデフォルト値は、in-outです。

修正したのが以下の通りです。

Item.vue
1
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
<template>
<div>
<button @click="show = !show">ボタン</button>
<transition name="fade" mode="out-in">
<h1 v-if="show">Item</h1>
<h1 v-else>NoItem</h1>
</transition>
</div>
</template>

<script>
export default {
data: function () {
return {
show: false,
};
},
};
</script>

<style lang="scss" scoped>
.fade-enter-active {
transition: opacity 3s;
opacity: 0;
}
.fade-enter-to {
opacity: 1;
}

.fade-leave-active {
transition: opacity 3s;
opacity: 1;
}
.fade-leave-to {
opacity: 0;
}
</style>

修正できたので、以下のように削除が先に行われ、追加が後から行われるようになりました。

Vue-Router + Transition の動作確認

続いて Vue-Router と Transition を組み合わせてみます。
Vue-Router と Transition の組み合わせについては、Vue Router - トランジションに解説があります。

シンプルには、次のようにすればいいです。

1
2
3
<transition>
<router-view></router-view>
</transition>

具体定期に、2 つのItem1.vueItem2.vueを router で切り替えるものを実装してみます。

App.vue
1
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
50
51
52
53
54
55
56
57
58
59
<template>
<div>
<div id="nav" class="has-text-info">
<router-link to="/item1">Item1</router-link>|
<router-link to="/item2">Item2</router-link>
</div>
<div>
<transition name="fade">
<router-view />
</transition>
</div>
</div>
</template>
<script>
import { Transition } from "vue";

export default {
name: "App",
data: function () {
return { show: true };
},
};
</script>
<style lang="scss">
.fade-enter-active {
transition: opacity 3s;
opacity: 0;
}
.fade-enter-to {
opacity: 1;
}

.fade-leave-active {
transition: opacity 3s;
opacity: 1;
}
.fade-leave-to {
opacity: 0;
}

#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}

#nav {
padding: 30px;
}

#nav a {
color: #2c3e50;
}

#nav a.router-link-exact-active {
color: #42b983;
}
</style>
views/Item1.vue
1
2
3
4
5
<template>
<div>
<h1>Item1</h1>
</div>
</template>
views/Item2.vue
1
2
3
4
5
<template>
<div>
<h1>Item2</h1>
</div>
</template>

こちらを動作させると、次のようになります。

んっ?、leave-active と leave-to 効いてなくね?

開発ツールでも確認してみました。
やはりリンクを押してすぐ router-view が書き換えされ、leave-active と leave-to は付与されていませんでした。
SPA ではあるもののページ遷移ではあるので、そういう動作も正しいのだと感じます。
調べてもこちらについての対応方法は見つかりませんでした。

Vue-Router で、Leave 系(leave-active, leave-to)を使うためのアイデア

先に確認した通り、Transition要素で囲まれたrouter-viewは、leave-active, leave-to が付与されません。
調べても出てこないので、自分なりに対応してみました。以下の実装を行います。

App.vue
1
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
<template>
<div>
<div id="nav" class="has-text-info">
<router-link to="/item1">Item1</router-link>|
<router-link to="/item2">Item2</router-link>
</div>
<div>
<!-- router-viewに対してはtransitionを設定しない -->
<router-view />
</div>
</div>
</template>
<script>
import { Transition } from "vue";

export default {
name: "App",
data: function () {
return { show: true };
},
};
</script>
<style lang="scss">
#app {
font-family: Avenir, Helvetica, Arial, sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
color: #2c3e50;
}

#nav {
padding: 30px;
}

#nav a {
color: #2c3e50;
}

#nav a.router-link-exact-active {
color: #42b983;
}
</style>
views/Item1.vue
1
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
<template>
<div>
<transition name="fade">
<h1 v-if="show">Item1</h1>
</transition>
</div>
</template>

<script>
export default {
data: function () {
return {
show: false,
};
},
mounted() {
//コンポーネントがマウントされるタイミングでフラグを書き換え->表示アニメーション
this.show = true;
},
beforeRouteLeave(to, from, next) {
//vue-routerでの遷移発生イベント時にフラグを書き換え->削除アニメーション
this.show = false;
setTimeout(() => {
//setTimeoutにより、3秒後にページ遷移を実行
next();
}, 3000);
},
};
</script>
<style lang="scss" scoped>
.fade-enter-active {
transition: opacity 3s;
opacity: 0;
}
.fade-enter-to {
opacity: 1;
}

.fade-leave-active {
transition: opacity 3s;
opacity: 1;
}
.fade-leave-to {
opacity: 0;
}
</style>
views/Item2.vue
1
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
<template>
<div>
<transition name="fade">
<h1 v-if="show">Item2</h1>
</transition>
</div>
</template>

<script>
export default {
data: function () {
return {
show: false,
};
},
mounted() {
//コンポーネントがマウントされるタイミングでフラグを書き換え->表示アニメーション
this.show = true;
},
beforeRouteLeave(to, from, next) {
//vue-routerでの遷移発生イベント時にフラグを書き換え->削除アニメーション
this.show = false;
setTimeout(() => {
//setTimeoutにより、3秒後にページ遷移を実行
next();
}, 3000);
},
};
</script>
<style lang="scss" scoped>
.fade-enter-active {
transition: opacity 3s;
opacity: 0;
}
.fade-enter-to {
opacity: 1;
}

.fade-leave-active {
transition: opacity 3s;
opacity: 1;
}
.fade-leave-to {
opacity: 0;
}
</style>

mounted でコンポーネント全体を transaction をさせるフラグの書き換え、beforeRouteLeave でもフラグを書き換えてアニメーションさせる時間だけ遅延させます。

こちらを動作させると、以下のように動作します。

表示・削除ともに確認のため 3 秒に設定しているので操作性は落ちますが、例えば 1 秒未満の設定であれば操作性と両立できるでしょう。

beforeRouteLeave の話

今回 beforeRouteLeave を使用してフラグの書き換えと、アニメーションする時間の確保をしました。

beforeRouteLeave は、ナビゲーションガードの機能を目的外の利用をしていると解釈しています。

Vue Router - ナビゲーションガード

本来であれば、以下のことなどに使用するようです。

  • ページ遷移の際にログイン済みかチェックして遷移のブロック・遷移先の変更
  • ページから離れるときに「本当に離れますか?」などの確認

router-view以下では、Vue ライフサイクルイベントに加えてナビゲーションガードを使用できます。

確認のために、以下のコンポーネントを実装してみました。

Test.vue
1
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
50
51
52
<template>
<div>
<button @click="show = !show">ボタン</button>
<h1 v-if="show">Test</h1>
</div>
</template>

<script>
export default {
data: function () {
return {
show: false,
};
},
beforeCreate() {
console.log("beforeCreate");
},
Created() {
console.log("Created");
},
beforeMount() {
console.log("beforeMount");
},
mounted() {
console.log("mounted");
},
beforeUpdate() {
console.log("beforeUpdate");
},
updated() {
console.log("updated");
},
beforeUnmount() {
console.log("beforeUnmount");
},
unmounted() {
console.log("unmounted");
},
beforeRouteEnter(to, from, next) {
console.log("beforeRouteEnter");
next();
},
beforeRouteUpdate(to, from, next) {
console.log("beforeRouteUpdate");
next();
},
beforeRouteLeave(to, from, next) {
console.log("beforeRouteLeave");
next();
},
};
</script>

こちらを以下のように動作させました。

  1. Test コンポーネントへの遷移
  2. Test コンポーネント内での更新
  3. Test コンポーネントからの遷移

この中で、beforeRouteEnter と beforeRouteLeave が呼び出されることを確認できました。

beforeRouteUpdateは、同じコンポーネントを使用し/1から/2に変わった時などに呼び出しの対象になるそう。
なので今回の確認では呼び出されませんでした。
タイミングとしては、beforeRouteLeavebeforeRouteUpdateは同じになるそう。


今回は、Vue-Router と Transition の動作確認。ページ遷移時のアニメーション発生の対策を行ってみました。
スタイルの記述の順番が大事になったりということがあるので、気をつけて使っていきたいと感じます。

最後にアニメーションをもっとリッチにしてみる実験をしてみました。

ではでは。