How to implement sub menu of the current route's children with vue.js and vue router
You don't need to create new router instances, instead watch the $route
property and create the sidebar nav menu as it changes. You'll need to pull the child routes from the $router.options.routes
. Here's an example:
const router = new VueRouter({
routes: [{
name: 'home',
path: '/',
component: {
template: '<div>Home</div>'
}
},
{
name: 'foo',
path: '/foo',
component: {
template: '<div>Foo<router-view></router-view></div>'
},
children: [{
name: 'foo.baz',
path: '/baz',
component: {
template: '<div>Baz</div>'
}
}, {
name: 'foo.tar',
path: '/tar',
component: {
template: '<div>Tar</div>'
}
}]
},
{
name: 'bar',
path: '/bar',
component: {
template: '<div>Bar<router-view></router-view></div>'
},
children: [{
name: 'bar.zim',
path: '/zim',
component: {
template: '<div>Zim</div>'
}
}, {
name: 'bar.zug',
path: '/zug',
component: {
template: '<div>Zug</div>'
}
}]
}
]
})
new Vue({
el: '#app',
router,
data() {
return {
children: []
}
},
watch: {
$route: function(current) {
const route = this.$router.options.routes.find(route => route.path === current.path)
if (route && Array.isArray(route.children)) {
this.children = route.children
} else if (route) {
this.children = []
}
}
}
})
* {
margin: 0;
padding: 0;
}
html,
body,
#app {
width: 100%;
height: 100%;
}
#top {
border: 1px solid black;
}
#top ul {
display: flex;
flex-direction: row;
justify-content: flex-start;
list-style-type: none;
}
li {
padding: 1rem;
text-align: center;
text-transform: uppercase;
}
li a {
text-decoration: none;
font-weight: bold;
color: black;
}
#sidebar {
height: 50%;
width: 100px;
border: 1px solid black;
}
#content {
width: 50%;
}
#content,
#content>div {
display: flex;
flex-direction: column;
justify-content: space-around;
align-items: center;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script src="https://npmcdn.com/vue-router/dist/vue-router.js"></script>
<div id="app">
<div id="top">
<ul>
<li v-for="item in $router.options.routes" :key="item.path" :style="{ 'background-color': $route.name.includes(item.name) ? 'rgba(197,225,165,1)' : 'white' }">
<router-link :to="item.path">{{ item.name }}</router-link>
</li>
</ul>
</div>
<div style="display: flex; flex-direction: row; height: 100%; width: 100%;">
<div id="sidebar">
<ul>
<li v-for="item in children" :key="item.path" :style="{ 'background-color': $route.name === item.name ? 'rgba(197,225,165,1)' : 'white' }">
<router-link :to="item.path">{{ item.name.split('.').pop() }}</router-link>
</li>
</ul>
</div>
<div id="content">
<router-view></router-view>
</div>
</div>
</div>
Related videos on Youtube
Mark
Updated on August 31, 2022Comments
-
Mark about 1 year
I'm trying to have the navigation on my Vue app to have a main menu up top populated by all the root level routes, and a side menu if the route has any children. Like so:
The current Design I have has the main navigation with routing links on the top with the router-view below. I have been able to get the side menu working so it it only shows up when I choose Travellers and updates the components/content correctly. However I'm having trouble routing things correctly, when I click on one of the links in the sub menu it does not append to the current path. So when I click on View when I'm in localhost/Traveler and click View the url changes to localhost/View/ instead of localhost/Traveler/View. Also the selection on the top menu gets unselected when I choose something in the child menu.
And I cannot get to the pages via something like localhost/Traveler/View only localhost/View
I started rereading the documentation on nested routes as I began making this post and I think I realized that I should be creating an new router at each level which is not something I have done in my code below.
I'm also not sure how to access the children of the current route. I've tried to display them like so:
<h2>Route: {{ $route.name }}</h2> <ul id="example-1"> <li v-for="child in $route.children"> {{ child.name }} </li> </ul>
But I get nothing. Should they be passed as Params or something? Or are the not that easily accessible?
Any advice or help will be greatly appreciated.
Root
Contains top Menu-Nav with router links and router-view below.
<template> <div id="app" class="container-fluid"> <div class="row"> <div style="width:100%"> <nav-menu params="route: route"></nav-menu> </div> </div> <div class="row"> <div> <router-view></router-view> </div> </div> </div> </template> <script> import NavMenu from './nav-menu' export default { components: { 'nav-menu': NavMenu }, data() { return {} } } </script>
Top Nav-Menu
Gets populated with the routes
<template> <nav class="site-header sticky-top py-1"> <div class="container d-flex flex-column flex-md-row justify-content-between"> <a class="nav-item" v-for="(route, index) in routes" :key="index"> <router-link :to="{path: route.path, params: { idk: 1 }}" exact-active-class="active"> <icon :icon="route.icon" class="mr-2" /><span>{{ route.display }}</span> </router-link> </a> </div> </nav> </template> <script> import { routes } from '../router/routes' export default { data() { return { routes, collapsed: true } }, methods: { toggleCollapsed: function (event) { this.collapsed = !this.collapsed } } } </script>
Traveler Page/View
Currently the Traveller Page which has a side bar menu and another router view for the content:
<template> <div id="app" class="container-fluid"> <div class="wrapper"> <traveler-menu params="route: route"></traveler-menu> <div id="content"> <router-view name="travlerview"></router-view> </div> </div> </div> </template> <script> import TravelerMenu from './traveler-menu' export default { components: { 'traveler-menu': TravelerMenu }, data() { return {} } } </script>
Side Bar/ Traveler Menu
<template> <nav id="sidebar"> <div class="sidebar-header"> <h3>Route's Children:</h3> </div> <ul class="list-unstyled components"> <li> <a class="nav-item" v-for="(route, index) in travelerroutes" :key="index"> <router-link :to="{path: route.path, params: { idk: 1 }}" exact-active-class="active"> <icon :icon="route.icon" class="mr-2" /><span>{{ route.display }}</span> </router-link> </a> </li> </ul> </nav> </template> <script> import { travelerroutes } from '../../router/travelerroutes' export default { data() { console.log(travelerroutes); return { travelerroutes, collapsed: true } }, methods: { toggleCollapsed: function (event) { this.collapsed = !this.collapsed } } } </script>
Routes
import CounterExample from 'components/counter-example' import FetchData from 'components/fetch-data' import HomePage from 'components/home-page' import TestPage from 'components/test-page' import Travelers from 'components/Traveler/traveler-root' import { travelerroutes } from './travelerroutes' export const routes = [ { name: 'home', path: '/', component: HomePage, display: 'Home', icon: 'home' }, { name: 'counter', path: '/counter', component: CounterExample, display: 'Counter', icon: 'graduation-cap' }, { name: 'fetch-data', path: '/fetch-data', component: FetchData, display: 'Fetch data', icon: 'list' }, { name: 'test-page', path: '/test-page', component: TestPage, display: 'Test Page', icon: 'list' }, { name: 'traveler-root', path: '/traveler', component: Travelers, display: 'Travelers', icon: 'list', children: travelerroutes } ]
Traveler Routes (travelerroutes.js)
import TestPage from 'components/test-page' import ViewTravelers from 'components/Traveler/TravelerPages/view-travelers' export const travelerroutes = [{ name: 'View', path: '/View', display: 'View', icon: 'list', components: { travlerview: TestPage } }, { name: 'Create', path: '/Create', display: 'Create', icon: 'list', components: { travlerview: ViewTravelers } }, { name: 'Edit', path: '/Edit', display: 'Edit', icon: 'list', components: { travlerview: ViewTravelers } }];
router/index.js
import Vue from 'vue' import VueRouter from 'vue-router' import { routes } from './routes' Vue.use(VueRouter) let router = new VueRouter({ mode: 'history', routes }) export default router
app.js
import Vue from 'vue' import axios from 'axios' import router from './router/index' import store from './store' import { sync } from 'vuex-router-sync' import App from 'components/app-root' import { FontAwesomeIcon } from './icons' // Registration of global components Vue.component('icon', FontAwesomeIcon) Vue.prototype.$http = axios sync(store, router) const app = new Vue({ store, router, ...App }) export { app, router, store }
Let me know if you you need anymore details, context or code.
-
Mark almost 5 yearsThank you! I'll try this out if I have time this weekend. Or on Monday.
-
Mark almost 5 yearsI added another answer using using template syntax. I'm not sure it's as good as yours though.