🔉

【Vue3】Teleportでtransitionが効かない時の対処法

2021/03/26に公開

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
<teleport to="body">
  <p>任意の場所に表示したい要素</p>
</teleport>

Teleportの詳細は公式ドキュメントをご覧ください。

Teleportするとtransitionが効かない問題

前述した通り、Teleportはモーダルウィンドウと相性が良い都合上、transitionを使ってアニメーションをつける機会も多いと思います。
例えば、モーダルウィンドウの表示/非表示時にフェードイン/アウトをする、といったアニメーションです。

以下のようなモーダルウィンドウコンポーネントがあるとします。(CSSは省略します)
body直下に表示するために、teleportを利用しています。

ModalWindow.vue(子コンポーネント)
<template>
  <teleport to="body">
    <div class="modal-window">
      <div class="modal-content">
        <p>モーダルウィンドウの中身</p>
      </div>
    </div>
  </teleport>
</template>

上記コンポーネントを親コンポーネントであるHome.vueで表示切り替えする場合は次のようになります。

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で受け取ります。

ModalWindow.vue(子コンポーネント)
<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>
Home.vue(親コンポーネント)
<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