<script lang="ts">
import {
FlexRender,
columnSizingFeature,
createSortedRowModel,
createTable,
rowSelectionFeature,
rowSortingFeature,
sortFns,
tableFeatures,
} from '@tanstack/svelte-table'
import type { ColumnDef } from '@tanstack/svelte-table'
import { createVirtualizer } from '@tanstack/svelte-virtual'
import { get } from 'svelte/store'
import { makeData } from './makeData'
import type { Person } from './makeData'
import './index.css'
const features = tableFeatures({
columnSizingFeature,
rowSelectionFeature,
rowSortingFeature,
sortedRowModel: createSortedRowModel(),
sortFns,
})
const columns: Array<ColumnDef<typeof features, Person>> = [
{
id: 'select',
header: '',
cell: '',
size: 40,
},
{
accessorKey: 'id',
header: 'ID',
size: 60,
},
{
accessorKey: 'firstName',
cell: (info) => info.getValue(),
},
{
accessorFn: (row) => row.lastName,
id: 'lastName',
cell: (info) => info.getValue(),
header: 'Last Name',
},
{
accessorKey: 'age',
header: 'Age',
size: 50,
},
{
accessorKey: 'visits',
header: 'Visits',
size: 50,
},
{
accessorKey: 'status',
header: 'Status',
},
{
accessorKey: 'progress',
header: 'Profile Progress',
size: 80,
},
{
accessorKey: 'createdAt',
header: 'Created At',
cell: (info) => info.getValue<Date>().toLocaleString(),
size: 250,
},
]
let data = $state(makeData(200_000))
const refreshData = () => { data = makeData(200_000) }
const stressTest = () => { data = makeData(1_000_000) }
function setIndeterminate(node: HTMLInputElement, value: boolean) {
node.indeterminate = value
return {
update(newValue: boolean) {
node.indeterminate = newValue
},
}
}
const table = createTable({
features,
columns,
get data() {
return data
},
getRowId: (row) => String(row.id),
debugTable: true,
})
let tableContainerRef = $state<HTMLDivElement | undefined>(undefined)
const rows = $derived(table.getRowModel().rows)
const rowVirtualizer = createVirtualizer({
get count() {
return rows.length
},
estimateSize: () => 33,
getScrollElement: () => tableContainerRef ?? null,
measureElement:
typeof window !== 'undefined' &&
navigator.userAgent.indexOf('Firefox') === -1
? (element) => element.getBoundingClientRect().height
: undefined,
overscan: 5,
})
$effect(() => {
if (tableContainerRef) {
get(rowVirtualizer).setOptions({
getScrollElement: () => tableContainerRef ?? null,
})
}
})
$effect(() => {
get(rowVirtualizer).setOptions({ count: rows.length })
})
function measureElement(node: HTMLTableRowElement) {
get(rowVirtualizer).measureElement(node)
}
</script>
<div class="app">
<div>
<button onclick={() => refreshData()}>Regenerate Data</button>
<button onclick={() => stressTest()}>Stress Test (1M rows)</button>
</div>
({data.length.toLocaleString()} rows)
<p>Hold Shift while selecting rows to select or deselect a range.</p>
<p>{table.getSelectedRowIds().length.toLocaleString()} rows selected</p>
<div
class="container"
bind:this={tableContainerRef}
style="overflow: auto; position: relative; height: 800px;"
>
<table style="display: grid;">
<thead style="display: grid; position: sticky; top: 0px; z-index: 1;">
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)
}
<tr style="display: flex; width: 100%;">
{#each headerGroup.headers as header (header.id)}
<th style="display: flex; width: {header.getSize()}px;">
{#if header.column.id === 'select'}
<input
type="checkbox"
checked={table.getIsAllRowsSelected()}
use:setIndeterminate={!table.getIsAllRowsSelected() && table.getIsSomeRowsSelected()}
onchange={table.getToggleAllRowsSelectedHandler()}
/>
{:else}
<div
class={header.column.getCanSort()
? 'sortable-header'
: ''}
role="button"
tabindex="0"
onclick={header.column.getToggleSortingHandler()}
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
header.column.getToggleSortingHandler()?.(e)
}
}}
>
<FlexRender header={header} />
{#if header.column.getIsSorted() === 'asc'}
{' '}🔼
{:else if header.column.getIsSorted() === 'desc'}
{' '}🔽
{/if}
</div>
{/if}
</th>
{/each}
</tr>
{/each}
</thead>
<tbody
style="display: grid; height: {$rowVirtualizer.getTotalSize()}px; position: relative;"
>
{#each $rowVirtualizer.getVirtualItems() as virtualRow (virtualRow.index)}
{@const row = rows[virtualRow.index]}
<tr
data-index={virtualRow.index}
use:measureElement
style="display: flex; position: absolute; transform: translateY({virtualRow.start}px); width: 100%;"
>
{#each row.getAllCells() as cell (cell.id)}
<td style="display: flex; width: {cell.column.getSize()}px;">
{#if cell.column.id === 'select'}
<input
type="checkbox"
checked={row.getIsSelected()}
disabled={!row.getCanSelect()}
use:setIndeterminate={!row.getIsSelected() && row.getIsSomeSelected()}
onclick={row.getToggleSelectedHandler({
})}
/>
{:else}
<FlexRender cell={cell} />
{/if}
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
</div>