Warre Buysse
Warre Buysse

Reputation: 1345

Fully dynamic vue-router

We are building an enormous website based on Vue and Nuxt with over 25 different page types that cannot be matched with standard /:id or /overview/:slug logic that comes out of the box with Vue Router.

As slug-matching isn't an option, we are thinking about the following solution:

  1. User visits page "/this-is-a-topic-page"
  2. Server calls API that returns the pageType topicPage
  3. topicPage relates to the nuxt page WpTopicPage
  4. We set WpTopicPage as our component within our wildcard instance of Vue Router

This looks like the following in code:

export function createRouter() {
  return new Router({
    mode: 'history',
    routes: [
      // 1. User visits page "/this-is-a-topic-page"
      {
        name: 'wildcard',
        path: '*',
        component: *, // this should be dynamic
        beforeEnter: (to, from, next) => {
          // 2. Server calls API that returns the pageType `topicPage`
          this.$axios.get(`/call-to-the-api?slug=${to.params.slug}`)
            .then((res) => {
              // 3. `topicPage` relates to the nuxt page `WpTopicPage`
              if(res.data.pageType === 'topicPage') {
                // 4. Set `WpTopicPage` as our Page component
                return WpTopicPage;
              }
            })
        },
      },
    ],
  });
}

The above obviously doesn't work. Is there a way to set the component within a route dynamically in the beforeEnter function?

Upvotes: 7

Views: 2558

Answers (5)

arivero
arivero

Reputation: 955

Our current solution does not violate the requirement of "read-only" to and from parameters in the guard, but it needs an access to the internals of the route to install a proxy, so it is less standard that the recommended method "substitute route and redirect to the substituted one". Yet, it can be useful if you do not want to rewrite the route tree each time.

First we install a guard to save the new route in a global variable. Global enough for the context of the router.

router.beforeEach( (to,from) => {nuevaRuta = to })

We save the to, but it is still "read only". See how: we define the components and the props using proxies:

    var nuevaRuta = 'None'
    const proxyComponentes = {
        get: function (target, prop, receiver) {
          return FriendlyIframe;
        },
        ownKeys: (oTarget, sKey) => { 
          if (nuevaRuta == 'None' || typeof nuevaRuta.params.panes == 'undefined')
             return Reflect.ownKeys(oTarget,sKey)
          else {
             return nuevaRuta.params.panes
          } 
        },
        getOwnPropertyDescriptor(target, prop) { // called for every property
          return {
            enumerable: true , configurable: true  /* ...other flags, probable "value:..." */
          };
        }
      };
   const proxyPropiedades = {
       get: function (target, prop, receiver) {
         console.log(prop)
         if (prop.startsWith('user_')){
           var id = prop.replace('user_','')
           return {src: `/u/${id}?noheader=true`}
         }else{
           return {src: `/p/${window.PRID}/${prop}?noheader=true`}
         }
        }};
    var myComponents = {};  
    var myProps= {}; 

and we install them when building the route match. More precisely, for version 4 of the router, the proxy for components can be installed during the route definition:

routes:[
    {
      name: "generica",
      path: '/:panes+',
      components: new Proxy ( myComponents, proxyComponentes),
      props: false

but props is "cached" via a normalisation, so we need to install it after. This is the most troubling detail, in my opinion

router.getRoutes().find( (e) => e.name=="generica").props = new Proxy (myProps, proxyPropiedades)

Of course, the method could stop working if the router implements caching and further optimisations in the processing of components. If you want a long term safe solution, keep updating and redirecting.

Upvotes: 0

Gordon Freeman
Gordon Freeman

Reputation: 3421

I've struggled some time ago with a similar task. I also needed a fully dynamic router but my app initialising sequence was a bit different.

  1. At the time Vue gets instantiated (@main.js new Vue({...})), I have no routes and no related components.
  2. In the same time I'm asking asynchronously for the initial data from the server (and showing a loading animation)
  3. As soon as it arrives I'm mapping my router

The cool thing is, it is possible to map and re-map the router at any point in time.
I think you can make use of my implementation even with your initialisation sequence.

So here is my router.js

import Vue from 'vue';
import Router from 'vue-router';

Vue.use(Router);

const createRouter = () =>
  new Router({
    mode: 'history',
    linkActiveClass: 'active',
    base: __dirname,
    routes: []
  });

const router = createRouter();

export function resetRouter() {
  const newRouter = createRouter();
  router.matcher = newRouter.matcher;
}

export default router;

Take note that there is the resetRouter function. I think it is self-explanatory what it does.

As soon as your app knows what kind of routes and components need to be mapped/used, you can create the route collection and map them. Like so:

import { default as router, resetRouter } from '@/router';

// ...

let routes = [];

// some magic to fill the routes array
// I.e. items coming from API
items.forEach(item => {
  if (item.view) {
    const component = () => import(`@/views/${item.view}.vue`);

    const route = {
      name: null, // prevent duplicate named routes warning
      path: item.path,
      component,
      meta: {
        title: item.title
      }
    };
    routes.push(route);
  }
});

resetRouter();
router.addRoutes(routes);

Upvotes: 2

arivero
arivero

Reputation: 955

The two answers from @AndrewShmig and @fjemi are excellent ideas, and the only problem is that they do not agree with the expected design of Vue Router. It seems that Vue developers expect "to" to be static except perhaps for a meta field, and the function in component is expected to be a promise that will be cached after first use of the route.

The only way I can see agreeing with the documentation is to use the BeforeEnter hook to push the new component in a "/*" route, mark the meta field, and then return redirect. This will cause a re-entry in the hook, this time with the right component. It is a bit disturbing to keep replacing the route with each entry but it should not cause memory leaks, hopefully. And as we are substituting a pre-existent route, we could expect it to be optimised in the side of the router -but I have not checked-.

Upvotes: 0

fjemi
fjemi

Reputation: 17

Here is a solution.

  • Routes for Home.vue and NotFound.vue are manually set
  • Routes are dynamically created for the other views (*.vue) in the the views directory
  • The path for routes is the lowercase of the .vue file, except for Home.vue, which is set at /. Ex) the path for FileName.vue is /filename

project structure

app
|__router
   |__index.js
|__views
   |__Home.vue
   |__NotFound.vue
   |__*.vue

index.js

import Vue from "vue";
import VueRouter from "vue-router";

// list of files in the `views` directory
const views = require.context(
  `../views`,
  true,
  /^.*\.vue$/
)

Vue.use(VueRouter);

// routes
const routes = [{
  path: "/",
  name: "Home",
  component: import("../views/Home.vue")
}]

// dynamically add all routes in views directory
for (var i = 0; i < views.keys().length; i++) {
  // skip home and notfound since it is defined above
  if (
    views.keys()[i].slice(2, -4) !== 'Home' &&
    views.keys()[i].slice(2, -4) !== 'NotFound' 
    ) {
    // file path for the route
    let filePath = `views/${views.keys()[i].slice(2, -4)}.vue`
    // add routes
    routes.push({
      path: views.keys()[i].slice(1, -4).toLowerCase(),
      name: views.keys()[i].slice(2, -4),
      component: () =>
        import("../" + filePath)
    })
  }
}

// directs any undefined route to not found 
routes.push({
  path: "*",
  name: "NotFound",
  component: () => import("../views/NotFound.vue")
})

const router = new VueRouter({
  routes: routes
});

export default router

Upvotes: 0

AndrewShmig
AndrewShmig

Reputation: 4923

It's possible to do. I have created a codepen for you to test:

Here it is:

Vue.use(VueRouter);

let A = {
  mounted() {
    console.log('Mouted component A');
  },
};
let B = {
  mounted() {
    console.log('Mouted component B');
  },
};
let C = {
  mounted() {
    console.log('Mouted component C');
  },
};

const router = new VueRouter({
  mode: "hash",
  routes: [
    {
      path: '*',
      beforeEnter(to, from, next) {
        let components = {
          default: [A, B, C][Math.floor(Math.random() * 100) % 3],
        };
        to.matched[0].components = components;
        
        next();
      }
    },
  ]
});

app = new Vue({
  router,
  el: '#app',
  components: { A, B, C }
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue-router/3.0.2/vue-router.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
  <router-link :to="'/' + Math.random()">anything</router-link>
  <router-view></router-view>
</div>

This is the output:

enter image description here

As you can see in the console logs - each time something changes we get random component loaded and mounted.

Upvotes: 3

Related Questions