13
votes

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?

6

6 Answers

6
votes

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.

5
votes

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;
3
votes

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>
1
votes

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 })
  }
},
1
votes

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.

-1
votes

One way to solve this is to create three separate components DashboardForAdmin, DashBoardForSeller, and DashBoardForBuyer for three types of users.

Then use a mixin.js

export default {
    data: function () {
        return {
          userType : "buyer"; // replace this with a function that returns "seller", "buyer", or "admin"
        }
    }
}

Create a Vue component DashboardContainer renders the correct dashboard component based on mixin return value

    <template>
        <div>
            <div v-if="userType === 'admin'">
                <DashboardForAdmin />
            </div>
            <div v-else-if="userType === 'buyer'">
                <DashboardForBuyer />
            </div>
            <div v-else>
                <DashboardForSeller />
            </div>
        </div>
    </template>

    <script>
        import mixin from '@/mixin.js';

        import DashboardForAdmin from '@/components/DashboardForAdmin.vue';
        import DashBoardForSeller from '@/components/DashBoardForSeller.vue';
        import DashBoardForBuyer from '@/components/DashBoardForBuyer.vue';

        export default {
            mixins: [mixin],
            components: {
                DashboardForAdmin, DashBoardForSeller, DashBoardForBuyer 
            },
        };
    </script>

Now you can add a single route for the DashboardContainer