點燈坊

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

實作唯讀 Treeview

Sam Xiao's Avatar 2021-10-06

若資料本身具有階層特性,則適合使用 Treeview 呈現,若要能支援無限階層,則必須在 Component 內 Recursive 使用自身 Component。

Version

Tailwind CSS 2.2.0

Treeview

readonly000

  • folder 以紅色表示
  • + 展開 tree,- 收縮 tree

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: 'Hello' },
          ]
        },
        { name: 'Hello' },
        {
          name: 'Children',
          isOpened: false,
          children: [
            { name: 'Hello' },
          ]
        }
      ]
    }
  ]
})

let onSelect = x => console.log (x.name)
</script>

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

使用 <Treeview> 的 page。

37 行

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

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

  • class:使 <Treeview> 的 cursor 有 pointer
  • :item:傳入 data 供 <Treeview> data binding
  • @select:處理當 item 被選擇時所發出的 select event

第 4 行

let tree = $ref ({
  name: 'My Treeview',
  isOpened: true,
  children: [
    { name: 'Hello' },
    {
      name: 'Children',
      isOpened: true,
      children: [
        {
          name: 'Children',
          isOpened: true,
          children: [
            { name: 'Hello' },
          ]
        },
        { name: 'Hello' },
        {
          name: 'Children',
          isOpened: false,
          children: [
            { name: 'Hello' },
          ]
        }
      ]
    }
  ]
})

Folder 須提供以下 property:

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

Item 須提供以下 property:

  • name:item 顯示名稱

33 行

let onSelect = x => console.log (x.name)

當 item 被選擇時,<Treeview> 將發出 select event,user 可獲得所選擇 item 名稱。

Treeview.vue

<script setup>
let props = defineProps ({ item: Object })
let emits = defineEmits (['select'])

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

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

let onSelect = x => emits ('select', x)
</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>
      <div @click="onSelect (item)">{{ item.name }}</div>
    </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" @select="emits ('select', $event)"/>
    </ul>
  </li>
</template>

<Treeview> component 部分。

15 行

<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

16 行

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

若是 folder 則為紅色粗體。

17 行

<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 處理。

29 行

<div @click="onSelect (item)">{{ item.name }}</div>

顯示 folder 或 item,若選擇則呼叫 onSelect

32 行

<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" @select="emits ('select', $event)"/>
</ul>
  • 顯示 folder,會再 recursive 以 v-forchildren 使用 <Treeview>,並將 data 傳入 item prop
  • <Treeview>@select 也須 recursive 繼續使用 emits 觸發

第 2 行

let props = defineProps ({ item: Object })
let emits = defineEmits (['select'])
  • 使用 defineProps 宣告 item prop
  • 使用 defineEmits 宣告 select event

第 5 行

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

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

第 7 行

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

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

11 行

let onSelect = x => emits ('select', x)

當選擇 folder 或 item 時發出 select event。

Conclusion

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

Reference

Vue, Tree View