We've all created a web app using Vue and for sure these apps have a menu in it.
The main idea came to life when I was developing a side project and I had to create too many routes and child routes too. So, I decided to create this tutorial to show you how I solved my problem.
Let's get started
First thing first, create a project with Vue CLI
vue create dynamic-menu-vue
Vue CLI v3.11.0
? Please pick a preset: Manually select features
? Check the features needed for your project: Babel, Router, CSS Pre-processors, Linter
? Use history mode for router? (Requires proper server setup for index fallback in production) Yes
? Pick a CSS pre-processor (PostCSS, Autoprefixer and CSS Modules are supported by default): Sass/SCSS (with node-sass)
? Pick a linter / formatter config: Basic
? Pick additional lint features: (Press <space> to select, <a> to toggle
all, <i> to invert selection)Lint on save
? Where do you prefer placing config for Babel, PostCSS, ESLint, etc.? In package.json
? Save this as a preset for future projects? No
Once your project is ready, you can serve it as normal
npm run serve -- --open
Let's break down the code I wrote for src/router.js
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
}
}
]
});
- meta: This attribute is used to enhance routes. In this situation, we just use it to make routes visible on the menu,
- lazy loading: there is no actual need to use lazy loading at this project but it's a good trick to downsize the bundle size,
- base: I set this base URL in order to deploy this project on GitHub Pages.
OK, now that we have routes we need to create the menu component. I'm going to use bootstrap for this one. Let's add bootstrap to the project.
npm install bootstrap --save
Then create a new file named styles.scss
under the src
folder and add these lines of code
@import "./assets/variables";
@import "node_modules/bootstrap/scss/bootstrap.scss";
@import "./assets/bootswatch";
Now, add styles.scss
in main.js
...
import "./styles.scss";
...
Menu Component
Under the src/components
create a new folder named Menu and create two new files in it.
Navbar.vue
MenuItem.vue
Let's continue with 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>
<style>
</style>
The computed property routes()
returns the content of the router.js
file.
Then the MenuItem.vue
<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>
Properties
-
route
the exact route that comes from parent elementNavbar.vue
Computed properties
-
isVisible()
returns true if a route should be on the menu, false otherwise -
name()
returns the route's name
Active route class
-
exact-active-class="text-success"
this, adds a class to the active menu item.
As you can notice we have a recursive call to the same component if the route has child component.
That's all for now. If you have any questions let me know, I'll be glad to help.
Happy coding!
If you found this post helpful or enjoyed it, consider supporting me by buying me a coffee. Your support helps me create more valuable content. ☕ Thank you!
Top comments (6)
Have you tried to apply this to routes that require dynamic route data? For example, a user's profile page? Or a link to a specific article ID?
Curious how that would be wired in the router-link element...
Sorry for late answer Karl and @ahmed7fathi
Sure you can have dynamic routes like in every VueJS project. You will need to specify the names of the routes, but you can easily have dynamic route matching.
I'll update the project if you would like to take a look.
Have u figured out this yet ?
How to show nav items based on user roles like admin guest in vue dynamically
My first thought is about using vue-store. State management will help you to check if you have logged in user or not. Based on that info you can dynamically display wanted
nav-item
sYes. I found the solution with vuex and dynamic routing. Thanks for reply.