Dynamic Menu in Vue
Build a sidebar from Vue Router routes with meta.visible and a recursive MenuItem component. Vue CLI 3 era, still useful pattern.
Every Vue app has navigation. When routes multiply, hand-writing menu links gets old fast.
I hit this on a side project with nested routes. Here's the pattern: read routes from the router, filter with meta.visible, recurse on children.
Scaffold
vue create dynamic-menu-vue
Pick Router, history mode, Sass, lint on save. Then:
npm run serve -- --open
Router setup
meta.visible controls menu visibility. Lazy imports keep bundles smaller. base is set for GitHub Pages.
import Vue from "vue";
import Router from "vue-router";
Vue.use(Router);
export default new Router({
mode: "history",
base: "/dynamic-menu-vue/",
routes: [
{
path: "/",
redirect: { path: "/home" },
meta: {
visible: false,
},
},
{
path: "/home",
name: "home",
component: () => import(/* webpackChunkName: "home" */ "./views/Home.vue"),
meta: {
visible: true,
},
children: [
{
path: "sub-view-1",
name: "sub-view-1",
component: () =>
import(/* webpackChunkName: "home-sub-view-1" */ "./components/Home/SubView1.vue"),
meta: {
visible: true,
},
},
{
path: "sub-view-2",
name: "sub-view-2",
component: () =>
import(/* webpackChunkName: "home-sub-view-2" */ "./components/Home/SubView2.vue"),
meta: {
visible: true,
},
},
],
},
{
path: "/about",
name: "about",
component: () => import(/* webpackChunkName: "about" */ "./views/About.vue"),
meta: {
visible: true,
},
},
{
path: "*",
name: "not-found",
component: () => import(/* webpackChunkName: "not-found" */ "./views/NotFound.vue"),
meta: {
visible: false,
},
},
],
});
Bootstrap styles
npm install bootstrap --save
src/styles.scss:
@import "./assets/variables";
@import "node_modules/bootstrap/scss/bootstrap.scss";
@import "./assets/bootswatch";
Import in main.js:
import "./styles.scss";
Navbar + recursive MenuItem
src/components/Menu/Navbar.vue:
<template>
<nav class="nav flex-column p-3">
<menu-item v-for="(r, i) in routes" :key="i" :route="r"></menu-item>
</nav>
</template>
<script>
export default {
name: "navbar",
components: {
MenuItem: () => import(/* webpackChunkName: "menu-item" */ "./MenuItem"),
},
computed: {
routes() {
return this.$router.options.routes;
},
},
};
</script>
MenuItem.vue checks visibility, prettifies the route name, and recurses into children:
<template>
<div>
<li v-if="isVisible" class="nav-item rounded shadow-sm mb-2">
<router-link exact-active-class="text-success" :to="{ name: route.name }" class="nav-link">{{
name
}}</router-link>
</li>
<div v-if="route.children && route.children.length">
<menu-item v-for="(r, i) in route.children" :key="i" :route="r" class="ml-3"></menu-item>
</div>
</div>
</template>
<script>
export default {
name: "menu-item",
props: {
route: {
type: Object,
},
},
computed: {
isVisible() {
if (this.route.meta && (this.route.meta.visible === undefined || this.route.meta.visible)) {
return true;
}
return false;
},
name() {
return this.route.name
.toLowerCase()
.split("-")
.map((s) => s.charAt(0).toUpperCase() + s.slice(1))
.join(" ");
},
},
};
</script>
exact-active-class highlights the current link. Nested routes indent with ml-3.
That's the core loop: one source of truth in the router table, menu follows automatically.
Questions welcome. Happy coding.