Skip to content

Commit 2ba45f6

Browse files
committed
teleport
1 parent adee035 commit 2ba45f6

File tree

9 files changed

+185
-350
lines changed

9 files changed

+185
-350
lines changed

src/.vitepress/headerMdPlugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export const headerPlugin = (md) => {
2121
return originalOpen.call(null, tokens, i, ...rest)
2222
}
2323

24-
md.renderer.rules.heading_close = (tokens, i, options, env, self) => {
24+
md.renderer.rules.heading_close = (tokens, i, options, _env, self) => {
2525
const headers = md.__data?.headers
2626
if (headers) {
2727
const last = headers[headers.length - 1]
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
<button id="show-modal" @click="showModal = true">Show Modal</button>
22

3-
<teleport to="body">
3+
<Teleport to="body">
44
<!-- use the modal component, pass in the prop -->
55
<modal :show="showModal" @close="showModal = false">
66
<template #header>
77
<h3>custom header</h3>
88
</template>
99
</modal>
10-
</teleport>
10+
</Teleport>

src/examples/src/modal/Modal/template.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
<transition name="modal">
1+
<Transition name="modal">
22
<div v-if="show" class="modal-mask">
33
<div class="modal-wrapper">
44
<div class="modal-container">
@@ -22,4 +22,4 @@
2222
</div>
2323
</div>
2424
</div>
25-
</transition>
25+
</Transition>
32.4 KB
Loading

src/guide/built-ins/images/transitions.svg

Lines changed: 0 additions & 241 deletions
This file was deleted.

src/guide/built-ins/keep-alive.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import SwitchComponent from './keep-alive-demos/SwitchComponent.vue'
44

55
# KeepAlive
66

7+
`<KeepAlive>` is a built-in component that allows us to conditionally cache component instances when dynamically switching between multiple components.
8+
79
## Basic Usage
810

911
In the Component Basics chapter, we introduced the syntax for [Dynamic Components](/guide/essentials/component-basics.html#dynamic-components), using the `<component>` special element:
@@ -80,3 +82,9 @@ We can limit the maximum number of component instances that can be cached via th
8082
<component :is="activeComponent" />
8183
</KeepAlive>
8284
```
85+
86+
---
87+
88+
**Related**
89+
90+
- [`<KeepAlive>` API reference](/api/built-in-components.html#keepalive)

src/guide/built-ins/teleport.md

Lines changed: 157 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -1,141 +1,197 @@
11
# Teleport
22

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.
44

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
66

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.
810

911
Consider the following HTML structure.
1012

1113
```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 />
1818
</div>
19-
</body>
19+
</div>
2020
```
2121

22-
Let's take a look at `modal-button`.
22+
And here is the implementation of `<MyModal>`:
2323

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">
2525

26-
```js
27-
const app = Vue.createApp({});
26+
```vue
27+
<script setup>
28+
import { ref } from 'vue'
2829
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>
3432
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>
5052
```
5153

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 {
7760
data() {
7861
return {
79-
modalOpen: false
62+
open: false
8063
}
8164
}
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>
8387
```
8488

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>
116110
```
117111

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.
119154

120155
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.
121156

122-
## Using multiple teleports on the same target
157+
## Disabling Teleport
123158

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:
125160

126161
```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">
128177
<div>A</div>
129-
</teleport>
130-
<teleport to="#modals">
178+
</Teleport>
179+
<Teleport to="#modals">
131180
<div>B</div>
132-
</teleport>
181+
</Teleport>
182+
```
183+
184+
The rendered result would be:
133185

134-
<!-- result-->
186+
```html
135187
<div id="modals">
136188
<div>A</div>
137189
<div>B</div>
138190
</div>
139191
```
140192

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)

src/guide/built-ins/transition-group.md

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,9 @@ import ListStagger from './transition-demos/ListStagger.vue'
66

77
# TransitionGroup
88

9-
## The `<TransitionGroup>` Component
9+
`<TransitionGroup>` is a built-in component designed for animating the insertion, removal, and order change of elements or components that are rendered in a list.
1010

11-
In the last chapter we covered the `<Transition>` component which is designed for entering / leaving animations where only one element or component is rendered at a time. In comparison, `<TransitionGroup>` is another built-in component which is designed for animating elements or components that are rendered in a list.
11+
## Difference from `<Transition>`
1212

1313
`<TransitionGroup>` supports the same props, CSS transition classes, and JavaScript hook listeners as `<Transition>`, with the following differences:
1414

@@ -127,3 +127,9 @@ function onEnter(el, done) {
127127
[Full Example in the Playground](https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHNjcmlwdD5cbmltcG9ydCBnc2FwIGZyb20gJ2dzYXAnXG5cbmNvbnN0IGxpc3QgPSBbXG4gIHsgbXNnOiAnQnJ1Y2UgTGVlJyB9LFxuICB7IG1zZzogJ0phY2tpZSBDaGFuJyB9LFxuICB7IG1zZzogJ0NodWNrIE5vcnJpcycgfSxcbiAgeyBtc2c6ICdKZXQgTGknIH0sXG4gIHsgbXNnOiAnS3VuZyBGdXJ5JyB9XG5dXG5cbmV4cG9ydCBkZWZhdWx0IHtcbiAgZGF0YSgpIHtcbiAgICByZXR1cm4ge1xuICAgICAgcXVlcnk6ICcnXG4gICAgfVxuICB9LFxuICBjb21wdXRlZDoge1xuICAgIGNvbXB1dGVkTGlzdCgpIHtcbiAgICAgIHJldHVybiBsaXN0LmZpbHRlcigoaXRlbSkgPT4gaXRlbS5tc2cudG9Mb3dlckNhc2UoKS5pbmNsdWRlcyh0aGlzLnF1ZXJ5KSlcbiAgICB9XG4gIH0sXG4gIG1ldGhvZHM6IHtcbiAgICBvbkJlZm9yZUVudGVyKGVsKSB7XG4gICAgICBlbC5zdHlsZS5vcGFjaXR5ID0gMFxuICAgICAgZWwuc3R5bGUuaGVpZ2h0ID0gMFxuICAgIH0sXG4gICAgb25FbnRlcihlbCwgZG9uZSkge1xuICAgICAgZ3NhcC50byhlbCwge1xuICAgICAgICBvcGFjaXR5OiAxLFxuICAgICAgICBoZWlnaHQ6ICcxLjZlbScsXG4gICAgICAgIGRlbGF5OiBlbC5kYXRhc2V0LmluZGV4ICogMC4xNSxcbiAgICAgICAgb25Db21wbGV0ZTogZG9uZVxuICAgICAgfSlcbiAgICB9LFxuICAgIG9uTGVhdmUoZWwsIGRvbmUpIHtcbiAgICAgIGdzYXAudG8oZWwsIHtcbiAgICAgICAgb3BhY2l0eTogMCxcbiAgICAgICAgaGVpZ2h0OiAwLFxuICAgICAgICBkZWxheTogZWwuZGF0YXNldC5pbmRleCAqIDAuMTUsXG4gICAgICAgIG9uQ29tcGxldGU6IGRvbmVcbiAgICAgIH0pXG4gICAgfVxuICB9XG59XG48L3NjcmlwdD5cblxuPHRlbXBsYXRlPlxuICA8aW5wdXQgdi1tb2RlbD1cInF1ZXJ5XCIgLz5cbiAgPFRyYW5zaXRpb25Hcm91cFxuICAgIHRhZz1cInVsXCJcbiAgICA6Y3NzPVwiZmFsc2VcIlxuICAgIEBiZWZvcmUtZW50ZXI9XCJvbkJlZm9yZUVudGVyXCJcbiAgICBAZW50ZXI9XCJvbkVudGVyXCJcbiAgICBAbGVhdmU9XCJvbkxlYXZlXCJcbiAgPlxuICAgIDxsaVxuICAgICAgdi1mb3I9XCIoaXRlbSwgaW5kZXgpIGluIGNvbXB1dGVkTGlzdFwiXG4gICAgICA6a2V5PVwiaXRlbS5tc2dcIlxuICAgICAgOmRhdGEtaW5kZXg9XCJpbmRleFwiXG4gICAgPlxuICAgICAge3sgaXRlbS5tc2cgfX1cbiAgICA8L2xpPlxuICA8L1RyYW5zaXRpb25Hcm91cD5cbjwvdGVtcGxhdGU+XG4iLCJpbXBvcnQtbWFwLmpzb24iOiJ7XG4gIFwiaW1wb3J0c1wiOiB7XG4gICAgXCJnc2FwXCI6IFwiaHR0cHM6Ly91bnBrZy5jb20vZ3NhcD9tb2R1bGVcIixcbiAgICBcInZ1ZVwiOiBcImh0dHBzOi8vc2ZjLnZ1ZWpzLm9yZy92dWUucnVudGltZS5lc20tYnJvd3Nlci5qc1wiXG4gIH1cbn0ifQ==)
128128

129129
</div>
130+
131+
---
132+
133+
**Related**
134+
135+
- [`<TransitionGroup>` API reference](/api/built-in-components.html#transitiongroup)

src/guide/built-ins/transition.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ When an element in a `<Transition>` component is inserted or removed, this is wh
8383

8484
There are six classes applied for enter / leave transitions.
8585

86-
![Transition Diagram](./images/transitions.svg)
86+
![Transition Diagram](./images/transition-classes.png)
8787

8888
1. `v-enter-from`: Starting state for enter. Added before the element is inserted, removed one frame after the element is inserted.
8989

@@ -577,3 +577,9 @@ Here's the previous demo with `mode="out-in"`:
577577
This can be useful when you've defined CSS transitions / animations using Vue's transition class conventions and want to switch between them.
578578

579579
You can also apply different behavior in JavaScript transition hooks based on current state of your component. Finally, the ultimate way of creating dynamic transitions is through [reusable transition components](#reusable-transitions) that accept props to change the nature of the transition(s) to be used. It may sound cheesy, but the only limit really is your imagination.
580+
581+
---
582+
583+
**Related**
584+
585+
- [`<Transition>` API reference](/api/built-in-components.html#transition)

0 commit comments

Comments
 (0)