Skip to content

Commit ada8743

Browse files
committed
fix(VTreeview): faster interactions with large trees
1 parent 22f666d commit ada8743

File tree

5 files changed

+73
-22
lines changed

5 files changed

+73
-22
lines changed

packages/api-generator/src/locale/en/VList.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"disabled": "Puts all children inputs into a disabled state.",
77
"filterable": "**FOR INTERNAL USE ONLY** Prevents list item selection using [space] key and pass it back to the text input. Used internally for VAutocomplete and VCombobox.",
88
"inactive": "If set, the list tile will not be rendered as a link even if it has to/href prop or @click handler.",
9+
"itemsRegistration": "When set to 'props', skips rendering collpased items/nodes (for significant performance gains). Does not support links, so you might need to set **item-value** to `href`, `to` or a custom resolver function.",
910
"lines": "Designates a **minimum-height** for all children `v-list-item` components. This prop uses [line-clamp](https://developer.mozilla.org/en-US/docs/Web/CSS/-webkit-line-clamp) and is not supported in all browsers.",
1011
"link": "Applies `v-list-item` hover styles. Useful when using the item is an _activator_.",
1112
"nav": "An alternative styling that reduces `v-list-item` width and rounds the corners. Typically used with **[v-navigation-drawer](/components/navigation-drawers)**.",

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,11 @@
182182
"glow": "3.8.0"
183183
}
184184
},
185+
"VList": {
186+
"props": {
187+
"itemsRegistration": "3.11.0"
188+
}
189+
},
185190
"VListItem": {
186191
"props": {
187192
"baseColor": "3.3.0",
@@ -290,7 +295,8 @@
290295
"hideNoData": "3.10.0",
291296
"noDataText": "3.10.0",
292297
"separateRoots": "3.9.0",
293-
"indentLines": "3.9.0"
298+
"indentLines": "3.9.0",
299+
"itemsRegistration": "3.11.0"
294300
},
295301
"slots": {
296302
"header": "3.10.0",

packages/vuetify/src/components/VList/VList.tsx

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import { makeThemeProps, provideTheme } from '@/composables/theme'
2222
import { makeVariantProps } from '@/composables/variant'
2323

2424
// Utilities
25-
import { computed, ref, shallowRef, toRef } from 'vue'
25+
import { computed, ref, shallowRef, toRef, watch } from 'vue'
2626
import {
2727
EventProp,
2828
focusChild,
@@ -169,7 +169,35 @@ export const VList = genericComponent<new <
169169
const { dimensionStyles } = useDimension(props)
170170
const { elevationClasses } = useElevation(props)
171171
const { roundedClasses } = useRounded(props)
172-
const { children, open, parents, select, getPath } = useNested(props)
172+
const { children, disabled, open, parents, select, getPath } = useNested(props)
173+
174+
function flatten (items: InternalListItem<any>[]): InternalListItem<any>[] {
175+
return [
176+
...items,
177+
...items.length ? flatten(items.flatMap(x => x.children ?? [])) : [],
178+
]
179+
}
180+
181+
watch(items, val => {
182+
if (props.itemsRegistration === 'render') return
183+
const allNodes = flatten(val)
184+
children.value = new Map(
185+
allNodes
186+
.filter(item => item.children)
187+
.map(item => [item.value, item.children!.map(x => x.value)])
188+
)
189+
parents.value = new Map(
190+
allNodes
191+
.filter(item => !val.includes(item))
192+
.map(item => [item.value, allNodes.find(x => x.children?.includes(item))?.value])
193+
)
194+
disabled.value = new Set(
195+
allNodes
196+
.filter(item => (item as any).disabled)
197+
.map(item => item.value)
198+
)
199+
}, { immediate: true })
200+
173201
const lineClasses = toRef(() => props.lines ? `v-list--${props.lines}-line` : undefined)
174202
const activeColor = toRef(() => props.activeColor)
175203
const baseColor = toRef(() => props.baseColor)

packages/vuetify/src/components/VList/VListGroup.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ import { VDefaultsProvider } from '@/components/VDefaultsProvider'
66
import { useList } from './list'
77
import { makeComponentProps } from '@/composables/component'
88
import { IconValue } from '@/composables/icons'
9-
import { useNestedGroupActivator, useNestedItem } from '@/composables/nested/nested'
9+
import { useNestedGroupActivator, useNestedItem, VNestedSymbol } from '@/composables/nested/nested'
1010
import { useSsrBoot } from '@/composables/ssrBoot'
1111
import { makeTagProps } from '@/composables/tag'
1212
import { MaybeTransition } from '@/composables/transition'
1313

1414
// Utilities
15-
import { computed } from 'vue'
15+
import { computed, inject, toRef } from 'vue'
1616
import { defineComponent, genericComponent, propsFactory, useRender } from '@/util'
1717

1818
export type VListGroupSlots = {
@@ -67,6 +67,9 @@ export const VListGroup = genericComponent<VListGroupSlots>()({
6767
const list = useList()
6868
const { isBooted } = useSsrBoot()
6969

70+
const parent = inject(VNestedSymbol)
71+
const renderWhenClosed = toRef(() => parent?.root?.itemsRegistration.value === 'render')
72+
7073
function onClick (e: Event) {
7174
if (['INPUT', 'TEXTAREA'].includes((e.target as Element)?.tagName)) return
7275
open(!isOpen.value, e)
@@ -114,9 +117,16 @@ export const VListGroup = genericComponent<VListGroupSlots>()({
114117
)}
115118

116119
<MaybeTransition transition={{ component: VExpandTransition }} disabled={ !isBooted.value }>
117-
<div class="v-list-group__items" role="group" aria-labelledby={ id.value } v-show={ isOpen.value }>
118-
{ slots.default?.() }
119-
</div>
120+
{ renderWhenClosed.value
121+
? (
122+
<div class="v-list-group__items" role="group" aria-labelledby={ id.value } v-show={ isOpen.value }>
123+
{ slots.default?.() }
124+
</div>
125+
) : isOpen.value && (
126+
<div class="v-list-group__items" role="group" aria-labelledby={ id.value }>
127+
{ slots.default?.() }
128+
</div>
129+
)}
120130
</MaybeTransition>
121131
</props.tag>
122132
))

packages/vuetify/src/composables/nested/nested.ts

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export type SelectStrategyProp =
5757
| SelectStrategy
5858
| ((mandatory: boolean) => SelectStrategy)
5959
export type OpenStrategyProp = 'single' | 'multiple' | 'list' | OpenStrategy
60+
export type ItemsRegistrationType = 'props' | 'render'
6061

6162
export interface NestedProps {
6263
activatable: boolean
@@ -68,6 +69,7 @@ export interface NestedProps {
6869
selected: any
6970
opened: any
7071
mandatory: boolean
72+
itemsRegistration: ItemsRegistrationType
7173
'onUpdate:activated': EventProp<[any]> | undefined
7274
'onUpdate:selected': EventProp<[any]> | undefined
7375
'onUpdate:opened': EventProp<[any]> | undefined
@@ -86,6 +88,7 @@ type NestedProvide = {
8688
activated: Ref<Set<unknown>>
8789
selected: Ref<Map<unknown, 'on' | 'off' | 'indeterminate'>>
8890
selectedValues: Ref<unknown[]>
91+
itemsRegistration: Ref<ItemsRegistrationType>
8992
register: (id: unknown, parentId: unknown, isDisabled: boolean, isGroup?: boolean) => void
9093
unregister: (id: unknown) => void
9194
open: (id: unknown, value: boolean, event?: Event) => void
@@ -101,6 +104,7 @@ export const VNestedSymbol: InjectionKey<NestedProvide> = Symbol.for('vuetify:ne
101104
export const emptyNested: NestedProvide = {
102105
id: shallowRef(),
103106
root: {
107+
itemsRegistration: ref('render'),
104108
register: () => null,
105109
unregister: () => null,
106110
children: ref(new Map()),
@@ -130,6 +134,10 @@ export const makeNestedProps = propsFactory({
130134
activated: null,
131135
selected: null,
132136
mandatory: Boolean,
137+
itemsRegistration: {
138+
type: String as PropType<ItemsRegistrationType>,
139+
default: 'render',
140+
},
133141
}, 'nested')
134142

135143
export const useNested = (props: NestedProps) => {
@@ -242,6 +250,7 @@ export const useNested = (props: NestedProps) => {
242250

243251
return arr
244252
}),
253+
itemsRegistration: toRef(() => props.itemsRegistration),
245254
register: (id, parentId, isDisabled, isGroup) => {
246255
if (nodeIds.has(id)) {
247256
const path = getPath(id).map(String).join(' -> ')
@@ -389,26 +398,23 @@ export const useNestedItem = (id: MaybeRefOrGetter<unknown>, isDisabled: MaybeRe
389398
}
390399

391400
onBeforeMount(() => {
392-
if (!parent.isGroupActivator) {
393-
nextTick(() => {
394-
parent.root.register(computedId.value, parent.id.value, toValue(isDisabled), isGroup)
395-
})
396-
}
401+
if (parent.isGroupActivator || parent.root.itemsRegistration.value === 'props') return
402+
nextTick(() => {
403+
parent.root.register(computedId.value, parent.id.value, toValue(isDisabled), isGroup)
404+
})
397405
})
398406

399407
onBeforeUnmount(() => {
400-
if (!parent.isGroupActivator) {
401-
parent.root.unregister(computedId.value)
402-
}
408+
if (parent.isGroupActivator || parent.root.itemsRegistration.value === 'props') return
409+
parent.root.unregister(computedId.value)
403410
})
404411

405412
watch(computedId, (val, oldVal) => {
406-
if (!parent.isGroupActivator) {
407-
parent.root.unregister(oldVal)
408-
nextTick(() => {
409-
parent.root.register(val, parent.id.value, toValue(isDisabled), isGroup)
410-
})
411-
}
413+
if (parent.isGroupActivator || parent.root.itemsRegistration.value === 'props') return
414+
parent.root.unregister(oldVal)
415+
nextTick(() => {
416+
parent.root.register(val, parent.id.value, toValue(isDisabled), isGroup)
417+
})
412418
})
413419

414420
isGroup && provide(VNestedSymbol, item)

0 commit comments

Comments
 (0)