點燈坊

失くすものさえない今が強くなるチャンスよ

實作 Menu Treeview

Sam Xiao's Avatar 2021-10-06

Menu 若使用 Treeview,通常都是 URL 即可,若要能支援無限階層,則必須在 Component 內 Recursive 使用自身 Component。

Version

Tailwind CSS 2.2.0

Treeview

menu000

  • folder 以紅色表示
  • + 展開 tree,- 收縮 tree
  • GoogleApple 都只是 <a> 而已

App.vue

<script setup>
import Treeview from '/src/components/Treeview.vue'

let tree = $ref ({
  name: 'My Treeview',
  isOpened: true,
  children: [
    { name: 'Hello' },
    {
      name: 'Children',
      isOpened: true,
      children: [
        {
          name: 'Children',
          isOpened: true,
          children: [
            {
              name: 'Google',
              href: 'https://www.google.com'
            },
          ]
        },
        { name: 'Hello' },
        {
          name: 'Children',
          isOpened: false,
          children: [
            {
              name: 'Apple',
              href: 'https://www.apple.com'
            },
          ]
        }
      ]
    }
  ]
})
</script>

<template>
  <ul>
    <Treeview class="cursor-pointer" :item="tree"/>
  </ul>
</template>

使用 <Treeview> 的 page。

41 行

<ul>
  <Treeview class="cursor-pointer" :item="tree"/>
</ul>

<Treeview> 使用方式必須包在 <ul> 內:

  • class:使 <Treeview> 的 cursor 有 pointer
  • :item:傳入 data 供 <Treeview> data binding

第 4 行

let tree = $ref ({
  name: 'My Treeview',
  isOpened: true,
  children: [
    { name: 'Hello' },
    {
      name: 'Children',
      isOpened: true,
      children: [
        {
          name: 'Children',
          isOpened: true,
          children: [
            {
              name: 'Google',
              href: 'https://www.google.com'
            },
          ]
        },
        { name: 'Hello' },
        {
          name: 'Children',
          isOpened: false,
          children: [
            {
              name: 'Apple',
              href: 'https://www.apple.com'
            },
          ]
        }
      ]
    }
  ]
})

Folder 須提供以下 property:

  • name:folder 顯示名稱
  • isOpened:是否展開 folder,folder 必須另外提供此 property

Item 須提供以下 property:

  • name:item 顯示名稱
  • href:item 所對應 URL

Treeview.vue

<script setup>
let props = defineProps ({ item: Object })

let isFolder = $computed (_ => props.item.children && props.item.children.length)

let onToggle = x => {
  if (isFolder) x.isOpened = !x.isOpened
}
</script>

<template>
  <li>
    <div :class="{ 'font-bold': isFolder, 'text-red-500': isFolder }" class="flex">
      <div v-if="isFolder" class="mr-1">
        <div v-if="!item.isOpened" @click="onToggle (item)">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
          </svg>
        </div>
        <div v-else @click="onToggle (item)">
          <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
            <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6" />
          </svg>
        </div>
      </div>
      <a :href="item.href">{{ item.name }}</a>
    </div>

    <ul v-show="item.isOpened" v-if="isFolder" class="pl-4 leading-6">
      <Treeview class="item" v-for="(x, i) in item.children" :key="i" :item="x"/>
    </ul>
  </li>
</template>

<Treeview> component 部分。

12 行

<li>
  <span>{{ item.name }}</span>
  <ul>
    <TreeItem v-for="(x, i) in item.children" :key="i" :item="x"/>
  </ul>
</li>

<Treeview> 本質是 <li>,包含兩部分:

  • <span>:顯示 item
  • <ul>:顯示 folder,會再 recursive 以 v-forchildren 使用 <Treeview>,並將 data 傳入 item prop

13 行

<div :class="{ 'font-bold': isFolder, 'text-red-500': isFolder }">

若是 folder 則為紅色粗體。

14 行

<div v-if="isFolder" class="mr-1">
  <div v-if="!item.isOpened" @click="onToggle (item)">
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
    </svg>
  </div>
  <div v-else @click="onToggle (item)">
    <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18 12H6" />
    </svg>
  </div>
</div>

若 item 為 folder 則顯示 + 展開 folder 或 - 收縮 folder,並都呼叫 onToggle 處理。

26 行

<a :href="item.href">{{ item.name }}</a>

改用 <a> 顯示 folder 或 item,若有提供 href 則會綁定。

29 行

<ul v-show="item.isOpened" v-if="isFolder" class="pl-4 leading-6">
  <Treeview class="item" v-for="(x, i) in item.children" :key="i" :item="x"/>
</ul>

顯示 folder,會再 recursive 以 v-forchildren 使用 <Treeview>,並將 data 傳入 item prop。

第 2 行

let props = defineProps ({ item: Object })

使用 defineProps 宣告 item prop。

第 4 行

let isFolder = $computed (_ => props.item.children && props.item.children.length)

根據是否有 children 且非 Empty Array 定義 isFolder computed。

第 6 行

let onToggle = x => {
  if (isFolder) x.isOpened = !x.isOpened
}

當按下 +- 時對 folder 的 isOpened toggle。

Conclusion

  • 雖然有很多 package 提供 treeview,但自行實作 treeview 可對其更深入客製化

Reference

Vue, Tree View