Vueでコンテキストメニューみたいにネストしたメニューを作る

Vue

本記事では以下のコードを参考にさせていただきました。

[Feature Request] Nested menus · Issue #1877 · vuetifyjs/vuetify
What will it allow you to do that you can't do today? Create nested menus according to MD spec How will it make current work-arounds straightforward? A current ...

v-menuなどを使用するため、Vuetifyが必須となっています。

完成形イメージ

上のような、ネストしたメニューを作ります。
好きなところにDividerをつけたり、それぞれクリックしたときのアクションなどを定義することができます。

サンプルコード

以下、コンポーネント化したネストメニューです。
ご自身に合うようにカスタムしてください。

<template>
  <v-menu :offset-x='isOffsetX' :offset-y='isOffsetY' :open-on-hover='isOpenOnHover' :transition='transition'>
    <template v-slot:activator="{ on }">
      <v-btn v-if='icon' :color='color' v-on="on"><v-icon>{{ icon }}</v-icon></v-btn>
      <v-list-item v-else-if='isSubMenu' class='d-flex justify-space-between' v-on="on">
        {{ name }}<v-icon>far fa-chevron-right</v-icon>
      </v-list-item>
      <v-btn v-else :color='color' v-on="on" text tile>{{ name }}</v-btn>
    </template>
    <v-list>
      <template v-for="(item, index) in menuItems">
        <v-divider v-if='item.isDivider' :key='index' />
        <nested-menu v-else-if='item.menu' :key='index' :name='item.name' :menu-items='item.menu' @nested-menu-click='emitClickEvent'
          :is-open-on-hover=false :is-offset-x=true :is-offset-y=false :is-sub-menu=true
        />
        <v-list-item v-else :key='index' @click='emitClickEvent(item)'>
          <v-list-item-title>{{ item.name }}</v-list-item-title>
        </v-list-item>
      </template>
    </v-list>
  </v-menu>
</template>

<script>
export default {
  name: 'NestedMenu',
  props: {
    name: String,
    icon: String,
    menuItems: Array,
    color: { type: String, default: 'secondary' },
    isOffsetX: { type: Boolean, default: false },
    isOffsetY: { type: Boolean, default: true },
    isOpenOnHover: { type: Boolean, default: false },
    isSubMenu: { type: Boolean, default: false },
    transition: { type: String, default: 'scale-transition' }
  },
  methods: {
    emitClickEvent (item) {
      // this.closeAllMenus() // Theoretically, create a method that does this as a workaround
      this.$emit('nested-menu-click', item)
    }
  }
}
</script>

以下、親コンポーネントになります。
fileMenuItemsという配列の中身によってメニューの構成が変わります。

<template>
  <v-app>
    <v-main>
      <nested-menu name='ネストしたメニューを開く' :menu-items='fileMenuItems' @nested-menu-click='onMenuItemClick' />
    </v-main>
  </v-app>
</template>

<script>
import NestedMenu from './components/NestedMenu.vue';

export default {
  name: 'App',

  components: {
    NestedMenu
  },

  data: () => ({
    fileMenuItems: [
      { name: 'メニュー1', action: () => { console.log('menu-item-1') } },
      { isDivider: true },
      { name: 'メニュー2' },
      {
        name: 'サブメニュー1',
        menu: [
          { name: '1.1' },
          { name: '1.2' },
          {
            name: 'サブメニュー2',
            menu: [
              { name: '2.1' },
              { name: '2.2' },
              {
                name: 'サブメニュー3',
                menu: [
                  { name: '3.1' },
                  { name: '3.2' },
                  {
                    name: 'サブメニュー4',
                    menu: [
                      { name: '4.1'},
                      { name: '4.2' },
                      { name: '4.3' }
                    ]
                  }
                ]
              }
            ]
          }
        ]
      },
      { name: 'メニュー3' },
      { isDivider: true },
      { name: 'メニュー4', action: () => { console.log('menu-item-4') } },
      { name: 'メニュー5', action: () => { console.log('menu-item-5') } }
    ]
  }),
  methods: {
    onMenuItemClick (item) {
      console.log(`onMenuItemClick(), item=${item}`)
      if (item.action) {
        item.action()
      }
    }
  }
};
</script>

クリック時のアクションは、

action: () => { console.log('menu-item-1') }

といったように定義します。

クリック時にサブメニューを開く場合は、以下のようにmenu配列を指定します。

menu: [
  { name: '4.1'},
  { name: '4.2' },
  { name: '4.3' }
]

また、

{ isDivider: true },

とすることでDivider(区切り線)を表示させることができます。

まとめ

以上になります。

コンポーネント化することでシンプルに使えるようになっていいですね!

この記事を書いた人
Tenma Endou

15歳からプログラミングを始め、現在は正社員+個人事業主でほぼ休まず労働
2018年に開発したアプリ「LIVLE」がTwitterで12000RTされる。(過去の栄光)
Flutter / Nuxt.js / Laravel / Go / React
お仕事依頼はこちらから⇢engineer@tepci.me

Tenma Endouをフォローする
Vue
Tenma Endouをフォローする
目に優しいエンジニアブログ

コメント

タイトルとURLをコピーしました