本文章では、vue.jsのVuetifyでv-list-groupコンポーネントを利用してナビゲーションメニューを作成した場合のメニュー項目のリンク設定について説明を行なっています。

メニュー項目に階層がない場合どのようにVuetifyを設定していくかに注目しています。別の方法もあると思うので参考程度で利用してください。

ナビゲーションメニューの画面遷移
ナビゲーションメニューの画面遷移

動作確認を行う環境は下記の文書で構築したものを利用します。

利用するメニューデータ

サイドにあるナビゲーションバーを構成するデータは下記となります。下の階層があるメニュー項目とないメニュー項目が混在している状態です。


nav_lists:[
  {
    name: 'Getting Started',
    icon: 'mdi-speedometer',
    link: '',
    lists:[{
        name:'Quick Start',link:'/quick-start'
      },
      {
        name:'Pre-made layouts',link:'/pre-made-layouts'
      }
    ]
  },
  {
    name: 'Customization',
    icon: 'mdi-cogs',
    link: '/customization'
  },
  {
    name: 'Styles & animations',
    icon: 'mdi-palette',
    link: '',
    lists:[{
      name :'Colors', link:'/colors'
      },
      {
      name :'Content', link:'/content'
      },
      {
      name :'Display', link:'/display'}
    ]
  },
  {
    name: 'UI Components',
    icon: 'mdi-view-dashboard',
    link: '/components'
  },
  {
    name: 'Directives',
    icon: 'mdi-function',
    link: '/directives'
  },
  {
    name: 'Preminum themes',
    icon: 'mdi-vuetify',
    link: '/preminum_themes'
  },
]

階層があるものには下の階層のリストにリンクを設定し、下の階層のないものはそのメニュー項目にリンクを設定しています。

v-list-groupのみを使用した場合の問題点

v-list-groupだけを利用すると下の階層がないメニュー項目のリンクをうまく設定することができません。設定がうまくいかないとはどのような状況なのかを下記で説明しています。

v-list-groupでサイドにメニューを作成するためのコードは以下の通りです。


<v-list nav dense>
  <v-list-group 
  v-for="nav_list in nav_lists" 
  :key="nav_list.name" 
  :prepend-icon="nav_list.icon" 
  no-action 
  :append-icon="nav_list.lists ? undefined : ''"> 
    <template v-slot:activator>
      <v-list-item-content>
        <v-list-item-title>{{ nav_list.name }}</v-list-item-title>
      </v-list-item-content>
    </template>
    <v-list-item v-for="list in nav_list.lists" 
    :key="list.name"
    :to="list.link">
      <v-list-item-content>
        <v-list-item-title>{{ list.name }}</v-list-item-title>
      </v-list-item-content>
    </v-list-item>
  </v-list-group>
</v-list>

上記のコードでは下の階層のメニュー項目(サブメニュー)のみリンクの設定を行なっているため、下の階層のメニュー項目をクリックするとURLがQuick Startのページの/quick-startに変わっていることが確認できます。

Routerの設定を行なっていないので移動先にはコンテンツが存在しないため何も表示されていません。上部のURLに注目してください。
サブメニューはリンク先に移動
サブメニューはリンク先に移動

下の階層のないメニュー項目であるUI ComponentsをクリックしてもURLは変わらないことが確認できます。URLはquick-startのままです。

サブメニューのないメニューをクリック
サブメニューのないメニューをクリック

下の階層のないメニュー項目にリンクを設定しようとしてもv-group-listのtemplate内にあるv-list-item-content, v-list-item-titleコンポーネントはslotだけしか設定できないためリンク情報を渡すことはできません。

v-list-itemであればpropsのtoを利用してリンク情報を渡すことができます。どのコンポーネントがどのpropsを利用できるかはマニュアルで確認する必要があります。

v-list-itemならpropsのtoでリンクを渡せるのでtemplateタグの中にv-list-itemコンポーネントを追加し下の階層がないメニュー項目だけにlinkを設定するようにコードの更新を行います。v-ifとv-elseを利用して下の階層があるものが(nav_list.listsがtrue)とないものでわけています。


<v-list nav dense>
  <v-list-group 
  v-for="nav_list in nav_lists" 
  :key="nav_list.name" 
  :prepend-icon="nav_list.icon" 
  no-action 
  :append-icon="nav_list.lists ? undefined : ''"> 
    <template v-slot:activator>
      <v-list-item v-if="nav_list.lists">
      <v-list-item-content>
        <v-list-item-title>{{ nav_list.name }}</v-list-item-title>
      </v-list-item-content>
      </v-list-item>
      <v-list-item v-else :to="nav_list.link">
      <v-list-item-content>
        <v-list-item-title>{{ nav_list.name }}</v-list-item-title>
      </v-list-item-content>
      </v-list-item>
    </template>
    <v-list-item v-for="list in nav_list.lists" 
    :key="list.name"
    :to="list.link">
      <v-list-item-content>
        <v-list-item-title>{{ list.name }}</v-list-item-title>
      </v-list-item-content>
    </v-list-item>
  </v-list-group>
</v-list>

リンクを設定することはできましたがインデントがずれ、クリックを押すと下記のように背景色が2色ずれて表示されます。動作はしていますが、見た目が非常に悪い状態です。

v-list-itemで分岐を行う
v-list-itemで分岐を行う

分岐を使ってv-group-listの外で階層がありとなしをわける

先程の例ではv-group-listの中で分岐を行なっていましたが、今回はv-group-listの外側で分岐を行います。分岐によって下の階層メニューのあるものはv-group-listを使い、下の階層メニューのないものはv-group-listを利用しない設定を行います。


<v-list nav dense>
    <template v-for="nav_list in nav_lists">
        <v-list-item
            v-if="!nav_list.lists" 
            :to="nav_list.link"
            :key="nav_list.name"
        >
            <v-list-item-icon>
              <v-icon>{{ nav_list.icon }}</v-icon>
            </v-list-item-icon>
            <v-list-item-content>
              <v-list-item-title>
                {{ nav_list.name }}
              </v-list-item-title>
            </v-list-item-content>
        </v-list-item>
        <v-list-group
            v-else
            no-action
            :prepend-icon="nav_list.icon"
            :key="nav_list.name"
            v-model="nav_list.active"
        >
            <template v-slot:activator>
                <v-list-item-content>
                  <v-list-item-title>
                    {{ nav_list.active}}{{ nav_list.name }}
                  </v-list-item-title>
                </v-list-item-content>
            </template>
            <v-list-item
                v-for="list in nav_list.lists"
                :key="list.name"
                :to="list.link"
            >
            <v-list-item-title>
              {{ list.name }}
            </v-list-item-title>
            </v-list-item>
        </v-list-group>
    </template>
</v-list>

templateタグを追加し、v-forを使ってnav_listsを展開します。次にnav_lists.listsで下の階層メニュー(サブメニュー)を持っているものと持っていないもので分岐を行います。

下の階層メニュー持っていない場合は、v-group-listを使わずv-list-itemの中でメニュー項目等を設定します。リンクはそのv-list-itemのpropsのtoを利用します。メニューのアイコンについてはv-list-item-iconコンポーネントを使います。

下の階層メニューを持っている場合はv-group-listを利用します。v-group-list内のv-list-itemのpropのtoでリンクを設定します。

動作確認を行うと下の階層メニューがないメニュー項目をクリックすると指定したページに移動することができます。またインデント等も崩れていません。しかし、サブメニューが開いた状態で下の階層メニューがないメニュー項目をクリックしても開いたサブメニューは閉じることなく開いたままの状態になります。

サブメニューが開いた状態になる
サブメニューが開いた状態になる

Styles&animationのメニュー項目をクリックしてサブメニューを開いた後にDirectivesをクリックしても開いたサブメニューが開いたままの状態。

開いたサブメニューを閉じる方法

開いたサブメニューを閉じるための制御を行うためにnav_listsに新たにactiveプロパティを追加します。下の階層を持つメニュー項目すべてにactiveプロパティの追加を行なってください。


nav_lists:[
  {
    name: 'Getting Started',
    icon: 'mdi-speedometer',
    active: false,
    link: '',
    lists:[{
        name:'Quick Start',link:'/quick-start'
      },
      {
        name:'Pre-made layouts',link:'/pre-made-layouts'
      }
    ]
  },
//省略
デフォルトからサブメニューを開いた状態にしたい場合は、activeの値をtrueに設定してください。

このactiveの値でメニューが開いているか閉じているかを管理します。そのためv-list-groupにv-modelを追加します。

v-list-groupではpropsのvalueでメニューの開け閉めを制御することができます。マニュアルでv-list-groupのpropsを確認してください。v-modelを設定することでv-list-groupのvalueの値を設定しています。

<v-list-group
    v-else
    no-action
    :prepend-icon="nav_list.icon"
    :key="nav_list.name"
    v-model="nav_list.active" //追加
>

下の階層を持たないメニュー項目をクリックした際に開いたサブメニューを閉じたいので下の階層を持たないメニュー項目のv-list-itemにclickイベントを追加します。メソッド名はmenu_closeとします。


<v-list nav dense>
    <template v-for="nav_list in nav_lists">
      <v-list-item
          v-if="!nav_list.lists" 
          :to="nav_list.link"
          :key="nav_list.name"
            @click="menu_close" //追加
      >

vueに最後にmenu_closeメソッドを追加します。forEachを使ってnav_listsのオブジェクトのactiveプロパティをすべてfalseに設定しています。


export default {
  methods:{
          menu_close(){
            this.nav_lists.forEach( nav_list => nav_list.active = false)
          }
        },
  data(){
    return{
        drawer: null,
        supports:[

これらの設定が完了すると下の階層を持たないメニュー項目をクリックすると開いたサブメニューを閉じることができます。

開いたサブメニューが閉じる
開いたサブメニューが閉じる

下記のように画面を移動することができます。

ナビゲーションメニューの画面遷移
ナビゲーションメニューの画面遷移

作成したコード全体は下記の通りです。


<template>
  <v-app>
  <v-navigation-drawer app v-model="drawer" clipped >
    <v-container>
      <v-list-item>
        <v-list-item-content>
          <v-list-item-title class="title grey--text text--darken-2">
            Navigation lists
          </v-list-item-title>
        </v-list-item-content>
      </v-list-item>
      <v-divider></v-divider>

<v-list nav dense>
    <template v-for="nav_list in nav_lists">
        <v-list-item
            v-if="!nav_list.lists" 
            :to="nav_list.link"
            :key="nav_list.name"
              @click="menu_close"
        >
            <v-list-item-icon>
              <v-icon>{{ nav_list.icon }}</v-icon>
            </v-list-item-icon>
            <v-list-item-content>
              <v-list-item-title>
                {{ nav_list.name }}
              </v-list-item-title>
            </v-list-item-content>
        </v-list-item>
        <v-list-group
            v-else
            no-action
            :prepend-icon="nav_list.icon"
            :key="nav_list.name"
            v-model="nav_list.active"
        >
            <template v-slot:activator>
                <v-list-item-content>
                  <v-list-item-title>
                    {{ nav_list.name }}
                  </v-list-item-title>
                </v-list-item-content>
            </template>
            <v-list-item
                v-for="list in nav_list.lists"
                :key="list.name"
                :to="list.link"
            >
            <v-list-item-title>
              {{ list.name }}
            </v-list-item-title>
            </v-list-item>
        </v-list-group>
    </template>
</v-list>

    </v-container>
  </v-navigation-drawer>
    <v-app-bar color="primary" dark app clipped-left>
      <v-app-bar-nav-icon @click="drawer=!drawer"></v-app-bar-nav-icon>
      <v-toolbar-title>Vuetify</v-toolbar-title>

    </v-app-bar>
    <v-content>
      <router-view />
    </v-content>
    <v-footer color="primary" dark app>
      Vuetify
    </v-footer>
  </v-app>
</template>

<script>
export default {
  methods:{
          menu_close(){
            this.nav_lists.forEach( nav_list => nav_list.active = false)
          }
        },
  data(){
    return{
        drawer: null,
        supports:[
          {
            name: 'Consulting and suppourt',
            icon: 'mdi-vuetify',
            link:'/consulting-and-support'
          },
          {
            name: 'Discord community',
            icon: 'mdi-discord',
            link:'/discord-community'},
          {
            name: 'Report a bug',
            icon: 'mdi-bug',
            link:'/report-a-bug'
          },
          {
            name: 'Github issue board',
            icon: 'mdi-github-face',
            link:'/guthub-issue-board'
          },
          {
            name: 'Stack overview',
            icon: 'mdi-stack-overflow',
            link:'/stack-overview'
          },
        ],
nav_lists:[
  {
    name: 'Getting Started',
    icon: 'mdi-speedometer',
    active: false,
    link: '',
    lists:[{
        name:'Quick Start',link:'/quick-start'
      },
      {
        name:'Pre-made layouts',link:'/pre-made-layouts'
      }
    ]
  },
  {
    name: 'Customization',
    icon: 'mdi-cogs',
    link: '/customization'
  },
  {
    name: 'Styles & animations',
    icon: 'mdi-palette',
    link: '',
    active: false,
    lists:[{
      name :'Colors', link:'/colors'
      },
      {
      name :'Content', link:'/content'
      },
      {
      name :'Display', link:'/display'}
    ]
  },
  {
    name: 'UI Components',
    icon: 'mdi-view-dashboard',
    link: '/components'
  },
  {
    name: 'Directives',
    icon: 'mdi-function',
    link: '/directives'
  },
  {
    name: 'Preminum themes',
    icon: 'mdi-vuetify',
    link: '/preminum_themes'
  },
]
    }
  }
}
</script>