|
2 | 2 |
|
3 | 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.
|
4 | 4 |
|
5 |
| -However, sometimes a part of a component's template belongs into 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 Vue app. For example, due to styling requirements, we want to move `<p id="content">` from it's deeply nested position to the `<div>` with `id="endofbody"` |
| 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. |
| 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. |
| 8 | + |
| 9 | +Consider the following HTML structure. |
6 | 10 |
|
7 | 11 | ```html
|
8 | 12 | <body>
|
9 |
| - <div id="app" class="demo"> |
10 |
| - <h3>Move the #content with the portal component</h3> |
| 13 | + <div style="position: relative;"> |
| 14 | + <h3>Tooltips with Vue 3 Teleport</h3> |
11 | 15 | <div>
|
12 |
| - <p id="content"> |
13 |
| - This should be moved to #endofbody. |
14 |
| - </p> |
15 |
| - <span>This content should be nested</span> |
| 16 | + <modal-button></modal-button> |
16 | 17 | </div>
|
17 | 18 | </div>
|
18 |
| - <div id="endofbody"></div> |
19 | 19 | </body>
|
20 | 20 | ```
|
21 | 21 |
|
22 |
| -To do so, we can use Vue's built-in `<teleport>` component: |
| 22 | +Let's take a look at `modal-button`. |
23 | 23 |
|
24 |
| -```html |
25 |
| -<body> |
26 |
| - <div id="app" class="demo"> |
27 |
| - <h3>Move the #content with the portal component</h3> |
28 |
| - <div> |
29 |
| - <teleport to="#endofbody"> |
30 |
| - <p id="content"> |
31 |
| - This should be moved to #endofbody. |
32 |
| - </p> |
33 |
| - </teleport> |
34 |
| - <span>This content should be nested</span> |
| 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. |
| 25 | + |
| 26 | +```js |
| 27 | +const app = Vue.createApp({}); |
| 28 | + |
| 29 | +app.component('modal-button', { |
| 30 | + template: ` |
| 31 | + <button @click="modalOpen = true"> |
| 32 | + Open full screen modal! |
| 33 | + </button> |
| 34 | +
|
| 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> |
35 | 42 | </div>
|
36 |
| - </div> |
37 |
| - <div id="endofbody"></div> |
38 |
| -</body> |
| 43 | + `, |
| 44 | + data() { |
| 45 | + return { |
| 46 | + modalOpen: false |
| 47 | + } |
| 48 | + } |
| 49 | +}) |
39 | 50 | ```
|
40 | 51 |
|
41 |
| -As a result, we will have `teleport` content moved in the rendered DOM tree: |
| 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". |
42 | 57 |
|
43 |
| -<p class="codepen" data-height="300" data-theme-id="39028" data-default-tab="html,result" data-user="Vue" data-slug-hash="WNrXYXd" data-preview="true" data-editable="true" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Teleport"> |
44 |
| - <span>See the Pen <a href="https://codepen.io/team/Vue/pen/WNrXYXd"> |
45 |
| - Teleport</a> by Vue (<a href="https://codepen.io/Vue">@Vue</a>) |
| 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 | + `, |
| 77 | + data() { |
| 78 | + return { |
| 79 | + modalOpen: false |
| 80 | + } |
| 81 | + } |
| 82 | +}) |
| 83 | +``` |
| 84 | + |
| 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 | +<p class="codepen" data-height="300" data-theme-id="39028" data-default-tab="js,result" data-user="Vue" data-slug-hash="gOPNvjR" data-preview="true" data-editable="true" style="height: 300px; box-sizing: border-box; display: flex; align-items: center; justify-content: center; border: 2px solid; margin: 1em 0; padding: 1em;" data-pen-title="Vue 3 Teleport"> |
| 88 | + <span>See the Pen <a href="https://codepen.io/team/Vue/pen/gOPNvjR"> |
| 89 | + Vue 3 Teleport</a> by Vue (<a href="https://codepen.io/Vue">@Vue</a>) |
46 | 90 | on <a href="https://codepen.io">CodePen</a>.</span>
|
47 | 91 | </p>
|
48 | 92 | <script async src="https://static.codepen.io/assets/embed/ei.js"></script>
|
49 | 93 |
|
50 |
| -As you can see, all of the children of `<teleport>` will be appended to `<div id="endofbody">`. |
51 |
| - |
52 | 94 | ## Using with Vue components
|
53 | 95 |
|
54 | 96 | If `<teleport>` contains a Vue component, it will remain a logical child component of the `<teleport>`'s parent:
|
|
0 commit comments