Ive been stuck on the best way to manage a header navigation in Vue 2, vuex with VueRouter.
I have a main 'header' type navigation in my App.vue
<div id="nav">
<!--Try and create a dynamic nav-->
<span v-for="route in routes" :key="route.to">
<router-link :to="`${route.to}`">{{route.text}}</router-link> |
</span>
</div>
<b-container>
<router-view />
</b-container>
</div>
I was hoping as, the user navigates around the page and into other views and components within views. That i could update the 'routes' array to push the current location they are in. Obviously this is very Non Vue since it would require a global variable of some sort. I was curious if the best way to go about updating a header nav when you are working with nested routes.
Example of routes:
{
path: '/Teams',
name: 'Teams',
component: Teams
},
{
path: '/:teamName/Athletes',
name: 'Athletes',
component: Athletes
},
{
path: '/:teamName/Athletes/:id',
name: 'Athlete',
component: Athlete
}
What i would like is for when you are on the Athlete page, you could have a route back to athletes ON the App.vue header nav, that is dynamic with the selected :teamName. That could be removed and added as necessary.
Example of user story: /Teams-> Selects team 'Team1' -> Sends them to /Team1/Atheltes -> clicks an athlete -> sends them to /Team1/Athlete/1. In the header nav (Which is cucrently in App.vue) how can i add a router-link to include the appropriate ':teamName' to be able to go back to /Team1/Athletes?
It seems to use Vuex will simplify your work:
Put your routes on a state:
// ...
state: {
routes: [{
path: '/Teams',
name: 'Teams',
}]
}
/Teams-> Just remove others routes that can be added.
// ...
mutations: {
deafultRoute(state, payload) {
state.routes = state.routes.slice(0, 1); // Be sure to have only 1 route
}
}
/Team1/Atheltes -> Add the team route for team:
// ...
mutations: {
addTeamRoute (state, payload) {
state.routes = state.routes.slice(0, 2); // Be sure to have only 2 routes
let newPAth = {
path: `/${payloadteamName}/Athletes`,
name: 'Athletes'
}
state.routes[1] = newPAth
}
}
/Team1/Athlete/1. -> Add the team route for team:
// ...
mutations: {
addAthleteRoute (state, payload) {
state.routes = state.routes.slice(0, 3);
let newPAth = {
path: `/${payload.teamName}/Athletes/${payload.id}`,
name: 'Athlete'
}
state.routes[2] = newPAth
}
}
I think you donĀ“t need to save Component on routes array.
I do something like this:
{
path: '/Teams',
name: 'Teams',
component: Teams,
meta: {
module:'Teams'
}
}
You can then use the meta property to direct your "back" (among other things)
Related
Using Vue 3, i have my router file set up this way
import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/Home.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home,
},
{
path: "/Portfolio",
name: "Portfolio",
component: () =>
import(/*webpackChunkName: "DestinationDetails" */ "../views/Portfolio"),
},
{
path: "/Services",
name: "Services",
component: () =>
import(/*webpackChunkName: "DestinationDetails" */ "../views/Services"),
},
{
path: "/details/:id",
name: "PortfolioDetails",
component: () =>
import(
/*webpackChunkName: "DestinationDetails" */ "../views/PortfolioDetails"
),
},
{
path: "/:pathMatch(.*)*",
redirect: "/Home",
},
];
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes,
});
export default router;
I am also running a v-for loop to get paths from an API "https://api.fake.rest/ca2a6662-22d0-4010-ba08-0440ffe813ab/menu". 3 of the 5 url paths have a value of "#". the remaining two have normal paths.
<div
v-for="(men, index) in webMenu.menu_items"
:key="index" class=" mt-32"
>
<!-- <SidebarLink class="w-full" :to="{path:men.url}" icon="fas fa-home">{{
men.name
}}</SidebarLink> -->
<router-link class="w-full" :to="men.url"> {{men.name}} </router-link>
</div>
<p class="font-bold">{{webMenu.menu_text}}</p>
</div>
Problem is now when the webpage loads initially, it works fine but after clicking on the portfolio or services link, the paths to the others are changed.
e.g if i was on the portfolio page and tried switching back to the home page, it would change the route path to "portfolio#" and refuse to switch pages.
Can anyone help explain why this is and a possible way to resolve it?
I was also facing the similar issue.
For me the issue was that I was redirecting to new page using router.push() on click of a div.
And I was using #click.stop = "myFunction" . This was adding a # in the url and not redirecting the user.
I tried different variation of this as well. I tried using event object as well like event.stopPropagation(). With this as well got same result.
When I used #click.prevent, this everything worked as expected.
PS:
You can use catch block to trace the error as well.
this.$router..push({
name: "PAthName",
params: { id },
query: { id }
})
.catch(e => {
console.log("Errors", e);
});
Should have updated this earlier.
The fix was relatively simple, all i did was change the binding from
<router-link class="w-full" :to="men.url"> {{men.name}} </router-link>
to
<router-link class="w-full" :to="`/${men.url}`"> {{men.name}} </router-link>
and voila!
I am wondering how to display the content of a child page without showing the parent.
I figured this would be straightforward, but I've found nothing about how to do this. My current output has page 'app/parent' rendering some content for the parent, and 'app/parent/child-A' displays that same content from the parent with the child's content at the bottom. I'd like to only display the child's content while maintaining the 'app/parent/child-A' URL.
I suspect that I may be approaching the parent/child functionality in nuxt wrong and there is some better option for what I'm trying to do.
you can use v-if
router.js
....
const page = path => () => import(`./pages/${path}.vue`).then(m => m.default || m);
const routers = [
{
path: '/posts',
component: page('posts/layout'),
props: true,
children: [
{
path: '',
name: 'posts/index',
component: page('posts/index'),
props: true
},
{
path: ':id',
name: 'posts/show',
component: page('posts/show'),
props: true
}
]
},
...
];
...
pages/posts/layout.vue
<template>
<main>
<h1 v-if="$router.name !== 'posts/index'">from parent layout</h1>
<nuxt/>
</main>
</template>
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
Suppose I have a Vue.js component like this:
var Bar = Vue.extend({
props: ['my-props'],
template: '<p>This is bar!</p>'
});
And I want to use it when some route in vue-router is matched like this:
router.map({
'/bar': {
component: Bar
}
});
Normally in order to pass 'myProps' to the component I would do something like this:
Vue.component('my-bar', Bar);
and in the html:
<my-bar my-props="hello!"></my-bar>
In this case, the router is drawing automatically the component in the router-view element when the route is matched.
My question is, in this case, how can I pass the the props to the component?
<router-view :some-value-to-pass="localValue"></router-view>
and in your components just add prop:
props: {
someValueToPass: String
},
vue-router will match prop in component
sadly non of the prev solutions actually answers the question so here is a one from quora
basically the part that docs doesn't explain well is
When props is set to true, the route.params will be set as the component props.
so what you actually need when sending the prop through the route is to assign it to the params key ex
this.$router.push({
name: 'Home',
params: {
theme: 'dark'
}
})
so the full example would be
// component
const User = {
props: ['test'],
template: '<div>User {{ test }}</div>'
}
// router
new VueRouter({
routes: [
{
path: '/user',
component: User,
name: 'user',
props: true
}
]
})
// usage
this.$router.push({
name: 'user',
params: {
test: 'hello there' // or anything you want
}
})
In the router,
const router = new VueRouter({
routes: [
{ path: 'YOUR__PATH', component: Bar, props: { authorName: 'Robert' } }
]
})
And inside the <Bar /> component,
var Bar = Vue.extend({
props: ['authorName'],
template: '<p>Hey, {{ authorName }}</p>'
});
This question is old, so I'm not sure if Function mode existed at the time the question was asked, but it can be used to pass only the correct props. It is only called on route changes, but all the Vue reactivity rules apply with whatever you pass if it is reactive data already.
// Router config:
components: {
default: Component0,
named1: Component1
},
props: {
default: (route) => {
// <router-view :prop1="$store.importantCollection"/>
return {
prop1: store.importantCollection
}
},
named1: function(route) {
// <router-view :anotherProp="$store.otherData"/>
return {
anotherProp: store.otherData
}
},
}
Note that this only works if your prop function is scoped so it can see the data you want to pass. The route argument provides no references to the Vue instance, Vuex, or VueRouter. Also, the named1 example demonstrates that this is not bound to any instance either. This appears to be by design, so the state is only defined by the URL. Because of these issues, it could be better to use named views that receive the correct props in the markup and let the router toggle them.
// Router config:
components:
{
default: Component0,
named1: Component1
}
<!-- Markup -->
<router-view name="default" :prop1="$store.importantCollection"/>
<router-view name="named1" :anotherProp="$store.otherData"/>
With this approach, your markup declares the intent of which views are possible and sets them up, but the router decides which ones to activate.
const User = {
props: ['id'],
template: '<div>User {{ id }}</div>'
}
const router = new VueRouter({
routes: [
{ path: '/user/:id', component: User, props: true }
// for routes with named views, you have to define the props option for each named view:
{
path: '/user/:id',
components: { default: User, sidebar: Sidebar },
props: { default: true, sidebar: false }
}
]
})
Object mode
const router = new VueRouter({
routes: [
{ path: '/promotion/from-newsletter', component: Promotion, props: { newsletterPopup: false } }
]
})
That is the official answer.
link
Use:
this.$route.MY_PROP
to get a route prop
I have these routes
const routes: Routes = [
{ path: 'home', component: HomeComponent },
{
path: 'explore',
component: ExploreComponent,
children: [
{ path: '', component: ProductListComponent },
{ path: ':categorySlug', component: ProductListComponent }
]
}
];
This means that the user can go to
/explore (no category)
or
/explore/computers (category computers)
From the parent (ExploreComponent), I want to be able to subscribe to the categorySlug param change, and handle the event of course. How can I do this?
EDIT:
I tried subscribing using:
this.activatedRoute.firstChild.params.subscribe(console.log);
And it gives me exactly what I want, but it dies once I go to /explore (without category). It only works when navigating using /explore/:categorySlug links.
You can subscribe to the params in your component ang get the parameter, e.g. like this:
import { Router, ActivatedRoute } from '#angular/router';
constructor(private route: ActivatedRoute) {}
ngOnInit() {
this.route.params.subscribe(params => {
this.categorySlug= params['categorySlug '];
});
// do something with this.categorySlug
}
Side note: In general you use a kind of master detail structure in your web app, so the first path goes to the master and the second one goes to the detail, each served with a different component, but in case that you want to use the same component for both of them, or there is no such a master-detail relationship, you should check if the parameter is null.