Search code examples
vue.jsvuetify.jsvuetifyjs3

Vuetify 3 v-list-group - collapse other sub-menus when opening group


I'm using Vuetify 3 and I'm trying to implement a collapsible vertical menu inside the Vuetify navigation drawer using the v-list-group component. I need the currently expanded menu item to collapse when another menu item is clicked to expand (Accordion behavior). I've created the menu list, but the accordion effect does not work as expected.

Template:

<v-navigation-drawer
        v-model="drawer"
        temporary
        location="right"
        theme="dark"
    >
        <v-list density="compact">
            <v-list-group v-model="item.active" v-for="item in menuItems">
                <template v-slot:activator="{ props }">
                    <v-list-item
                        :key="item.title"
                        v-bind="props"
                        :title="item.title"
                    ></v-list-item>
                </template>

                <v-list-item
                    v-for="subMenu in item.subMenuItems"
                    :key="subMenu"
                    :title="subMenu"
                ></v-list-item>
            </v-list-group>
        </v-list>
    </v-navigation-drawer>

Script:

<script setup>

const menuItems = ref([
    {
        title: "Menu Item 1",
        active: false,
        subMenuItems: ["Sub Menu Item 1", "Sub Menu Item 2", "Sub Menu Item 3"]
    },
    {
        title: "Menu Item 2",
        active: false,
        subMenuItems: ["Sub Menu Item 1", "Sub Menu Item 2", "Sub Menu Item 3"]
    },
    {
        title: "Menu Item 3",
        active: false,
        subMenuItems: ["Sub Menu Item 1", "Sub Menu Item 2", "Sub Menu Item 3"]
    }
])

</script>

In Vuetify 2, I could assign a variable to the v-model of v-list-group component, but this does not work with Vuetify 3 (Changing the value does not change the collapse/expand state).

The v-list-group component does not contain an is-open prop to manually modify its state (there is an isOpen flag which is passed to the activator slot, but it is read-only).


Solution

  • Yup, looks like that was changed, you need to do it using the surrounding v-list now.

    The :opened property of v-list is an array that corresponds to which groups are opened. Which values are put into the list is determined by the :value property of v-list-group. To have at most one group open, you can listen to the @update:opened event and fix the list accordingly.

    So with a ref opened = ref([]), you can do:

    <v-list
      :opened="opened"
      @update:opened="newOpened => opened = newOpened.slice(-1)"
    >
      <v-list-group
        v-for="item in menuItems"
        :key="item.title"
        :value="item"
      >
      ...
    

    With @update:opened="opened = $event.slice(-1)", all but the last element will be removed when the list is updated. This will close all groups except the one clicked last.

    The :value="item" determines what will be put into the opened array. It does not really matter what it is, as long as it is unique for each group (unless you want items to open and close together, then they should have the same value).

    Here it is in a snippet:

    const {
      createApp,
      ref,
      computed
    } = Vue;
    const {
      createVuetify
    } = Vuetify
    const vuetify = createVuetify()
    createApp({
      setup() {
        const menuItems = ref([{
            title: "Menu Item 1",
            active: false,
            subMenuItems: ["Sub Menu Item 1", "Sub Menu Item 2", "Sub Menu Item 3"]
          },
          {
            title: "Menu Item 2",
            active: true,
            subMenuItems: ["Sub Menu Item 1", "Sub Menu Item 2", "Sub Menu Item 3"]
          },
          {
            title: "Menu Item 3",
            active: false,
            subMenuItems: ["Sub Menu Item 1", "Sub Menu Item 2", "Sub Menu Item 3"]
          }
        ])
        const opened = ref([])
    
        return {
          opened,
          drawer: ref(true),
          menuItems,
        }
      }
    }).use(vuetify).mount('#app')
    <link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/vuetify@3.1.9/dist/vuetify.min.css" />
    <link href="https://cdn.jsdelivr.net/npm/@mdi/font@5.x/css/materialdesignicons.min.css" rel="stylesheet">
    <div id="app" class="d-flex justify-center">
      <v-app>
        <v-card>
          <v-layout>
    
            <v-navigation-drawer v-model="drawer" temporary>
              <v-list density="compact" :opened="opened" @update:opened="opened = $event.slice(-1)">
                <v-list-group v-for="item in menuItems" :key="item.title" :value="item">
                  <template v-slot:activator="{props, isOpen}">
                        <v-list-item
                            v-bind="props"
                            :key="item.title" 
                            :title="item.title"
                        ></v-list-item>
                    </template>
    
                  <v-list-item v-for="subMenu in item.subMenuItems" :key="subMenu" :title="subMenu"></v-list-item>
                </v-list-group>
              </v-list>
            </v-navigation-drawer>
    
            <v-main style="height: 500px;">
    
            </v-main>
          </v-layout>
        </v-card>
      </v-app>
    </div>
    <script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/vuetify@3.1.9/dist/vuetify.min.js"></script>