Skip to content

Commit 355e37a

Browse files
authored
feat(VDataTable): add v-model:opened for group-by (#22772)
resolves #22300 resolves #17707
1 parent 9a3bdff commit 355e37a

11 files changed

Lines changed: 541 additions & 45 deletions

File tree

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
{
22
"props": {
33
"groupBy": "Defines the grouping of the table items.",
4+
"groupKey": "Custom function to generate group IDs. Receives `{ key, value, parentKey }` where `parentKey` is `null` for top-level groups. Useful when group values contain special characters or are non-string types.",
5+
"openAll": "Opens all groups by default. Synchronizes with **v-model:opened**, so closed groups are not re-opened accidentally.",
6+
"opened": "Array of group IDs that should be open. Can be bound to external variable using **v-model:opened**.",
47
"pageBy": "Controls how pagination counts items.\n- **item** paginates by individual items,\n- **auto** paginates by top-level groups and falls back to regular items if **group-by** is empty,\n- **any** paginates by both items and groups combined"
58
}
69
}

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

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,16 @@
144144
},
145145
"VDataIterator": {
146146
"props": {
147-
"initialSortOrder": "3.11.0"
147+
"groupKey": "4.1.0",
148+
"initialSortOrder": "3.11.0",
149+
"openAll": "4.1.0",
150+
"opened": "4.1.0"
151+
},
152+
"events": {
153+
"update:opened": "4.1.0"
154+
},
155+
"examples": {
156+
"prop-grouping": "4.1.0"
148157
}
149158
},
150159
"VDataTable": {
@@ -153,16 +162,23 @@
153162
"expandIcon": "3.10.0",
154163
"groupCollapseIcon": "3.10.0",
155164
"groupExpandIcon": "3.10.0",
165+
"groupKey": "4.1.0",
156166
"headerProps": "3.5.0",
157167
"initialSortOrder": "3.11.0",
168+
"openAll": "4.1.0",
169+
"opened": "4.1.0",
158170
"pageBy": "3.12.0",
159171
"showFirstLastPage": "4.1.0",
160172
"sortIcon": "3.12.0"
161173
},
174+
"events": {
175+
"update:opened": "4.1.0"
176+
},
162177
"slots": {
163178
"group-summary": "3.10.0"
164179
},
165180
"examples": {
181+
"prop-grouping": "4.1.0",
166182
"prop-sort-icon": "3.12.0",
167183
"slot-group-summary": "3.10.0"
168184
}
@@ -173,11 +189,17 @@
173189
"expandIcon": "3.10.0",
174190
"groupCollapseIcon": "3.10.0",
175191
"groupExpandIcon": "3.10.0",
192+
"groupKey": "4.1.0",
176193
"initialSortOrder": "3.11.0",
194+
"openAll": "4.1.0",
195+
"opened": "4.1.0",
177196
"pageBy": "3.12.0",
178197
"showFirstLastPage": "4.1.0",
179198
"sortIcon": "3.12.0"
180199
},
200+
"events": {
201+
"update:opened": "4.1.0"
202+
},
181203
"slots": {
182204
"group-summary": "3.10.0"
183205
}
@@ -188,9 +210,15 @@
188210
"expandIcon": "3.10.0",
189211
"groupCollapseIcon": "3.10.0",
190212
"groupExpandIcon": "3.10.0",
213+
"groupKey": "4.1.0",
191214
"initialSortOrder": "3.11.0",
215+
"openAll": "4.1.0",
216+
"opened": "4.1.0",
192217
"sortIcon": "3.12.0"
193218
},
219+
"events": {
220+
"update:opened": "4.1.0"
221+
},
194222
"slots": {
195223
"group-summary": "3.10.0"
196224
}
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<template>
2+
<div>
3+
<div class="d-flex ga-4 mb-4 align-center">
4+
<v-switch v-model="openAll" label="Open all groups" hide-details></v-switch>
5+
<v-btn :disabled="openAll" size="small" variant="tonal" @click="opened = []">Close all</v-btn>
6+
</div>
7+
8+
<v-data-iterator
9+
v-model:opened="opened"
10+
:group-by="[{ key: 'category' }]"
11+
:group-key="({ value }) => value"
12+
:items="items"
13+
:items-per-page="-1"
14+
:open-all="openAll"
15+
>
16+
<template v-slot:default="{ groupedItems, toggleGroup, isGroupOpen }">
17+
<v-row>
18+
<template v-for="groupOrItem in groupedItems" :key="groupOrItem.type === 'group' ? groupOrItem.id : groupOrItem.raw.name">
19+
<v-col v-if="groupOrItem.type === 'group'" cols="12">
20+
<v-card
21+
variant="tonal"
22+
@click="toggleGroup(groupOrItem)"
23+
>
24+
<v-card-title class="d-flex align-center">
25+
<v-icon
26+
:icon="isGroupOpen(groupOrItem) ? 'mdi-chevron-down' : 'mdi-chevron-right'"
27+
class="me-2"
28+
></v-icon>
29+
{{ groupOrItem.value }}
30+
<v-chip class="ms-2" size="small" variant="outlined">
31+
{{ groupOrItem.items.length }}
32+
</v-chip>
33+
</v-card-title>
34+
</v-card>
35+
</v-col>
36+
37+
<v-col v-else cols="12" md="4" sm="6">
38+
<v-card>
39+
<v-card-title>{{ groupOrItem.raw.name }}</v-card-title>
40+
<v-card-subtitle>{{ groupOrItem.raw.origin }}</v-card-subtitle>
41+
<v-card-text>{{ groupOrItem.raw.calories }} cal</v-card-text>
42+
</v-card>
43+
</v-col>
44+
</template>
45+
</v-row>
46+
</template>
47+
</v-data-iterator>
48+
</div>
49+
</template>
50+
51+
<script setup>
52+
import { ref } from 'vue'
53+
54+
const opened = ref([])
55+
const openAll = ref(false)
56+
57+
const items = [
58+
{ name: 'Frozen Yogurt', calories: 159, category: 'Dairy', origin: 'USA' },
59+
{ name: 'Ice cream sandwich', calories: 237, category: 'Dairy', origin: 'USA' },
60+
{ name: 'Cheese', calories: 402, category: 'Dairy', origin: 'France' },
61+
{ name: 'Butter', calories: 717, category: 'Dairy', origin: 'France' },
62+
{ name: 'Eclair', calories: 262, category: 'Pastry', origin: 'France' },
63+
{ name: 'Cupcake', calories: 305, category: 'Pastry', origin: 'USA' },
64+
{ name: 'Croissant', calories: 231, category: 'Pastry', origin: 'France' },
65+
{ name: 'Baklava', calories: 334, category: 'Pastry', origin: 'Turkey' },
66+
{ name: 'Oreo', calories: 160, category: 'Cookie', origin: 'USA' },
67+
{ name: 'Macaron', calories: 404, category: 'Cookie', origin: 'France' },
68+
{ name: 'Biscotti', calories: 410, category: 'Cookie', origin: 'Italy' },
69+
{ name: 'KitKat', calories: 518, category: 'Candy', origin: 'UK' },
70+
{ name: 'Snickers', calories: 488, category: 'Candy', origin: 'USA' },
71+
{ name: 'Haribo', calories: 340, category: 'Candy', origin: 'Germany' },
72+
]
73+
</script>
74+
75+
<script>
76+
export default {
77+
data: () => ({
78+
opened: [],
79+
openAll: false,
80+
items: [
81+
{ name: 'Frozen Yogurt', calories: 159, category: 'Dairy', origin: 'USA' },
82+
{ name: 'Ice cream sandwich', calories: 237, category: 'Dairy', origin: 'USA' },
83+
{ name: 'Cheese', calories: 402, category: 'Dairy', origin: 'France' },
84+
{ name: 'Butter', calories: 717, category: 'Dairy', origin: 'France' },
85+
{ name: 'Eclair', calories: 262, category: 'Pastry', origin: 'France' },
86+
{ name: 'Cupcake', calories: 305, category: 'Pastry', origin: 'USA' },
87+
{ name: 'Croissant', calories: 231, category: 'Pastry', origin: 'France' },
88+
{ name: 'Baklava', calories: 334, category: 'Pastry', origin: 'Turkey' },
89+
{ name: 'Oreo', calories: 160, category: 'Cookie', origin: 'USA' },
90+
{ name: 'Macaron', calories: 404, category: 'Cookie', origin: 'France' },
91+
{ name: 'Biscotti', calories: 410, category: 'Cookie', origin: 'Italy' },
92+
{ name: 'KitKat', calories: 518, category: 'Candy', origin: 'UK' },
93+
{ name: 'Snickers', calories: 488, category: 'Candy', origin: 'USA' },
94+
{ name: 'Haribo', calories: 340, category: 'Candy', origin: 'Germany' },
95+
],
96+
}),
97+
methods: {
98+
groupKey ({ value }) {
99+
return value
100+
},
101+
},
102+
}
103+
</script>

packages/docs/src/examples/v-data-table/prop-grouping.vue

Lines changed: 31 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,36 @@
11
<template>
2-
<v-data-table
3-
:group-by="groupBy"
4-
:headers="headers"
5-
:items="desserts"
6-
:sort-by="sortBy"
7-
item-value="name"
8-
></v-data-table>
2+
<div>
3+
<div class="d-flex ga-4 mb-4 align-center flex-wrap">
4+
<v-switch v-model="openAll" label="Open all groups" hide-details></v-switch>
5+
<v-btn :disabled="openAll" size="small" variant="tonal" @click="opened = []">Close all</v-btn>
6+
</div>
7+
8+
<pre class="mb-4 pa-2 bg-surface-variant rounded text-body-2">opened: {{ opened }}</pre>
9+
10+
<v-data-table
11+
v-model:opened="opened"
12+
:group-by="groupBy"
13+
:group-key="groupKey"
14+
:headers="headers"
15+
:items="desserts"
16+
:open-all="openAll"
17+
:sort-by="sortBy"
18+
item-value="name"
19+
></v-data-table>
20+
</div>
921
</template>
1022

1123
<script setup>
1224
import { ref } from 'vue'
1325
26+
const opened = ref([])
27+
const openAll = ref(false)
28+
1429
const sortBy = ref([{ key: 'name', order: 'asc' }])
1530
const groupBy = ref([{ key: 'category', order: 'asc' }, { key: 'status', order: 'asc' }])
1631
32+
const groupKey = ({ key, value, parentKey }) => `${parentKey ?? 'root'}/${key}:${value}`
33+
1734
const headers = [
1835
{ key: 'data-table-group', title: 'Category' },
1936
{
@@ -91,6 +108,8 @@
91108
<script>
92109
export default {
93110
data: () => ({
111+
opened: [],
112+
openAll: false,
94113
sortBy: [{ key: 'name', order: 'asc' }],
95114
groupBy: [{ key: 'category', order: 'asc' }, { key: 'status', order: 'asc' }],
96115
headers: [
@@ -166,5 +185,10 @@
166185
},
167186
],
168187
}),
188+
methods: {
189+
groupKey ({ key, value, parentKey }) {
190+
return `${parentKey ?? 'root'}/${key}:${value}`
191+
},
192+
},
169193
}
170194
</script>

packages/docs/src/pages/en/components/data-iterators.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,14 @@ The following code snippet is an example of a basic `v-data-iterator` component:
7474

7575
The following are a collection of examples that demonstrate more advanced and real world use of the `v-data-iterator` component.
7676

77+
### Props
78+
79+
#### Grouping
80+
81+
Use the **group-by** prop to group items, and **v-model:opened** to control which groups are open. The **group-key** prop allows customizing group IDs, and **open-all** opens all groups by default.
82+
83+
<ExamplesExample file="v-data-iterator/prop-grouping" />
84+
7785
### Slots
7886

7987
The `v-data-iterator` component has 4 main slots

packages/vuetify/src/components/VDataIterator/VDataIterator.tsx

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
// Components
22
import { VFadeTransition } from '@/components/transitions'
33
import { makeDataTableExpandProps, provideExpanded } from '@/components/VDataTable/composables/expand'
4-
import { makeDataTableGroupProps, provideGroupBy, useGroupedItems } from '@/components/VDataTable/composables/group'
4+
import {
5+
createGroupBy,
6+
makeDataTableGroupProps,
7+
provideGroupBy,
8+
useGroupedItems,
9+
useOpenAllGroups,
10+
} from '@/components/VDataTable/composables/group'
511
import { useOptions } from '@/components/VDataTable/composables/options'
612
import {
713
createPagination,
@@ -17,7 +23,6 @@ import { makeDataIteratorItemsProps, useDataIteratorItems } from './composables/
1723
import { makeComponentProps } from '@/composables/component'
1824
import { makeFilterProps, useFilter } from '@/composables/filter'
1925
import { LoaderSlot } from '@/composables/loader'
20-
import { useProxiedModel } from '@/composables/proxiedModel'
2126
import { makeTagProps } from '@/composables/tag'
2227
import { useToggleScope } from '@/composables/toggleScope'
2328
import { makeTransitionProps, MaybeTransition } from '@/composables/transition'
@@ -105,11 +110,12 @@ export const VDataIterator = genericComponent<new <T> (
105110
'update:sortBy': (value: any) => true,
106111
'update:options': (value: any) => true,
107112
'update:expanded': (value: any) => true,
113+
'update:opened': (value: string[]) => true,
108114
'update:currentItems': (value: any) => true,
109115
},
110116

111117
setup (props, { slots }) {
112-
const groupBy = useProxiedModel(props, 'groupBy')
118+
const { groupBy, opened, openAll, groupKey } = createGroupBy(props)
113119
const search = toRef(() => props.search)
114120

115121
const { items } = useDataIteratorItems(props)
@@ -119,10 +125,17 @@ export const VDataIterator = genericComponent<new <T> (
119125
const { page, itemsPerPage } = createPagination(props)
120126

121127
const { toggleSort } = provideSort({ initialSortOrder, sortBy, multiSort, mustSort, page })
122-
const { sortByWithGroups, opened, extractRows, isGroupOpen, toggleGroup } = provideGroupBy({ groupBy, sortBy })
128+
const {
129+
sortByWithGroups,
130+
opened: openedGroups,
131+
extractRows,
132+
isGroupOpen,
133+
toggleGroup,
134+
} = provideGroupBy({ groupBy, sortBy, opened })
123135

124136
const { sortedItems } = useSortedItems(props, filteredItems, sortByWithGroups, { transform: item => item.raw })
125-
const { flatItems } = useGroupedItems(sortedItems, groupBy, opened, false)
137+
useOpenAllGroups(openedGroups, openAll, sortedItems, groupBy, groupKey)
138+
const { flatItems } = useGroupedItems(sortedItems, groupBy, openedGroups, false, isGroupOpen, groupKey)
126139

127140
const manualPagination = toRef(() => !isEmpty(props.itemsLength))
128141
const itemsLength = toRef(() => manualPagination.value ? Number(props.itemsLength) : flatItems.value.length)

packages/vuetify/src/components/VDataTable/VDataTable.tsx

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { makeVTableProps, VTable } from '@/components/VTable/VTable'
1010

1111
// Composables
1212
import { makeDataTableExpandProps, provideExpanded } from './composables/expand'
13-
import { createGroupBy, makeDataTableGroupProps, provideGroupBy, useGroupedItems } from './composables/group'
13+
import { createGroupBy, makeDataTableGroupProps, provideGroupBy, useGroupedItems, useOpenAllGroups } from './composables/group'
1414
import { createHeaders, makeDataTableHeaderProps } from './composables/headers'
1515
import { makeDataTableItemsProps, useDataTableItems } from './composables/items'
1616
import { useOptions } from './composables/options'
@@ -132,11 +132,12 @@ export const VDataTable = genericComponent<new <T extends readonly any[], V>(
132132
'update:options': (value: any) => true,
133133
'update:groupBy': (value: any) => true,
134134
'update:expanded': (value: any) => true,
135+
'update:opened': (value: string[]) => true,
135136
'update:currentItems': (value: any) => true,
136137
},
137138

138139
setup (props, { attrs, slots }) {
139-
const { groupBy } = createGroupBy(props)
140+
const { groupBy, opened, openAll, groupKey } = createGroupBy(props)
140141
const { initialSortOrder, sortBy, multiSort, mustSort } = createSort(props)
141142
const { page, itemsPerPage } = createPagination(props)
142143
const { disableSort } = toRefs(props)
@@ -162,13 +163,20 @@ export const VDataTable = genericComponent<new <T extends readonly any[], V>(
162163
})
163164

164165
const { toggleSort } = provideSort({ initialSortOrder, sortBy, multiSort, mustSort, page })
165-
const { sortByWithGroups, opened, extractRows, isGroupOpen, toggleGroup } = provideGroupBy({ groupBy, sortBy, disableSort })
166+
const {
167+
sortByWithGroups,
168+
opened: openedGroups,
169+
extractRows,
170+
isGroupOpen,
171+
toggleGroup,
172+
} = provideGroupBy({ groupBy, sortBy, disableSort, opened })
166173

167174
const { sortedItems } = useSortedItems(props, filteredItems, sortByWithGroups, {
168175
transform: item => ({ ...item.raw, ...item.columns }),
169176
sortFunctions,
170177
sortRawFunctions,
171178
})
179+
useOpenAllGroups(openedGroups, openAll, sortedItems, groupBy, groupKey)
172180

173181
const pageBy = computed(() => {
174182
if (props.pageBy === 'auto') {
@@ -195,7 +203,7 @@ export const VDataTable = genericComponent<new <T extends readonly any[], V>(
195203
const { paginatedItems } = usePaginatedItems({ items, startIndex, stopIndex, itemsPerPage })
196204
return { paginatedItems, pageCount, setItemsPerPage, prevPage, nextPage, setPage }
197205
},
198-
group: items => useGroupedItems(items, groupBy, opened, () => !!slots['group-summary']),
206+
group: items => useGroupedItems(items, groupBy, openedGroups, () => !!slots['group-summary'], isGroupOpen, groupKey),
199207
})
200208

201209
const paginatedItemsWithoutGroups = computed(() => extractRows(paginatedItems.value))

0 commit comments

Comments
 (0)