Vue Router Active Link Not working on parent but works on children - vue-router

My Setup
I have vue application using vue router with nested routes. I'm using vuetify as well. My issue is this....
My Issue
When I navigate on the Navbar, the active class isn't moving on the navbar. The menu item has a border-bottom on it to be able to have a "selected" feel to it.
In the code below are my routes. This pretty much speaks to the structure of my project too.
My approach is Views/Topic/ and I have a Page/Home/History/Detail
Expectation of my navbar functionality:
Approvals -- If you are on any of these these below, This will be highlighted
-> ApprovalPage -- This contains the menu and a router-view in a column
-> ApprovalHome -- This is a menu item in the approval page. It will be highlighted if it is selected
-> ApprovalHistory -- This is a menu item in the approval page. It will be highlighted if it is selected
-> ApprovalDetail -- This is a menu item in the approval page. It will be highlighted if it is selected
Example of my folder setup:
Approvals
-> ApprovalPage -- This contains the menu and a router-view in a column
-> ApprovalHome -- This will be rendered on the Approval Page
-> ApprovalHistory -- This will be rendered on the Approval Page
-> ApprovalDetail -- This will be rendered on the Approval Page
The navigation menu inside the ApprovalPage can navigate and highlight correctly. However, it's just the top nav that doesn't move.
The other thing is, everything renders correctly. It's just the active part of the top nav that doesn't work.
Here is my code
Routes
export default new Router({
mode: 'history',
base: process.env.BASE_URL,
routes: [
{
path: '/',
name: 'home',
component: Home,
},
{
path: '/requests',
name: 'requests',
component: RequestsPage,
children: [
{
path: '',
name: 'RequestsHome',
component: RequestsHome,
},
{
path: 'history',
name: 'RequestHistory',
component: RequestHistory,
},
{
path: ':id',
name: 'RequestDetail',
component: RequestDetail,
},
],
},
{
path: '/approvals',
name: 'approvals',
component: ApprovalsPage,
children: [
{
path: '',
name: 'ApprovalsHome',
component: ApprovalHome,
},
{
path: 'history',
name: 'ApprovalsHistory',
component: ApprovalHistory,
},
{
path: ':id',
name: 'ApprovalsDetail',
component: ApprovalDetail,
},
],
},
{
path: '/workflows',
name: 'workflows',
component: WorkflowPage,
children: [
{
path: '',
name: 'workflowsHome',
component: WorkflowHome,
},
{
path: 'creator',
name: 'creator',
component: WorkflowCreator,
},
],
},
],
});
NavBar toolbar items
<v-toolbar-items
class="ml-5"
>
<v-btn
text
to="/"
exact
>
Home</v-btn>
<v-btn
text
to="/approvals/"
>
Approvals</v-btn>
<v-btn
text
to="/requests/"
>
Requests</v-btn>
<v-menu
tile
right
offset-y
open-on-hover
>
<template v-slot:activator="{ on }">
<v-btn
text
v-on="on"
to="/workflows/"
>
Workflows</v-btn>
</template>
<v-list>
<v-list-item to="/workflows/" exact>
<v-list-item-title>Workflows</v-list-item-title>
</v-list-item>
<v-list-item to="/workflows/creator" exact>
<v-list-item-title>Workflow Creator</v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-toolbar-items>

Related

Vue.js – New routes are always placed on top of current route

In my Vue.js application I use a navigation drawer on the left side of the screen for navigation purposes. I am using Vuetify and Vue Router.
Problem
After changing my routes a lot of my previously routing functionalities got messed up. Whenever a user clicks on an element to navigate to a new site the route get's attached on top of the previous one like this.
Route before a user clicks on a link:
http://localhost:8080/organization/:organization_id/planner/events
New route after a user clicks on a link:
http://localhost:8080/organization/:organization_id/planner/management/contracts
Router
routes: [
// Login
{
path: '/login',
component: Login,
},
// Organization
{
path: '/organization',
component: Organization,
},
// Organization Detail Page
{
path: '/organization/:organization_id',
component: OrganizationDetail,
children: [
// Module: Planner
{
path: 'planner',
component: Planner
},
// Planner > Events
{
path: 'planner/events',
component: Events
},
{
path: 'planner/events/new',
component: NewEvent
},
{
path: 'planner/events/details/:event_id',
component: EventDetails
},
// Planner > Calendar
{
path: 'planner/calendar',
component: Calendar
},
// Module: Management
{
path: 'management',
component: Management
},
// Management > Users
{
path: 'management/users',
component: Events
},
{
path: 'management/users/new',
component: NewUser
},
{
path: 'management/users/details/:user_id',
component: UserDetails
},
// Planner > Contracts
{
path: 'management/contracts',
component: Contracts
}
]
}
]
NavigationDrawer.vue (updated):
<template>
<div id="navigation">
<nav>
<v-navigation-drawer
v-model="drawer"
:clipped="$vuetify.breakpoint.lgAndUp"
width="400"
fixed
app
>
<v-list>
<template v-for="item in items">
<v-row
v-if="item.heading"
:key="item.heading"
align="center"
>
</v-row>
<v-list-group
v-else-if="item.children"
:key="item.text"
v-model="item.model"
append-icon="mdi-chevron-down"
class="nav"
link
router :to="item.route"
>
<v-list-item
v-for="(child, i) in item.children"
:key="i"
link
router :to="child.route"
>
<v-list-item-action v-if="child.icon">
<v-icon>{{ child.icon }}</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
{{ child.text }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
</v-list-group>
</template>
</v-list>
</v-navigation-drawer>
<v-app-bar
:clipped-left="$vuetify.breakpoint.lgAndUp"
app
dark
>
<v-app-bar-nav-icon #click.stop="drawer = !drawer"></v-app-bar-nav-icon>
</v-app-bar>
</nav>
</div>
</template>
<script>
export default {
name: 'NavigationDrawer',
el: '#app',
data: () => ({
drawer: true,
menu: false,
items: [
{
text: 'Planner',
children: [
{ text: 'Events', route:'/organization/' + this.$route.params.organization_id + '/planner/events'},
{ text: 'Calendar', route:'/organization/' + this.$route.params.organization_id + '/planner/calendar'}
],
},
{
text: 'Management',
children: [
{ text: 'Users', route:'/organization/' + this.$route.params.organization_id + '/management/users'},
{ text: 'Contracts', route:'/organization/' + this.$route.params.organization_id + '/management/contracts'}
],
},
]
}),
methods: {
logout() {
// some awesome stuff
}
}
}
</script>
Other routes like (NewUser, NewEvent) are not stored in the navigation drawer and called manually by a button.
I know that this has something to do with my routes. However those aren't misconfigured because the relations between parent and children are important for displaying the right content and implementing some header informations. The navigation drawer works also the right way. I didn't change anything there.
Also if I use router.back() on NewEvent or NewUser nothing happens. :(
Help is very much appreciated, thanks!
The issue is because of
{ text: 'Events', route:'/planner/events'},
The above route redirects the website to http://localhost:8080/planner/events because of initial forward slash.
Use the $route.params to get params from URL like the following.
<div>User {{ $route.params.organization_id }}</div>
or
this.$route.params.organization_id

Vue-router: How can I use named routes within a data object?

I am using vue-router (and vuetify) to build my website. I have everything working great, but I would like to understand how to use the named routes instead of a path.
Here is a partial of my template as follows:
<v-list-item v-else :key="item.text" :to="item.to" link>
<v-list-item-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
{{ item.text }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
...
Here is my data object:
data: () =>({
items: [
{icon: 'mdi-home', text: 'Home', to: '/'},
{icon: 'mdi-view-dashboard', text: 'Dashboard', to: '/dashboard'},
{
icon: 'mdi-chevron-up', 'icon-alt': 'mdi-chevron-down',
text: 'Locations',
model: false,
children: [
{
icon: 'mdi-map-marker',
text: 'Location One',
to: {name: 'location.show', params: {id: 1}} . // works perfectly
},
{
icon: 'mdi-map-marker',
text: 'Location Two',
to: {name: 'location.show', params: {id: 2}} . // works perfectly
},
],
},
],
}),
This is what I am trying to do:
{icon: 'mdi-view-dashboard', text: 'Dashboard', to: {name: 'dashboard'},
Instead of:
{icon: 'mdi-view-dashboard', text: 'Dashboard', to: '/dashboard'},
However, when I use the named route, Vuetify is applying the active class to the element. So, now all of my nav items are active.
How can I use the named route instead of a path? Thank you for any suggestions!
EDIT
Here is what my vue-router looks like:
{
path: "/dashboard",
name: "dashboard",
component: () =>
import(/* webpackChunkName: "dashboard" */ "../views/admin/Dashboard"),
meta: {requiresAuth: true}
},
{
path: "/location/:id",
name: "location.show",
component: () =>
import(/* webpackChunkName: "location.show" */ "../views/admin/location/Show"),
meta: {requiresAuth: true},
props(route) {
const props = {...route.params};
props.id = +props.id;
return props; // returns the param as a string
},
},
First of all, make sure that your route name is existing. Like below
export default [
{
path: '/admin/dashboard',
name: 'dashboard',
component: dashboard,
meta: {
title:'Dashboard'
}
},
]
So, in your items you can already use this one.
{icon: 'mdi-home', text: 'Home', to: {name: 'dashboard'}},
You should add the exact -prop to your list item. This way it will only apply the active class to the exact link.
<v-list-item v-else :key="item.text" :to="item.to" link exact>
<v-list-item-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-item-action>
<v-list-item-content>
<v-list-item-title>
{{ item.text }}
</v-list-item-title>
</v-list-item-content>
</v-list-item>
https://router.vuejs.org/api/#exact

How to exhange a full route? Router-link exchanges only the last part of the route

I am new to vue and vue router and am using Vue router in history mode. The app contains a menu which is dynamically loaded
<router-link
tag="li"
class="link"
v-for="item in menu"
v-bind:key="item.id"
:to="item.slug"
:exact="item.slug === '/' ? true : false">{{ item.content }}
</router-link>
It works well as long as I stay in the parent routes like http://localhost:8080/posts As soon as I navigate a level deeper, e.g. to a post with the id 8 http://localhost:8080/posts/8 with a routerlink inside the Template
<router-link
tag="h2"
class="link"
:to="{ name: 'post', params: { id: post.id }}">
{{ post.title.rendered }}
</router-link>
it works in one way, but doesnt go back to the parent route when I click the main navigation links. Instead just adds the link of the main menu to the end of the route, e.g. instead of http://localhost:8080/posts
http://localhost:8080/posts/posts
The router
const router = new Router({
mode: 'history',
base: '',
routes: [
{ path: '/', name: 'home', component: HomePage },
{ path: '/posts', name: 'posts', component: PostsPage },
{ path: '/posts/:id', name: 'post', component: SinglePost },
{ path: '/projects', name: 'projects', component: ProjectsPage },
{ path: '/projects/:id', name: 'project', component: ProjectPage },
{ path: '/:page', name: 'page', component: SinglePage },
{ path: "*", redirect: '/' },
],
// etc..
});
I guess I am making a common mistake, but I can´t find a solution. Any help appreciated.
You can use absolute paths. Instead of
<router-link
tag="h2"
class="link"
:to="{ name: 'post', params: { id: post.id }}">
{{ post.title.rendered }}
</router-link>
use
<router-link
tag="h2"
class="link"
:to="{ name: '/post', params: { id: post.id }}">
{{ post.title.rendered }}
</router-link>
The change is /post vs post in router-link's :to attribute.
You could and probably should use nested routes and components for posts posts/:id, but this a little more work and you my not need it at the moment. More on nested routes here.

Dynamic Buttons in Navigation Bar using Vuetify

I have a problem...
My VueJS Application using Vuetify.
I have a <v-toolbar>, and on the right, I want to place some buttons that change depending on the component shown in <router-view>, but i can't access to component properties from $route or $route for get objects and methods bind to model of my component.
I would like to know if there is any way to assign a model to from my main component.
I have tried with "named-routes" but I do not know what is the way that properties can be shared between components that are managed by an <router-view> and updated live.
In resume:
I have my application skeleton with a navigation bar, additionally in the dynamic content I have a <router-view>. Depending on the component that is displayed in <router-view>, I would like to see buttons in the navigation bar corresponding to that component, which interact and change the data or execute methods of the component.
App.vue
<template>
<v-app>
<router-view></router-view>
</v-app>
</template>
<script>
export default {
name: 'App',
data() {
return {
};
}
};
</script>
index.js (router)
import Vue from 'vue'
import Router from 'vue-router'
import AppLogin from '#/components/AppLogin'
import Skeleton from '#/components/Skeleton'
import ShoppingCart from '#/components/ShoppingCart'
import ShoppingCartButtons from '#/components/ShoppingCartButtons'
import ProductSelection from '#/components/ProductSelection'
import ProductSelectionButtons from '#/components/ProductSelectionButtons'
import ProductDetail from '#/components/ProductDetail'
Vue.use(Router)
export default new Router({
routes: [
{
path : '/login',
name : 'AppLogin',
component : AppLogin
},
{
path : '/app',
name : 'Skeleton',
component : Skeleton,
children : [{
path : 'shopping-cart',
components : {
navigation : ShoppingCart,
navButtons : ShoppingCartButtons
}
}, {
path: 'product-selection',
name : 'ProductSelection',
components : {
navigation : ProductSelection,
navButtons : ProductSelectionButtons
}
},
{
path: 'product-detail',
name : 'ProductDetail',
components : {
navigation : ProductDetail
},
props : true
}
]
}
]
})
Skeleton.vue
<template>
<v-container fluid>
<v-navigation-drawer
persistent
:mini-variant="miniVariant"
:clipped="true"
v-model="drawer"
enable-resize-watcher
fixed
app
>
<v-list>
<v-list-tile
value="true"
v-for="(item, i) in items"
:key="i"
:to="item.path">
<v-list-tile-action>
<v-icon v-html="item.icon"></v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title v-text="item.title"></v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<v-toolbar
app
:clipped-left="clipped"
>
<v-toolbar-side-icon #click.stop="drawer = !drawer">
</v-toolbar-side-icon>
<v-toolbar-title v-text="$route.meta.title"></v-toolbar-title>
<v-spacer></v-spacer>
<router-view name="navButtons"></router-view>
</v-toolbar>
<v-content>
<router-view name="navigation"/>
</v-content>
<v-footer :fixed="true" app>
<p style="text-align : center; width: 100%">© CONASTEC 2018</p>
</v-footer>
</v-container>
</template>
<script>
export default {
data() {
return {
clipped: true,
drawer: false,
fixed: false,
items: [
{
icon: "shopping_cart",
title: "Carrito de Compras",
path : "/app/shopping-cart"
},
{
icon: "attach_money",
title: "Facturas"
},
{
icon: "account_balance_wallet",
title: "Presupuestos"
},
{
icon: "insert_chart",
title: "Informes"
},
{
icon: "local_offer",
title: "Productos"
},
{
icon: "person",
title: "Clientes"
},
{
icon: "layers",
title: "Cuenta"
},
{
icon: "comment",
title: "Comentarios"
},
{
icon: "settings",
title: "Ajustes"
}
],
buttons : [],
miniVariant: false,
right: true,
rightDrawer: false
};
},
name: "Skeleton"
};
</script>
EDITED
My solution is create a new component Toolbar and add slots for buttons to right and left.
<template>
<div>
<v-navigation-drawer persistent :mini-variant="false" :clipped="true" v-model="drawer" enable-resize-watcher fixed app>
<v-list>
<v-list-tile value="true" v-for="(item, i) in items" :key="i" :replace="true" :to="item.path">
<v-list-tile-action>
<v-icon v-html="item.icon"></v-icon>
</v-list-tile-action>
<v-list-tile-content>
<v-list-tile-title v-text="item.title"></v-list-tile-title>
</v-list-tile-content>
</v-list-tile>
</v-list>
</v-navigation-drawer>
<v-toolbar app :clipped-left="true" color="primary" :dark="true" flat>
<v-toolbar-side-icon v-if="showDrawer" #click.stop="drawer = !drawer">
</v-toolbar-side-icon>
<v-toolbar-side-icon v-if="!!back" #click="back">
<v-icon>keyboard_backspace</v-icon>
</v-toolbar-side-icon>
<v-toolbar-title v-text="title" style="font-size: 1.4em"></v-toolbar-title>
<v-spacer></v-spacer>
<v-card-actions>
<slot name="right"></slot>
</v-card-actions>
</v-toolbar>
<v-snackbar
:timeout="5000"
:top="true"
:multi-line="true"
:vertical="true"
v-model="snackbar.show"
>
{{ snackbar.content }}
<v-btn flat color="white" #click.native="snackbar.show = false">Cerrar</v-btn>
</v-snackbar>
</div>
</template>
<script>
export default {
name: 'app-toolbar',
props: ['title','showDrawer', 'back'],
data() {
return {
drawer : false,
items: [{
icon: "shopping_cart",
title: "Carrito de Compras",
path: "/carrito-compras"
}, {
icon: "attach_money",
title: "Facturas",
path: "/documentos-tributarios"
}, {
icon: "account_balance_wallet",
title: "Presupuestos"
}, {
icon: "insert_chart",
title: "Informes"
}, {
icon: "local_offer",
title: "Productos"
}, {
icon: "person",
title: "Clientes"
}, {
icon: "layers",
title: "Cuenta"
}, {
icon: "comment",
title: "Comentarios"
}, {
icon: "settings",
title: "Ajustes"
}]
};
},
computed : {
snackbar() {
return this.$store.getters.snackbar;
}
}
}
</script>
and use is:
<app-toolbar title="Carrito de Compras" :showDrawer="true">
<template slot="right">
<v-toolbar-side-icon #click="confirm">
<v-icon>monetization_on</v-icon>
</v-toolbar-side-icon>
</template>
</app-toolbar>
I did the same thing as you in a recent project and found altering the structure was the easier way to fix issues like this.
My structure was as follows:
app.vue: Only contains <router-view> no other components
router.js: Parent route is a layout component, all sub routes which contains my toolbars and other layout components and it's own <router-view> which receives child routes
ex:
{
path: '/login',
name: 'Login',
component: load('login')
},
{
path: '/',
component: load('main-layout'),
children: [
{
path: '',
name: 'Home Page',
component: load('homePage')
},
{
path: '/settings',
name: 'Settings',
component: load('settings'),
}
]
}
Now in your main-layout:
computed: {
showHomeButton () {
if (this.$route.path === '/') {
return true
}
return false
// Repeat for other routes, etc...
},
}
If you are using Vuex, you can use vuex-router-sync, then you can
access the route from any component with this.$state.route.path.
If not, Scott's answer is probably the best way to do it.
I had the same problem, My solution was manage the left button action from de meta of vue router like this:
{
path: '/feedstocks/:categoryId/:id',
name: 'Feedstock',
component: () =>
import(
/* webpackChunkName: "client-chunk-feedstock-details" */ '#/views/FeedstockDetails.vue'
),
props: true,
meta: {
authNotRequired: true,
backRoute: 'Material'
}
}
Then I'm able to check that metadata in app bar button action:
<v-app-bar app color="primary" dark>
<v-btn text icon color="white" #click="leftButtonAction">
<v-icon>{{ leftButtonIcon }}</v-icon>
</v-btn>
<v-toolbar-title>
{{ currentAppTitle }}
</v-toolbar-title>
<v-spacer></v-spacer>
</v-app-bar>
leftButtonIcon() {
if (this.$route.meta.backRoute) {
return 'mdi-chevron-left'
}
return 'mdi-menu'
}
leftButtonAction() {
if (this.$route.meta.backRoute) {
this.$router.push({ name: this.$route.meta.backRoute })
} else {
this.toggleDrawer()
}
}
I guess it's a time for a more up-to-date answer.
For my requirement to dynamically modify the item list in the app-bar based on the selected view, the solution was to use the Named Views

vue-router, can't refresh routes with dynamic id's

I'm having trouble with my vue-router. I've copied a portion of my router for you to view and also a portion of the component in which I am using the the router-view. As you can see, I am using props as it's my understanding that props are the advanced way of passing $router.params. Also, this is a nested router-view. I can route to and from the nested routes just fine, but when I click refresh, I get these errors in the console.
"Failed to load resource: the server responded with a status of 404 (Not Found)"
"GET http://localhost:8080/distilleries/1/dist/build.js net::ERR_ABORTED"
I am wondering if anyone can help.
{
path: '/distilleries/:id',
component: Distillery,
props: true,
children: [
{
path: '',
component: DistilleryAbout,
name: 'DistilleryAbout',
props: true
},
{
path: 'spirits',
props: true,
component: DistilleryProducts,
name: 'DistilleryProducts'
},
{
path: 'awards',
props: true,
component: DistilleryAwards,
name: 'DistilleryAwards'
},
{
path: 'reviews',
props: true,
component: DistilleryReviews,
name: 'DistilleryReviews'
},
{
path: 'recipes',
props: true,
component: DistilleryRecipes,
name: 'DistilleryRecipes'
},
{
path: 'media',
props: true,
component: DistilleryMedia,
name: 'DistilleryMedia'
}
]
}
<v-flex xs12>
<v-toolbar class="lb" flat>
<v-toolbar-items style="margin: 0 auto">
<v-layout row wrap>
<v-flex xs6 md12 class="mt-3">
<router-link
tag="span"
v-for="items in menuItems"
:key="items.id"
class="itemsButton ma-3 pa-3 cb"
:to="{ name: items.link }"
>{‌{ items.title }}
</router-link>
</v-flex>
</v-layout>
</v-toolbar-items>
</v-toolbar>
<router-view :distillery="distillery"></router-view>
</v-flex>
From vue-router docs:
Since our app is a single page client side app, without a proper
server configuration, the users will get a 404 error if they access
http://oursite.com/user/id directly in their browser. Now that's ugly.
So you need to add mode: 'history' in router:
const router = new VueRouter({
mode: 'history',
routes: [...],
})
And add configuration depending on your server.