How to implement sub menu of the current route's children with vue.js and vue router

13,417

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>
Share:
13,417

Related videos on Youtube

Mark
Author by

Mark

Updated on August 31, 2022

Comments

  • Mark
    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:

    Goal

    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.

    Unselecting

    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
    Mark almost 5 years
    Thank you! I'll try this out if I have time this weekend. Or on Monday.
  • Mark
    Mark almost 5 years
    I added another answer using using template syntax. I'm not sure it's as good as yours though.