|
1 | 1 | # Teleport
|
2 | 2 |
|
3 |
| -Vue encourages us to build our UIs by encapsulating UI and related behavior into components. We can nest them inside one another to build a tree that makes up an application UI. |
| 3 | +`<Teleport>` is a built-in component that allows us to "teleport" a part of a component's template into a DOM node that exists outside the DOM hierarchy of that component. |
4 | 4 |
|
5 |
| -However, sometimes a part of a component's template belongs to this component logically, while from a technical point of view, it would be preferable to move this part of the template somewhere else in the DOM, outside of the Vue app. |
| 5 | +## Basic Usage |
6 | 6 |
|
7 |
| -A common scenario for this is creating a component that includes a full-screen modal. In most cases, you'd want the modal's logic to live within the component, but the positioning of the modal quickly becomes difficult to solve through CSS, or requires a change in component composition. |
| 7 | +Sometimes we may run into the following scenario: a part of a component's template belongs to it logically, but from a visual standpoint, it should be displayed somewhere else in the DOM, outside of the Vue application. |
| 8 | + |
| 9 | +The most common example of this is when building a full-screen modal. Ideally, we want the modal's button and the modal itself to live within the same component, since they are both related to the open / close state of the modal. But that means the modal will be rendered alongside the button, deeply nested in the application's DOM hierarchy. This can create some tricky issues when positioning the modal via CSS. |
8 | 10 |
|
9 | 11 | Consider the following HTML structure.
|
10 | 12 |
|
11 | 13 | ```vue-html
|
12 |
| -<body> |
13 |
| - <div style="position: relative;"> |
14 |
| - <h3>Tooltips with Vue 3 Teleport</h3> |
15 |
| - <div> |
16 |
| - <modal-button></modal-button> |
17 |
| - </div> |
| 14 | +<div class="outer"> |
| 15 | + <h3>Vue Teleport Example</h3> |
| 16 | + <div> |
| 17 | + <MyModal /> |
18 | 18 | </div>
|
19 |
| -</body> |
| 19 | +</div> |
20 | 20 | ```
|
21 | 21 |
|
22 |
| -Let's take a look at `modal-button`. |
| 22 | +And here is the implementation of `<MyModal>`: |
23 | 23 |
|
24 |
| -The component will have a `button` element to trigger the opening of the modal, and a `div` element with a class of `.modal`, which will contain the modal's content and a button to self-close. |
| 24 | +<div class="composition-api"> |
25 | 25 |
|
26 |
| -```js |
27 |
| -const app = Vue.createApp({}); |
| 26 | +```vue |
| 27 | +<script setup> |
| 28 | +import { ref } from 'vue' |
28 | 29 |
|
29 |
| -app.component('modal-button', { |
30 |
| - template: ` |
31 |
| - <button @click="modalOpen = true"> |
32 |
| - Open full screen modal! |
33 |
| - </button> |
| 30 | +const open = ref(false) |
| 31 | +</script> |
34 | 32 |
|
35 |
| - <div v-if="modalOpen" class="modal"> |
36 |
| - <div> |
37 |
| - I'm a modal! |
38 |
| - <button @click="modalOpen = false"> |
39 |
| - Close |
40 |
| - </button> |
41 |
| - </div> |
42 |
| - </div> |
43 |
| - `, |
44 |
| - data() { |
45 |
| - return { |
46 |
| - modalOpen: false |
47 |
| - } |
48 |
| - } |
49 |
| -}) |
| 33 | +<template> |
| 34 | + <button @click="open = true">Open Modal</button> |
| 35 | +
|
| 36 | + <div v-if="open" class="modal"> |
| 37 | + <p>Hello from the modal!</p> |
| 38 | + <button @click="open = false">Close</button> |
| 39 | + </div> |
| 40 | +</template> |
| 41 | +
|
| 42 | +<style scoped> |
| 43 | +.modal { |
| 44 | + position: fixed; |
| 45 | + z-index: 999; |
| 46 | + top: 20%; |
| 47 | + left: 50%; |
| 48 | + width: 300px; |
| 49 | + margin-left: -150px; |
| 50 | +} |
| 51 | +</style> |
50 | 52 | ```
|
51 | 53 |
|
52 |
| -When using this component inside the initial HTML structure, we can see a problem - the modal is being rendered inside the deeply nested `div` and the `position: absolute` of the modal takes the parent relatively positioned `div` as reference. |
53 |
| - |
54 |
| -Teleport provides a clean way to allow us to control under which parent in our DOM we want a piece of HTML to be rendered, without having to resort to global state or splitting this into two components. |
55 |
| - |
56 |
| -Let's modify our `modal-button` to use `<teleport>` and tell Vue "**teleport** this HTML **to** the "**body**" tag". |
57 |
| - |
58 |
| -```js |
59 |
| -app.component('modal-button', { |
60 |
| - template: ` |
61 |
| - <button @click="modalOpen = true"> |
62 |
| - Open full screen modal! (With teleport!) |
63 |
| - </button> |
64 |
| -
|
65 |
| - <teleport to="body"> |
66 |
| - <div v-if="modalOpen" class="modal"> |
67 |
| - <div> |
68 |
| - I'm a teleported modal! |
69 |
| - (My parent is "body") |
70 |
| - <button @click="modalOpen = false"> |
71 |
| - Close |
72 |
| - </button> |
73 |
| - </div> |
74 |
| - </div> |
75 |
| - </teleport> |
76 |
| - `, |
| 54 | +</div> |
| 55 | +<div class="options-api"> |
| 56 | + |
| 57 | +```vue |
| 58 | +<script> |
| 59 | +export default { |
77 | 60 | data() {
|
78 | 61 | return {
|
79 |
| - modalOpen: false |
| 62 | + open: false |
80 | 63 | }
|
81 | 64 | }
|
82 |
| -}) |
| 65 | +} |
| 66 | +</script> |
| 67 | +
|
| 68 | +<template> |
| 69 | + <button @click="open = true">Open Modal</button> |
| 70 | +
|
| 71 | + <div v-if="open" class="modal"> |
| 72 | + <p>Hello from the modal!</p> |
| 73 | + <button @click="open = false">Close</button> |
| 74 | + </div> |
| 75 | +</template> |
| 76 | +
|
| 77 | +<style scoped> |
| 78 | +.modal { |
| 79 | + position: fixed; |
| 80 | + z-index: 999; |
| 81 | + top: 20%; |
| 82 | + left: 50%; |
| 83 | + width: 300px; |
| 84 | + margin-left: -150px; |
| 85 | +} |
| 86 | +</style> |
83 | 87 | ```
|
84 | 88 |
|
85 |
| -As a result, once we click the button to open the modal, Vue will correctly render the modal's content as a child of the `body` tag. |
86 |
| - |
87 |
| -<!-- <common-codepen-snippet title="Vue 3 Teleport" slug="gOPNvjR" tab="js,result" /> --> |
88 |
| - |
89 |
| -## Using with Vue components |
90 |
| - |
91 |
| -If `<teleport>` contains a Vue component, it will remain a logical child component of the `<teleport>`'s parent: |
92 |
| - |
93 |
| -```js |
94 |
| -const app = Vue.createApp({ |
95 |
| - template: ` |
96 |
| - <h1>Root instance</h1> |
97 |
| - <parent-component /> |
98 |
| - ` |
99 |
| -}) |
100 |
| - |
101 |
| -app.component('parent-component', { |
102 |
| - template: ` |
103 |
| - <h2>This is a parent component</h2> |
104 |
| - <teleport to="#endofbody"> |
105 |
| - <child-component name="John" /> |
106 |
| - </teleport> |
107 |
| - ` |
108 |
| -}) |
109 |
| - |
110 |
| -app.component('child-component', { |
111 |
| - props: ['name'], |
112 |
| - template: ` |
113 |
| - <div>Hello, {{ name }}</div> |
114 |
| - ` |
115 |
| -}) |
| 89 | +</div> |
| 90 | + |
| 91 | +The component contains a `<button>` to trigger the opening of the modal, and a `<div>` with a class of `.modal`, which will contain the modal's content and a button to self-close. |
| 92 | + |
| 93 | +When using this component inside the initial HTML structure, there are a number of potential issues: |
| 94 | + |
| 95 | +- `position: fixed` only places the element relative to the viewport when no ancestor element has `transform`, `perspective` or `filter` property set. If, for example, we intend to animate the ancestor `<div class="outer">` with a CSS transform, it would break the modal layout! |
| 96 | + |
| 97 | +- The modal's `z-index` is constrained by its containing elements. If there is another element that overlaps with `<div class="outer">` and has a higher `z-index`, it would cover our modal. |
| 98 | + |
| 99 | +`<Teleport>` provides a clean way to work around these, by allowing us to break out of the nested DOM structure. Let's modify `<MyModal>` to use `<Teleport>`: |
| 100 | + |
| 101 | +```vue-html{3,8} |
| 102 | +<button @click="open = true">Open Modal</button> |
| 103 | +
|
| 104 | +<Teleport to="body"> |
| 105 | + <div v-if="open" class="modal"> |
| 106 | + <p>Hello from the modal!</p> |
| 107 | + <button @click="open = false">Close</button> |
| 108 | + </div> |
| 109 | +</Teleport> |
116 | 110 | ```
|
117 | 111 |
|
118 |
| -In this case, even when `child-component` is rendered in the different place, it will remain a child of `parent-component` and will receive a `name` prop from it. |
| 112 | +The `to` target of `<Teleport>` expects a CSS selector string or an actual DOM node. Here, we are essentially telling Vue to "**teleport** this template fragment **to** the **`body`** tag". |
| 113 | + |
| 114 | +You can click the button below and inspect the `<body>` tag via browser devtools: |
| 115 | + |
| 116 | +<script setup> |
| 117 | +let open = $ref(false) |
| 118 | +</script> |
| 119 | + |
| 120 | +<div class="demo"> |
| 121 | + <button @click="open = true">Open Modal</button> |
| 122 | + <Teleport to="body"> |
| 123 | + <div v-if="open" class="demo modal-demo"> |
| 124 | + <p style="margin-bottom:20px">Hello from the modal!</p> |
| 125 | + <button @click="open = false">Close</button> |
| 126 | + </div> |
| 127 | + </Teleport> |
| 128 | +</div> |
| 129 | + |
| 130 | +<style> |
| 131 | +.modal-demo { |
| 132 | + position: fixed; |
| 133 | + z-index: 999; |
| 134 | + top: 20%; |
| 135 | + left: 50%; |
| 136 | + width: 300px; |
| 137 | + margin-left: -150px; |
| 138 | + background-color: var(--vt-c-bg); |
| 139 | + padding: 30px; |
| 140 | + border-radius: 8px; |
| 141 | + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15); |
| 142 | +} |
| 143 | +</style> |
| 144 | + |
| 145 | +You can combine `<Teleport>` with [`<Transition>`](./transition) to create animated modals - see [Example here](/examples/#modal). |
| 146 | + |
| 147 | +:::tip |
| 148 | +The teleport `to` target must be already in the DOM when the `<Teleport>` component is mounted. Ideally, this should be an element outside the entire Vue application. If targeting another element rendered by Vue, you need to make sure that element is mounted before the `<Teleport>`. |
| 149 | +::: |
| 150 | + |
| 151 | +## Using with Components |
| 152 | + |
| 153 | +`<Teleport>` only alters the rendered DOM structure - it does not affect the logical hierarchy of the components. That is to say, if `<Teleport>` contains a component, that component will remain a logical child of the parent component containing the `<Teleport>`. Props passing and event emitting will continue to work the same way. |
119 | 154 |
|
120 | 155 | This also means that injections from a parent component work as expected, and that the child component will be nested below the parent component in the Vue Devtools, instead of being placed where the actual content moved to.
|
121 | 156 |
|
122 |
| -## Using multiple teleports on the same target |
| 157 | +## Disabling Teleport |
123 | 158 |
|
124 |
| -A common use case scenario would be a reusable `<Modal>` component of which there might be multiple instances active at the same time. For this kind of scenario, multiple `<teleport>` components can mount their content to the same target element. The order will be a simple append - later mounts will be located after earlier ones within the target element. |
| 159 | +In some cases, we may want to conditionally disable `<Teleport>`. For example, we may want to render a component as an overlay for desktop, but inline on mobile. `<Teleport>` supports the `disabled` prop which can be dynamically toggled: |
125 | 160 |
|
126 | 161 | ```vue-html
|
127 |
| -<teleport to="#modals"> |
| 162 | +<Teleport :disabled="isMobile"> |
| 163 | + ... |
| 164 | +</Teleport> |
| 165 | +``` |
| 166 | + |
| 167 | +Where the `isMobile` state can be dynamically updated by detecting media query changes. |
| 168 | + |
| 169 | +## Multiple Teleports on the Same Target |
| 170 | + |
| 171 | +A common use case scenario would be a reusable `<Modal>` component of which there might be multiple instances active at the same time. For this kind of scenario, multiple `<Teleport>` components can mount their content to the same target element. The order will be a simple append - later mounts will be located after earlier ones within the target element. |
| 172 | + |
| 173 | +Given the following usage: |
| 174 | + |
| 175 | +```vue-html |
| 176 | +<Teleport to="#modals"> |
128 | 177 | <div>A</div>
|
129 |
| -</teleport> |
130 |
| -<teleport to="#modals"> |
| 178 | +</Teleport> |
| 179 | +<Teleport to="#modals"> |
131 | 180 | <div>B</div>
|
132 |
| -</teleport> |
| 181 | +</Teleport> |
| 182 | +``` |
| 183 | + |
| 184 | +The rendered result would be: |
133 | 185 |
|
134 |
| -<!-- result--> |
| 186 | +```html |
135 | 187 | <div id="modals">
|
136 | 188 | <div>A</div>
|
137 | 189 | <div>B</div>
|
138 | 190 | </div>
|
139 | 191 | ```
|
140 | 192 |
|
141 |
| -You can check `<teleport>` component options in the [API reference](/api/built-in-components.html#teleport). |
| 193 | +--- |
| 194 | + |
| 195 | +**Related** |
| 196 | + |
| 197 | +- [`<Teleport>` API reference](/api/built-in-components.html#teleport) |
0 commit comments