若資料本身具有階層特性,則適合使用 Treeview 呈現,若要能支援無限階層,則必須在 Component 內 Recursive 使用自身 Component。
Version
Tailwind CSS 2.2.0
Treeview
- 一開始自動將所有 tree 展開
- folder 以紅色表示
- 按下
+
可新增 item,自動變成 textbox 供輸入 - 按下
-
可刪除 item
- 將 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-for
對children
使用<Treeview>
,並將 data 傳入item
prop
21 行
<li v-if="!item.isDeleted" />
當 user 按下 -
可刪除 item,但並不是真的刪除 item,而是將 isDeleted
設定為 true
,因此需判斷 isDeleted
為 false
才顯示 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>
- 若
isSelected
為true
則顯示 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>
若 isSelected
為 true
則顯示 -
可刪除 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-for
對children
使用<Treeview>
,並將 data 傳入item
prop <Treeview>
的@makeFolder
與@addItem
也須 recursive 繼續使用emits
觸發
第 2 行
let props = defineProps ({ item: Object })
let emits = defineEmits (['makeFolder', 'addItem'])
- 使用
defineProps
宣告item
prop - 使用
defineEmits
宣告makeFolder
與addItem
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 可對其更深入客製化