Skip to content

Commit bd9a5ed

Browse files
LinusBorgnachoddyyx990803
authored
<Teleport> built-in component (#112)
* add portals rfc * update: target prop should accept an HTMLElement * fix: example markup * Fixed typo (#123) Javascript controls essentially *he* whole page ---> Javascript controls essentially *the* whole page * update: - support of multiple sources for a target support for prop "disabled" - rename of component to <teleport> - minor corrections and clarifications * Update active-rfcs/0000-portals.md Co-Authored-By: Evan You <yyx990803@gmail.com> * Update active-rfcs/0000-portals.md Co-Authored-By: Evan You <yyx990803@gmail.com> * Update active-rfcs/0000-portals.md Co-Authored-By: Evan You <yyx990803@gmail.com> * Rename 0000-portals.md to 0025-teleport.md Co-authored-by: Ignacio Durand <nachodurand@gmail.com> Co-authored-by: Evan You <yyx990803@gmail.com>
1 parent e772b64 commit bd9a5ed

File tree

1 file changed

+321
-0
lines changed

1 file changed

+321
-0
lines changed

active-rfcs/0025-teleport.md

Lines changed: 321 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,321 @@
1+
- Start Date: 2020-01-20
2+
- Target Major Version: (3.x)
3+
- Reference Issues: (fill in existing related issues, if any)
4+
- Implementation PR:
5+
6+
# Summary
7+
8+
- Adds a `<teleport>` component to Vue core
9+
- the component requires a target element, provided through a prop which expects an `HTMLElement` or a `querySelector` string.
10+
- the component moves its children to the element identified by the DOM selector
11+
- At the virtual DOM level, the children stay descendants of the `<teleport>` though, so they i.e. have access to injections from its ancestors
12+
13+
# Basic example
14+
15+
```html
16+
<body>
17+
<div id="app">
18+
<h1>Move the #content with the portal component</h1>
19+
<teleport to="#endofbody">
20+
<div id="content">
21+
<p>
22+
this will be moved to #endofbody.<br />
23+
Pretend that it's a modal
24+
</p>
25+
<Child />
26+
</div>
27+
</teleport>
28+
</div>
29+
<div id="endofbody"></div>
30+
<script>
31+
new Vue({
32+
el: "#app",
33+
components: {
34+
Child: { template: "<div>Placeholder</div>" }
35+
}
36+
});
37+
</script>
38+
</body>
39+
```
40+
41+
This will result in the following behaviour:
42+
43+
1. All of the children of `<teleport>` - in this example: `<div id="content">` and `<Child />` - will be appended to `<div id="endofbody">`
44+
2. the `<Child>` component as one of these children will remain a child component of the `<teleport>`'s parent (the `<teleport>` is transparent).
45+
46+
```html
47+
<div id="app">
48+
<!-- -->
49+
</div>
50+
<div id="endofbody">
51+
<div id="content">
52+
<p>
53+
this will be moved to #endofbody.<br />
54+
Pretend that it's a modal
55+
</p>
56+
<div>Placeholder</div>
57+
</div>
58+
</div>
59+
```
60+
61+
# Motivation
62+
63+
Vue encourages us to build our UIs by encapsulating UI and related behaviour into components, which we can nest inside one another to build a tree of components that make up your application UI. That model has proven itself in Vue and other frameworks in many ways, but there's one weakness this RFC seeks to address:
64+
65+
Sometimes, a part of a component's template belongs into this component _logically_, while from a technical point of view (i.e.: styling requirements), it would be preferable to move this part of the template somewhere else in the DOM, breaking it out of it's deeply nested position without our DOM tree.
66+
67+
## Use cases
68+
69+
### z-Index
70+
71+
The main use cases for such a behaviour are usually styling-related. Various common UI patterns such as modals, dialogs, dropdown menus, notifications etc. require fixed or absolute positioning and management of their z-index.
72+
73+
In order to work around issues with [z-index Stacking Context](https://philipwalton.com/articles/what-no-one-told-you-about-z-index/) behaviour, it's a common pattern to put the DOM elements of those components right before the `</body>` tag in order to move them out of any parent element's z-index stacking context.
74+
75+
### Widgets
76+
77+
Many apps have the concept of widgets, where their UI has an outlet (i.e. in a sidebar or dashboard) where other parts of the application, i.e. plugins, can inject small pieces of UI.
78+
79+
In Single Page Applications, where our Javascript controls essentially the whole page, this is generally not a challenge. But in situations where our Vue app only controls a part of the page, it currently proves to be challenging (but impossible) to mount individual elements and components in other parts of the page.
80+
81+
With `<teleport>`, we have a straightforward way to mount child components to other locations in the DOM declaratively.
82+
83+
# Detailed design
84+
85+
## Implementation as an internal component
86+
87+
The `<teleport>` "component" is an internal component like `<transition>` and `<keep-alive>`. It's tree-shakable, so if you don't use the feature, the component code will be dropped from the final bundle.
88+
89+
### Usage in templates
90+
91+
In templates, the compiler will add the import for the `<teleport>` component to the generated code, so it can be used just like this:
92+
93+
```js
94+
export default {
95+
template: `
96+
<div>
97+
<teleport to="#endofbody">
98+
Some content.
99+
</teleport>
100+
<div>
101+
`
102+
};
103+
```
104+
105+
When using a render function or JSX, component has to imported first, like any other component:
106+
107+
```js
108+
import { Teleport, h } from "vue";
109+
export default {
110+
render() {
111+
return h("div", [h(Teleport, { to: "#endofbody" }, ["Some content"])]);
112+
},
113+
// or with JSX:
114+
render() {
115+
<div>
116+
<Teleport to="#endofbody">Some content</Teleport>
117+
</div>;
118+
}
119+
};
120+
```
121+
122+
## using multiple portals on the same target
123+
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.
125+
126+
```html
127+
<teleport to="#modals">
128+
<div>A</div>
129+
</teleport>
130+
<teleport to="#modals">
131+
<div>B</div>
132+
</teleport>
133+
134+
<!-- result-->
135+
<div id="modals">
136+
<div>A</div>
137+
<div>B</div>
138+
</div>
139+
```
140+
141+
In the discussions for this RFC so far, more complex behaviour (optional prepending, defining the order ...) was discussed, but concerns about complexity and foreseeable issues with SSR and hydration lead us to limit this behaviour to a simple `append`.
142+
143+
## Props
144+
145+
### `to`
146+
147+
The component has only one _required_ prop, named `to`. It accepts a string wich has to be a valid query selector, or an HTMLElement (if used in a browser environment).
148+
149+
```html
150+
<!-- ok -->
151+
<teleport to="#some-id" />
152+
<teleport to=".some-class" />
153+
<teleport to="[data-portal]" />
154+
<!--
155+
probably too unspecific, but technically valid
156+
should we allow this or block it?
157+
-->
158+
<teleport to="h1" />
159+
<!-- Wrong -->
160+
<teleport to="some-string" />
161+
></teleport>
162+
```
163+
164+
### `disabled`
165+
166+
This optional prop can be used to disable the portal's functionality, which means that its slot content will not be moved anywhere and instead be rendered where you specified the `<teleport>` in the surrounding parent component.
167+
168+
```html
169+
<teleport to="#popup" :disabled="displayVideoInline">
170+
<video src="./my-movie.mp4">
171+
</teleport>
172+
```
173+
174+
Changing its value dynamically allows to move the same DOM elements between the target specified by the `to` prop, and the actual location in the surrounding parent component. This means that any components inside of the `<teleport>` will be kept alive and keep their internal state. Likewise, a `<video>` element will keep its playback state while being moved betweeen these locations.
175+
176+
## Lifecycle
177+
178+
### Mounting
179+
180+
When the `<teleport>` component is mounted by its parent, it will use the `to` prop's value as a selector.
181+
182+
- If the query returns an element, the slot children of the `<teleport>` will be mounted as child nodes of that element in the DOM
183+
- If this element doesn't exist in the DOM at the moment that this `<teleport>` is mounted, a warning like the following would be logged during development (nothing would happen in production):
184+
185+
```js
186+
`Teleport content could not be mounted to element with selector '${props.to}': element not found.
187+
188+
// following would be a display where in the component tree this happened etc.
189+
```
190+
191+
#### \$parent
192+
193+
If the children of `<teleport>` contain any components, their `this.$parent` property should reference the `<teleport>`'s parent component. In other words, these components stay in their original spot in the _component tree_, even though they ended up mounted somewhere else in the _DOM tree_.
194+
195+
`<teleport>`, not being a real component at all, is transparent and will not appear as an ancestor in the `$parent`chain.
196+
197+
```html
198+
<template>
199+
<teleport v-bind:to="targetName">
200+
<Child />
201+
</teleport>
202+
</template>
203+
<script>
204+
export default {
205+
name: 'Parent'
206+
components: {
207+
Child: {
208+
template: '<div/>',
209+
mounted() {
210+
console.log(this.$parent.$options.name )
211+
// => 'Parent'
212+
}
213+
}
214+
},
215+
}
216+
</script>
217+
```
218+
219+
Similarly, using `inject` in `Child` should be able to inject any provided content from `Paren` or one of its ancestors.
220+
221+
### Updating
222+
223+
The `to` prop can be changed dynamically with `v-bind`. When the value changes, `<teleport>` will remove the children from the previous target and move them to the new one.
224+
225+
If the children contain any component instances, these will not be influenced by this. The instances will be kept alive, keep their state etc.
226+
227+
```html
228+
<template>
229+
<teleport v-bind:to="targetName">
230+
<p>This can be moved around with the button below</p>
231+
</teleport>
232+
<button v-on:click="toggleTarget">Toggle</button>
233+
<hr />
234+
<div id="A"></div>
235+
<div id="B"></div>
236+
</template>
237+
<script>
238+
export default {
239+
data: () => ({
240+
targetName: "A"
241+
}),
242+
methods: {
243+
toggleTarget() {
244+
this.targetName = this.targetName == "A" ? "B" : "A";
245+
}
246+
}
247+
};
248+
</script>
249+
```
250+
251+
If the new target selector doesn't match any elements:
252+
253+
1. a warning should be logged during development.
254+
2. The content would stay mounted to the previous target element.
255+
256+
### Destruction
257+
258+
When a `<teleport>` is being destroyed (e.g. because its parent component is being destroyed or because of a `v-if`), its children are removed from the DOM and any component instances destroyed just like they were still children iof the parent.
259+
260+
## Miscellaneous
261+
262+
### Naming conflict with native portals
263+
264+
the component introduced by this RFC was named `<portal>` in an earlier version of this RFC. But there's proposal for native portals:
265+
266+
- Spec: https://wicg.github.io/portals/
267+
- Introduction: https://web.dev/hands-on-portals/
268+
269+
Sinc we don't want to have a naming conflict with a future HTML element that may be called `<portal>`, especially since it's functionality is about something completely different form what portals in libs like Vue or React mean right now, we chose to rename the component to `<teleport>`
270+
271+
Or should we keep it as the concept of what a portal is in Vue, React e.t al. is already "common knowledge" and a new term might confuse people more than it would help?
272+
273+
### dev-tools
274+
275+
The `<teleport>` should not appear in the chain of parent components (`this.$parent`), but it should be identifiable within the virtual DOM so that Vue's dee-tools can show them in their visualisation of the component tree.
276+
277+
### Using a `<teleport>` on an element within a Vue app
278+
279+
Technically, this proposal allows to select _any_ element in the DOM , including elements that are rendered by our Vue app in some other part of the component tree.
280+
281+
But that puts the portal'd slot content under the control of that other component's lifecycle, which means the content can possibly be removed from the DOM if that component gets destroyed.
282+
283+
Any component that came through a `<teleport>` would effectively have its DOM removed by still be in the original virtual DOM tree, which would lead to patch errors when these components tried to update.
284+
285+
Handling this relaibly would require lots of additional logic and as such, this use case is explicitly **excluded from this RFC**. Teleporting to any DOM element that is controlled by Vue is considered to be an anti-pattern and can lead to the real dom and virtual dom being out of sync.
286+
287+
# Drawbacks
288+
289+
The only notable drawback that we see is the additional code required to implement this. But judging from experiments in the prototype, that code will be very light, as it's just a slightly different way to mount elements at the virtualDOM level, and the component itself is tree-shakable.
290+
291+
As it's an additive feature and the functionality is pretty straightforward (one prop defining as target selector), this should also not add much complexity to Vue in terms of documentation or teaching.
292+
293+
When considering how popular current userland solutions are even with their caveats and limitations, the cost/benefit ratio seems clear.
294+
295+
# Alternatives
296+
297+
No other designs were considered so far.
298+
299+
## What happens if we don't do this
300+
301+
There are currently several userland implementations of this feature available, which usually suffer some caveats and drawbacks that stem of the fact that portals are not supported at the virtual DOM level in Vue 2.
302+
303+
People could continue to use these with their existing limitations and drawbacks.
304+
305+
# Adoption strategy
306+
307+
`<teleport>` is a new feature and as such purely additive in nature. As such, this feature does not have any impact on the migration of Vue 2.0 applications to Vue 3.0 except for apps that might have chosen to name one of their comconents `<teleport>` - but that is easily fixable by changing that component's registration name to something else.
308+
309+
Users new to Vue 3.0 or Vue in general will be able to learn about this feature from the docs in the usual way and gradually introduce it into their projects where it makes sense.
310+
311+
## Existing 3rd party solutions
312+
313+
As mentioned, several 3rd party plugins/libs implement similar functionality right now.
314+
315+
Some of them may become irrelevant though this RFC, while others, offering functionality that exceeds what this proposal describes, would could adapt their implementation to make use of this proposal's "native" `<teleport>` component internally.
316+
317+
If RFC vuejs/vue-next#28 (Render function change) is adopted, these libraries will have to be reworked either way, at which point they can adopt this new feature.
318+
319+
# Unresolved questions
320+
321+
n/a

0 commit comments

Comments
 (0)