Search code examples
vue.jsrecursionvue-componentquasar-framework

How to make a recursive menu using Quasar QexpansionItem


I want create a component that it can to scale with a nested object structure using the QExpansionItem from Quasar Framework.

I made a recursive component to try achieve this but doesn't shows like i hope. The items are repeated in a wrong way and I don't know why.

I am using Quasar V1.0.5, the component that i used QexpansionItem

Here the menu object


[
{
    name: '1',
    icon: 'settings',
    permission: 'configuration',
    description: '1',
    url: '',
    children: [
      {
        name: '1.1',
        permission: 'configuration',
        url: '/insuranceTypes',
        icon: 'add',
        description: '1.1'
      },
      {
        name: '1.2',
        permission: 'configuration',
        url: '/insuranceTypes2',
        icon: 'phone',
        description: '1.2'
      }
    ]
  }, {
    name: '2',
    icon: 'person',
    permission: 'configuration',
    url: 'contacts',
    description: '2'
  }
  ]

MenuComponent.vue where i call side-tree-menu component

<q-list
        bordered
        class="rounded-borders q-pt-md"
      >
        <side-tree-menu :menu="menu"></side-tree-menu>

      </q-list>

SideTreeMenuComponent.vue

<template>
  <div>

    <q-expansion-item
      expand-separator
      :icon="item.icon"
      :label="item.name"
      :caption="item.description"
      header-class="text-primary"
      :key="item.name"
      :to="item.url"
      v-for="(item) in menu"
    >

      <template>
        <side
          v-for="(subitem) in item.children"
          :key="subitem.name"
          :menu="item.children"
        >
        </side>
      </template>

    </q-expansion-item>

  </div>
</template>

<script>
import { mapGetters } from 'vuex'
export default {
  name: 'side',
  props: ['menu', 'children'],
  data () {
    return {
      isOpen: false,
      algo: 0
    }
  },
  mounted () {
    console.log('menu', this.menu)
  },
  computed: {
    ...mapGetters('generals', ['can'])
  }
}
</script>

The elements 1.1 and 1.2 are repeated and I don't know fix it


Solution

  • I got stuck at the same problem and did not find any solution online. I managed to get it working with the below approach. This could be helpful for someone in the future :)

    I am adding here the 2 most important code files that will get this working. Rest of my setup is nothing more than what is created by the quasar create [project-name] CLI command.

    When you create the project with the above command, you get the MainLayout.vue and EssentialLink.vue file. I have modified those to achieve the required result.

    **My MainLayout.vue file - the template **

    EssentialLink below is the component that renders the menu recursively using q-expansion-item inside the drawer on the main layout page.

    <template>
      <q-layout view="hHh Lpr lFf">
        <q-header elevated>
           <q-toolbar>
            <q-btn flat dense round icon="menu" aria-label="Menu"  
              @click="leftDrawerOpen = !leftDrawerOpen" />
            <q-toolbar-title>
              {{appTitle}}
            </q-toolbar-title>
            <div>Release {{ appVersion }}</div>
          </q-toolbar>
        </q-header>
        <q-drawer
          v-model="leftDrawerOpen" show-if-above bordered
          content-class="bg-grey-1">
          <q-list>
            <q-item-label
              header
              class="text-grey-8">
              Essential Links
            </q-item-label>
            <EssentialLink
              v-for="link in essentialLinks"
              :key="link.title"
              v-bind="link">
            </EssentialLink>
          </q-list>
        </q-drawer>
        <q-page-container>
          <router-view />
        </q-page-container>
      </q-layout>
    </template>
    

    script section of MainLayout.vue file. Key properties to note - children and level.

    <script>
    
    import EssentialLink from 'components/EssentialLink.vue'
    
    export default {
      name: 'MainLayout',
    
      components: {
        EssentialLink
      },
    
      data () {
        return {
          appTitle: 'Project Name',appVersion: 'v0.1',leftDrawerOpen: false,
          essentialLinks: [
            {
              title: 'Search', caption: 'quasar.dev', icon: 'school',
              link: 'https://quasar.dev', 
              level: 0,
              children: [{
                title: 'Documents', caption: 'quasar.dev',icon: 'school',
                link: 'https://quasar.dev',
                level: 1, 
                children: [{
                  title: 'Search (level 3)',
                  caption: 'quasar.dev',
                  icon: 'school',
                  link: 'https://quasar.dev',
                  level: 2,
                  children: []
                }]
              }]
            },
            {
              title: 'Github',caption: 'github.com/quasarframework',
              icon: 'code',link: 'https://github.com/quasarframework',
              level: 0,
              children: [{
                title: 'Github Level 2',caption: 'quasar.dev',icon: 'school',
                link: 'https://quasar.dev',level: 1,
                children: []
              }]
            },
            {
              title: 'Forum',caption: 'forum.quasar.dev',
              icon: 'record_voice_over',link: 'https://forum.quasar.dev',
              level: 0,
              children: [{
                title: 'Forum Level 2',caption: 'quasar.dev',icon: 'school',
                link: 'https://quasar.dev',
                level: 1,
                children: []
              }]
            }
          ]
        }
      }
    }
    </script>
    

    Finally the EssentialLink.vue component

    The code below recursively calls itself when it encounters more than 1 item in its children property. The level property is used to indent the menus as you drill down.

        <template>
      <div>
        <div v-if="children.length == 0">
          <q-item clickable v-ripple :inset-level="level">
            <q-item-section>{{title}}</q-item-section>
          </q-item>
        </div>
        <div v-else>
          <div v-if="children.length > 0">
            <!-- {{children}} -->
            <q-expansion-item
                expand-separator
                icon="mail"
                :label="title"
                :caption="caption"
                :header-inset-level="level"
                default-closed>
              <EssentialLink
                v-for="child in children"
                :key="child"
                v-bind="child">
              </EssentialLink>
            </q-expansion-item>
          </div>
          <div v-else>
            <q-item clickable v-ripple :inset-level="level">
              <q-item-section>{{title}}</q-item-section>
            </q-item>
          </div>
        </div>
      </div>
    </template>
    

    *script section of the EssentialLink.vue component

        <script>
    export default {
      name: 'EssentialLink',
      props: {
        title: {
          type: String,
          required: true
        },
    
        caption: {
          type: String,
          default: ''
        },
    
        link: {
          type: String,
          default: '#'
        },
    
        icon: {
          type: String,
          default: ''
        },
    
        level: {
          type: String,
          default: ''
        },
    
        children: []
      }
    }
    </script>
    

    Final output looks like this (image)enter image description here