Skip to content

Commit 8499a47

Browse files
committed
feat(VDataTable): add v-model:opened for group-by
resolves #22300 resolves #17707
1 parent 66f09ce commit 8499a47

11 files changed

Lines changed: 528 additions & 33 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+
"openAllGroups": "Opens all groups by default. Individual groups can still be toggled closed by the user.",
6+
"opened": "An array of group IDs that should be open. Supports two-way binding with `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: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,13 @@
121121
},
122122
"VDataIterator": {
123123
"props": {
124-
"initialSortOrder": "3.11.0"
124+
"groupKey": "4.1.0",
125+
"initialSortOrder": "3.11.0",
126+
"openAllGroups": "4.1.0",
127+
"opened": "4.1.0"
128+
},
129+
"events": {
130+
"update:opened": "4.1.0"
125131
}
126132
},
127133
"VDataTable": {
@@ -130,12 +136,18 @@
130136
"expandIcon": "3.10.0",
131137
"groupCollapseIcon": "3.10.0",
132138
"groupExpandIcon": "3.10.0",
139+
"groupKey": "4.1.0",
133140
"headerProps": "3.5.0",
134141
"initialSortOrder": "3.11.0",
142+
"openAllGroups": "4.1.0",
143+
"opened": "4.1.0",
135144
"pageBy": "3.12.0",
136145
"showFirstLastPage": "4.1.0",
137146
"sortIcon": "3.12.0"
138147
},
148+
"events": {
149+
"update:opened": "4.1.0"
150+
},
139151
"slots": {
140152
"group-summary": "3.10.0"
141153
}
@@ -146,11 +158,17 @@
146158
"expandIcon": "3.10.0",
147159
"groupCollapseIcon": "3.10.0",
148160
"groupExpandIcon": "3.10.0",
161+
"groupKey": "4.1.0",
149162
"initialSortOrder": "3.11.0",
163+
"openAllGroups": "4.1.0",
164+
"opened": "4.1.0",
150165
"pageBy": "3.12.0",
151166
"showFirstLastPage": "4.1.0",
152167
"sortIcon": "3.12.0"
153168
},
169+
"events": {
170+
"update:opened": "4.1.0"
171+
},
154172
"slots": {
155173
"group-summary": "3.10.0"
156174
}
@@ -161,9 +179,15 @@
161179
"expandIcon": "3.10.0",
162180
"groupCollapseIcon": "3.10.0",
163181
"groupExpandIcon": "3.10.0",
182+
"groupKey": "4.1.0",
164183
"initialSortOrder": "3.11.0",
184+
"openAllGroups": "4.1.0",
185+
"opened": "4.1.0",
165186
"sortIcon": "3.12.0"
166187
},
188+
"events": {
189+
"update:opened": "4.1.0"
190+
},
167191
"slots": {
168192
"group-summary": "3.10.0"
169193
}
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 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-groups="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 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-groups="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}/${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}/${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-groups** 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: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,11 +105,15 @@ export const VDataIterator = genericComponent<new <T> (
105105
'update:sortBy': (value: any) => true,
106106
'update:options': (value: any) => true,
107107
'update:expanded': (value: any) => true,
108+
'update:opened': (value: string[]) => true,
108109
'update:currentItems': (value: any) => true,
109110
},
110111

111112
setup (props, { slots }) {
112113
const groupBy = useProxiedModel(props, 'groupBy')
114+
const openedModel = useProxiedModel(props, 'opened')
115+
const openAllGroups = toRef(() => props.openAllGroups)
116+
const groupKeyFn = toRef(() => props.groupKey)
113117
const search = toRef(() => props.search)
114118

115119
const { items } = useDataIteratorItems(props)
@@ -119,10 +123,16 @@ export const VDataIterator = genericComponent<new <T> (
119123
const { page, itemsPerPage } = createPagination(props)
120124

121125
const { toggleSort } = provideSort({ initialSortOrder, sortBy, multiSort, mustSort, page })
122-
const { sortByWithGroups, opened, extractRows, isGroupOpen, toggleGroup } = provideGroupBy({ groupBy, sortBy })
126+
const {
127+
sortByWithGroups,
128+
opened,
129+
extractRows,
130+
isGroupOpen,
131+
toggleGroup,
132+
} = provideGroupBy({ groupBy, sortBy, opened: openedModel, openAllGroups })
123133

124134
const { sortedItems } = useSortedItems(props, filteredItems, sortByWithGroups, { transform: item => item.raw })
125-
const { flatItems } = useGroupedItems(sortedItems, groupBy, opened, false)
135+
const { flatItems } = useGroupedItems(sortedItems, groupBy, opened, false, isGroupOpen, groupKeyFn)
126136

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

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

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -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, openAllGroups, groupKey } = createGroupBy(props)
140141
const { initialSortOrder, sortBy, multiSort, mustSort } = createSort(props)
141142
const { page, itemsPerPage } = createPagination(props)
142143
const { disableSort } = toRefs(props)
@@ -162,7 +163,13 @@ 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, openAllGroups })
166173

167174
const { sortedItems } = useSortedItems(props, filteredItems, sortByWithGroups, {
168175
transform: item => ({ ...item.raw, ...item.columns }),
@@ -195,7 +202,7 @@ export const VDataTable = genericComponent<new <T extends readonly any[], V>(
195202
const { paginatedItems } = usePaginatedItems({ items, startIndex, stopIndex, itemsPerPage })
196203
return { paginatedItems, pageCount, setItemsPerPage, prevPage, nextPage, setPage }
197204
},
198-
group: items => useGroupedItems(items, groupBy, opened, () => !!slots['group-summary']),
205+
group: items => useGroupedItems(items, groupBy, openedGroups, () => !!slots['group-summary'], isGroupOpen, groupKey),
199206
})
200207

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

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,11 @@ export const VDataTableServer = genericComponent<new <T extends readonly any[],
6565
'update:options': (options: any) => true,
6666
'update:expanded': (options: any) => true,
6767
'update:groupBy': (value: any) => true,
68+
'update:opened': (value: string[]) => true,
6869
},
6970

7071
setup (props, { attrs, slots }) {
71-
const { groupBy } = createGroupBy(props)
72+
const { groupBy, opened, openAllGroups, groupKey } = createGroupBy(props)
7273
const { initialSortOrder, sortBy, multiSort, mustSort } = createSort(props)
7374
const { page, itemsPerPage } = createPagination(props)
7475
const { disableSort } = toRefs(props)
@@ -84,11 +85,16 @@ export const VDataTableServer = genericComponent<new <T extends readonly any[],
8485

8586
const { toggleSort } = provideSort({ initialSortOrder, sortBy, multiSort, mustSort, page })
8687

87-
const { opened, isGroupOpen, toggleGroup, extractRows } = provideGroupBy({ groupBy, sortBy, disableSort })
88+
const {
89+
opened: openedGroups,
90+
isGroupOpen,
91+
toggleGroup,
92+
extractRows,
93+
} = provideGroupBy({ groupBy, sortBy, disableSort, opened, openAllGroups })
8894

8995
const { pageCount, setItemsPerPage, prevPage, nextPage, setPage } = providePagination({ page, itemsPerPage, itemsLength })
9096

91-
const { flatItems } = useGroupedItems(items, groupBy, opened, () => !!slots['group-summary'])
97+
const { flatItems } = useGroupedItems(items, groupBy, openedGroups, () => !!slots['group-summary'], isGroupOpen, groupKey)
9298

9399
const { isSelected, select, selectAll, toggleSelect, someSelected, allSelected } = provideSelection(props, {
94100
allItems: items,

0 commit comments

Comments
 (0)