-
Notifications
You must be signed in to change notification settings - Fork 542
<Teleport> built-in component #112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
Great work! 🎉 Is it worth mentioning about SSR? I know that Portal Vue is suffering with it. |
active-rfcs/0000-portals.md
Outdated
} | ||
}); | ||
</script> | ||
<body></body> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[Q.] What does this body tag mean?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It means I have to fix a typo and remove the superflous opening tag.
That there's a body tag at all is because the example is means to be in HTML, not in an SFC.
This will be super useful to have built in. Great job @LinusBorg! Below are some thoughts and questions that came to mind as I read through the proposal (apologies if I misunderstood anything): Selector I wonder if it might be simpler to strictly allow only element ids (w/o #) and DOM node objects as the input to the Vue Control That could also be the way to handle missing targets, in general: don’t unmount whatever was mounted to old target unless the new target value is an empty string, meanwhile emit an event so an acceptable target can be found, or the old target can be reverted to, or the logic can give up by using an empty string to unmount. This way the behavior is intentional. Multiple Portals
What happens to any existing children under the target? If it’s just a matter of managing order, might it suffice to have a prop called insert-before which accepts a Dom node reference before which to mount the children (this can be queried in setup)? That way a small amount of custom logic or a library can manage placement of multiple portals by determining where to insert based on what already exists under that target (eg: examine data-order attribute and sort). By default just append to end. Naming |
The name I find this proposal useful, but I do not have enough information on how it should behave. <Portal target=".content">Portal content</Portal>
<div class="content">Div Content</div>
<Portal target=".component">Portal content</Portal>
<my-component class="component">Component Content</my-component>
<Portal class="portal" target=".another-portal">Portal content</Portal>
<Portal class="another-portal" target=".portal">Another Portal content</Portal> |
This comment has been minimized.
This comment has been minimized.
If we start calling new components |
This comment has been minimized.
This comment has been minimized.
+1 for |
If Example:
|
I'm currently using a custom written mixin to make components that can be portaled around, I'll include it in the end in case it's of use. Two things I'd like to point out are:
export default {
props: {
parent: {
type: HTMLElement,
required: true
}
},
watch: {
parent(newValue, oldValue) {
if (oldValue) this.detachFromElement(oldValue);
if (newValue) this.attachToElement(newValue);
}
},
methods: {
attachToElement(parent) {
if (!parent) return;
parent.appendChild(this.$el);
this.$emit('attachedToElement', parent);
},
detachFromElement(parent) {
if (!parent) return;
this.$emit('detachedFromElement', parent);
}
},
mounted() {
this.attachToElement(this.parent);
},
beforeDestroy() {
this.detachFromElement(this.parent);
}
}; |
This comment has been minimized.
This comment has been minimized.
How does Portal work with refs? Should you put a ref on a Portal or on its contents only? |
If we can't use the same target for multiple portals, how can we use it, for example, for multiple stacked modals? Would we need to manually create additional portal targets in setup? This is slowly starting to feel less data driven, and more like raw DOM manipulation. |
@JosephSilber That's what the RFC is for: To answer these questions. |
I it should not be possible to put a ref on the If we find, over the course of this discussion, that portals need to have an instance to provide additional functionality not yet in the RFC, then this might change. But putting it on the content should always work. Do you have a use case for putting a ref on the |
I agree, will add this.
Should be considered, would also solve @JosephSilber's issue, presumably. |
@kiaking SSR and Portals are tricky as usually the app is rendered in one go. I didn't add it to the RFC for now as we are still exploring SSR architecture for Vue 3 and want to see what we come up with in general before re-visiting this in the RFC |
@LinusBorg Make sense. Thanks for the confirmation! 🤝 |
Yes. I see you suggest to limit it to ids. I'm unsure about that, it feels unnecessessarily limiting.
I'll need to think about this bit more, will get back to you about it.
Yeah I see that many people like to have multiple portals moving stuff to the same place, but I'm torn if this should be part of core or we can design it in a way that a small lib can make this happen with little code. |
Who said you can't? You can use Portal to mount as many elements as you want in the same target node. You can create a functional component that renders Portal and mounts target element in the DOM just once, then you can reuse this component anywhere you want. // DialogPortal.js import { h, Portal } from 'vue';
let target = null
export function DialogPortal(_, { slots }) {
if (!target) {
target = document.createElement('div')
target.id = 'dialog-root';
document.body.appendChild(target)
}
return h(Portal, { target }, slots.default())
} // Dialog.vue <template>
<DialogPortal>
<div class="dialog">
dialog content
</div>
</DialogPortal>
</template> |
Maybe make portals look like scoped slot? Source component:
Target component:
Pros:
Cons:
P.S. It's may be a repetition of the situation with slots. At first there were slots, then scoped slots were added, after they were merged (more precisely, slots were removed). |
🙏🏽❤️ Great work with this RFC, @LinusBorg , contributors and core team with Vue 3. I have a few thoughts on handling portals with no visible children and I'd be happy to hear some thoughts about this. It's not a very urgent requirement/feature for the I'll also go ahead and say that I might be doing this wrong, and there may be a better way to implement this than I am doing. But this currently is what would work for me. So feel free to suggest your thoughts about this. Handling portal targets with no childrenOne of the things I've noticed as a library author is that I might have multiple portals for different kinds of components. I'd have a single portal for toast components, a separate one for popovers, and for tooltips as well, etc. My preference would be to have them exists in different portal targets. So, naturally I'd build my components using this pattern. In consumer applications, however, if their production app has many implementations of popovers and tooltips, etc, they would end up having multiple portal targets in the DOM since I'm creating the portal targets and mounting them in the DOM only when the Tooltip/Popover/Menu components are consumed by the user. So we would wind up with markup somewhat similar to this: <!DOCTYPE html>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8">
<title>Consumer App</title>
</head>
<body>
<!-- Vue app is mounted here -->
<div id="app"></div>
<!-- Portal targets are mounted here -->
<div id="popover-portal"></div>
<div id="toast-portal"></div>
<div id="menu-portal"></div>
<div id="tooltip-portal"></div>
</body>
</html> On demand Portal target creation and removal.This results in having multiple portal targets in the markup that (may not be used). This led me down a path of desiring to create the my own portal targets only when it's children are present/visible either by This may require to some specific internal behaviour from the
Implementing this could them result in markup shown below: 📭 Targets with invisible childrenIn Vue template <template>
<main>
<Portal on-demand target="#popover-portal">
<p v-if="false">Portal target will not be mounted because I'm set to v-if=false</p>
</Portal>
</main>
</template>
<script>
import { Portal } from "vue"
export default {
components: {
Portal
}
}
</script> Rendered markup <html lang="en" dir="ltr">
<body>
<!-- Vue app is mounted here -->
<div id="app">...</div>
<!-- #popover-target not created and mounted -->
</body>
</html> ✅ Targets with visible childrenIn Vue template <template>
<main>
<!--
on-demand prop set to true to activate
internal portal target creation and and removal
-->
<Portal on-demand target="#popover-portal">
<p v-if="true">Mounted because I'm set to v-if true</p>
</Portal>
</main>
</template>
<script>
import { Portal } from "vue"
export default {
components: {
Portal
}
}
</script> Rendered markup <html lang="en" dir="ltr">
<body>
<!-- Vue app is mounted here -->
<div id="app">...</div>
<!-- Portal targets are mounted here -->
<div id="popover-portal">
<p>Mounted because I'm set to v-if true</p>
</div>
</body>
</html> Having this behaviour would keep markup clean and also be useful for authors to not have to create their own wrapper Portals to create this kind of behaviour for their consumers :) What do you think about this? 🙏🏽❤️ |
@codebender828 Sounds pretty helpful. However, I see the need to specify WHERE it is necessary to create a target for the portal. And I don't see a way to conveniently set the location of this target. Maybe someone more experienced will point it out. It might be better for these tasks to implement a new universal component, or directive, etc. that would remove a node if it had no child nodes? In Vue template <template>
<main>
<div v-remove-if-empty>
<p v-if="false">Portal target will not be mounted because I'm set to v-if=false</p>
</div>
</main>
</template> Rendered markup <main>
<!-- div not created and mounted -->
</main> In Vue template <template>
<main>
<div v-remove-if-empty>
<p v-if="true">Mounted because I'm set to v-if true</p>
</div>
</main>
</template> Rendered markup <main>
<div>
<p>Mounted because I'm set to v-if true</p>
</div>
</main> |
What if the syntax for controlling a component's mounting root was a global directive; <template>
<main v-mount="$root">
<!-- content -->
</main>
</template> *Assuming the mounting target is |
While I appreciate the concerns you have for keeping the dom uncluttered, I feel like this could be premature optimization, I can hardly see how a few (even tens) empty divs could harm an application's performance to make the complexity this behavior would add to the implementation worth it. @cawa-93 suggestion is great for it makes it an independent feature rather than making portals more complex, even feels like a directive that could be implemented in userland |
A way t essentially "disable" a portal conditionally was already proposed, and I think it would be quite useful:
Not sure I totally get that usecase - I get the direction but, lack a detailed image in my head. can you add some details for me? |
I like this, but it has to be kept simple. What kinds of strategies would we see here?
I wonder though, if it would make more sense to let these cases be solved in userland as long as we can make sure in Vue's core that they are possible to be implemented.
That's already part of the RFC. |
Was kind of thinking about being able to trigger callbacks based on action from the portal - done moving the element etc. to be able to execute further actions without having to resort to <portal :disabled="screenWidth < 768">
<div @after-move-to-portal="attachPopper" @before-move-to-host="detachPopper"/>
</portal> (I know the event names make no sense) methods: {
attachPopper(el) {
el._popper = createPopper(el);
},
detachPopper(el) {
el._popper.destroy();
},
}, or something along those lines, but I realize that this kind of makes no sense? like if it's a component and contains multiple root elements etc. it would fail pretty fast. Most likely this should be encapsulated as a component and just toggle the "use-popper" on and off based on the same condition as the parent. Main problem with something like popper is that it uses the parent element on bind, but if you move it using portal, it breaks real fast. At the moment for something similar I use directives |
Javascript controls essentially *he* whole page ---> Javascript controls essentially *the* whole page
We have implemented multi-portal-same-target append support and the Regarding the name conflict, we are currently leaning towards <Teleport to="#modal-layer" :disabled="isMobile">
<div class="modal">
hello
</div>
</Teleport> |
I had to read about <Teleport :to="null" /> The example above would look like this: <Teleport :to="isMobile ? '#modal-layer' : null">
<div class="modal">
hello
</div>
</Teleport> Also this is a somewhat duplicate of <component :is="isMobile ? Teleport : 'div'" to="#modal-layer">
<div class="modal">
hello
</div>
</component> |
I just pushed the update! |
@CyberAP neither of your suggestions are as explicit as a |
Co-Authored-By: Evan You <yyx990803@gmail.com>
Co-Authored-By: Evan You <yyx990803@gmail.com>
Co-Authored-By: Evan You <yyx990803@gmail.com>
This RFC is now in final comments stage. An RFC in final comments stage means that: The core team has reviewed the feedback and reached consensus about the general direction of the RFC and believe that this RFC is a worthwhile addition to the framework. |
Something that occurred me: why not name it It'd be one less buzzword to teach and learn, except if it's somehow misleading. Is it? (Sorry to address this so late...) |
@leopiccionia thanks for the suggestion, but the naming has been extensively discussed and the current choice of |
Are there any plans on events for teleport? At my case I need to teleport an element but adjust the coords to old location. So events like: are really needed. @donnysim have you solved it? |
This RFC introduces a
<teleport>
component, which allows to move its slot content to another part of the document.Rendered