First of all I'm learning Vue JS 2 since about a week and I'm stuck on a problem with Vue Router. I'm pretty sure the solution is right in front of my eyes but I wasn't able to find it yet.
So : On my root route I have 2 views in components Home and NotAuth.
My aim is to display the Login component and others routes related to it when the user isn't logged.
And when the user logs in the Home component is displayed on '/' and all login related routes are not accessible by him.
It works when I'm logged if I try to access '/register' Im instantly redirected to 'Home'.
But when I'm not logged if I try to access say '/add_device' a blank page is displayed.
What I'd like to do is define other routes as Home's children but I cant find anything in the doc explaining how to do so.
I've tried several things and none worked and I'm kind of lost on how to proceed.
If anyone has a clue on how to proceed that would be awesome.
Thanks !!
index.js
const routes = [
{
path: '/',
name: '',
components: {
login: NotAuth,
default: Home
},
children: [
{
path: '/',
name: 'Login',
component: Login
},
{
path: '/register',
name: 'Register',
component: Register
},
{
path: '/reset',
name: 'ResetPassword',
component: ResetPassword
},
{
path: '/reset-sent',
name: 'ResetSent',
component: ResetSent
},
{
path: '/change-password/:token/:mail',
name: 'ChangePassword',
component: ChangePassword
},
{
path: '/confirmation',
name: 'Confirmation',
component: Confirmation
}
]
},
{
path: '/add_device',
name: 'device',
component: Device
},
{
path: '/add_scene',
name: 'scene',
component: Scene
},
{
path: '/add_room',
name: 'room',
component: Room
},
]
App.vue
<div id="app">
<router-view v-if='userAuthenticated' :username='this.user.first_name' :user='this.user'></router-view>
<router-view name="login" v-else #isAuth='handle'></router-view>
</div>
I have created a sort of First page for my project, but I don't know why it renders everything twice on my page:
here is my First.vue component that is used by the router as the first page:
<template>
<h1>Bienvenue</h1>
<router-view></router-view>
<div class="routing" v-if="this.$route.path == '/'">
<router-link to='/app'>Go to application</router-link>
<br>
<router-link to='/test'>Go to test</router-link>
</div>
</template>
and here is what I get on the page when I npm run serve
Does anyone knows where it comes from?
UPDATE
When I delete the router-view element, the components appear once but when I click on one of the links, it changes the URL of the page but the page in itself is not showing the component.
And when I try to put everything in my router-view, like this:
<template>
<router-view>
<div class="routing" v-if="this.$route.path == '/'">
<h1>Bienvenue</h1>
<router-link to='/app'>Go to application</router-link>
<br>
<router-link to='/test'>Go to test</router-link>
</div>
</router-view>
</template>
it appears once, but like the other case, when I click on a link, it is just changing the URL and not the page.
Here is my index.js to show you how my routes are defined:
import {createRouter, createWebHistory} from 'vue-router'
import App from '../App.vue'
import Test from '../Views/Test.vue'
import First from '../Views/First.vue'
export const router = createRouter({
history: createWebHistory(),
routes: [
{
path:'/',
name: 'First',
component: First,
},
{
path:'/test',
name: 'Test',
component: Test
},
{
path:'/app',
name: 'App',
component: App
}
]
})
Just to clarify what results I expect from my app:
When I run the project, I want to launch on a page with just a header saying 'Hello' at the top of the page and two links where I can either click on 'Go to the application' or 'Go to test'.
Then, when I click on one of the links, I want to see the content of the component (so either test or the app), but I don't want to see the header and the links anymore.
I am not sure but try this:
{
path: "/app",
component: () => import("../App.vue"),
name: "App"
},
{
path: "/test",
component: () => import("../Views/Test.vue"),
name: "Test"
},
{
path: "/",
component: () => import("../Views/First.vue"),
name: "First"
}
Update with sample code or you can refer to live code here.
// App.vue
const First = {
template: `<div class = "container" id="app">
<h1>Hi from First</h1>
<hr>
<router-link to="/foo">Foo</router-link>
<router-link to="/bar">Bar</router-link>
</div>
`
}
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
const routes = [
{
path:'/',
name: 'First',
component: First,
default: First
},
{
path:'/foo',
name: 'Foo',
component: Foo
},
{
path:'/bar',
name: 'Bar',
component: Bar
}
]
const router = new VueRouter({
routes
})
const app = new Vue({
router
}).$mount('#app')
// main.js
<div id="app">
<!-- route outlet -->
<!-- component matched by the route will render here -->
<router-view></router-view>
</div>
I'm having trouble writing vue. I am using vue and vuetify.
There are A andB pages. There is no problem with entering the A orB page only once.
However, when the page is entered as below, the created function of A is called twice.
A -> B -> A
menuselector.vue
<template>
<v-list>
<template v-for='(eachmenu) in menu'>
<v-list-item
:to='eachmenu.path'
>
<v-list-item-title>
{{eachmenu.title}}
</v-list-item-title>
</v-list-item>
</template>
</v-list>
</template>
<script>
export default {
name: 'selector',
data() {
return {
menu: [
{
title: 'A',
path: '/A',
}
{
title: 'B',
path: '/B',
}
]
}
}
}
</script>
router.vue
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
redirect: '/A',
component: TestComponent,
children: [
{
path: 'A',
component: () => import('#/component/A.vue'),
name: 'Acomponent',
},
{
path: 'B',
component: () => import('#/component/B.vue'),
name: 'Bcomponent',
}
]
}
]
})
A.vue&B.vue
<template>
test
</template>
<script>
export default {
beforeCreate() {
console.log('beforeCreate');
}
created() {
console.log('created');
}
}
</script>
Console output is below.
what was problem???
I don't think "loading" is correct word here. Code needed for component is loaded from server only once as you can check in Dev Tools Network tab
When switching routes, component for old route is destroyed and component for new route is created. Its default behavior of Vue dynamic component (it is what <router-view> uses for switching components). You can change that by using <keep-alive>. Be sure to check documentation and understand implications - your app will use more memory
<keep-alive>
<router-view :key="$route.fullPath"></router-view>
</keep-alive>
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?
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.
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;
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>
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 })
}
},
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.
Vue Router 4 (Vue 3)
If you are using Vue Router 4 (usable only with Vue 3), one alternative solution is to use dynamic routing
This new feature allows us to remove/add routes on the fly.
// router.js
import { createRouter, createWebHistory } from 'vue-router'
import store from "../store";
import Home from "../views/Home.vue";
import About from "../views/About.vue";
// all routes independent of user role
const staticRoutes = [
{
path: "/",
name: "home",
component: Home,
},
{
path: "/about",
name: "about",
component: About,
},
]
const getRoutesForRole = (role) => {
// imlementation can vary - see the rest of the answer
}
// routes used at app initialization
const initialRoutes = [...staticRoutes, ...getRoutesForRole(store.state.userRole)]
const router = createRouter({
history: createWebHistory(),
routes: initialRoutes,
})
export default router
export const updateRoutesForRole = () {
// implementation can vary - see the rest of the answer
}
How to generate dynamic routes - getRoutesForRole
The implementation of course depends on many factors - how many routes (and also roles) do you have is probably most important.
With just 2 or 3 routes (and not many roles) it is just fine to use a static definition:
const routesPerRole = {
"admin": [
{
path: "/dashboard",
name: "dashboard",
component: () => import("../components/AdminDashboard.vue")
}, // more routes follow....
],
"seller": [
{
path: "/dashboard",
name: "dashboard",
component: () => import("../components/SellerDashboard.vue")
}, // more routes follow....
],
"buyer": [
{
path: "/dashboard",
name: "dashboard",
component: () => import("../components/BuyerDashboard.vue")
}, // more routes follow....
],
}
const getRoutesForRole = (role) => {
if(!role) return []
return routesPerRole[role]
}
If you have many routes and/or many roles, you probably want something more generic. First we need some good naming convention - for example lets say that we will organize our components in a directory structure like this: #/components/${role}/${componentName}.vue
Then we can use Webpacks dynamic import
const routeTemplates = [
{
path: "/dashboard",
name: "dashboard",
component: 'Dashboard'
},
]
const getRoutesForRole = (role) => {
if(!role) return []
const routesForRole = routeTemplates.map(route => ({
...route,
component: () => import(`#/components/${role}/${route.component}.vue`)
}))
return routesForRole
}
Note that thanks to how import() with dynamic expression works in Webpack this will make Webpack to create new JS chunk for each component in #/components folder which may be not what you want.
Easy fix is to move the "role dependent" components into it's own subfolder so instead of using #/components/admin/.... just use #/components/perRoleComponents/admin/.... and
import(`#/components/perRoleComponents/${role}/${route.component}.vue`)
Other solution is to use different import() statement for each role. This will also allow us to use Webpacks "magic comments" and for example force Webpack to pack all components for each role into single js chunk:
const routeTemplates = [
{
path: "/dashboard",
name: "dashboard",
component: 'Dashboard'
},
]
const getComponentLoader = (role, componentName) => {
switch(role) {
"admin": return () => import(
/* webpackChunkName: "admin-components" */
/* webpackMode: "lazy-once" */
`#/components/admin/${componentName}.vue`)
"seller": return () => import(
/* webpackChunkName: "seller-components" */
/* webpackMode: "lazy-once" */
`#/components/seller/${componentName}.vue`)
"buyer": return () => import(
/* webpackChunkName: "buyer-components" */
/* webpackMode: "lazy-once" */
`#/components/buyer/${componentName}.vue`)
}
}
const getRoutesForRole = (role) => {
if(!role) return []
const routesForRole = routeTemplates.map(route => ({
...route,
component: getComponentLoader(role, route.component)
}))
return routesForRole
}
How to update routes - updateRoutesForRole()
Easiest scenario is when each role has same set of routes and just wants to use a different component. In this case to switch the routes when role changes we can just use addRoute
Add a new route record to the router. If the route has a name and there is already an existing one with the same one, it removes it first.
export const updateRoutesForRole = () {
const role = store.state.userRole
const routesForRole = getRoutesForRole(role)
routesForRole.forEach(r => router.addRoute(r))
}
For more complicated scenarios where not all routes are available for all roles, previous routes (for previous active role - if any) must be removed 1st using removeRoute function. Also our getRoutesForRole() must be different. One solution is to use route meta fields
const routeTemplates = [
{
path: "/dashboard",
name: "dashboard",
component: 'Dashboard',
meta: { forRoles: ['admin', 'seller'] }
},
]
const getRoutesForRole = (role) => {
if(!role) return []
const routesForRole = routeTemplates
.filter(route => route.meta?.forRoles?.includes(role))
.map(route => ({
...route,
component: () => import(`#/components/${role}/${route.component}.vue`)
}))
return routesForRole
}
export const updateRoutesForRole = () {
const role = store.state.userRole
// delete previous 1st
router.getRoutes()
.filter(route => route.meta?.forRoles)
.forEach(route => router.removeRoute(route.name))
const routesForRole = getRoutesForRole(role)
routesForRole.forEach(r => router.addRoute(r))
}
Router v3 (for Vue 2)
Note that Router v3 (and earlier) was never designed with dynamic routing in mind. There is no removeRoute() function. There is a addRoute() so some of the scenarios described above could be probably possible but it currently (Router v3.5.3) does not work as described in the documentation
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
This is my app-routing-module.ts
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{
path: 'parent',
component: ParentComponent,
children: [
{ path: ':id/:name', component: ChildComponent }
]
}
]
},
];
I've written a function to check if the URL is working with a static value in parent.component.ts.
goToChild() {
this.router.navigate(['1/john'], { relativeTo: this.route });
}
And I call the function in the parent.component.html.
<button class="btn btn-primary" (click)="goToChild()">Search</button>
When I click the button, I get the correct URL in the address bar,
localhost:3000/parent/1/john
But the view never loads, it stays on the parent.component.html. I'm fairly new to Angular and I'm using version 5.
And if I have my routes like this, it works fine.
const routes: Routes = [
{
path: '',
component: LayoutComponent,
children: [
{
path: 'parent',
component: ParentComponent
},
{
path: 'parent/:id/:name', component: ChildComponent
}
]
},
];
It feels more appropriate for me to put the ChildComponent route under children array of the ParentComponent, but when I do it. Only the URL changes, not the view.
Any help will be much appreciated.
You need to add a <router-outlet></router-outlet> inside the template. That’s how you allow child navigation