vuejs / vue

This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core
http://v2.vuejs.org
MIT License
207.71k stars 33.68k forks source link

Portal implementation in Vue. #4841

Closed roman-vanesyan closed 7 years ago

roman-vanesyan commented 7 years ago

Hello there! Sorry, if it's wrong place to post such a thing. It's more like a question, but may be as feature request as well. What is the current official way of creating portal component? Portal is a component that helps mount the child component to targeted DOM point. For example, React exposes official API for doing portals (i.e. React.unstable_renderSubtreeIntoContainer).

roman-vanesyan commented 7 years ago

Ping @yyx990803

posva commented 7 years ago

React exposes official API for doing portals (i.e. React.unstable_renderSubtreeIntoContainer)

That's not what I call an official API 😛 There's no official way of doing it right now. That's why there's a feature request label. You don't need to worry, we'll come back to discuss this as soon as we can, so please don't ping people like that, it spams all contributors for nothing, thanks 🙂

roman-vanesyan commented 7 years ago

Ok, sorry for that! :)

fnlctrl commented 7 years ago

For now, it can be implemented with render functions, by passing the rendered VNodes to another component. Here is a basic example: https://jsfiddle.net/fnlCtrl/dn2qa2fg/

I've been using something like the above in production for some time, and it seems to work well.

LinusBorg commented 7 years ago

FYI, I'm currently playing with this idea as well, and on a bigger scale, with a target anywhere in the DOM (using an event bus - one of the few situations where i found it useful). You can see my progress here:

https://github.com/LinusBorg/vue-portal/tree/develop

As far as I have tested it, it works pretty well, but I don't think I have catched all edge cases and I'm not sure that this works in all cases.

It works like this:

<!-- From.. -->
<portal to="destination">
  <ul class="todo-list">
    <li v-for="(todo, index) in todos">
      {{todo}} (<a href="#" @click.prevent="remove(index)">x</a>)
    </li>
  </ul>
</portal>

<!-- ..to_ -->
<portal-target name="destination"/>

So far, this only works with the <portal-target> component, but I'm playing with the idea of a kind of forceMount prop that will make the <portal> create a target component anywhere (which would be mostly useful to do outside of the DOM that is managed by Vue, so more useful for Apps that use Vue to enhance parts of their page)

roman-vanesyan commented 7 years ago

Yes, I'm talking mostly about the last paragraph of your article. I've only found the way where I can use vm.$mount('DOM_ENDTRY_POINT') inside of Portal component by using event bus approach as well.

edgardleal commented 7 years ago

I don't know if I understand your question, but dynamic component should resolve your problem ? Dynamic-Components

`

`

LinusBorg commented 7 years ago

No, dynamic components do not do what OP wants.

yyx990803 commented 7 years ago

Closing this as I believe it's possible to implement Portals in Vue 2.x in userland with current feature set. If anyone is interested in it, keep an eye on @LinusBorg 's repo!

LinusBorg commented 7 years ago

I hope to release the first version in a week or two.

LinusBorg commented 7 years ago

Done. See here: https://linusborg.github.io/portal-vue

Justineo commented 6 years ago

Is there any chance that we support portal natively in Vue? Although portal-vue is working nicely in most cases, it still have side effects like leaving an empty wrapper element thus may interfere with something (especially in styles) relying on DOM structure like .a + .b { margin-top: 1em } or .a:last-child { margin-bottom: 1em }. If the <portal> and <portal-target> can just work as abstract components it would be great. IMO we cannot eliminate the empty wrapper but still get everything right unless the feature is supported by Vue's core (hacking render functions to return an inner VNode may still cause problems).

LinusBorg commented 6 years ago

Maybe start with a feature request for portal-vue, I'm quite open to those :)

We don't have portals in the roadmap for Vue at the moment.

donnysim commented 6 years ago

I still think this should be implemented natively into Vue because this is one of the most common things that libraries implement their own way of doing it and it result only in code bloat, bugs and increased bundle size. In other words, it would stop people from reinventing the wheel.

donnysim commented 6 years ago

Just as an example, a user imports different "select", "tags", "dropdown" and he himself uses vue-portal, now what that means is that there's now 4 instances of custom "portal" code. Could we re-evaluate the necessity of this feature?

LinusBorg commented 6 years ago

Just as an example, a user imports different "select", "tags", "dropdown" and he himself uses vue-portal, now what that means is that there's now 4 instances of custom "portal" code.

That can be said about many, many things/features. Looking at react, where the core lib comes with even less features than Vue (and yes, I they now seem to have Portals), like no transition system, the community simply comes up with a bunch of solutions, eventually one of those libs becomes a de-facto-standard.

It's not the goal and mission of Vue to provide a core feature for every common need in web development - the goal is to provide the basic toolset to build solutions that satisfy these needs. And while portal-vue doesn't have thousands of stars, it's gaining in popularity and could solve the problem in that way - all of the above solution would converge on using portal-vue in that case.

Even if that doesn't happen, the code involved to build a bare-bones portal solution for a single library like a drodown is very small, so I don't see a big overhead here.

That being said, we might add portals at some time in the future, when we work on Vue 3, rewrite the virtualdom and simply do that in a way that keeps portals in mind as a core feature.

Right now the problem can be solved elegantly enough in userland, so there's no immediate need to put a lot of work into a core implementation.


Disclaimer: I'm the author of portal-vue and a Vue core team member, so there may be some conflict of interest, but at the same time, I know both perspectives

donnysim commented 6 years ago

I understand that, but as I did try writing a portal'ish logic in a couple different ways (directive, component), there aren't many ways you can do that to avoid DOM mess where it should clean up but leaves stale elements, or if the component is standalone, there is no "portal-target" thing, I just need it to add the dropdown to the body, so the solutions are not that elegant and become hacky - like the whole portal functionality feels at the moment.

I know we all have different mindsets what "portal" should do, but in my mindset, it should not deal with "element by id" or allow dropping part of component into another component etc., that just allows users instead of structuring a data driven codebase, to turn it into a mess that's hard to manage and could be solved in more traditional and simpler ways (don't mean to attack portal-vue or You). It should be simple, allow dropping into specified DOM element - document.body, document.createElement etc. (and the developer is responsible for it, no wrapper etc.) and just work with transitions while cleaning up whats needed on component removal which at the moment the only guarantee seems to be when each portal'ed element is wrapped into a component, or is a component that does the cleaning, but at the end of the day - a wrapper is a wrapper, and we hate wrappers.

In theory the portal thing sounds easier than it actually is, for example, if the portal is as a directive, it needs to know if the element has transition, otherwise you get big fat error when the transition itself tries to remove the element and many other small details that you come across when trying to implement such thing. So a somewhat supported solution would solve many headaches and unexpected outcomes.

LinusBorg commented 6 years ago

I know we all have different mindsets what "portal" should do [...] It should be simple, allow dropping into specified DOM element - document.body, document.createElement etc. (and the developer is responsible for it, no wrapper etc.) and just work with transitions [...]

I'm not sure what the API for that should be if you don't want it to be done with a wrapper component (You later mention directives, about which I write a few paragaphs further down).

From your description I have get a good picture about how you imagine this to work.

a wrapper is a wrapper, and we hate wrappers.

I'm not sure who you refer to as "we", but I can say that we from the Vue team do not hate wrappers - much to the contrary, encapsulating behaviour in wrapper components is a concept we use for many such things, like <keep-alive>, <transition> or <transition-group>, <router-view> and <router-link>.

I bet if we implemented a Portal functionality in core, it would use some sort of wrapper as well, just with a deeper integration with the underlying virtualdom, probably.

In theory the portal thing sounds easier than it actually is

If you want to create a feature-rich solution for many scenarios, that's right. If all you need is to move a modal to the end of the body, that's not really hard.

for example, if the portal is as a directive, it needs to know if the element has transition [...]

Well, implementing it with a directive - which is aware of transitions in Vue by design - would approach this at the wrong level of abstraction

Directives are meant for low-level DOM access which Vue's transitions are highly abstracted - that's part of why I did solve it with a component.

Back to the top a bit:

it should not deal with "element by id" or allow dropping part of component into another component etc., that just allows users instead of structuring a data driven codebase, to turn it into a mess that's hard to manage and could be solved in more traditional and simpler ways (don't mean to attack portal-vue or You).

I don't take it as an attack, no worries. You raise a valid critisism against using portal-vue excessively - that can indeed happen if users don't approach their app with a data-centric mind.

But that doesn't invalidate the whole concept to be useful for solving the specific problems that it does solve nicely, like moving modals, dropdown menus etc to the end of <body>.

donnysim commented 6 years ago

I'm not sure who you refer to as "we", but I can say that we from the Vue team do not hate wrappers - much to the contrary, encapsulating behaviour in wrapper components is a concept we use for many such things, like , or , and .

Should've been more clear, by a wrapper, I don't mean a wrapper component, but rather a wrapper element, which introduces a headaches when used with bought templates (mostly admin/dashboard templates) that are not framework based, where there are a lot of direct child selectors and a wrapper element can cause the theme to break.

Well, implementing it with a directive - which is aware of transitions in Vue by design - would approach this at the wrong level of abstraction

Directives are meant for low-level DOM access which Vue's transitions are highly abstracted - that's part of why I did solve it with a component.

It is true, but at the end of the day it kind of achieves the same result. Most common component based portal usages are implemented in the following way (sticking to body only):

export default {
    mounted() {
        document.body.appendChild(this.$el);
    },

    beforeDestroy() {
        document.body.removeChild(this.$el);
    },

    render() {
        return this.$slots.default[0];
    },
}

while directive equivalent is similar:

export default {
    inserted(el) {
        document.body.appendChild(el);
    },

    unbind(el) {
        if (el._transitionClasses && el.__vue__) {
            // Will be removed by transition
            return;
        }

        if (el.parentNode) {
            el.parentNode.removeChild(el);
        }
    },
};

Which both work for what they're worth while keeping them without wrapper elements, keeps inject/provide working etc.. Though directive will have problems when the transition is not on the directive element without advanced lookups, yet most big component libraries used both, yet more directive based approach (at least last time I checked them).

The nice thing about component based approach is that it keeps transitions working and cleans up the element, and with the undocumented abstract: true (may the Vue god forgive us), the separated element is not displayed in the component tree.

Maybe it's not as bad as I make it seem to myself, maybe I just need to step back and think it over again 🤔

edit: I remember Evan said this shouldn't be done, maybe that's why I feel bad about it 😄

Justineo commented 6 years ago

If portals in userland can eliminate extra DOM wrapper elements while the implementation still keep robust, I’ll be happy to use them. I believe React wouldn’t provide that in core if there can be such solution purely in userland…

LinusBorg commented 6 years ago

portals in userland can eliminate extra DOM wrapper elements while the implementation still keep robust

I'll look into that for portal-vue. It's possible now if:

But support and docs for that can always be improved.

lxjwlt commented 6 years ago

portal component may need Vue's official support as

kvedantmahajan commented 5 years ago

@LinusBorg Just curious on how to use these portals in render functions? Will the attributes of portal elements be the same as in template style?

jmduke commented 1 year ago

Apologies for necro-ing an older thread but Google ranks this pretty highly for "portal vue", so I am letting any future searchers know that Vue 3 has a first-party portal component called Teleport.