【Vue3】Teleportでtransitionが効かない時の対処法
Vue3から新たに追加されたTeleport
を使ってモーダルウィンドウをbody直下で表示した際に、書き方によってはtransition
が効かないので、備忘録として残します。
Teleport とは
Vue3から追加された、要素を任意の場所で表示させる機能です。
Vue2までは、子コンポーネントからモーダルウィンドウを表示したくても、CSSのposition: relative
の下でposition: absolute
を指定すると、relative
起点の絶対値指定になってしまいます。(VueというよりCSSの仕様)
relative
よりも手前に要素が存在する場合、モーダルウィンドウをページの最前面に出すには、子コンポーネントからグローバルステートで管理しているモーダルの表示切り替えよう変数を操作するなどして対応するなど工夫が必要でした。
そんな悩みを解決すべく登場したのがTeleport
です。
Teleport
は、以下のように任意の場所に表示したい要素を<teleport>
タグで囲み、to
属性にCSSセレクターで場所を指定します。
(<div id="app">
の下に表示したい場合は、<teleport to="#app">
とします。)
<teleport to="body">
<p>任意の場所に表示したい要素</p>
</teleport>
Teleportの詳細は公式ドキュメントをご覧ください。
Teleportするとtransitionが効かない問題
前述した通り、Teleportはモーダルウィンドウと相性が良い都合上、transitionを使ってアニメーションをつける機会も多いと思います。
例えば、モーダルウィンドウの表示/非表示時にフェードイン/アウトをする、といったアニメーションです。
以下のようなモーダルウィンドウコンポーネントがあるとします。(CSSは省略します)
body直下に表示するために、teleport
を利用しています。
<template>
<teleport to="body">
<div class="modal-window">
<div class="modal-content">
<p>モーダルウィンドウの中身</p>
</div>
</div>
</teleport>
</template>
上記コンポーネントを親コンポーネントであるHome.vue
で表示切り替えする場合は次のようになります。
<template>
<div class="home">
<button @click="isModalOpen=!isModalOpen">モーダルウィンドウ表示切り替え</button>
<trainsition name="fade">
<modal-window v-if="isModalOpen"></modal-window>
</transition>
</div>
</template>
<script>
import { defineComponent, ref } from 'vue';
import ModalWindow from "@/path/to/component/ModalWindow.vue"
export default defineComponent({
name: 'Home',
components: {
ModalWindow,
},
setup() {
const isModalOpen = ref(false);
return { isModalOpen };
},
});
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
これでHome.vue
からbody直下のモーダルウィンドウの表示切り替えができます。
ですが、これでは以下のような警告が表示され、<transition name="fade">
が効きません。
inside <Transition> renders non-element root node that cannot be animated.
これは、Home.vue
の<transition name="fade">
で囲んでいる中身が、teleportによってbody直下に移動されたため、要素がなくアニメーションできない、という警告です。
解決方法
上記問題は、<transition name="fade">
を、<teleport to="body">
の中に移動することで解決できます。
また、<transition>
をモーダルウィンドウコンポーネント内に移動するので、モーダルウィンドウを表示するかどうかの変数をpropsで受け取ります。
<template>
<teleport to="body">
<transition name="fade">
<div v-if="isOpen" class="modal-window">
<div class="modal-content">
<p>モーダルウィンドウの中身</p>
</div>
</div>
</transition>
</teleport>
</template>
<script>
import { defineComponent } from 'vue';
export default defineComponent({
name: 'ModalWindow',
props: {
isOpen: {
type: Boolean,
required: true,
},
}
});
</script>
<style scoped>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.3s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* モーダルウィンドウのstyleは省略 */
</style>
<template>
<div class="home">
<button @click="isModalOpen=!isModalOpen">モーダルウィンドウ表示切り替え</button>
<modal-window :isOpen="isModalOpen"></modal-window>
</div>
</template>
<script>
import { defineComponent, ref } from 'vue';
import ModalWindow from "@/path/to/component/ModalWindow.vue"
export default defineComponent({
name: 'Home',
components: {
ModalWindow,
},
setup() {
const isModalOpen = ref(false);
return { isModalOpen };
},
});
</script>
これでteleportを利用してもtransitionによるアニメーションが有効になります。
Discussion