Skip to content

Commit 6ebe9fd

Browse files
authored
feat(VTreeview): add no-data slot (#22031)
resolves #21954
1 parent 2e2cddb commit 6ebe9fd

File tree

4 files changed

+211
-1
lines changed

4 files changed

+211
-1
lines changed

packages/docs/src/data/new-in.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,12 +279,15 @@
279279
"VTreeview": {
280280
"props": {
281281
"hideActions": "3.9.0",
282+
"hideNoData": "3.10.0",
283+
"noDataText": "3.10.0",
282284
"separateRoots": "3.9.0",
283285
"indentLines": "3.9.0"
284286
},
285287
"slots": {
286288
"header": "3.10.0",
287289
"footer": "3.11.0",
290+
"no-data": "3.11.0",
288291
"toggle": "3.10.0"
289292
}
290293
},
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
<template>
2+
<v-treeview
3+
v-model:activated="activated"
4+
:items="items"
5+
item-key="id"
6+
item-value="id"
7+
activatable
8+
open-all
9+
>
10+
<template v-slot:append="{ item, depth, isFirst, isLast }">
11+
<v-icon-btn :disabled="!depth" icon="mdi-arrow-left" @click.stop="move(item, 'left')"></v-icon-btn>
12+
<v-icon-btn :disabled="isFirst" icon="mdi-arrow-up" @click.stop="move(item, 'up')"></v-icon-btn>
13+
<v-icon-btn :disabled="isLast" icon="mdi-arrow-down" @click.stop="move(item, 'down')"></v-icon-btn>
14+
<v-icon-btn :disabled="isFirst" icon="mdi-arrow-right" @click.stop="move(item, 'right')"></v-icon-btn>
15+
</template>
16+
</v-treeview>
17+
</template>
18+
19+
<script setup>
20+
import { ref, shallowRef } from 'vue'
21+
22+
const activated = ref([])
23+
const root = {
24+
id: 0,
25+
children: [
26+
{
27+
id: 1,
28+
title: 'Office Tools',
29+
children: [
30+
{ id: 2, title: 'Calendar' },
31+
{ id: 3, title: 'Notepad' },
32+
],
33+
},
34+
{
35+
id: 4,
36+
title: 'Dev Tools',
37+
children: [
38+
{ id: 5, title: 'VS Code' },
39+
{ id: 6, title: 'Figma' },
40+
{ id: 7, title: 'Webstorm' },
41+
],
42+
},
43+
],
44+
}
45+
const items = shallowRef([...root.children])
46+
47+
function findParent (id, items = [root]) {
48+
if (items.length === 0) return null
49+
return items.find(item => item.children?.some(c => c.id === id)) ??
50+
findParent(id, items.flatMap(item => item.children ?? []))
51+
}
52+
53+
function findItemBefore (item) {
54+
return findParent(item.id).children
55+
.find((_, i, all) => all[i + 1]?.id === item.id)
56+
}
57+
58+
function findItemAfter (item) {
59+
return findParent(item.id).children
60+
.find((_, i, all) => all[i - 1]?.id === item.id)
61+
}
62+
63+
function detach (item) {
64+
const parent = findParent(item.id)
65+
parent.children.splice(parent.children.indexOf(item), 1)
66+
if (parent.children.length === 0) parent.children = undefined
67+
}
68+
69+
function injectNextTo (item, target, after = true) {
70+
if (!target || target === root) return
71+
detach(item)
72+
const targetParent = findParent(target.id)
73+
targetParent.children.splice(targetParent.children.indexOf(target) + (after ? 1 : 0), 0, item)
74+
activated.value = [item.id]
75+
}
76+
77+
function appendTo (item, target) {
78+
if (!target) return
79+
detach(item)
80+
target.children ??= []
81+
target.children.push(item)
82+
activated.value = [item.id]
83+
}
84+
85+
function move (item, direction) {
86+
switch (direction) {
87+
case 'left':
88+
injectNextTo(item, findParent(item.id))
89+
break
90+
case 'up':
91+
injectNextTo(item, findItemBefore(item), false)
92+
break
93+
case 'right':
94+
appendTo(item, findItemBefore(item))
95+
break
96+
case 'down':
97+
injectNextTo(item, findItemAfter(item))
98+
break
99+
}
100+
items.value = [...root.children]
101+
}
102+
</script>
103+
104+
<script>
105+
export default {
106+
data: () => ({
107+
activated: [],
108+
root: {
109+
id: 0,
110+
children: [
111+
{
112+
id: 1,
113+
title: 'Office Tools',
114+
children: [
115+
{ id: 2, title: 'Calendar' },
116+
{ id: 3, title: 'Notepad' },
117+
],
118+
},
119+
{
120+
id: 4,
121+
title: 'Dev Tools',
122+
children: [
123+
{ id: 5, title: 'VS Code' },
124+
{ id: 6, title: 'Figma' },
125+
{ id: 7, title: 'Webstorm' },
126+
],
127+
},
128+
],
129+
},
130+
items: [],
131+
}),
132+
mounted () {
133+
this.items = [...this.root.children]
134+
},
135+
methods: {
136+
findParent (id, items) {
137+
items ??= [this.root]
138+
if (items.length === 0) return null
139+
return items.find(item => item.children?.some(c => c.id === id)) ??
140+
this.findParent(id, items.flatMap(item => item.children ?? []))
141+
},
142+
findItemBefore (item) {
143+
return this.findParent(item.id).children
144+
.find((_, i, all) => all[i + 1]?.id === item.id)
145+
},
146+
findItemAfter (item) {
147+
return this.findParent(item.id).children
148+
.find((_, i, all) => all[i - 1]?.id === item.id)
149+
},
150+
detach (item) {
151+
const parent = this.findParent(item.id)
152+
parent.children.splice(parent.children.indexOf(item), 1)
153+
if (parent.children.length === 0) parent.children = undefined
154+
},
155+
injectNextTo (item, target, after = true) {
156+
if (!target || target === this.root) return
157+
this.detach(item)
158+
const targetParent = this.findParent(target.id)
159+
targetParent.children.splice(targetParent.children.indexOf(target) + (after ? 1 : 0), 0, item)
160+
this.activated = [item.id]
161+
},
162+
appendTo (item, target) {
163+
if (!target) return
164+
this.detach(item)
165+
target.children ??= []
166+
target.children.push(item)
167+
this.activated = [item.id]
168+
},
169+
move (item, direction) {
170+
switch (direction) {
171+
case 'left':
172+
this.injectNextTo(item, this.findParent(item.id))
173+
break
174+
case 'up':
175+
this.injectNextTo(item, this.findItemBefore(item), false)
176+
break
177+
case 'right':
178+
this.appendTo(item, this.findItemBefore(item))
179+
break
180+
case 'down':
181+
this.injectNextTo(item, this.findItemAfter(item))
182+
break
183+
}
184+
this.items = [...this.root.children]
185+
},
186+
},
187+
}
188+
</script>

packages/docs/src/pages/en/components/treeview.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@ Both **append**, and **prepend** slots get additional information about the item
136136

137137
<ExamplesExample file="v-treeview/slot-append-and-prepend-item" />
138138

139+
#### No data
140+
141+
When searching within the treeview, you might want to show custom **no-data** slot to provide context or immediate action.
142+
143+
<ExamplesExample file="v-treeview/slot-no-data" />
144+
139145
#### Title
140146

141147
In this example we use a custom **title** slot to apply a line-through the treeview item's text when selected.

packages/vuetify/src/components/VTreeview/VTreeview.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Components
22
import { makeVTreeviewChildrenProps, VTreeviewChildren } from './VTreeviewChildren'
33
import { makeVListProps, useListItems, VList } from '@/components/VList/VList'
4+
import { VListItem } from '@/components/VList/VListItem'
45

56
// Composables
7+
import { useLocale } from '@/composables'
68
import { provideDefaults } from '@/composables/defaults'
79
import { makeFilterProps, useFilter } from '@/composables/filter'
810
import { useProxiedModel } from '@/composables/proxiedModel'
@@ -31,6 +33,11 @@ export const makeVTreeviewProps = propsFactory({
3133
openAll: Boolean,
3234
indentLines: [Boolean, String] as PropType<boolean | IndentLinesVariant>,
3335
search: String,
36+
hideNoData: Boolean,
37+
noDataText: {
38+
type: String,
39+
default: '$vuetify.noDataText',
40+
},
3441

3542
...makeFilterProps({ filterKeys: ['title'] }),
3643
...omit(makeVTreeviewChildrenProps(), [
@@ -53,7 +60,9 @@ export const VTreeview = genericComponent<new <T>(
5360
props: {
5461
items?: T[]
5562
},
56-
slots: VTreeviewChildrenSlots<T>
63+
slots: VTreeviewChildrenSlots<T> & {
64+
'no-data': never
65+
}
5766
) => GenericProps<typeof props, typeof slots>>()({
5867
name: 'VTreeview',
5968

@@ -69,6 +78,7 @@ export const VTreeview = genericComponent<new <T>(
6978
},
7079

7180
setup (props, { slots, emit }) {
81+
const { t } = useLocale()
7282
const { items } = useListItems(props)
7383
const activeColor = toRef(() => props.activeColor)
7484
const baseColor = toRef(() => props.baseColor)
@@ -175,6 +185,9 @@ export const VTreeview = genericComponent<new <T>(
175185
v-model:activated={ activated.value }
176186
v-model:selected={ selected.value }
177187
>
188+
{ visibleIds.value?.size === 0 && !props.hideNoData && (
189+
slots['no-data']?.() ?? (<VListItem key="no-data" title={ t(props.noDataText) } />)
190+
)}
178191
<VTreeviewChildren
179192
{ ...treeviewChildrenProps }
180193
density={ props.density }

0 commit comments

Comments
 (0)