Thorbear
Thorbear

Reputation: 123

v-list-group sub-groups won't open based on path defined in group prop

I have a navigation sidebar with nested v-list-groups. According to the documentation the "group" prop of v-list-group will expand the group based on the route namespace.

See: https://vuetifyjs.com/en/components/lists/

This works well with the top level v-list-group.

It opens up if I reload the page and the route matches the "group" prop.

It does not work for the sub-group v-list-groups. When I reload the page I can see the subgroup closing, so I think the group prop is working. But something is making it close immediately after.

My code is a little complex, and I have to excuse my variable naming. I am rendering a list of navigation items, if they don't have a subgroup defined it renders as a regular item. If they have a subgroup it renders a subgroup. This goes two levels down.

This way I can define the whole navigation bar in a single json file and import it.

<template>
    <v-list dense nav>
        <template v-for="(sidebarItem, sidebarIndex) in sidebarItems">
            <v-list-item
                v-if="arrayIsEmptyOrUndefined(sidebarItem.subGroup)"
                :key="sidebarIndex"
                :to="sidebarItem.linkTo"
                nuxt
            >
                <v-list-item-action>
                    <v-icon>{{ sidebarItem.icon }}</v-icon>
                </v-list-item-action>
                <v-list-item-content>
                    <v-list-item-title v-text="sidebarItem.title" />
                </v-list-item-content>
            </v-list-item>

            <v-list-group
                v-if="
                    (!arrayIsEmptyOrUndefined(sidebarItem.subGroup) &&
                    typeof sidebarItem.subGroup !== 'undefined')
                "
                :key="sidebarIndex"
                :prepend-icon="sidebarItem.icon"
                :group="sidebarItem.linkTo"
            >
                <template v-slot:activator>
                    <v-list-item-content>
                        <v-list-item-title>{{ sidebarItem.title }}</v-list-item-title>
                    </v-list-item-content>
                </template>

                <v-list-item
                    v-if="arrayIsEmptyOrUndefined(sidebarItem.subGroup)"
                    :key="subGroupIndex"
                    :to="sidebarItem.linkTo"
                    nuxt
                >
                    <v-list-item-action>
                        <v-icon>{{ sidebarItem.icon }}</v-icon>
                    </v-list-item-action>
                    <v-list-item-content>
                        <v-list-item-title v-text="sidebarItem.title" />
                    </v-list-item-content>
                </v-list-item>

                <template v-for="(subGroupItem, subGroupIndex) in sidebarItem.subGroup">
                    <v-list-item
                        v-if="arrayIsEmptyOrUndefined(subGroupItem.subGroup)"
                        :key="subGroupIndex"
                        :to="subGroupItem.linkTo"
                        nuxt
                    >
                        <v-list-item-action>
                            <v-icon>{{ subGroupItem.icon }}</v-icon>
                        </v-list-item-action>
                        <v-list-item-content>
                            <v-list-item-title v-text="subGroupItem.title" />
                        </v-list-item-content>
                    </v-list-item>

                    <v-list-group
                        v-if="
                            (!arrayIsEmptyOrUndefined(subGroupItem.subGroup) &&
                            typeof subGroupItem.subGroup !== 'undefined')
                        "
                        :key="subGroupIndex"
                        sub-group
                        :group="subGroupItem.linkTo"
                    >
                        <template v-slot:activator>
                            <v-list-item-content>
                                <v-list-item-title>{{ subGroupItem.title }}</v-list-item-title>
                            </v-list-item-content>
                        </template>

                        <v-list-item
                            v-for="(subGroupSubGroupItem, subGroupSubGroupIndex) in subGroupItem.subGroup"
                            :key="subGroupSubGroupIndex"
                            :to="subGroupSubGroupItem.linkTo"
                            nuxt
                            exact
                        >
                            <v-list-item-content>
                                <v-list-item-title v-text="subGroupSubGroupItem.title" />
                            </v-list-item-content>
                            <v-list-item-action>
                                <v-icon>{{ subGroupSubGroupItem.icon }}</v-icon>
                            </v-list-item-action>
                        </v-list-item>
                    </v-list-group>
                </template>
            </v-list-group>
        </template>
    </v-list>
</template>

If the route namespace matches both the group and the subgroup only the group opens up. The vuetify documentation site has a working implementation of this. If you go there and navigate down to a sub group and refresh the page. The subgroup will be open.

Subgroup open will stay open upon refresh

Upvotes: 2

Views: 7605

Answers (2)

ThananutK
ThananutK

Reputation: 71

Another way to do this is by using 'to' as a routing, This key will automatically detect the current route.

      <v-list v-model:opened="open">
      <template v-for="(menu, j) in mainMenu">
        <v-list-group :value="menu.title" >
          <template v-slot:activator="{ props }">
            <v-list-item
              v-bind="props"
              :key="menu.title + j"
              :value="menu.title + j"
            >
              <v-icon>{{ menu.icon }}</v-icon>
              {{ $t(menu.title) }}
            </v-list-item>
          </template>

          <v-list-item
            dense
            v-for="(item, i) in menu.subMenu"
            :key="item.title + j + i"
            :value="item.title + j + i"
            :title="item.title"
            :to="item.path"
          >
          </v-list-item>
        </v-list-group>
      </template>
    </v-list>

Upvotes: 0

morphatic
morphatic

Reputation: 7975

Wow! This ended up being more difficult than I originally expected. I solved this by looking directly at the source code for the Vuetify docs website, specifically the main nav drawer component, the base-group component, the base-subgroup component, and the base-item component. These components were separated out into separate files for ease of composition and maintainability, but I've recombined them in the template below:

<template>
  <v-navigation-drawer permanent>
    <v-toolbar color="indigo" dark>
      <v-app-bar-nav-icon></v-app-bar-nav-icon>  
      <v-toolbar-title>Main Menu</v-toolbar-title>
      <v-spacer></v-spacer>
      <v-btn icon>
        <v-icon>mdi-dots-vertical</v-icon>
      </v-btn>
    </v-toolbar>
    <v-list
      dense
      expand
      nav
    >
      <!-- ENTIRE list is wrapped in a template -->
      <template v-for="item in $router.options.routes">
        <!-- use v-if to determine if 2nd level nesting is needed -->
        <!-- if it's a menu item with no children -->
        <v-list-item
          v-if="!item.children"
          color="indigo"
          :key="item.name`"
          :to="item.path"
        >
          <v-list-item-icon>
            <v-icon>{{ `mdi-${item.meta.icon}` }}</v-icon>
          </v-list-item-icon>
          <v-list-item-title>{{ item.name }}</v-list-item-title>
        </v-list-item>
        <!-- else if it has children -->
        <v-list-group
          v-else
          :group="item.path"
          color="indigo"
        >
          <!-- this template is for the title of top-level items with children -->
          <template #activator>
            <v-list-item-content>
              <v-list-item-title>
                <v-icon>{{ `mdi-${item.meta.icon}` }}</v-icon>
                {{ item.name }}
              </v-list-item-title>
            </v-list-item-content>
          </template>
          <!-- this template is for the children/sub-items (2nd level) -->
          <template v-for="subItem in item.children">
            <!-- another v-if to determine if there's a 3rd level -->
            <!-- if there is NOT a 3rd level -->
            <v-list-item
              v-if="!subItem.children"
              class="ml-5"
              :key="subItem.name"
              :to="item.path + subItem.path"
            >
              <v-list-item-icon class="mr-4">
                <v-icon v-text="`mdi-${subItem.meta.icon}`" />
              </v-list-item-icon>
              <v-list-item-title class="ml-0">
                {{ subItem.name }}
              </v-list-item-title>
            </v-list-item>
            <!-- if there is a 3rd level -->
            <v-list-group
              v-else
              color="indigo"
              :group="subItem.path"
              sub-group
            >
              <template #activator>
                <v-list-item-content>
                  <v-list-item-title>
                    <v-icon>{{ `mdi-${subItem.meta.icon}` }}</v-icon>
                    {{ subItem.name }}
                  </v-list-item-title>
                </v-list-item-content>
              </template>
              <template v-for="(subSubItem, k) in subItem.children">
                <v-list-item
                  :key="`subheader-${k}`"
                  color="indigo"
                  :value="true"
                  :to="item.path + subItem.path + subSubItem.path"
                >
                  <v-list-item-title>{{ subSubItem.name }}</v-list-item-title>
                  <v-list-item-icon>
                    <v-icon>{{ `mdi-${subSubItem.meta.icon}` }}</v-icon>
                  </v-list-item-icon>
                </v-list-item>
              </template>
            </v-list-group>
          </template>
        </v-list-group>
      </template>
    </v-list>
  </v-navigation-drawer>
</template>

As you may have noticed, the menu is generated from the list of routes in Vue Router. The router definition looks like this (NOTE: since this just an example, I did NOT associate a component with each route, but in practice, that would be necessary):

const router = new VueRouter({
  routes: [
    {
      name: 'No Children (1 level)',
      path: '/no-children',
      meta: {
        icon: 'baby-carriage-off',
      },
    },
    {
      name: 'Attractions (2 levels)',
      path: '/attractions',
      meta: {
        icon: 'airballoon',
      },
      children: [
        {
          name: 'Carnivals',
          path: '/carnivals',
          meta: {
            icon: 'drama-masks',
          },
        },
        {
          name: 'Museums',
          path: '/museums',
          meta: {
            icon: 'bank',
          },
        },
      ]
    },
    {
      name: 'Restaurants (3 levels)',
      path: '/restaurants',
      meta: {
        icon: 'silverware-fork-knife',
      },
      children: [
        {
          name: 'Japanese',
          path: '/japanese',
          meta: {
            icon: 'map-marker-radius-outline',
          },
          children: [
            {
              name: 'Hikari Sushi',
              path: '/hikari-sushi',
              meta: {
                icon: 'food-croissant',
              },
            },
            {
              name: 'Late Night Ramen',
              path: '/late-night-ramen',
              meta: {
                icon: 'noodles',
              },
            },
          ]
        },
        {
          name: 'Italian',
          path: '/italian',
          meta: {
            icon: 'map',
          },
          children: [
            {
              name: 'Jersey Pizza',
              path: '/jersey-pizza',
              meta: {
                icon: 'pizza',
              },
            },
            {
              name: 'Im-pasta-ble',
              path: '/im-pasta-ble',
              meta: {
                icon: 'pasta',
              },
            },
          ]
        },
        {
          name: 'Mexican',
          path: '/mexican',
          meta: {
            icon: 'map-marker',
          },
          children: [
            {
              name: 'Taco Gato',
              path: '/taco-gato',
              meta: {
                icon: 'taco',
              },
            },
            {
              name: 'A-maize-ing',
              path: '/a-maize-ing',
              meta: {
                icon: 'corn',
              },
            },
          ]
        },
      ]
    },
  ]
})

Here's a full, working example on codepen. I configured it to have a starting route of /restaurants/japanese/late-night-ramen and when you click on any of the endpoints, the path to which the application will be routed is logged in the console. Hope this helps!

Upvotes: 10

Related Questions