Vue-router: Allow nested routes' components to be mounted at root-level

Created on 13 Jun 2017  路  34Comments  路  Source: vuejs/vue-router

What problem does this feature solve?

I'm developing a Quasar app where I have, for example, the following route structure:

const router = new VueRouter({
  routes: [
    {
      path: '/clients',
      component: Clients,
      children: [
        {
          path: ':id',
          component: Client,
          children: [
            {
              path: 'visits',
              component: ClientVisits
            }
          ]
        }
      ]
    }
  ]
})

In all three routes I have a beforeRouteEnter guard that fetches data from an API and stores that data in a Vuex instance. Furthermore, ClientVisits uses the data that was fetched and stored in Client.

In terms of UI design, though, I do not want ClientVisits to be shown as a nested child view; it needs to be at the top level. If you know Android development, ClientVisits should work as an Activity and not a Fragment.

What does the proposed API look like?

Perhaps if Client had no <router-view> in its template, children would then be mounted at root level?

Most helpful comment

Just ran into this myself, making some similar incorrect assumptions as the above commenters.

You don't even really have to create a component for your extra router-view as you can just pass a plain object with a template property.

{ 
  path: '/projects',
  component: { template: '<router-view/>' },
  children: [
    { path: '', component: Projects },
    { path: 'new', component: NewProject }
  ]
},

All 34 comments

Another option could be to have named <router-views> (you couldn't use the name prop, obviously, as that's taken) where each route definition could specify where it wants to be rendered into?

I don't know why you want to put that view at the top level. Maybe you want to get a look at https://github.com/LinusBorg/portal-vue.
From what I understood you should be able to use named views to achieve it as well

I don't know why you want to put that view at the top level.

@posva, like I mentioned, this is for a mobile app. You probably see this behavior everyday when using your phone. https://stackoverflow.com/questions/20306091/dilemma-when-to-use-fragments-vs-activities. But I imagine this might be desirable in other cases, too.

If you can look at this codepen, you'll see a few things:

  • I have 4 components, all of which should be rendered at the top level. Some are nested.
  • The way I've configured Activity12 is how I'm currently doing it in my project. It is configured as a root-level route even though its path (/activity1/activity12) indicates it is a child of Activity1. Imagine Activity1 fetches some data from an API which Activity12 also needs. Now that data has to be fetched in Activity12, too, because Activity1's beforeRouteEnter will not be called when loading /activity1/activity2 directly (during development and pressing F5 and possibly in production when the OS puts my app in a dormant state and is later reactivated). This will make normal usage sluggish, having to fetch the data twice during navigation.
  • Activity1 and Activity11 are using portal-vue.
  • It definitely works for Activity1, though not in an ideal way since I had to do a few things to set it up:

    • Add a meta field to its route configuration.

    • Set up portals

    • In my root component, check if the current route has top === true. If so, hide its intended DOM so only portal-target is visible.

  • Activity11 does not work. Maybe something to do with portal-vue and 2 components trying to show their content in one target at the same time? Basically, nested components which need rendering at the top level cannot use this approach.

Honestly, I like the portal-vue approach, TBH. I could probably do something slick and get rid of the top meta field and see if portal-vue has an API that'll tell me if something is showing there. If so, hide Main's content.

Nested activity components would still be an issue.

see if portal-vue has an API that'll tell me if something is showing there

It doesn't at the moment, but I'm working on it. ;)

@LinusBorg, that's cool. Should I create an issue about the thing where it wont let me send a second component to the same portal target, or am I doing something wrong in my code?

If I can somehow resolve that, I would happily close this issue.

I'll have yet to take a look. Feel free to open an issue as a reminder for me. 馃槒

Hi everyone,

I think that I have the same issue.

I made a codepen to make my point easier to understand:
https://codepen.io/hekigan/pen/LLKZvW

We are building a fairly big site and the paths might change at some point in time.
Given that we have some nested paths, I want the route json to reflect that BUT at the same time, I don't want to use a <router-view> in a <router-view> in a <router-view>, etc...

It would be good to have a proper separation URL / templates.
A nested URL should not mean nested templates.
At the moment, the URL (path) dictates how I have to structure my pages. And I don't think it's a good thing to do.

99% of the sub level pages are full pages. Not embedded in other pages.
I could not find a clean way to do that.

thank you for your time

So if I understand correctly, you have a flat route structure (only one <router-view>), but personally like to write the routes like they were nested because it makes more semantic sense to you.

In that case, instead of vue-router changing all of the fundamental concepts and internals to accomodate this, you could write a small helper function that converts this:

{
const routerOptions = {
  routes: [
    { path: '/', name: 'home', component: Default },
    { path: '/foo',
      children: [
        {  path: '', name: 'foo', component: Foo },
        { path: '/bar', name: 'bar', component: Bar },
        { path: '/baz', name: 'baz', component: Baz }
      ]
    }
  ]
}

to this:

const routerOptions = {
  routes: [
    { path: '/', name: 'home', component: Default },
    {  path: '/foo', name : 'foo', component: Foo },
    { path: '/foo/bar', name: 'bar', component: Bar },
    { path: '/foo/baz', name: 'baz', component: Baz }
    }
  ]
}

befoe handing the routes to new VueRouter().

@LinusBorg Thank you for your quick reply.

Yes, that's what I would like, and I was wondering if I missed an option somewhere to be able to do what I wanted. Guess there is none at the moment.

I totally understand that changing the whole thing is a big breaking change.
But what about about taking the middle ground? What I mean is, what about an extra option in the path object to adapt to this?

something like:
{ path: '/foo/bar', name: 'bar', component: Bar, root: true }

root: true could stop at the first router-view tag.

There are other ways possible obviously.

I did not read yet the internals of vue-router, but I don't imagine that it would be a big impact.
But again, is it something that needs to be added? That will depends on you guys.
I do feel that a lot of people might be interested in that though.

In the meanwhile, I will have to do as you suggested and make a helper to adjust my needs.
I am not liking the slight overhead, but maintenance is a big things and I prefer to think long term.

Thank you again

to be honest and a bit blunt, you can already write this way:

 routes: [
    { path: '/', name: 'home', component: Default },
    {  path: '/foo', name : 'foo', component: Foo },
    { path: '/foo/bar', name: 'bar', component: Bar },
    { path: '/foo/baz', name: 'baz', component: Baz }
    }
  ]

The only advantage your request has it that its a bit more readable (to you) with nesting. To be, it sounds rather confusing, espcially if we think about how an options uch as the one you proposed (root:true) would mean that some children are really root routes, and some aren't - so the visual guidance of the nesting structure becomes a double meaning.

The solution with a helper function is totally doable and requires virutally no additional code once you implemented it.

Right now, I don't see a worthwhile advantage to implement this specifically.

I agree with your remark about root: true, it's just a quick random solution I thought of so as not to break the existing implementation. But yes, not the best solution out there for sure.

And concerning the readability, it depends on each project.
The one I am building is going to have several hundreds of urls. So indentation for sub-routes is a big plus for readability.

We can write javascript with only 1 line too. But it's not very readable passed a certain number of characters. :)

Anyway, we can close this matter.
And I really do appreciate the speed for the reply. Thanks and keep up the good work.

It's a shame to see this pushed aside. This isn't a matter of "semantics" or "readability" - it's entirely about having a _useful_ data structure for working with routes.

Lets consider a concrete example: Breadcrumbs.

The router is basically useless here. There's a single $route.match. You're stuck having to hint at the hierarchy by hand, either through metadata on the route or otherwise.

IMO, the nested <router-view> behavior shouldn't be the default. It would be great if Vue just let us structure the hierarchy of our routes _accurately_.

An idea off the top of my head: Include a secondary key. { 'path' | 'scope': String }. path would be akin to OP's root: true option, and scope would render the nested <router-view>'s.

Or switch it around so it's backwards compatible. I just find this way more clear because in my mind a scope "cascades".

Would love to hear your thoughts.

Hi, thanks for joining the discussion.

I'm not sure that your breadcrumbs example applies to what was suggested here?

But in any case your analysis on this one is wrong. $route.matched gives you an array of all matches components in the hierarchy, from which you can automate bread crumbs creation very nicely, in fact.

So given that your example doesn't apply, can you give another use case of this that isn't only a "nicer" way to write the route definitions for some cases, which I already discussed above?

I'm not categorically opposed to such niceties, but this one would require significant rewrites of some core mechanisms, and I don't think we can invest the time necessary to do that for the benefits I've seen so far, in terms of a cost/benefit ratio, so someone else would have to volunteer in this.

I think you may be misunderstanding. Hopefully this jsfiddle clears things up. As you can see, $route.matched does not in fact give you all matched routes when using "root" paths. This is my issues I have with it. It only works when using children routes, but that's undesirable when you don't want the "cascading" effect of <router-view>.

I think what @sudochop wants is something like this.

The example dynamically displays a page title, menu, and breadcrumbs based on route hierarchy. The actual page content does not work due to the issue being discussed here. portal-vue still does not handle multiple components trying to render content to the same target.

This sort of thing can easily be done in Ember with named outlets.

Correct. I want the ability (as I imagine others would) to structure routes with children - but w/o the cascaded <router-view> effect - so I can make use of the $route.matched data structure.

I'm on the same boat, actually. I'm working on a new boilerplate for an SPA needing this exact functionality and am running into the same issue I had with Quasar.

 routes: [
    { path: '/', name: 'home', component: Default },
    {  path: '/foo', name : 'foo', component: Foo },
    { path: '/foo/bar', name: 'bar', component: Bar },
    { path: '/foo/baz', name: 'baz', component: Baz }
    }
  ]

There are many advantages to writing routes in an actual hierarchy instead of flat as in the quoted example. Chaining route guards for non-redundant fetching of data and for authorization (e.g. no access to /admin means no access to /admin/users). Auto-generation of menus, breadcrumbs. There are probably several more.

I think it is a worthwhile task to allow for optional exclusion of <router-view> and instead render children anywhere in the app depending on the route's configuration. For example a route defined as { path: 'users', component: Users, renderTo: 'content' } would target a router view such as <router-view as-target="content"></router-view>.

If its parent has no <router-view> a component should be rendered at the target <router-view> specified in route configuration, but if there is none specified we should be able to have a default one. Something like <router-view default></router-view> . There could be several defaults in the visual tree, but the component will be rendered at the closest ancestor.

I'm having this issue as well. I assumed routes contained in the children array would replace the entire root component with the child component.

The current implementation is strange in my opinion.

It would just make more sense as other users have mentioned here that having the meta passed down to child components to require auth is my main use here.

Otherwise, I have to insert the meta tag with meta: { auth: true } into every single route that requires authorization.

I ran into this same problem when trying to implement breadcrumbs as well.

I don't want the nested router views, but because the behavior is enforced, I have to switch to a flat structure. Switching to the flat structure loses the $route.matched associations that are needed for breadcrumbs.

The default behavior of nested routes is strange to me, especially because this behavior can't be overridden.

Agreed @Dylan-Chapman.

This leads to some immense code duplication depending on the size of your project, and worse, if you forget to add the meta tag to the route when your list gets massive, you have some unprotected routes (and yes I know we shouldn't be relying on javascript authorization, but still an issue).

Is it maybe possible to check if there is no component property in a parent route object, that it utilizes the children's component as the root?

I think this would be a nice feature and wouldn't need a new major version since there would be no BC issues.

Thoughts?

+1 to add an option to render eveything at the root "router-view"

Just ran into this myself, making some similar incorrect assumptions as the above commenters.

You don't even really have to create a component for your extra router-view as you can just pass a plain object with a template property.

{ 
  path: '/projects',
  component: { template: '<router-view/>' },
  children: [
    { path: '', component: Projects },
    { path: 'new', component: NewProject }
  ]
},

I wrote a plugin for this problem;

https://github.com/abdullah/safranbolu

@jondavidjohn, Excellent answer!

This can also be solved by conditionally toggling the content of the root page:

_Clients.vue_

<template>
    <div v-if="!isCurrentPageRootLevel">
        Content
        <router-view/>
    </div>
    <router-view v-else />
</template>
isCurrentPageRootLevel () {
    return this.$route.meta.root
}

(this would imply adding a root: true attribute in route declaration)

Regarding https://github.com/vuejs/vue-router/issues/1507#issuecomment-345490711, if you don't want to create a component but need to display the nested route instead, you can also use a render function (to not include the full build of vue):

component: { functional: true, render: h => h('router-view') },

Another solution would be creating a custom RouterView component that handles this logic for you using whatever technique you want (could be using the one explained above) and matches better that mobile-specific behavior

Based on https://github.com/vuejs/vue-router/issues/1507#issuecomment-404592319 and https://github.com/vuejs/vue-router/issues/1507#issuecomment-404592319 comments I was able to use my breadcrumbs in a hierarchical order. Taking advantage of ES6 object spreading syntax I implemented it as follow for a more readable code:

const parentRoute = { component: { render: h => h('router-view') } }

{ 
  path: '/projects',
  ...parentRoute,
  children: [
    { path: '', component: Projects },
    { path: 'new', component: NewProject }
  ]
},
{ 
  path: '/tasks',
  ...parentRoute,
  children: [
    { path: '', component: Tasks },
    { path: 'new', component: NewTask }
  ]
},

Based on @ThePJMP , I tried to create my breadcumbs as follows:

      <v-breadcrumbs>
        <v-breadcrumbs-item
          v-for="route in $route.matched"
          v-if="route.meta.fullname"
          :to="route"
          exact>
          {{ route.meta.fullname }}
        </v-breadcrumbs-item>
        <v-icon slot="divider">mdi-arrow-right-bold</v-icon>
      </v-breadcrumbs>

But when I click on any link on the breadcrumbs, I am directed to the parentRoute, which contains nothing on the view. Is there any way I could 'force' the route to go to the child with path: ''?

@jondavidjohn Thank you, you saved my day! I was struggling and frustrating about why vue-router not taking my nested children array for 2 hours!!

I changed a little about that. (to use vue runtime only)

{ 
  path: '/projects',
  component: { render: h => h('router-view') },
  children: [
    { path: '', component: Projects },
    { path: 'new', component: NewProject }
  ]
},

Any updates on this issue? Has this been moved or solved? This is something I鈥檓 running into myself.

Also running into this, and this doesn't seem to be an uncommon problem for a lot of people. While its true that it could be accomplished by listing a bunch of routes at the same level and "nesting" them by defining their full paths, as many other people have mentioned, it provides a lot of value having them structured properly with children properties (eg. breadcrumbs, and programmatically rendering menus, etc.)

EDIT: As a super-cheap-and-hacky way of accomplishing this, instead of using the default <router-view /> component, its fairly easy to wire up a custom component to use instead of <router-view/>...

export default {
  render(ce) {
    if (!this.$route.matched.length) {
      return;
    }
    return ce(this.$route.matched.slice(-1)[0].components.default);
  },
};

This does not account for firing any vue-router lifecycle events, or caching, or acting as a cheap functional component (which I believe the component does?) Its also only lightly tested, but works for me. Hope it helps someone else.

@posva Can we re-open this for consideration?

Please check https://github.com/vuejs/vue-router/issues/1507#issuecomment-404592319 for a solution

Was this page helpful?
0 / 5 - 0 ratings

Related issues

druppy picture druppy  路  3Comments

yyx990803 picture yyx990803  路  3Comments

thomas-alrek picture thomas-alrek  路  3Comments

shinygang picture shinygang  路  3Comments

saman picture saman  路  3Comments