Dynamic Menu In Vue
Tags: vue, tutorial, javascript, webdev
Published: 2019-08-26 18:40:12

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

```bash
vue create dynamic-menu-vue
```



```bash
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

```bash
npm run serve -- --open
```

[View source code](https://github.com/ThPadelis/dynamic-menu-vue)

[View live demo](https://thpadelis.github.io/dynamic-menu-vue/)

---

Let's break down the code I wrote for `src/router.js`

```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.

```bash
npm install bootstrap --save
```

Then create a new file named `styles.scss` under the `src` folder and add these lines of code

```scss
@import "./assets/variables";
@import "node_modules/bootstrap/scss/bootstrap.scss";
@import "./assets/bootswatch";
```

Now, add `styles.scss` in `main.js`

```js
...
import "./styles.scss";
...
```

---

### Menu Component

Under the `src/components` create a new folder named Menu and create two new files in it.

1. `Navbar.vue`
2. `MenuItem.vue`

Let's continue with `Navbar.vue` 

```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`

```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 element `Navbar.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](https://www.buymeacoffee.com/ptheodosiou). Your support helps me create more valuable content. ☕ Thank you!