Skip to content

Commit 4aecd84

Browse files
authored
docs: Add example for vue virtualized-rows (#5772)
1 parent 6625ec6 commit 4aecd84

File tree

15 files changed

+1196
-107
lines changed

15 files changed

+1196
-107
lines changed

docs/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -728,6 +728,10 @@
728728
{
729729
"to": "framework/vue/examples/filters",
730730
"label": "Column Filters"
731+
},
732+
{
733+
"to": "framework/vue/examples/virtualized-rows",
734+
"label": "Virtualized Rows"
731735
}
732736
]
733737
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Logs
2+
logs
3+
*.log
4+
npm-debug.log*
5+
yarn-debug.log*
6+
yarn-error.log*
7+
pnpm-debug.log*
8+
lerna-debug.log*
9+
10+
node_modules
11+
dist
12+
dist-ssr
13+
*.local
14+
15+
# Editor directories and files
16+
.vscode/*
17+
!.vscode/extensions.json
18+
.idea
19+
.DS_Store
20+
*.suo
21+
*.ntvs*
22+
*.njsproj
23+
*.sln
24+
*.sw?
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
# Example
2+
3+
To run this example:
4+
5+
- `npm install` or `yarn`
6+
- `npm run dev` or `yarn dev`
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="vite/client" />
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<!doctype html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="UTF-8" />
5+
<link rel="icon" href="/favicon.ico" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7+
<title>Vite App</title>
8+
<script src="https://cdn.tailwindcss.com"></script>
9+
</head>
10+
<body>
11+
<div id="app"></div>
12+
<script type="module" src="/src/main.ts"></script>
13+
</body>
14+
</html>
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "tanstack-table-example-vue-virtualized-rows",
3+
"private": true,
4+
"version": "0.0.0",
5+
"scripts": {
6+
"dev": "vite",
7+
"build": "vite build",
8+
"preview": "vite preview",
9+
"test:types": "vue-tsc"
10+
},
11+
"dependencies": {
12+
"@tanstack/vue-table": "^8.20.5",
13+
"@tanstack/vue-virtual": "^3.10.8",
14+
"vue": "^3.5.11"
15+
},
16+
"devDependencies": {
17+
"@types/node": "^20.14.9",
18+
"@vitejs/plugin-vue": "^5.1.4",
19+
"typescript": "5.6.2",
20+
"vite": "^5.4.8",
21+
"vue-tsc": "^2.1.6"
22+
}
23+
}
4.19 KB
Binary file not shown.
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
<script setup lang="ts">
2+
import './index.css'
3+
import { computed, ref, h } from 'vue'
4+
import {
5+
type ColumnDef,
6+
FlexRender,
7+
useVueTable,
8+
getCoreRowModel,
9+
getSortedRowModel,
10+
} from '@tanstack/vue-table'
11+
import { useVirtualizer } from '@tanstack/vue-virtual'
12+
13+
import { makeData, type Person } from './makeData'
14+
15+
const search = ref('')
16+
17+
const data = ref<Person[]>(makeData(50_000))
18+
19+
const filteredData = computed<Person[]>(() => {
20+
const searchValue = search.value.toLowerCase();
21+
22+
// If no search value is present, return all data
23+
if (!searchValue) return data.value;
24+
25+
return data.value.filter(row => {
26+
return Object.values(row).some(value => {
27+
if (value instanceof Date) {
28+
return value.toLocaleString().toLowerCase().includes(searchValue);
29+
}
30+
// Stringify the value and check if it contains the search term
31+
return `${value}`.toLowerCase().includes(searchValue);
32+
});
33+
});
34+
});
35+
36+
let searchTimeout: NodeJS.Timeout
37+
function handleDebounceSearch(ev: Event) {
38+
if (searchTimeout) { clearTimeout(searchTimeout) }
39+
40+
searchTimeout = setTimeout(() => {
41+
search.value = (ev?.target as HTMLInputElement)?.value ?? ''
42+
}, 300)
43+
}
44+
45+
const columns = computed<ColumnDef<Person>[]>(() => [
46+
{
47+
accessorKey: 'id',
48+
header: 'ID',
49+
},
50+
{
51+
accessorKey: 'firstName',
52+
cell: info => info.getValue(),
53+
},
54+
{
55+
accessorFn: row => row.lastName,
56+
id: 'lastName',
57+
cell: info => info.getValue(),
58+
header: () => h('span', 'Last Name'),
59+
},
60+
{
61+
accessorKey: 'age',
62+
header: () => 'Age',
63+
},
64+
{
65+
accessorKey: 'visits',
66+
header: () => h('span', 'Visits'),
67+
},
68+
{
69+
accessorKey: 'status',
70+
header: 'Status',
71+
},
72+
{
73+
accessorKey: 'progress',
74+
header: 'Profile Progress',
75+
},
76+
{
77+
accessorKey: 'createdAt',
78+
header: 'Created At',
79+
cell: info => info.getValue<Date>().toLocaleString(),
80+
},
81+
])
82+
83+
84+
const table = useVueTable({
85+
get data() {
86+
return filteredData.value
87+
},
88+
columns: columns.value,
89+
getCoreRowModel: getCoreRowModel(),
90+
getSortedRowModel: getSortedRowModel(),
91+
debugTable: false,
92+
})
93+
94+
95+
const rows = computed(() => table.getRowModel().rows)
96+
97+
//The virtualizer needs to know the scrollable container element
98+
const tableContainerRef = ref<HTMLDivElement | null>(null)
99+
100+
101+
const rowVirtualizerOptions = computed(() => {
102+
return {
103+
count: rows.value.length,
104+
estimateSize: () => 33, //estimate row height for accurate scrollbar dragging
105+
getScrollElement: () => tableContainerRef.value,
106+
overscan: 5,
107+
}
108+
})
109+
110+
const rowVirtualizer = useVirtualizer(rowVirtualizerOptions)
111+
112+
const virtualRows = computed(() => rowVirtualizer.value.getVirtualItems())
113+
const totalSize = computed(() => rowVirtualizer.value.getTotalSize())
114+
115+
function measureElement(el?: Element) {
116+
if (!el) {
117+
return
118+
}
119+
120+
rowVirtualizer.value.measureElement(el)
121+
122+
return undefined
123+
}
124+
</script>
125+
126+
127+
<template>
128+
129+
<div>
130+
<p class="text-center">
131+
For tables, the basis for the offset of the translate css function is from
132+
the row's initial position itself. Because of this, we need to calculate
133+
the translateY pixel count different and base it off the the index.
134+
</p>
135+
<h1 class="text-3xl font-bold text-center">Virtualized Rows</h1>
136+
<div style="margin: 0 auto; width: min-content;">
137+
<input
138+
:modelValue="search"
139+
@input="handleDebounceSearch"
140+
placeholder="Search"
141+
class="p-2"
142+
/>
143+
{{ rows.length.toLocaleString() }} results
144+
</div>
145+
</div>
146+
<div
147+
class="container"
148+
ref="tableContainerRef"
149+
:style="{
150+
overflow: 'auto', //our scrollable table container
151+
position: 'relative', //needed for sticky header
152+
height: '800px', //should be a fixed height
153+
}"
154+
>
155+
156+
<div :style="{ height: `${totalSize}px` }">
157+
<!-- Even though we're still using sematic table tags, we must use CSS grid and flexbox for dynamic row heights -->
158+
<table :style="{ display: 'grid' }">
159+
<thead :style="{
160+
display: 'grid',
161+
position: 'sticky',
162+
top: 0,
163+
zIndex: 1,
164+
}">
165+
<tr
166+
v-for="headerGroup in table.getHeaderGroups()"
167+
:key="headerGroup.id"
168+
:style="{ display: 'flex', width: '100%' }"
169+
>
170+
<th
171+
v-for="header in headerGroup.headers"
172+
:key="header.id"
173+
:colspan="header.colSpan"
174+
:style="{ width: `${header.getSize()}px` }"
175+
>
176+
<div
177+
v-if="!header.isPlaceholder"
178+
:class="{ 'cursor-pointer select-none': header.column.getCanSort() }"
179+
@click="e => header.column.getToggleSortingHandler()?.(e)"
180+
>
181+
<FlexRender
182+
:render="header.column.columnDef.header"
183+
:props="header.getContext()"
184+
/>
185+
<span v-if="header.column.getIsSorted() === 'asc'"> 🔼</span>
186+
<span v-if="header.column.getIsSorted() === 'desc'"> 🔽</span>
187+
</div>
188+
</th>
189+
</tr>
190+
</thead>
191+
<tbody :style="{
192+
display: 'grid',
193+
height: `${totalSize}px`, //tells scrollbar how big the table is
194+
position: 'relative', //needed for absolute positioning of rows
195+
}">
196+
<tr
197+
v-for="vRow in virtualRows"
198+
:data-index="vRow.index /* needed for dynamic row height measurement*/"
199+
:ref="measureElement /*measure dynamic row height*/"
200+
:key="rows[vRow.index].id"
201+
:style="{
202+
display: 'flex',
203+
position: 'absolute',
204+
transform: `translateY(${vRow.start}px)`, //this should always be a `style` as it changes on scroll
205+
width: '100%',
206+
}"
207+
>
208+
<td
209+
v-for="cell in rows[vRow.index].getVisibleCells()"
210+
:key="cell.id"
211+
:style="{
212+
display: 'flex',
213+
width: `${cell.column.getSize()}px`
214+
}"
215+
>
216+
<FlexRender
217+
:render="cell.column.columnDef.cell"
218+
:props="cell.getContext()"
219+
/>
220+
</td>
221+
</tr>
222+
</tbody>
223+
</table>
224+
</div>
225+
</div>
226+
</template>
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
/// <reference types="vite/client" />
2+
3+
declare module '*.vue' {
4+
import type { DefineComponent } from 'vue'
5+
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
6+
const component: DefineComponent<{}, {}, any>
7+
export default component
8+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
html {
2+
font-family: sans-serif;
3+
font-size: 14px;
4+
}
5+
6+
table {
7+
border-collapse: collapse;
8+
border-spacing: 0;
9+
font-family: arial, sans-serif;
10+
table-layout: fixed;
11+
}
12+
13+
thead {
14+
background: lightgray;
15+
}
16+
17+
tr {
18+
border-bottom: 1px solid lightgray;
19+
}
20+
21+
th {
22+
border-bottom: 1px solid lightgray;
23+
border-right: 1px solid lightgray;
24+
padding: 2px 4px;
25+
text-align: left;
26+
}
27+
28+
td {
29+
padding: 6px;
30+
}
31+
32+
.container {
33+
border: 1px solid lightgray;
34+
margin: 1rem auto;
35+
}
36+
37+
.cursor-pointer {
38+
cursor: pointer;
39+
}
40+
41+
.select-none {
42+
-webkit-user-select: none;
43+
-moz-user-select: none;
44+
-ms-user-select: none;
45+
user-select: none;
46+
}
47+
48+
.text-left {
49+
text-align: left;
50+
}

0 commit comments

Comments
 (0)