LinusBorg / portal-vue

A feature-rich Portal Plugin for Vue 3, for rendering DOM outside of a component, anywhere in your app or the entire document. (Vue 2 version: v2.portal-vue.linusb.org)
http://portal-vue.linusb.org
MIT License
3.9k stars 186 forks source link

Nested routes from Vue Router break when used inside PortalVue #289

Open boardend opened 4 years ago

boardend commented 4 years ago

Version

2.1.7

Reproduction link

https://codesandbox.io/s/great-elgamal-y285j

Steps to reproduce

Use <router-view /> with nested routes inside a <portal> to render the nested routes somewhere else in the app (e.g. a lightbox modal that is logically a child of some other component/route, but should be rendered somewhere close to the root component).

What is expected?

The route matching will work the same, no matter if the <router-view /> is inside a <portal> or not.

What is actually happening?

When inside a <portal>, the nested route gets replaced with the parent route from where the <portal> was used.

tmorehouse commented 4 years ago

This would be because the child <router-view> is no longer nested within the parent <router-view> when portalled: the parent hierarchy changes (https://portal-vue.linusb.org/guide/caveats.html#known-caveats) if your <portal-target> is not rendered within your parent <router-view> container.

tmorehouse commented 4 years ago

One workaround would be to use named <router-view>s, and set the "child" route to be targeted to that name view (https://router.vuejs.org/guide/essentials/named-views.html#named-views)

boardend commented 4 years ago

@tmorehouse thanks for your quick reply!

I've just tried out your proposed workaround and it seems that nested views cannot use named views from "upstream" routes/components. It only works when the named <router-view> is defined inside the parent component.

tmorehouse commented 4 years ago

There is a way to force a particular parent on a component... it requires manual mounting though (note I haven't tested this example code, but I have done similar for other things):

<template>
  <div>
    <router-view ref="parentView" />
    <div ref="portalTarget" />
  </div>
</template>

<script>
import { PortalTarget } from 'portal-vue'

export default {
  mounted() {
    const parentComponent = this.$refs.parentView
    const childElement = this.$refs.portalTarget
    const pt = new PortalTarget({
      el: childElement,
      parent: parentComponent,
      propsData: {
        name: 'modal-portal'
      }
    })
    // Ensure the portal target is destroyed
    this.$once('hook:beforeDestroy', () => pt.$destroy())
  }
}
</script>

EDIT:

This will probably not work since <router-view> is a functional component (which doesn't have a Vue this instance).

You would need to render the portal target inside of the parent router-view, and pass the top route's parent instance to the parent of the dynamically added portal-target. Or one could manually mount the child router-view and pass an explicit parent to it.

boardend commented 4 years ago

As you already stated, I couldn’t get the functional router-view component mounted with a manually set parent component. And when I tried mounting it with a wrapper component (as proposed here https://stackoverflow.com/questions/54239343/how-do-you-unit-test-a-vue-js-functional-component-with-a-render-function-that-r/54277077#54277077) Vue Router didn’t pick up the router-view element.

I’m currently perusing a solution without Portal-Vue which works, but is not as nice if I were able to send the route component to the desired location.

If there isn’t a nice/easy solution to get Vue Router and Portal-Vue working together, we should use this issue to add some notes to the Portal-Vue Known Caveats.

LinusBorg commented 4 years ago

This is indeed a caveat, which is a consequence of the $parentcaveat.

<router-view> internally walks the $parent chain to find out how many (if any) parent <router-view> components there are.

We should indeed add a note to the docs.

Also note that this is not a problem in vue-simple-portal, and will also not be an issue with Vue 3's native <teleport> component.

boardend commented 4 years ago

I've switchd to vue-simple-protal which indeed fixes this problem. Added a small note to the docs anyway, see https://github.com/LinusBorg/portal-vue/pull/307

LinusBorg commented 4 years ago

Thanks! Will close then docs change has been deployed.