Vuetifyでサイドバーにメニューを追加したけれどもメニューに階層がある場合にうまくリンク設定、表示設定ができず悩んでいるという人を対象に階層があるメニューのリンク設定と表示設定について解説を行っています。

Vuefityでサイドメニューを表示させようとしている人はぜひ参考にしてみてください。

本文書を読み終えると下記のようなサイドメニューを実装すること可能です。

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

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

利用するメニューデータ

サイドにあるナビゲーションバーを構成するデータは下記となります。データプロパティnav_listsにリスト情報を設定しています。階層があるメニューと解消がないメニューの2つが混在している状態です。例えばGatting Startedは階層を持っているメニューでCustomizationは階層を持っていないメニューです。


data() {
  return {
    //略,
    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>

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

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

下の階層を持っていないメニューであるUI Componentsをクリックしてもブラウザに表示されているURLはquick-startのままで変わらないことが確認できます。

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

下の階層を持たないメニューはv-group-list内のtemplateタグにあるv-list-item-title中でメニューの表示を行っています。しかし、メニューをラップしているv-list-item-content, v-list-item-titleコンポーネントはslotだけしか設定できないためpropsのtoを利用してリンク情報を渡すことはできません。

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

v-list-itemコンポーネントであればpropsのtoでリンクを渡すことができるのでtemplateタグの中にv-list-itemコンポーネントを追加します。下の階層を持たないメニューだけにpropsのtoを設定すできるようにコードの更新を行います。v-ifとv-elseを利用して下の階層があるものとないものでわけています。nav_list.listsがない場合のみproprのtoを設定できるようにしています。


<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>

v-list-itemとif文での分岐を追加することにより下の層を持たないメニューに対してリンクを設定することはできました。しかしインデントがずれ、クリックを押すと下記のように背景色が2色ずれて表示されます。動作はしていますが、見た目が非常に悪い状態です。

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

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

先程までの例ではv-group-listタグのv-forディレクティブでデータプロパティnav_listsの展開を行い、v-group-listタグの中で分岐を行なっていました。今回はtemplateタグを追加し、その中でv-forディレクティブを利用してデータプロパティnav_listsしています。分岐を利用することで下の階層メニューを持つものは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"
    >
      <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-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をクリックすると開いたStyles&animationのサブメニューは開いたままとなります。この問題を解決するために次の処理を行います。

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

開いたサブメニューを閉じるための制御を行うためにnav_listsに新たにactiveプロパティを追加します。下の階層を持つメニューすべてにactiveプロパティの追加を行なってください。下記ではGetting Startedのみ表示していますがStyles & animationsでも行ってください。


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に設定してください。
fukidashi

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

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

<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に設定しています。


methods: {
  menu_close() {
    this.nav_lists.forEach((nav_list) => (nav_list.active = false));
  },
},

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

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

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

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

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


<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>