Vue-router: Dynamically add child routes to an existing route

Created on 11 Feb 2017  ·  42Comments  ·  Source: vuejs/vue-router


I am building a large scale application with "mini applications" hosted within the application.
Each mini application is being developed by another team.
I would like to dynamically add nested child routes to an existing route to allow dynamic registration of each of the mini apps.

for example:

const routes = [
        {path: 'a', component: ComponentA, name: 'a'},
        {path: 'b', component: ComponentB, name: 'b'},
];

//MiniApp.js
export default Vue.extend({
    beforeCreate () {
        this.$router.addChildRoutes(this.$router.currentRoute.path, routes);
    },
    template: `
<div>
  <div class="page-host">
  <router-link to="{name: 'a'}">a</router-link>
  <router-link to="{name: 'b'}">b</router-link>
  <transition name="fade" mode="out-in">
    <keep-alive><router-view class="view"></router-view></keep-alive>
  </transition>
  </div>
</div>`,
});

//The app itself
Vue.use(Router);
import MiniApp from "./MiniApp";

const config = {
    linkActiveClass: 'active',
    scrollBehavior: () => ({x: 0, y: 0}),
    routes: [
        {path: '/mini', component: MiniApp, name: 'mini'}},
    ]
};
let router = new Router(config);
const vue = new Vue({
   router,
   template: `
<div>
  <div class="page-host">
  <router-link to="/mini">a</router-link>
  <transition name="fade" mode="out-in">
    <keep-alive><router-view class="view"></router-view></keep-alive>
  </transition>
  </div>
</div>`,
}).$mount('#app');
feature request fixed on 4.x group[dynamic routing]

Most helpful comment

Yeah, but I think that taking an optional first parameter for the parent may be better:

// parentName is a string and must match a route's name
addRoutes([parentName, ]routes)

For the moment, nobody is looking into this. We'll come when we can 🙂

All 42 comments

Since the MiniApp is loaded synchronously (not lazy loaded), why not simply expose its routes and use it when creating the router?
e.g.

//MiniApp.js
export const MiniAppRoutes = [
        {path: 'a', component: ComponentA, name: 'a'},
        {path: 'b', component: ComponentB, name: 'b'},
];

export default Vue.extend({...});

//////////////////////////////////////////////////////////

//The app itself
Vue.use(Router);
import MiniApp, { MiniAppRoutes } from "./MiniApp";

const config = {
    routes: [
        {path: '/mini', component: MiniApp, name: 'mini', children: MiniAppRoutes }},
    ]
};
let router = new Router(config);

@fnlctrl you are right when the app is loaded synchronously, but this is just an example.
The real world example will require lazy loading.

But the routes must be loaded synchronously, otherwise when visiting a url that uses a lazy loaded component, the router won't be able to recognize that route.

e.g.

const config = {
    routes: [
        {path: '/mini', component: () => System.import('mini-app'), name: 'mini' }},
    ]
};

Now, when directly visiting /mini/foo, the router won't be able to match it to /mini and load MiniApp, since it has no knowledge of such child routes (foo) yet.

Same case in my project.
In order to disable some routes, the child routes depend on the role of user. And after someon logined, add child routes to an existing route with lazy loading.

@varHarrie For that use case, we already have a better and simpler solution:
Add a check for login(auth) state inside a beforeEach hook, and redirect users elsewhere if they're not logged in, while all routes are statically loaded. Check out navigation guards docs and the auth flow example

Need this also as my app loads modules, each module has a setup method that registers their properties and I need a way to add a child route dynamically.

Tried:

        this._dashboardRoutes = [
            { name: 'dashboard', path: '', component: require('../../pages/dashboard/Index.vue') },
        ];
        this._globalRoutes = [{
            path: '/',
            component: require('../../pages/DashboardWrapper.vue'),
            meta: { auth: true },
            children: this._dashboardRoutes
        }];

        this._router = new VueRouter({
            mode: 'hash',
            scrollBehavior: () => ({ y: 0 }),
            routes: this._globalRoutes,
        });

but after digging inside realized that it generates a route map only when addRoutes is called, so it does not find other routes :\

Honestly, a refresh function would be all I need 🗡
Edit: nwm, it copies the route so it doesn't refresh it from the same list :\

@posva Is there a plan to implement this? Being able to specify parent: "someRoute" when using addRoutes seems like a no brainer. In my case the application has 2 completely different layouts for authed users vs users that have not signed in. A nice approach would be to have everything that is authed under /app for example, but almost all my routes are added dynamically using addRoutes, so there is no way of specifying a parent right now.

Yeah, but I think that taking an optional first parameter for the parent may be better:

// parentName is a string and must match a route's name
addRoutes([parentName, ]routes)

For the moment, nobody is looking into this. We'll come when we can 🙂

I like it @posva, I will take a stab at it as soon as I have some free time and submit a PR.

Could we go one step further and add support for lazy loaded children. This could be done by allowing children to be of types: RouteConfig[] | () => RouteConfig[] | () => Promise<RouteConfig[]>?

We will also need support for deep linking into an async child route.

@patrickhousley It's unrelated to this issue, but you can already lazy load children components

@posva I assume you are talking about being able to lazy load components in general. If so, I am already doing that. What I am saying is it would be nice if we could lazy load the children router configuration.

Lazy load components:

// src/routes.ts
export const routes: VueRouter.RouteConfig[] = [
  {
    path: '/lazy',
    children: [
      {
        path: '/one',
        component: async () => new Promise<Vue>(resolve => {
          require.ensure(
            [],
            async require => {
              resolve((require('./one.component.ts') as { OneComponent: Vue }).OneComponent);
            },
            'lazy'
          );
        })
      },
      {
        path: '/two',
        component: async () => new Promise<Vue>(resolve => {
          require.ensure(
            [],
            async require => {
              resolve((require('./two.component.ts') as { TwoComponent: Vue }).TwoComponent);
            },
            'lazy'
          );
        })
      }
    ]
  }
];

There is nothing wrong with this code. It is just very verbose to read and write.

Lazy loaded children configuration:

// src/routes.ts
export const routes: VueRouter.RouteConfig[] = [
  {
    path: '/lazy',
    children: async () => new Promise<Vue>(resolve => {
      require.ensure(
        [],
        async require => {
          resolve((require('./lazy/routes') as { routes: VueRouter.RouteConfig[] }).Routes);
        },
        'lazy'
      );
    })
  }
];

// /lazy/routes.ts
export const routes: VueRouter.RouteConfig[] = [
  {
    {
      path: '/one',
      component: OneComponent
    },
    {
      path: '/two',
      component: TwoComponent
    }
  }
];

Please take a look to vue-tidyroutes it's a simple package to handle routes registration in your project.

I want the components to be able to add its own routes once they are registered in the app.
I believe that it could be done by having a
"addRoutes([parentName, ]routes)" just like @ktmswzw said.

yeah ,maybe need append some childrens.
"addRoutes([parentName, ]routes)" it's sounds great

:+1:

Much needed feature in our code, to handle an infinte levels hierarchy.
React Router does it : https://reacttraining.com/react-router/web/example/recursive-paths

Nested dynamic routers would be extremely useful. The composability of vue is its strong point, and that shouldn't stop at the router. Does anyone know of an alternative Vue router that has this functionality?

Last night I worked on this, maybe this could be a possible solution or maybe keep 2 parameters in the next PR #2064.

Greetings.
Thanks.

@Dri4n could you explain a little bit how the new behavior from your PR works? I read it but not being very familiar with Vue internally, I'm not sure sure.

Would that allow me to add routes to a router and Vue instance that has already been initiated?

@light24bulbs I'm sorry, but this PR is not yet accepted, basically it's about adding routes to a parent route dynamically (once this instanced vue router), this depends on the name or path of the parent route, if they accept it I will add the corresponding documentation, thank you!

@Dri4n Thanks for the pr. It works in my case.

I have the some issue, I found a solution fortunately. I have a static router options like below:

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/portal',
      name: 'portal',
      component: Portal,
      children: [
        {
          path: 'component1',
          name: 'component1',
          component: Component1,
          meta: {
            requiresAuth: true
          }
        }, {
          path: 'component2',
          name: 'component2',
          component: Component2,
          meta: {
            requiresAuth: true
          }
        }]
    }, {
      path: '*',
      name: 'dispatcher',
      component: Dispatcher,
      meta: {
        requiresAuth: true
      }
    }
  ]
})

i want to add one route 'component3' under the 'portal' parent rule dynamically, below:

const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/portal',
      name: 'portal',
      component: Portal,
      children: [
        {
          path: 'component1',
          name: 'component1',
          component: Component1,
          meta: {
            requiresAuth: true
          }
        }, {
          path: 'component2',
          name: 'component2',
          component: Component2,
          meta: {
            requiresAuth: true
          }
        }, {
          path: 'component3',
          name: 'component3',
          component: Component3,
          meta: {
            requiresAuth: true
          }
        }]
    }, {
      path: '*',
      name: 'dispatcher',
      component: Dispatcher,
      meta: {
        requiresAuth: true
      }
    }
  ]
})

and i construct one routes:

router.addRoutes([
  {
    path: '/portal',
    component: Portal,
    children: [
      {
        path: 'component3',
        name: 'component3',
        component: Component3,
        meta: {
          requiresAuth: true
        }
      }]
  }])

The parent rule should not contain name fields to avoid vue-router internal check logical. and some extra work should do , duplicate url check in your app

@by46 go into your /portal/component3 and refresh the browser. The problem isn't adding routes, the problem is that they do not persist via a page refresh.

@EvanBurbidge when i refresh the browser, my app will reload the routes into the router.

@by46 from experiments myself, i've found if you route into one of these dynamically added routes e.g. component3, and then hit the refresh button it will break. as you're not hitting the parent and then in turn not loading those routes.

@EvanBurbidge Please, move the conversation to a chat, that would avoid spamming everybody here 😄

sorry!

Any update on adding this much needed feature?

I have written a simple library for dynamically add routes to an existing route, vue-mfe/src/core/router/index.js. the idea was inspired from @coxy's comment, thanks for that suggestion.

this library was not only to enhance vue-router, it be designed for Vue micro-frontends solution. so you can copy those code into your project what you need.

hope it can be helpful for someone who needs.

Still waiting for this feature~🌟

https://github.com/vuejs/vue-router/blob/5118824ece97d76654f535dfb498cd0e99787626/src/create-route-map.js#L24-L26

 function createRouteMap (
    routes,
    oldPathList,
    oldPathMap,
    oldNameMap
  ) {
    // the path list is used to control path matching priority
    var pathList = oldPathList || [];
    // $flow-disable-line
    var pathMap = oldPathMap || Object.create(null);
    // $flow-disable-line
    var nameMap = oldNameMap || Object.create(null);

    routes.forEach(function (route) {

/* ---- MODIFICATION:start  --------------------------- */

      let parent  = route.parent && oldNameMap[route.parent.name] ||
                    route.parent && oldPathMap[route.parent.path] ;

      route.parent && route.parent.name && assert(parent , 'Inexistant parent with name :\n\t' + route.parent.name);
      route.parent && route.parent.path && assert(parent , 'Inexistant parent with path :\n\t' + route.parent.path);

      /*addRouteRecord(pathList, pathMap, nameMap, route);*/
      addRouteRecord(pathList, pathMap, nameMap, route, parent);

/* ---- MODIFICATION:end  --------------------------- */


    });

    // ensure wildcard routes are always at the end
    for (var i = 0, l = pathList.length; i < l; i++) {
      if (pathList[i] === '*') {
        pathList.push(pathList.splice(i, 1)[0]);
        l--;
        i--;
      }
    }

. . .

By adding this modification, i think it will be possible to

Dynamically add child routes to an existing route

let initialConfig = [
  {
    name : 'app-home',
    path : '/' ,
    component : lazyLoader('/path/to/app/home.js')
  },
  { 
    name : 'app-404' ,
    path : '*' , 
    component : lazyLoader('/path/to/app/404.js') 
  } , 
];
// classic and actual way to init routes
const myRouter = new VueRouter({
  routes : initialConfig ,
})



/* Later you can do */
let modulesHolder = [
  {
    name : 'app-modules' ,
    path : '/module' ,
    component : lazyLoader('/path/to/app/modules.js')
  }
];
// classic and actual way to add routes
myRouter.addRoutes( modulesHolder ); 




/* Later again you can do */
let moduleTest = [
  {
    name : 'app-module-test' ,
    path : 'test' ,
/* - - New Key = parent : define parent by path  - - - - - - - - */
    parent : { path : '/module' } ,
    component : lazyLoader('/path/to/app/modules/test/index.js')
  }
];

// matched route = /module/test
myRouter.addRoutes( moduleTest ); 






/* Later again you can do */
let moduleOther = [
  {
    name : 'app-module-other' ,
    path : 'other' ,
/* - - New Key = parent : define parent by name  - - - - - - - - */
    parent : { name : 'app-modules' } ,
    component : lazyLoader('/path/to/app/modules/other/index.js')  
  }
];

// matched route = /module/other
myRouter.addRoutes( moduleOther );

@donnysim what happens when you're on a child root and refresh the page with that?

when you're on a child root and refresh the page

Mmm...
I don't really understand, if you refresh the page so the router will be refreshed too ...
But if the router is not refreshed, so you'll keep everything !
Every route is hold by the router !

Once the modification on main code done, it will open possibility ...

myRouter.addAbsentChildRoutes = ( parent, children ) {
    let list = children.filter( child =>
        let result = myRouter.match({path: child.path , name: child.name});
        let haveResult = result.matched && result.matched.length;
        if(!haveResult) return true;
        return result.matched[0].path === '*' ? true : false;
    });
    return myRouter.addChildRoutes( parent, list);
}

myRouter.addChildRoutes = ( parent, children ) {
    /* check and filter what you want */
    return myRouter.addRoutes( children.map( child => { 
        if( ! child.parent ) child.parent = parent;
        return child;
    });
}

/* and more and more . . . */

@posva what do you think about this addition ?

https://github.com/vuejs/vue-router/blob/5118824ece97d76654f535dfb498cd0e99787626/src/create-route-map.js#L24-L26

have to become :

    routes.forEach(function (route) {
      let parent  = route.parent && oldNameMap[route.parent.name] ||
                    route.parent && oldPathMap[route.parent.path] ;

      route.parent && route.parent.name && assert(parent , 'Inexistant parent with name :\n\t' + route.parent.name);
      route.parent && route.parent.path && assert(parent , 'Inexistant parent with path :\n\t' + route.parent.path);

      /*addRouteRecord(pathList, pathMap, nameMap, route);*/
      addRouteRecord(pathList, pathMap, nameMap, route, parent);
    });

And this little modification will permit to dynamically add child routes to an existing route !

Has this been implemented yet??

Apparently no addChildRoutes, but i am interested by this feature.

Currently i use this :

var route = routes.find(r => r.path === parent_route.path)
route.children = [{
    path: route.path + "/" + child_route.alias,
    name: child_route.alias,
    component: child_route.component
}]

router.addRoutes([route])

But he generate a warn : [vue-router] Duplicate named routes definition:

Anybody can't push a commit ?

registering child routes dynamically is a feature that every router (angular/react/...) supports. the priority of this feature should be on highest level IMHO.

@posva It seems from the comments and upvotes that this is a much needed feature. I too need it. Any news where it stands?

@JonathanDn This will be added once https://github.com/vuejs/rfcs/pull/122 is merged. That way we can keep the same api surface for addRoute in Vue Router 3 and 4. addRoutes will likely be deprecated in favor of addRoute too

@posva
Is it possible to add this modification to the 3.x without waiting the 4.x ?
From 11 Nov 2017 to now, we were waiting for this feature, on 10 Sep 2019 was posted this code :

    routes.forEach(function (route) {
      let parent  = route.parent && oldNameMap[route.parent.name] ||
                    route.parent && oldPathMap[route.parent.path] ;

      route.parent && route.parent.name && assert(parent , 'Inexistant parent with name :\n\t' + route.parent.name);
      route.parent && route.parent.path && assert(parent , 'Inexistant parent with path :\n\t' + route.parent.path);

      /*addRouteRecord(pathList, pathMap, nameMap, route);*/
        addRouteRecord(pathList, pathMap, nameMap, route, parent);
    });

It doesn't produce any breaking changes, it just adds such long-awaited functionality.
It is based on your own source code, there is no modification of the functions, it just provides a parameter which was not originally supplied, but present in the signature of the function.

I don't know if Vue-Router 4.x will only be compatible with Vue 3.0, so for now we can have a solution for Vue 2.x and Vue-Router 3.x.

It will be available now!
So I hope you will accept this change to help us.

Thank you for your work and your time.

The lack of this feature is causing a HUGE delay in my current project, and the greatly lacking documentation in regards to addRoutes makes it all the worse. Please release this for the Vue2 Router.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rr326 picture rr326  ·  3Comments

sqal picture sqal  ·  3Comments

shinygang picture shinygang  ·  3Comments

Atinux picture Atinux  ·  3Comments

posva picture posva  ·  3Comments