Teleport
<Teleport>
は、コンポーネントにあるテンプレートの一部を、そのコンポーネントの DOM 階層の外側に存在する DOM ノードに「テレポート」できる組み込みコンポーネントです。
基本的な使い方
ときどき、次のようなシナリオに遭遇することがあります: 論理的にはコンポーネントのテンプレートの一部は、コンポーネントに属していますが、視覚的な観点からすると、Vue アプリケーション外の DOM のどこかに表示されるべきものです。
もっとも一般的な例は、フルスクリーンのモーダルを構築するときです。理想的には、モーダルのボタンとモーダル自体を同じコンポーネントに収めたいものです。なぜなら、これらは両方ともモーダルの開閉状態に関連しているからです。しかし、これではモーダルがボタンと一緒にレンダリングされ、アプリケーションの DOM 階層に深くネストされることになります。これにより CSS でモーダルを配置する際に、いくつかの厄介な問題を引き起こす可能性があります。
次のような HTML 構造を考えてみましょう。
template
<div class="outer">
<h3>Vue Teleport Example</h3>
<div>
<MyModal />
</div>
</div>
そして、以下が <MyModal>
の実装です:
vue
<script setup>
import { ref } from 'vue'
const open = ref(false)
</script>
<template>
<button @click="open = true">Open Modal</button>
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</template>
<style scoped>
.modal {
position: fixed;
z-index: 999;
top: 20%;
left: 50%;
width: 300px;
margin-left: -150px;
}
</style>
このコンポーネントには、モーダルを開くためのトリガーとなる <button>
と、モーダルのコンテンツとセルフクローズするためのボタンを含む .modal
クラスの <div>
が含まれています。
このコンポーネントを初期の HTML 構造の中で使う場合、いくつかの問題が生じる可能性があります:
position: fixed
は、祖先の要素にtransform
、perspective
、filter
プロパティが設定されていない場合、ビューポートに対して相対的に要素を配置するだけです。例えば、祖先である<div class="outer">
を CSS transform でアニメーションさせようとすると、モーダルレイアウトが崩れてしまいます!モーダルの
z-index
は、それを含む要素によって制約されます。もし<div class="outer">
と重なった、より高いz-index
の値が設定された別の要素があれば、モーダルコンポーネントを覆ってしまうかもしれません。
<Teleport>
は、ネストされた DOM 構造から抜け出せるようにすることで、これらの問題を回避するクリーンな方法を提供します。それでは、<MyModal>
を修正して、 <Teleport>
を使用するようにしてみましょう:
template
<button @click="open = true">Open Modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
<p>Hello from the modal!</p>
<button @click="open = false">Close</button>
</div>
</Teleport>
<Teleport>
の to
ターゲットには、CSS セレクター文字列か、存在する DOM ノードが必要です。ここでは、Vue に「このテンプレートフラグメントを テレポート して、 body
タグに転送する」ように指示しています。
下のボタンをクリックして、ブラウザーの devtools で <body>
タグを検査できます:
<Teleport>
と <Transition>
を組み合わせると、アニメーションするモーダルを作ることができます - サンプルはこちら を参照してください。
TIP
テレポートの to
ターゲットは、 <Teleport>
コンポーネントがマウントされたときに、すでに DOM に存在している必要があります。理想的には、Vue アプリケーション全体の外側にある要素であるべきです。Vue でレンダリングされる別の要素をターゲットにする場合は、その要素が <Teleport>
の前にマウントされていることを確認する必要があります。
コンポーネントでの使用
<Teleport>
はレンダリングされた DOM 構造を変更するだけで、コンポーネントの論理的な階層構造には影響を与えません。つまり、<Teleport>
があるコンポーネントを含む場合、そのコンポーネントは <Teleport>
を含む親コンポーネントの論理的な子要素であることに変わりはありません。props の受け渡しやイベントの発行は、これまでと同じように動作します。
また、親コンポーネントからの注入は期待通りに動作し、子コンポーネントは実際のコンテンツが移動した場所に配置されるのではなく、Vue Devtools で親コンポーネントの下にネストされることになります。
Teleport を無効化する
場合によっては、条件付きで <Teleport>
を無効にしたいことがあります。例えば、デスクトップではオーバーレイとしてコンポーネントをレンダリングし、モバイルではインラインでレンダリングしたい場合があります。この場合、<Teleport>
は disabled
props をサポートし、動的にトグルできます:
template
<Teleport :disabled="isMobile">
...
</Teleport>
メディアクエリーの変更を検知して isMobile
の状態によって動的に更新できます。
同じターゲットに複数の Teleport
慣用的な例として、再利用可能な <Modal>
コンポーネントにおいて複数のインスタンスが同時にアクティブになる可能性があります。このようなシナリオの場合、複数の <Teleport>
コンポーネントが同じターゲット要素にコンテンツをマウントできます。順番は単純な追加で、ターゲット要素内で先にマウントされたものの後に配置されます。
次のような使い方があるとします:
template
<Teleport to="#modals">
<div>A</div>
</Teleport>
<Teleport to="#modals">
<div>B</div>
</Teleport>
レンダリング結果はこうなります:
html
<div id="modals">
<div>A</div>
<div>B</div>
</div>
関連