From 1eb90eec3ea3c31a6d337f202bace81187bc4cd5 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Mon, 20 Jan 2020 22:39:16 +0100 Subject: [PATCH 1/9] add portals rfc --- active-rfcs/0000-portals.md | 355 ++++++++++++++++++++++++++++++++++++ 1 file changed, 355 insertions(+) create mode 100644 active-rfcs/0000-portals.md diff --git a/active-rfcs/0000-portals.md b/active-rfcs/0000-portals.md new file mode 100644 index 00000000..84aa7374 --- /dev/null +++ b/active-rfcs/0000-portals.md @@ -0,0 +1,355 @@ +- Start Date: 2020-01-20 +- Target Major Version: (3.x) +- Reference Issues: (fill in existing related issues, if any) +- Implementation PR: + +# Summary + +- Adds a `` component to Vue core +- the component accepts a DOM selector via a prop +- the component moves its children to the element identified by the DOM selector +- At the virtual DOM level, the children stay descendants of the `` though, so they i.e. have access to injections from its ancestors + +# Basic example + +```html + +
+

Move the #content with the portal component

+ +
+

+ this will be moved to #endofbody.
+ Pretend that it's a modal +

+ +
+
+
+
+ + + +``` + +This will result in the following behaviour: + +1. All of the children of `` - in this example: `
` - will be appended to `
` +2. the `` component as one of these children will remain a child component of the ``'s parent (the Portal is transparent). + +```html +
+ +
+
+
+

+ this will be moved to #endofbody.
+ Pretend that it's a modal +

+
Placeholder
+
+
+``` + +# Motivation + +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: + +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. + +## Use cases + +### z-Index + +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. + +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 `` tag in order to move them out of any parent element's z-index stacking context. + +### Widgets + +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. + +In Single Page Applications, where our Javascript controls essentially he 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. + +With Portals, we have a straightforward way to mount child components to other locations in the DOM declaratively. + +# Detailed design + +## Globally imported `Portal` component + +The `'vue'` package has a named export for a `` "component". + +Since this component doesn't have any component logic of its own (Portal functionality would be implemented at the virtual DOM level), so this could be a `Symbol` instead of a full component. + +```js +import { Portal } from "vue"; +export default { + template: `
+ + Some content. + +
`, + components: { + Portal + } +}; +``` + +When using a render function, the component can be used directly without first registering it, like any other component: + +```js +import { Portal, h } from "vue"; +export default { + render() { + return h("div", [h(Portal, { target: "#endofbody" }, ["Some content"])]); + }, + // or with JSX: + render() { +
+ Some content +
; + } +}; +``` + +If used directly in the browser from a CDN, we can access the Portal as a property on the Vue constructor: + +```js +const App = { + template: `
+ + Some content. + +
`, + components: { + Portal: Vue.Portal + } +}; +``` + +## The `target` prop + +The component has only one _required_ prop, named `target`. It accepts a string wich has to be a valid query selector. + +```html + + + + + + + + +``` + +## Lifecycle + +### Mounting + +When the Portal component is mounted by its parent, it will use the `target` prop's value as a selector. + +- If the query returns an element, the slot children of the `Portal` will be mounted as child nodes of that element in the DOM +- If this element doesn't exist in the DOM at the moment that this `` is mounted, a warning will be logged during development (nothing would happen in production): + +```js +`Portal could not be mounted to element with selector '${props.target}': element not found in DOM. + +// following would be a display where in the component tree this happened etc. +``` + +#### \$parent + +If the children of `Portal` contain any components, their `this.$parent` property should reference the `Portal`'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_. + +`Portal`, not being a real component at all, is transparent and will not appear as an ancestor in the `$parent`chain. + +```html + +``` + +Similarly, using `inject` in `Child` should be able to inject any provided content from `Paren` or one of its ancestors. + +### Updating + +The `target` prop can be changed dynamically with `v-bind`. When the value changes, `Portal` will remove the children from the previous target and move them to the new one. + +If the children contain any component instances, these will not be influenced by this. The instances will be kept alive, keep their state etc. + +```html + +``` + +If the new target selector doesn't match any elements, a warning should be logged during development. + +> **Question:** Should the old content still be removed from the old target in that situation or should everything stay the way it is? + +> **Question** What should happen when the parent component re-renders? should the query Selector be run again? Seems like it's unnecessary in most situations, but if we don't, then having a selector that doesn't match anything initially will result in a stale component even if that element pops up later, wouldn't it? + +### Destruction + +When a `Portal` 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. + +### dev-tools + +The Portal 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. + +### Using a Portal on an element within a Vue app + +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. + +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. + +Any component that came through a `Portal` 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. + +# Drawbacks + +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. + +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. + +When considering how popular current userland solutions are even with their caveats and limitations, the cost/benefit ratio seems clear. + +# Alternatives + +No other designs were considered so far. + +## What happens if we don't do this + +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. + +People could continue to use these with their existing limitations and drawbacks. + +# Adoption strategy + +Portals is a new feature and as such purely additive in nature. + +Since the component is not globally registered (unlike e.g. `` was in Vue 2.0), there are also not risks if naming conflicts for applications that already use the name `` in some other context: + +```html + +``` + +As such, this feature does not have any impact on the migration of Vue 2.0 applications to Vue 3.0. + +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. + +## Existing 3rd party solutions + +As mentioned, several 3rd party plugins/libs implement similar functionality right now. + +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" `` component internally. + +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. + +# Unresolved questions + +### Mounting to elements controlled by Vue + +When portal-ing content to a target element that is controlled by Vue, the portal's content might be removed from the DOM by the component that controls that target element. + +- Can we handle this gracefully? +- If not: Are userland solutions on top of this basic implementations able to work around this (in my opinion: yes) +- Or is this feature strictly limited to mounting to elements _outside_ of the part of the DOM controlled by Vue? + +### Missing Targets + +- What do we do if the new target doesn't exist? unmount the old one anyway? +- Should target selectors re-run on every re-render of the `Portal`'s parent so that initially missing targets can be mounted to once they exist? + +### using multiple portals on the same target + +portal-vue supports sending content from multiple `` to the same target. it does so by using a `` component that manages this. + +The portal functionality proposed in this RFC requires that each portal has its own target element to mount to. + +Should we and if so how can we make n:1 work? + +### Naming conflict with native portals + +There's proposal for native portals: + +- Spec: https://wicg.github.io/portals/ +- Introduction: https://web.dev/hands-on-portals/ + +We probably don't want to have a naming conflict with a future HTML element that may be called ``, especially since it's functionality is about something completely different form what portals in libs like Vue or React mean right now. + +So should we give the concept of this RFC (and by extension, the component it introduces) another name to prepare for native `` elements becoming a standard? If so, what would we call it instead? + +- `` +- `` +- `<...?>` + +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? From 037f121f00126ae0f6ac523c41a464bf5aa1ab0d Mon Sep 17 00:00:00 2001 From: Thorsten Date: Wed, 22 Jan 2020 20:25:19 +0100 Subject: [PATCH 2/9] update: target prop should accept an HTMLElement --- active-rfcs/0000-portals.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/active-rfcs/0000-portals.md b/active-rfcs/0000-portals.md index 84aa7374..98f1487c 100644 --- a/active-rfcs/0000-portals.md +++ b/active-rfcs/0000-portals.md @@ -139,20 +139,20 @@ const App = { ## The `target` prop -The component has only one _required_ prop, named `target`. It accepts a string wich has to be a valid query selector. +The component has only one _required_ prop, named `target`. It accepts a string wich has to be a valid query selector, or an HTMLElement (if used in a browser environment). ```html - - - - - - - + + > ``` From 7b24f93bce15c614ebba67cb02f2c465ea83c2f8 Mon Sep 17 00:00:00 2001 From: Thorsten Date: Thu, 23 Jan 2020 20:15:22 +0100 Subject: [PATCH 3/9] fix: example markup --- active-rfcs/0000-portals.md | 1 - 1 file changed, 1 deletion(-) diff --git a/active-rfcs/0000-portals.md b/active-rfcs/0000-portals.md index 98f1487c..566a341e 100644 --- a/active-rfcs/0000-portals.md +++ b/active-rfcs/0000-portals.md @@ -37,7 +37,6 @@ } }); - ``` From 9a64e1a2271b299882137d6c57a460c9b04f46e2 Mon Sep 17 00:00:00 2001 From: Ignacio Durand Date: Wed, 4 Mar 2020 13:40:54 -0300 Subject: [PATCH 4/9] Fixed typo (#123) Javascript controls essentially *he* whole page ---> Javascript controls essentially *the* whole page --- active-rfcs/0000-portals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-portals.md b/active-rfcs/0000-portals.md index 566a341e..064aa75d 100644 --- a/active-rfcs/0000-portals.md +++ b/active-rfcs/0000-portals.md @@ -78,7 +78,7 @@ In order to work around issues with [z-index Stacking Context](https://philipwal 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. -In Single Page Applications, where our Javascript controls essentially he 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. +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. With Portals, we have a straightforward way to mount child components to other locations in the DOM declaratively. From 80ab8e67e20443c644fd9d871c4fc5b4a1e5368a Mon Sep 17 00:00:00 2001 From: Thorsten Date: Tue, 31 Mar 2020 21:31:07 +0200 Subject: [PATCH 5/9] update: - support of multiple sources for a target support for prop "disabled" - rename of component to - minor corrections and clarifications --- active-rfcs/0000-portals.md | 289 ++++++++++++++++-------------------- 1 file changed, 128 insertions(+), 161 deletions(-) diff --git a/active-rfcs/0000-portals.md b/active-rfcs/0000-portals.md index 566a341e..c323c63b 100644 --- a/active-rfcs/0000-portals.md +++ b/active-rfcs/0000-portals.md @@ -5,10 +5,10 @@ # Summary -- Adds a `` component to Vue core -- the component accepts a DOM selector via a prop +- Adds a `` component to Vue core +- the component requires a target element, provided through a prop which expects an `HTMLElement` or a `querySelector` string. - the component moves its children to the element identified by the DOM selector -- At the virtual DOM level, the children stay descendants of the `` though, so they i.e. have access to injections from its ancestors +- At the virtual DOM level, the children stay descendants of the `` though, so they i.e. have access to injections from its ancestors # Basic example @@ -16,7 +16,7 @@

Move the #content with the portal component

- +

this will be moved to #endofbody.
@@ -24,15 +24,13 @@

-
+
+ }, + } + ``` Similarly, using `inject` in `Child` should be able to inject any provided content from `Paren` or one of its ancestors. ### Updating -The `target` prop can be changed dynamically with `v-bind`. When the value changes, `Portal` will remove the children from the previous target and move them to the new one. +The `to` prop can be changed dynamically with `v-bind`. When the value changes, `` will remove the children from the previous target and move them to the new one. If the children contain any component instances, these will not be influenced by this. The instances will be kept alive, keep their state etc. ```html + + ``` -If the new target selector doesn't match any elements, a warning should be logged during development. - -> **Question:** Should the old content still be removed from the old target in that situation or should everything stay the way it is? +If the new target selector doesn't match any elements: -> **Question** What should happen when the parent component re-renders? should the query Selector be run again? Seems like it's unnecessary in most situations, but if we don't, then having a selector that doesn't match anything initially will result in a stale component even if that element pops up later, wouldn't it? +1. a warning should be logged during development. +2. The content would stay mounted to the previous target element. ### Destruction -When a `Portal` 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. +When a `` 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. + +## Miscellaneous + +### Naming conflict with native portals + +the component introduced by this RFC was named `` in an earlier version of this RFC. But there's proposal for native portals: + +- Spec: https://wicg.github.io/portals/ +- Introduction: https://web.dev/hands-on-portals/ + +Sinc we don't want to have a naming conflict with a future HTML element that may be called ``, 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 `` + +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? ### dev-tools -The Portal 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. +The `` 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. -### Using a Portal on an element within a Vue app +### Using a `` on an element within a Vue app 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. 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. -Any component that came through a `Portal` 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. +Any component that came through a `` 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. + +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. # Drawbacks -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. +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. 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. @@ -280,28 +304,7 @@ People could continue to use these with their existing limitations and drawbacks # Adoption strategy -Portals is a new feature and as such purely additive in nature. - -Since the component is not globally registered (unlike e.g. `` was in Vue 2.0), there are also not risks if naming conflicts for applications that already use the name `` in some other context: - -```html - -``` - -As such, this feature does not have any impact on the migration of Vue 2.0 applications to Vue 3.0. +`` 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 `` - but that is easily fixable by changing that component's registration name to something else. 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. @@ -309,46 +312,10 @@ Users new to Vue 3.0 or Vue in general will be able to learn about this feature As mentioned, several 3rd party plugins/libs implement similar functionality right now. -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" `` component internally. +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" `` component internally. 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. # Unresolved questions -### Mounting to elements controlled by Vue - -When portal-ing content to a target element that is controlled by Vue, the portal's content might be removed from the DOM by the component that controls that target element. - -- Can we handle this gracefully? -- If not: Are userland solutions on top of this basic implementations able to work around this (in my opinion: yes) -- Or is this feature strictly limited to mounting to elements _outside_ of the part of the DOM controlled by Vue? - -### Missing Targets - -- What do we do if the new target doesn't exist? unmount the old one anyway? -- Should target selectors re-run on every re-render of the `Portal`'s parent so that initially missing targets can be mounted to once they exist? - -### using multiple portals on the same target - -portal-vue supports sending content from multiple `` to the same target. it does so by using a `` component that manages this. - -The portal functionality proposed in this RFC requires that each portal has its own target element to mount to. - -Should we and if so how can we make n:1 work? - -### Naming conflict with native portals - -There's proposal for native portals: - -- Spec: https://wicg.github.io/portals/ -- Introduction: https://web.dev/hands-on-portals/ - -We probably don't want to have a naming conflict with a future HTML element that may be called ``, especially since it's functionality is about something completely different form what portals in libs like Vue or React mean right now. - -So should we give the concept of this RFC (and by extension, the component it introduces) another name to prepare for native `` elements becoming a standard? If so, what would we call it instead? - -- `` -- `` -- `<...?>` - -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? +n/a From e8a5066e9f14550206cb3df31f82518e30c0d95d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20L=C3=BCnborg?= Date: Tue, 31 Mar 2020 21:43:49 +0200 Subject: [PATCH 6/9] Update active-rfcs/0000-portals.md Co-Authored-By: Evan You --- active-rfcs/0000-portals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-portals.md b/active-rfcs/0000-portals.md index 450b251d..5de7f755 100644 --- a/active-rfcs/0000-portals.md +++ b/active-rfcs/0000-portals.md @@ -108,7 +108,7 @@ When using a render function or JSX, component has to imported first, like any o import { Teleport, h } from "vue"; export default { render() { - return h("div", [h(Teleport, { target: "#endofbody" }, ["Some content"])]); + return h("div", [h(Teleport, { to: "#endofbody" }, ["Some content"])]); }, // or with JSX: render() { From 8bdb5d61abaed16f627c9535e76545b550400f08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20L=C3=BCnborg?= Date: Tue, 31 Mar 2020 21:44:11 +0200 Subject: [PATCH 7/9] Update active-rfcs/0000-portals.md Co-Authored-By: Evan You --- active-rfcs/0000-portals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-portals.md b/active-rfcs/0000-portals.md index 5de7f755..4a05efca 100644 --- a/active-rfcs/0000-portals.md +++ b/active-rfcs/0000-portals.md @@ -266,7 +266,7 @@ the component introduced by this RFC was named `` in an earlier version - Spec: https://wicg.github.io/portals/ - Introduction: https://web.dev/hands-on-portals/ -Sinc we don't want to have a naming conflict with a future HTML element that may be called ``, 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 `` +Sinc we don't want to have a naming conflict with a future HTML element that may be called ``, 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 `` 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? From e1ea1f22e2f43c402689339cba3f490c6f370f15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thorsten=20L=C3=BCnborg?= Date: Tue, 31 Mar 2020 21:46:14 +0200 Subject: [PATCH 8/9] Update active-rfcs/0000-portals.md Co-Authored-By: Evan You --- active-rfcs/0000-portals.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active-rfcs/0000-portals.md b/active-rfcs/0000-portals.md index 4a05efca..788db375 100644 --- a/active-rfcs/0000-portals.md +++ b/active-rfcs/0000-portals.md @@ -113,7 +113,7 @@ export default { // or with JSX: render() {
- Some content + Some content
; } }; From e1ce36b5636b5161a3a3647ff1be6c06b3cd0824 Mon Sep 17 00:00:00 2001 From: Evan You Date: Thu, 9 Apr 2020 10:21:24 -0400 Subject: [PATCH 9/9] Rename 0000-portals.md to 0025-teleport.md --- active-rfcs/{0000-portals.md => 0025-teleport.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename active-rfcs/{0000-portals.md => 0025-teleport.md} (100%) diff --git a/active-rfcs/0000-portals.md b/active-rfcs/0025-teleport.md similarity index 100% rename from active-rfcs/0000-portals.md rename to active-rfcs/0025-teleport.md