點燈坊

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

實作可編輯 Treeview

Sam Xiao's Avatar 2021-10-06

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

Version

Tailwind CSS 2.2.0

Treeview

treeview000

  • 一開始自動將所有 tree 展開
  • folder 以紅色表示

treeview001

  • 按下 + 可新增 item,自動變成 textbox 供輸入
  • 按下 - 可刪除 item

treeview002

  • 將 item double click 可從 item 變成 folder

App.vue

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

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

let onMakeFolder = x => {
  x.isOpened = true
  x.children = [
    { name: '', isSelected: true, isDeleted: false }
  ]
}

let onAddItem = x =>
  x.children = [
    ...x.children,
    { name: '', isSelected: true, isDeleted: false }
  ]
</script>

<template>
  <ul>
    <Treeview class="cursor-pointer" :item="tree" @makeFolder="onMakeFolder" @addItem="onAddItem"/>
  </ul>
</template>

使用 <Treeview> 的 page。

56 行

<ul>
  <Treeview class="cursor-pointer" :item="tree" @makeFolder="onMakeFolder" @addItem="onAddItem"/>
</ul>

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

  • class:使 <Treeview> 的 cursor 有 pointer
  • :item:傳入 data 供 <Treeview> data binding
  • @makeFolder:處理 makeFolder event 提供新 folder 資料
  • @addItem:處理 addItem event 提供新 item 資料

10 行

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

Folder 須提供以下 property:

  • name:folder 顯示名稱
  • isSelected:是否被選擇,若被選擇將顯示 textbox,否則以 <span> 顯示
  • isDeleted:是否被刪除,若被刪除則 <Treeview> 不顯示
  • isOpened:是否展開 folder,folder 必須另外提供此 property

Item 須提供以下 property:

  • name:item 顯示名稱
  • isSelected:是否被選擇,若被選擇將顯示 textbox,否則以 <span> 顯示
  • isDeleted:是否被刪除,若被刪除則 <Treeview> 不顯示

41 行

let onMakeFolder = x => {
  x.isOpened = true
  x.children = [
    { name: '', isSelected: true, isDeleted: false }
  ]
}

當 item 被 double click 時,<Treeview> 將發出 makeFolder event,user 需準備好新 folder 資料。

48 行

let onAddItem = x =>
  x.children = [
    ...x.children,
    { name: '', isSelected: true, isDeleted: false }
  ]

當按下 + 時,<Treeview> 將發出 addItem event,user 需準備好新 item 資料。

Treeview.vue

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

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

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

let onMakeFolder = _ => {
  if (!isFolder) {
    emits ('makeFolder', props.item)
  }
}

let onDelete = x => x.isDeleted = true
</script>

<template>
  <li v-if="!item.isDeleted">
    <div :class="{ 'font-bold': isFolder, 'text-red-500': isFolder }">
      <input class="mr-2" type="checkbox" v-model="item.isSelected">
      <input v-if="item.isSelected" class="border" v-model="item.name" placeholder="Input item">
      <span v-else @click="onToggle (item)" @dblclick="onMakeFolder">{{ item.name }}</span>
      <button v-if="item.isSelected" class="px-2 ml-2 font-black" @click="onDelete (item)">-</button>
    </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" @makeFolder="emits ('makeFolder', $event)" @addItem="emits ('addItem', $event)"/>
      <li @click="emits ('addItem', item)">+</li>
    </ul>
  </li>
</template>

<Treeview> component 部分。

21 行

<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

21 行

<li v-if="!item.isDeleted" />

當 user 按下 - 可刪除 item,但並不是真的刪除 item,而是將 isDeleted 設定為 true,因此需判斷 isDeletedfalse 才顯示 item。

22 行

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

若是 folder 則為紅色粗體。

23 行

<input class="mr-2" type="checkbox" v-model="item.isSelected">

每個 item 前有 checkbox,且綁定到 item.isSelected

24 行

<input v-if="item.isSelected" class="border" v-model="item.name" placeholder="Input item">
<span v-else @click="onToggle (item)" @dblclick="onMakeFolder">{{ item.name }}</span>
  • isSelectedtrue 則顯示 textbox,否則以 <span> 顯示 item 或 folder
  • Item 的 double click 會使得 item 變成 folder
  • Folder 的 single click 則會 toggle 展開或關閉 folder

26 行

<button v-if="item.isSelected" class="px-2 ml-2 font-black" @click="onDelete (item)">-</button>

isSelectedtrue 則顯示 - 可刪除 item 或 folder。

29 行

<ul v-show="item.isOpened" v-if="isFolder" class="pl-4 leading-6">
  <TreeItem class="item" v-for="(x, i) in item.children" :key="i" :item="x" @makeFolder="emits ('makeFolder', $event)" @addItem="emits ('addItem', $event)"/>
  <li @click="emits ('addItem', item)">+</li>
</ul>
  • 顯示 folder,會再 recursive 以 v-forchildren 使用 <Treeview>,並將 data 傳入 item prop
  • <Treeview>@makeFolder@addItem 也須 recursive 繼續使用 emits 觸發

第 2 行

let props = defineProps ({ item: Object })
let emits = defineEmits (['makeFolder', 'addItem'])
  • 使用 defineProps 宣告 item prop
  • 使用 defineEmits 宣告 makeFolderaddItem 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
}

當對 item click 時,若為 folder 則對 isOpened toggle。

11 行

let onMakeFolder = _ => {
  if (!isFolder) {
    emits ('makeFolder', props.item)
  }
}

對 item double click 時,若為 folder 則發出 makeFolder event。

17 行

let onDelete = x => x.isDeleted = true

刪除時則對 isDeleted 設定為 true

Conclusion

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

Reference

Vue, Tree View