Vue router - how to have multiple components loaded on the same route path based on user role?

10,828

Solution 1

You might want to try something around this solution:

<template>
  <component :is="compName">
</template>
data: () {
 return {
      role: 'seller' //insert role here - maybe on `created()` or wherever
 }
},
components: {
 seller: () => import('/components/seller'),
 admin: () => import('/components/admin'),
 buyer: () => import('/components/buyer'),
}

Or if you prefer maybe a bit more neat (same result) :

<template>
  <component :is="loadComp">
</template>
data: () => ({compName: 'seller'}),
computed: {
 loadComp () {
  const compName = this.compName
  return () => import(`/components/${compName}`)
 }
}

This will give you the use of dynamic components without having to import all of the cmps up front, but using only the one needed every time.

Solution 2

One approach would be to use a dynamic component. You could have a single child route whose component is also non-specific (e.g. DashboardComponent):

router.js

const routes = [
  {
    path: '/',
    name: 'home',
    children: [
      {
        path: '',
        name: 'dashboard',
        component: () => import('@/components/Dashboard')
      }
    ]
  }
]

components/Dashboard.vue

<template>
  <!-- wherever your component goes in the layout -->
  <component :is="dashboardComponent"></component>
</template>

<script>
import AdminDashboard from '@/components/Admin/AdminDashboard'
import SellerDashboard from '@/components/Seller/SellerDashboard'
import BuyerDashboard from '@/components/Buyer/BuyerDashboard'

const RoleDashboardMapping = {
  admin: AdminDashboard,
  seller: SellerDashboard,
  buyer: BuyerDashboard
}

export default {
  data () {
    return {
      dashboardComponent: RoleDashboardMapping[this.$store.state.userRole]
    }
  }
}
</script>

Solution 3

Such code retrieves component code only for a given role:

import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
import store from "../store";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "home",
    component: () => {
      switch (store.state.userRole) {
        case "admin":
          return import("../components/AdminDashboard");
        case "buyer":
          return import("../components/BuyerDashboard");
        case "seller":
          return import("../components/SellerDashboard");
        default:
          return Home;
      }
    }
  }
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

export default router;

Solution 4

You run into the Maximum call stack size exceeded exception because the next({ name: store.state.userRole }) will trigger another redirection and call the beforeEnter again and thus results in infinite loop. To solve this, you can check on the to param, and if it is already set, you can call next() to confirm the navigation, and it will not cause re-direction. See code below:

beforeEnter: (to, from, next) => {
  // Helper to inspect the params.
  console.log("to", to, "from", from)

  // this is just an example, in your case, you may need
  // to verify the value of `to.name` is not 'home' etc. 
  if (to.name) { 
    next();
  } else {
    next({ name: store.state.userRole })
  }
},

Solution 5

I faced the same problem (I use Meteor JS with Vue JS) and I found the way to do it with the render function to load different components on the same route. So, in your case it should be:

import Vue from "vue";
import VueRouter from "vue-router";
import Home from "../views/Home.vue";
import AdminDashboard from "../components/AdminDashboard";
import BuyerDashboard from "../components/BuyerDashboard";
import SellerDashboard from "../components/SellerDashboard";
import store from "../store";

Vue.use(VueRouter);

const routes = [
  {
    path: "/",
    name: "home",
    component: {
      render: (h) => {
          switch (store.state.userRole) {
             case "admin":
               return h(AdminDashboard);
             case "buyer":
               return h(BuyerDashboard);
             case "seller":
               return h(SellerDashboard);
             default:
               return h(Home);
           }
       }
    }
   }
];

const router = new VueRouter({
  mode: "history",
  base: process.env.BASE_URL,
  routes
});

export default router;

Note that this solution also works but only for the first time, if you enter again to that route, the last component loaded it will keep (you will need to reload the page). So, with the render function it always load the new component.

Share:
10,828
mlst
Author by

mlst

Updated on June 07, 2022

Comments

  • mlst
    mlst almost 2 years

    I have app where user can login in different roles, eg. seller, buyer and admin. For each user I'd like to show dashboard page on the same path, eg. http://localhost:8080/dashboard However, each user will have different dashboard defined in different vue components, eg. SellerDashboard, BuyerDashboard and AdminDashboard.

    So basically, when user opens http://localhost:8080/dashboard vue app should load different component based on the user role (which I store in vuex). Similarly, I'd like to have this for other routes. For example, when user goes to profile page http://localhost:8080/profile app should show different profile component depending on the logged in user.

    So I'd like to have the same route for all users roles as opposed to have different route for each user role, eg. I don't want user role to be contained in url like following: http://localhost:8080/admin/profile and http://localhost:8080/seller/profile etc...

    How can I implement this scenario with vue router?

    I tried using combination of children routes and per-route guard beforeEnter to resolve to a route based on user role. Here is a code sample of that:

    in router.js:

    import Vue from 'vue'
    import VueRouter from 'vue-router'
    import Home from '../views/Home.vue'
    import store from '@/store'
    
    Vue.use(VueRouter)
    
    const routes = [
      {
        path: '/',
        name: 'home',
        component: Home,
    
        beforeEnter: (to, from, next) => {
          next({ name: store.state.userRole })
        },
    
        children: [
          {
            path: '',
            name: 'admin',
            component: () => import('@/components/Admin/AdminDashboard')
          },
          {
            path: '',
            name: 'seller',
            component: () => import('@/components/Seller/SellerDashboard')
          },
          {
            path: '',
            name: 'buyer',
            component: () => import('@/components/Buyer/BuyerDashboard')
          }
        ]
      },
    ]
    
    const router = new VueRouter({
      mode: 'history',
      base: process.env.BASE_URL,
      routes
    })
    
    export default router
    

    in store.js:

    import Vue from 'vue'
    import Vuex from 'vuex'
    
    Vue.use(Vuex)
    
    export default new Vuex.Store({
      state: {
        userRole: 'seller' // can also be 'buyer' or 'admin'
      }
    })
    

    App.vue contains parent router-view for top-level routes, eg. map / to Home component and /about to About component:

    <template>
      <router-view/>
    </template>
    
    <script>
    export default {
      name: 'App',
    }
    </script>
    

    And Home.vue contains nested router-view for different user's role-based components:

    <template>
      <div class="home fill-height" style="background: #ddd;">
        <h1>Home.vue</h1>
        <!-- nested router-view where user specific component should be rendered -->
        <router-view style="background: #eee" />
      </div>
    </template>
    
    <script>
    export default {
      name: 'home'
    }
    </script>
    

    But it doesn't work because I get Maximum call stack size exceeded exception in browser console when I call next({ name: store.state.userRole }) in beforeEnter. The exception is:

    vue-router.esm.js?8c4f:2079 RangeError: Maximum call stack size exceeded
        at VueRouter.match (vue-router.esm.js?8c4f:2689)
        at HTML5History.transitionTo (vue-router.esm.js?8c4f:2033)
        at HTML5History.push (vue-router.esm.js?8c4f:2365)
        at eval (vue-router.esm.js?8c4f:2135)
        at beforeEnter (index.js?a18c:41)
        at iterator (vue-router.esm.js?8c4f:2120)
        at step (vue-router.esm.js?8c4f:1846)
        at runQueue (vue-router.esm.js?8c4f:1854)
        at HTML5History.confirmTransition (vue-router.esm.js?8c4f:2147)
        at HTML5History.transitionTo (vue-router.esm.js?8c4f:2034)
    

    and thus nothing is rendered.

    Is there a way I can solve this?

  • mlst
    mlst over 4 years
    Thanks for the answer but it might not be what I need. Ideally I'd like to use webpack import() to dynamically fetch just the chunks of the application required for the current user role. Eg. if user is logged in as admin I don't want to load components for other roles. In your case I would statically import all components in Dashboard.vue and thus deliver them to all users no matter the role. The second thing is that I'd like to have this logic contained in router rather than spread across multiple generic components that are used only as containers for their role-specific versions.
  • Matt U
    Matt U over 4 years
    What about a single child route and setting name to store.state.userRole, then dynamically building the import path based on that value as well?
  • mlst
    mlst over 4 years
    Dynamically building import() path won't work because import() works only with static strings defined upfront at build time. This is required for webpack to know what js files should be bundled etc... But I tried that just in case now and do confirm that as I get runtime exception when I try to load route component dynamically vue-router.esm.js?8c4f:2079 Error: Cannot find module '@/components/Seller/SellerProfile' :(
  • Matt U
    Matt U over 4 years
    Ah, gotcha. Sorry my answer isn't what you were looking for, it was just my first thought. I'm relatively new to Vue (but have some experience in other front end frameworks).
  • Matt U
    Matt U over 4 years
    @mlst out of curiosity (I can't test it at the moment) what if you add a check in beforeEnter such as if (to.name !== store.state.userRole) { next({ name: store.state.userRole }) } ... and then an else { next() }?
  • winklerrr
    winklerrr over 3 years
    Where does compName in your first example come from?
  • Michal Levý
    Michal Levý over 2 years
    Just to clarify - Webpack import() in fact supports "partially dynamic" expressions - Webpack 5 and Webpack 4
  • Michal Levý
    Michal Levý over 2 years
    Just be warned that this method will not work if your app supports logout/login without full page refresh. Because Router will call that component function only once and then cache and reuse the component resolved from the Promise returned by the import