Vue router - how to have multiple components loaded on the same route path based on user role?
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.
mlst
Updated on June 07, 2022Comments
-
mlst almost 2 years
I have app where user can login in different roles, eg.
seller
,buyer
andadmin
. 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
andAdminDashboard
.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 pagehttp://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
andhttp://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
/
toHome
component and/about
toAbout
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 callnext({ name: store.state.userRole })
inbeforeEnter
. 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 over 4 yearsThanks 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 over 4 yearsWhat about a single child route and setting
name
tostore.state.userRole
, then dynamically building theimport
path based on that value as well? -
mlst over 4 yearsDynamically building
import()
path won't work becauseimport()
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 dynamicallyvue-router.esm.js?8c4f:2079 Error: Cannot find module '@/components/Seller/SellerProfile'
:( -
Matt U over 4 yearsAh, 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 over 4 years@mlst out of curiosity (I can't test it at the moment) what if you add a check in
beforeEnter
such asif (to.name !== store.state.userRole) { next({ name: store.state.userRole }) }
... and then anelse { next() }
? -
winklerrr over 3 yearsWhere does
compName
in your first example come from? -
Michal Levý over 2 years
-
Michal Levý over 2 yearsJust 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 theimport