<script lang="ts">
import { faker } from '@faker-js/faker'
import {
aggregationFns,
createExpandedRowModel,
createFacetedMinMaxValues,
createFacetedRowModel,
createFacetedUniqueValues,
createFilteredRowModel,
createGroupedRowModel,
createPaginatedRowModel,
createSortedRowModel,
createTableHook,
filterFns,
FlexRender,
sortFns,
stockFeatures,
} from '@tanstack/svelte-table'
import { compareItems, rankItem } from '@tanstack/match-sorter-utils'
import { makeData } from './makeData'
import type { Person } from './makeData'
import type {
Cell,
Column,
FilterFn,
Header,
Row,
SortFn,
} from '@tanstack/svelte-table'
import './index.css'
const fuzzyFilter: FilterFn<typeof stockFeatures, Person> = (
row,
columnId,
value,
addMeta,
) => {
const itemRank = rankItem(row.getValue(columnId), value)
addMeta?.({ itemRank })
return itemRank.passed
}
const fuzzySort: SortFn<typeof stockFeatures, Person> = (
rowA,
rowB,
columnId,
) => {
let dir = 0
if (rowA.columnFiltersMeta[columnId]) {
dir = compareItems(
rowA.columnFiltersMeta[columnId].itemRank!,
rowB.columnFiltersMeta[columnId].itemRank!,
)
}
return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir
}
const sortStatusFn: SortFn<typeof stockFeatures, Person> = (rowA, rowB) => {
const statusOrder = ['single', 'complicated', 'relationship']
return (
statusOrder.indexOf(rowA.original.status) -
statusOrder.indexOf(rowB.original.status)
)
}
const { createAppTable, createAppColumnHelper } = createTableHook({
features: stockFeatures,
rowModels: {
expandedRowModel: createExpandedRowModel(),
filteredRowModel: createFilteredRowModel({
...filterFns,
fuzzy: fuzzyFilter,
}),
facetedRowModel: createFacetedRowModel(),
facetedMinMaxValues: createFacetedMinMaxValues(),
facetedUniqueValues: createFacetedUniqueValues(),
groupedRowModel: createGroupedRowModel(aggregationFns),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(sortFns),
} as any,
})
const columnHelper = createAppColumnHelper<Person>()
const columns = columnHelper.columns([
columnHelper.display({
id: 'select',
size: 80,
minSize: 80,
maxSize: 80,
enableSorting: false,
enableGrouping: false,
enableHiding: false,
enableResizing: false,
header: ({ table }) =>
table.getIsAllPageRowsSelected()
? 'all'
: table.getIsSomePageRowsSelected()
? 'some'
: 'none',
cell: ({ row }) => (row.getIsPinned() === 'top' ? 'Pinned' : 'Pin'),
}),
columnHelper.accessor('firstName', {
id: 'firstName',
size: 200,
header: 'First Name',
filterFn: 'fuzzy',
sortFn: fuzzySort,
meta: { filterVariant: 'text' },
getGroupingValue: (row) => `${row.firstName} ${row.lastName}`,
}),
columnHelper.accessor((row) => row.lastName, {
id: 'lastName',
size: 180,
header: 'Last Name',
meta: { filterVariant: 'text' },
}),
columnHelper.accessor('age', {
id: 'age',
size: 200,
header: 'Age',
meta: { filterVariant: 'range' },
aggregationFn: 'median',
aggregatedCell: ({ getValue }) =>
Math.round(getValue<number>() * 100) / 100,
}),
columnHelper.accessor('visits', {
id: 'visits',
size: 200,
header: 'Visits',
meta: { filterVariant: 'range' },
aggregationFn: 'sum',
aggregatedCell: ({ getValue }) => getValue<number>().toLocaleString(),
}),
columnHelper.accessor('status', {
id: 'status',
size: 200,
header: 'Status',
sortFn: sortStatusFn,
meta: { filterVariant: 'select' },
}),
columnHelper.accessor('progress', {
id: 'progress',
size: 200,
header: 'Profile Progress',
meta: { filterVariant: 'range' },
aggregationFn: 'mean',
cell: ({ getValue }) => `${Math.round(getValue<number>() * 100) / 100}%`,
aggregatedCell: ({ getValue }) =>
`${Math.round(getValue<number>() * 100) / 100}%`,
}),
])
let data = $state(makeData(1_000))
const debounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
const table = createAppTable(
{
columns,
get data() {
return data
},
getSubRows: (row) => row.subRows,
globalFilterFn: 'fuzzy',
columnResizeMode: 'onChange',
defaultColumn: { minSize: 200, maxSize: 800 },
initialState: {
columnOrder: columns.map((c) => c.id!),
columnPinning: { left: ['select'], right: [] },
pagination: { pageIndex: 0, pageSize: 20 },
},
keepPinnedRows: true,
debugTable: true,
},
(state) => state,
)
function debounceSet(key: string, setValue: () => void) {
clearTimeout(debounceTimers.get(key))
debounceTimers.set(
key,
setTimeout(() => {
setValue()
debounceTimers.delete(key)
}, 300),
)
}
function getCommonPinningStyle(column: Column<typeof stockFeatures, Person>) {
const isPinned = column.getIsPinned()
const isLastLeftPinnedColumn =
isPinned === 'left' && column.getIsLastColumn('left')
const isFirstRightPinnedColumn =
isPinned === 'right' && column.getIsFirstColumn('right')
return [
isLastLeftPinnedColumn
? 'box-shadow: -4px 0 4px -4px gray inset'
: isFirstRightPinnedColumn
? 'box-shadow: 4px 0 4px -4px gray inset'
: '',
isPinned === 'left' ? `left: ${column.getStart('left')}px` : '',
isPinned === 'right' ? `right: ${column.getAfter('right')}px` : '',
`opacity: ${isPinned ? 0.97 : 1}`,
`position: ${isPinned ? 'sticky' : 'relative'}`,
`z-index: ${isPinned ? 1 : 0}`,
]
.filter(Boolean)
.join('; ')
}
function headerStyle(header: Header<typeof stockFeatures, Person, unknown>) {
return `${getCommonPinningStyle(header.column)}; white-space: nowrap; width: calc(var(--header-${header.id}-size) * 1px)`
}
function cellStyle(cell: Cell<typeof stockFeatures, Person, unknown>) {
return `${getCommonPinningStyle(cell.column)}; width: calc(var(--col-${cell.column.id}-size) * 1px)`
}
function cellClass(cell: Cell<typeof stockFeatures, Person, unknown>) {
const groupingActive = table.state.grouping.length > 0
const hasAggregation = !!cell.column.columnDef.aggregationFn
return !groupingActive
? undefined
: cell.getIsGrouped()
? 'cell-grouped'
: hasAggregation && cell.getIsAggregated()
? 'cell-aggregated'
: cell.getIsPlaceholder()
? 'cell-placeholder'
: undefined
}
function pinnedRowStyle(row: Row<typeof stockFeatures, Person>) {
const bottomRows = table.getBottomRows()
return [
'position: sticky',
row.getIsPinned() === 'top'
? `top: ${row.getPinnedIndex() * 32 + 48}px`
: '',
row.getIsPinned() === 'bottom'
? `bottom: ${(bottomRows.length - 1 - row.getPinnedIndex()) * 32}px`
: '',
'z-index: 1',
]
.filter(Boolean)
.join('; ')
}
function tableStyle() {
const styles = [`width: ${table.getTotalSize()}px`]
for (const header of table.getFlatHeaders()) {
styles.push(`--header-${header.id}-size: ${header.getSize()}`)
styles.push(`--col-${header.column.id}-size: ${header.column.getSize()}`)
}
return styles.join('; ')
}
function sortedUniqueValues(column: Column<typeof stockFeatures, Person>) {
if (column.columnDef.meta?.filterVariant === 'range') return []
return Array.from(column.getFacetedUniqueValues().keys())
.sort()
.slice(0, 5000)
}
function updateRangeFilter(
column: Column<typeof stockFeatures, Person>,
index: 0 | 1,
value: string,
) {
column.setFilterValue((old: [number, number] | undefined) =>
index === 0 ? [value, old?.[1]] : [old?.[0], value],
)
}
const refreshData = () => { data = makeData(1_000) }
const nestedData = () => { data = makeData(100, 5, 3) }
const stress10k = () => { data = makeData(10_000) }
const stress100k = () => { data = makeData(100_000) }
const shuffleColumns = () => {
table.setColumnOrder(
faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)),
)
}
</script>
<div class="demo-root">
<h1>Kitchen Sink - All Features</h1>
<div class="toolbar">
<div class="toolbar-row">
<input
class="global-filter-input"
placeholder="Fuzzy search all columns..."
value={(table.state.globalFilter ?? '') as string}
oninput={(event) =>
debounceSet('global', () =>
table.setGlobalFilter((event.target as HTMLInputElement).value),
)}
/>
</div>
<div class="toolbar-row">
<button onclick={refreshData} class="demo-button demo-button-sm">Flat 1k</button>
<button onclick={nestedData} class="demo-button demo-button-sm">Nested 100x5x3</button>
<button onclick={stress10k} class="demo-button demo-button-sm">Stress 10k (flat)</button>
<button onclick={stress100k} class="demo-button demo-button-sm">Stress 100k (flat)</button>
<button onclick={() => table.reset()} class="demo-button demo-button-sm">Reset Table</button>
<button onclick={shuffleColumns} class="demo-button demo-button-sm">Shuffle Columns</button>
<span class="nowrap">
{table.getSelectedRowModel().flatRows.length.toLocaleString()} of{' '}
{table.getCoreRowModel().flatRows.length.toLocaleString()} selected
</span>
</div>
<details class="column-toggle-panel">
<summary class="column-toggle-panel-header">Column visibility</summary>
<div class="column-toggle-row">
<label>
<input
type="checkbox"
checked={table.getIsAllColumnsVisible()}
onchange={table.getToggleAllColumnsVisibilityHandler()}
/>{' '}
Toggle All
</label>
</div>
{#each table.getAllLeafColumns() as column (column.id)}
<div class="column-toggle-row">
<label>
<input
type="checkbox"
checked={column.getIsVisible()}
disabled={!column.getCanHide()}
onchange={column.getToggleVisibilityHandler()}
/>{' '}
{column.id}
</label>
</div>
{/each}
</details>
</div>
<div class="table-container">
<table style={tableStyle()}>
<thead>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<tr>
{#each headerGroup.headers as header (header.id)}
<th colSpan={header.colSpan} style={headerStyle(header)}>
{#if !header.isPlaceholder}
<div class="header-row">
<div style="flex: 1; min-width: 0">
<div class="header-controls">
{#if header.column.getCanPin()}
<span class="pin-actions">
{#if header.column.getIsPinned() !== 'left'}
<button class="pin-button" onclick={() => header.column.pin('left')}>{'<'}</button>
{/if}
{#if header.column.getIsPinned()}
<button class="pin-button" onclick={() => header.column.pin(false)}>x</button>
{/if}
{#if header.column.getIsPinned() !== 'right'}
<button class="pin-button" onclick={() => header.column.pin('right')}>{'>'}</button>
{/if}
</span>
{/if}
{#if header.column.getCanGroup()}
<button class="pin-button" onclick={header.column.getToggleGroupingHandler()}>
{header.column.getIsGrouped()
? `Stop (${header.column.getGroupedIndex()})`
: 'Group'}
</button>
{/if}
</div>
{#if header.column.id === 'select'}
<input
type="checkbox"
checked={table.getIsAllPageRowsSelected()}
indeterminate={table.getIsSomePageRowsSelected()}
onchange={table.getToggleAllPageRowsSelectedHandler()}
/>
{:else if header.column.getCanSort()}
<button
type="button"
class="sortable-header header-sort-button"
onclick={header.column.getToggleSortingHandler()}
>
<FlexRender header={header} />
{header.column.getIsSorted() === 'asc'
? ' â–²'
: header.column.getIsSorted() === 'desc'
? ' â–¼'
: ''}
</button>
{:else}
<FlexRender header={header} />
{/if}
{#if header.column.getCanFilter()}
<div>
{#if header.column.columnDef.meta?.filterVariant === 'range'}
<div class="filter-row">
<input
type="number"
class="filter-input"
min={header.column.getFacetedMinMaxValues()?.[0] ?? ''}
max={header.column.getFacetedMinMaxValues()?.[1] ?? ''}
value={(header.column.getFilterValue() as [number, number] | undefined)?.[0] ?? ''}
placeholder={`Min${header.column.getFacetedMinMaxValues()?.[0] !== undefined ? ` (${header.column.getFacetedMinMaxValues()?.[0]})` : ''}`}
oninput={(event) =>
debounceSet(header.column.id + '-min', () =>
updateRangeFilter(
header.column,
0,
(event.target as HTMLInputElement).value,
),
)}
/>
<input
type="number"
class="filter-input"
min={header.column.getFacetedMinMaxValues()?.[0] ?? ''}
max={header.column.getFacetedMinMaxValues()?.[1] ?? ''}
value={(header.column.getFilterValue() as [number, number] | undefined)?.[1] ?? ''}
placeholder={`Max${header.column.getFacetedMinMaxValues()?.[1] !== undefined ? ` (${header.column.getFacetedMinMaxValues()?.[1]})` : ''}`}
oninput={(event) =>
debounceSet(header.column.id + '-max', () =>
updateRangeFilter(
header.column,
1,
(event.target as HTMLInputElement).value,
),
)}
/>
</div>
{:else if header.column.columnDef.meta?.filterVariant === 'select'}
<select
class="filter-select"
value={(header.column.getFilterValue() ?? '').toString()}
onchange={(event) =>
header.column.setFilterValue(
(event.target as HTMLSelectElement).value,
)}
>
<option value="">All</option>
{#each sortedUniqueValues(header.column) as value (String(value))}
<option value={String(value)}>{String(value)}</option>
{/each}
</select>
{:else}
<datalist id={header.column.id + 'list'}>
{#each sortedUniqueValues(header.column) as value (String(value))}
<option value={String(value)}></option>
{/each}
</datalist>
<input
type="text"
class="filter-select"
list={header.column.id + 'list'}
value={(header.column.getFilterValue() ?? '') as string}
placeholder={`Search (${header.column.getFacetedUniqueValues().size})`}
oninput={(event) =>
debounceSet(header.column.id, () =>
header.column.setFilterValue(
(event.target as HTMLInputElement).value,
),
)}
/>
{/if}
</div>
{/if}
</div>
</div>
{#if header.column.getCanResize()}
<button
type="button"
aria-label={`Resize ${header.column.id} column`}
ondblclick={() => header.column.resetSize()}
onmousedown={header.getResizeHandler()}
ontouchstart={header.getResizeHandler()}
class={`resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`}
></button>
{/if}
{/if}
</th>
{/each}
</tr>
{/each}
</thead>
<tbody>
{#each table.getTopRows() as row (row.id)}
<tr class="pinned-row" style={pinnedRowStyle(row)}>
{#each row.getVisibleCells() as cell (cell.id)}
<td style={cellStyle(cell)} class={cellClass(cell)}>
<FlexRender cell={cell} />
</td>
{/each}
</tr>
{/each}
{#each table.getCenterRows() as row (row.id)}
<tr>
{#each row.getVisibleCells() as cell (cell.id)}
<td style={cellStyle(cell)} class={cellClass(cell)}>
{#if cell.column.id === 'select'}
<div class="column-toggle-row">
<input
type="checkbox"
checked={cell.row.getIsSelected()}
disabled={!cell.row.getCanSelect()}
indeterminate={cell.row.getIsSomeSelected()}
onchange={cell.row.getToggleSelectedHandler()}
/>{' '}
<button
class="pin-button"
onclick={() =>
cell.row.pin(
cell.row.getIsPinned() === 'top' ? false : 'top',
)}
>
{cell.row.getIsPinned() === 'top' ? 'Pinned' : 'Pin'}
</button>
</div>
{:else if cell.column.id === 'firstName'}
<div style={`padding-left: ${cell.row.depth * 1.5}rem`}>
{#if cell.row.getCanExpand()}
<button
onclick={cell.row.getToggleExpandedHandler()}
style="cursor: pointer; margin-right: 0.25rem"
>
{cell.row.getIsExpanded() ? 'v' : '>'}
</button>
{:else}
<span style="margin-right: 0.25rem">-</span>
{/if}
<FlexRender cell={cell} />
</div>
{:else if cell.getIsGrouped()}
<button
onclick={cell.row.getToggleExpandedHandler()}
style:cursor={cell.row.getCanExpand() ? 'pointer' : 'normal'}
>
{cell.row.getIsExpanded() ? 'v' : '>'}
<FlexRender cell={cell} />
({cell.row.subRows.length.toLocaleString()})
</button>
{:else}
<FlexRender cell={cell} />
{/if}
</td>
{/each}
</tr>
{/each}
{#each table.getBottomRows() as row (row.id)}
<tr class="pinned-row" style={pinnedRowStyle(row)}>
{#each row.getVisibleCells() as cell (cell.id)}
<td style={cellStyle(cell)} class={cellClass(cell)}>
<FlexRender cell={cell} />
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
<div class="spacer-sm"></div>
<div class="controls">
<button class="demo-button demo-button-sm" onclick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()}>{'<<'}</button>
<button class="demo-button demo-button-sm" onclick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>{'<'}</button>
<button class="demo-button demo-button-sm" onclick={() => table.nextPage()} disabled={!table.getCanNextPage()}>{'>'}</button>
<button class="demo-button demo-button-sm" onclick={() => table.setPageIndex(table.getPageCount() - 1)} disabled={!table.getCanNextPage()}>{'>>'}</button>
<span class="inline-controls">
<div>Page</div>
<strong>
{(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '}
{table.getPageCount().toLocaleString()}
</strong>
</span>
<span class="inline-controls">
| Go to page:
<input
type="number"
min="1"
max={table.getPageCount()}
value={table.state.pagination.pageIndex + 1}
oninput={(event) => {
const page = (event.target as HTMLInputElement).value
? Number((event.target as HTMLInputElement).value) - 1
: 0
table.setPageIndex(page)
}}
class="page-size-input"
/>
</span>
<select
value={table.state.pagination.pageSize}
onchange={(event) =>
table.setPageSize(Number((event.target as HTMLSelectElement).value))}
>
{#each [10, 20, 30, 50, 100] as pageSize}
<option value={pageSize}>Show {pageSize}</option>
{/each}
</select>
</div>
<div class="spacer-sm"></div>
<div class="nowrap">
{table.getRowModel().rows.length.toLocaleString()} rows on this page (
{table.getFilteredRowModel().rows.length.toLocaleString()} filtered of{' '}
{table.getCoreRowModel().rows.length.toLocaleString()} total)
</div>
<div class="spacer-md"></div>
<details>
<summary>Table state (live)</summary>
<pre class="state-dump">{JSON.stringify(table.state, null, 2)}</pre>
</details>
</div>
<script lang="ts">
import { faker } from '@faker-js/faker'
import {
aggregationFns,
createExpandedRowModel,
createFacetedMinMaxValues,
createFacetedRowModel,
createFacetedUniqueValues,
createFilteredRowModel,
createGroupedRowModel,
createPaginatedRowModel,
createSortedRowModel,
createTableHook,
filterFns,
FlexRender,
sortFns,
stockFeatures,
} from '@tanstack/svelte-table'
import { compareItems, rankItem } from '@tanstack/match-sorter-utils'
import { makeData } from './makeData'
import type { Person } from './makeData'
import type {
Cell,
Column,
FilterFn,
Header,
Row,
SortFn,
} from '@tanstack/svelte-table'
import './index.css'
const fuzzyFilter: FilterFn<typeof stockFeatures, Person> = (
row,
columnId,
value,
addMeta,
) => {
const itemRank = rankItem(row.getValue(columnId), value)
addMeta?.({ itemRank })
return itemRank.passed
}
const fuzzySort: SortFn<typeof stockFeatures, Person> = (
rowA,
rowB,
columnId,
) => {
let dir = 0
if (rowA.columnFiltersMeta[columnId]) {
dir = compareItems(
rowA.columnFiltersMeta[columnId].itemRank!,
rowB.columnFiltersMeta[columnId].itemRank!,
)
}
return dir === 0 ? sortFns.alphanumeric(rowA, rowB, columnId) : dir
}
const sortStatusFn: SortFn<typeof stockFeatures, Person> = (rowA, rowB) => {
const statusOrder = ['single', 'complicated', 'relationship']
return (
statusOrder.indexOf(rowA.original.status) -
statusOrder.indexOf(rowB.original.status)
)
}
const { createAppTable, createAppColumnHelper } = createTableHook({
features: stockFeatures,
rowModels: {
expandedRowModel: createExpandedRowModel(),
filteredRowModel: createFilteredRowModel({
...filterFns,
fuzzy: fuzzyFilter,
}),
facetedRowModel: createFacetedRowModel(),
facetedMinMaxValues: createFacetedMinMaxValues(),
facetedUniqueValues: createFacetedUniqueValues(),
groupedRowModel: createGroupedRowModel(aggregationFns),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(sortFns),
} as any,
})
const columnHelper = createAppColumnHelper<Person>()
const columns = columnHelper.columns([
columnHelper.display({
id: 'select',
size: 80,
minSize: 80,
maxSize: 80,
enableSorting: false,
enableGrouping: false,
enableHiding: false,
enableResizing: false,
header: ({ table }) =>
table.getIsAllPageRowsSelected()
? 'all'
: table.getIsSomePageRowsSelected()
? 'some'
: 'none',
cell: ({ row }) => (row.getIsPinned() === 'top' ? 'Pinned' : 'Pin'),
}),
columnHelper.accessor('firstName', {
id: 'firstName',
size: 200,
header: 'First Name',
filterFn: 'fuzzy',
sortFn: fuzzySort,
meta: { filterVariant: 'text' },
getGroupingValue: (row) => `${row.firstName} ${row.lastName}`,
}),
columnHelper.accessor((row) => row.lastName, {
id: 'lastName',
size: 180,
header: 'Last Name',
meta: { filterVariant: 'text' },
}),
columnHelper.accessor('age', {
id: 'age',
size: 200,
header: 'Age',
meta: { filterVariant: 'range' },
aggregationFn: 'median',
aggregatedCell: ({ getValue }) =>
Math.round(getValue<number>() * 100) / 100,
}),
columnHelper.accessor('visits', {
id: 'visits',
size: 200,
header: 'Visits',
meta: { filterVariant: 'range' },
aggregationFn: 'sum',
aggregatedCell: ({ getValue }) => getValue<number>().toLocaleString(),
}),
columnHelper.accessor('status', {
id: 'status',
size: 200,
header: 'Status',
sortFn: sortStatusFn,
meta: { filterVariant: 'select' },
}),
columnHelper.accessor('progress', {
id: 'progress',
size: 200,
header: 'Profile Progress',
meta: { filterVariant: 'range' },
aggregationFn: 'mean',
cell: ({ getValue }) => `${Math.round(getValue<number>() * 100) / 100}%`,
aggregatedCell: ({ getValue }) =>
`${Math.round(getValue<number>() * 100) / 100}%`,
}),
])
let data = $state(makeData(1_000))
const debounceTimers = new Map<string, ReturnType<typeof setTimeout>>()
const table = createAppTable(
{
columns,
get data() {
return data
},
getSubRows: (row) => row.subRows,
globalFilterFn: 'fuzzy',
columnResizeMode: 'onChange',
defaultColumn: { minSize: 200, maxSize: 800 },
initialState: {
columnOrder: columns.map((c) => c.id!),
columnPinning: { left: ['select'], right: [] },
pagination: { pageIndex: 0, pageSize: 20 },
},
keepPinnedRows: true,
debugTable: true,
},
(state) => state,
)
function debounceSet(key: string, setValue: () => void) {
clearTimeout(debounceTimers.get(key))
debounceTimers.set(
key,
setTimeout(() => {
setValue()
debounceTimers.delete(key)
}, 300),
)
}
function getCommonPinningStyle(column: Column<typeof stockFeatures, Person>) {
const isPinned = column.getIsPinned()
const isLastLeftPinnedColumn =
isPinned === 'left' && column.getIsLastColumn('left')
const isFirstRightPinnedColumn =
isPinned === 'right' && column.getIsFirstColumn('right')
return [
isLastLeftPinnedColumn
? 'box-shadow: -4px 0 4px -4px gray inset'
: isFirstRightPinnedColumn
? 'box-shadow: 4px 0 4px -4px gray inset'
: '',
isPinned === 'left' ? `left: ${column.getStart('left')}px` : '',
isPinned === 'right' ? `right: ${column.getAfter('right')}px` : '',
`opacity: ${isPinned ? 0.97 : 1}`,
`position: ${isPinned ? 'sticky' : 'relative'}`,
`z-index: ${isPinned ? 1 : 0}`,
]
.filter(Boolean)
.join('; ')
}
function headerStyle(header: Header<typeof stockFeatures, Person, unknown>) {
return `${getCommonPinningStyle(header.column)}; white-space: nowrap; width: calc(var(--header-${header.id}-size) * 1px)`
}
function cellStyle(cell: Cell<typeof stockFeatures, Person, unknown>) {
return `${getCommonPinningStyle(cell.column)}; width: calc(var(--col-${cell.column.id}-size) * 1px)`
}
function cellClass(cell: Cell<typeof stockFeatures, Person, unknown>) {
const groupingActive = table.state.grouping.length > 0
const hasAggregation = !!cell.column.columnDef.aggregationFn
return !groupingActive
? undefined
: cell.getIsGrouped()
? 'cell-grouped'
: hasAggregation && cell.getIsAggregated()
? 'cell-aggregated'
: cell.getIsPlaceholder()
? 'cell-placeholder'
: undefined
}
function pinnedRowStyle(row: Row<typeof stockFeatures, Person>) {
const bottomRows = table.getBottomRows()
return [
'position: sticky',
row.getIsPinned() === 'top'
? `top: ${row.getPinnedIndex() * 32 + 48}px`
: '',
row.getIsPinned() === 'bottom'
? `bottom: ${(bottomRows.length - 1 - row.getPinnedIndex()) * 32}px`
: '',
'z-index: 1',
]
.filter(Boolean)
.join('; ')
}
function tableStyle() {
const styles = [`width: ${table.getTotalSize()}px`]
for (const header of table.getFlatHeaders()) {
styles.push(`--header-${header.id}-size: ${header.getSize()}`)
styles.push(`--col-${header.column.id}-size: ${header.column.getSize()}`)
}
return styles.join('; ')
}
function sortedUniqueValues(column: Column<typeof stockFeatures, Person>) {
if (column.columnDef.meta?.filterVariant === 'range') return []
return Array.from(column.getFacetedUniqueValues().keys())
.sort()
.slice(0, 5000)
}
function updateRangeFilter(
column: Column<typeof stockFeatures, Person>,
index: 0 | 1,
value: string,
) {
column.setFilterValue((old: [number, number] | undefined) =>
index === 0 ? [value, old?.[1]] : [old?.[0], value],
)
}
const refreshData = () => { data = makeData(1_000) }
const nestedData = () => { data = makeData(100, 5, 3) }
const stress10k = () => { data = makeData(10_000) }
const stress100k = () => { data = makeData(100_000) }
const shuffleColumns = () => {
table.setColumnOrder(
faker.helpers.shuffle(table.getAllLeafColumns().map((d) => d.id)),
)
}
</script>
<div class="demo-root">
<h1>Kitchen Sink - All Features</h1>
<div class="toolbar">
<div class="toolbar-row">
<input
class="global-filter-input"
placeholder="Fuzzy search all columns..."
value={(table.state.globalFilter ?? '') as string}
oninput={(event) =>
debounceSet('global', () =>
table.setGlobalFilter((event.target as HTMLInputElement).value),
)}
/>
</div>
<div class="toolbar-row">
<button onclick={refreshData} class="demo-button demo-button-sm">Flat 1k</button>
<button onclick={nestedData} class="demo-button demo-button-sm">Nested 100x5x3</button>
<button onclick={stress10k} class="demo-button demo-button-sm">Stress 10k (flat)</button>
<button onclick={stress100k} class="demo-button demo-button-sm">Stress 100k (flat)</button>
<button onclick={() => table.reset()} class="demo-button demo-button-sm">Reset Table</button>
<button onclick={shuffleColumns} class="demo-button demo-button-sm">Shuffle Columns</button>
<span class="nowrap">
{table.getSelectedRowModel().flatRows.length.toLocaleString()} of{' '}
{table.getCoreRowModel().flatRows.length.toLocaleString()} selected
</span>
</div>
<details class="column-toggle-panel">
<summary class="column-toggle-panel-header">Column visibility</summary>
<div class="column-toggle-row">
<label>
<input
type="checkbox"
checked={table.getIsAllColumnsVisible()}
onchange={table.getToggleAllColumnsVisibilityHandler()}
/>{' '}
Toggle All
</label>
</div>
{#each table.getAllLeafColumns() as column (column.id)}
<div class="column-toggle-row">
<label>
<input
type="checkbox"
checked={column.getIsVisible()}
disabled={!column.getCanHide()}
onchange={column.getToggleVisibilityHandler()}
/>{' '}
{column.id}
</label>
</div>
{/each}
</details>
</div>
<div class="table-container">
<table style={tableStyle()}>
<thead>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<tr>
{#each headerGroup.headers as header (header.id)}
<th colSpan={header.colSpan} style={headerStyle(header)}>
{#if !header.isPlaceholder}
<div class="header-row">
<div style="flex: 1; min-width: 0">
<div class="header-controls">
{#if header.column.getCanPin()}
<span class="pin-actions">
{#if header.column.getIsPinned() !== 'left'}
<button class="pin-button" onclick={() => header.column.pin('left')}>{'<'}</button>
{/if}
{#if header.column.getIsPinned()}
<button class="pin-button" onclick={() => header.column.pin(false)}>x</button>
{/if}
{#if header.column.getIsPinned() !== 'right'}
<button class="pin-button" onclick={() => header.column.pin('right')}>{'>'}</button>
{/if}
</span>
{/if}
{#if header.column.getCanGroup()}
<button class="pin-button" onclick={header.column.getToggleGroupingHandler()}>
{header.column.getIsGrouped()
? `Stop (${header.column.getGroupedIndex()})`
: 'Group'}
</button>
{/if}
</div>
{#if header.column.id === 'select'}
<input
type="checkbox"
checked={table.getIsAllPageRowsSelected()}
indeterminate={table.getIsSomePageRowsSelected()}
onchange={table.getToggleAllPageRowsSelectedHandler()}
/>
{:else if header.column.getCanSort()}
<button
type="button"
class="sortable-header header-sort-button"
onclick={header.column.getToggleSortingHandler()}
>
<FlexRender header={header} />
{header.column.getIsSorted() === 'asc'
? ' â–²'
: header.column.getIsSorted() === 'desc'
? ' â–¼'
: ''}
</button>
{:else}
<FlexRender header={header} />
{/if}
{#if header.column.getCanFilter()}
<div>
{#if header.column.columnDef.meta?.filterVariant === 'range'}
<div class="filter-row">
<input
type="number"
class="filter-input"
min={header.column.getFacetedMinMaxValues()?.[0] ?? ''}
max={header.column.getFacetedMinMaxValues()?.[1] ?? ''}
value={(header.column.getFilterValue() as [number, number] | undefined)?.[0] ?? ''}
placeholder={`Min${header.column.getFacetedMinMaxValues()?.[0] !== undefined ? ` (${header.column.getFacetedMinMaxValues()?.[0]})` : ''}`}
oninput={(event) =>
debounceSet(header.column.id + '-min', () =>
updateRangeFilter(
header.column,
0,
(event.target as HTMLInputElement).value,
),
)}
/>
<input
type="number"
class="filter-input"
min={header.column.getFacetedMinMaxValues()?.[0] ?? ''}
max={header.column.getFacetedMinMaxValues()?.[1] ?? ''}
value={(header.column.getFilterValue() as [number, number] | undefined)?.[1] ?? ''}
placeholder={`Max${header.column.getFacetedMinMaxValues()?.[1] !== undefined ? ` (${header.column.getFacetedMinMaxValues()?.[1]})` : ''}`}
oninput={(event) =>
debounceSet(header.column.id + '-max', () =>
updateRangeFilter(
header.column,
1,
(event.target as HTMLInputElement).value,
),
)}
/>
</div>
{:else if header.column.columnDef.meta?.filterVariant === 'select'}
<select
class="filter-select"
value={(header.column.getFilterValue() ?? '').toString()}
onchange={(event) =>
header.column.setFilterValue(
(event.target as HTMLSelectElement).value,
)}
>
<option value="">All</option>
{#each sortedUniqueValues(header.column) as value (String(value))}
<option value={String(value)}>{String(value)}</option>
{/each}
</select>
{:else}
<datalist id={header.column.id + 'list'}>
{#each sortedUniqueValues(header.column) as value (String(value))}
<option value={String(value)}></option>
{/each}
</datalist>
<input
type="text"
class="filter-select"
list={header.column.id + 'list'}
value={(header.column.getFilterValue() ?? '') as string}
placeholder={`Search (${header.column.getFacetedUniqueValues().size})`}
oninput={(event) =>
debounceSet(header.column.id, () =>
header.column.setFilterValue(
(event.target as HTMLInputElement).value,
),
)}
/>
{/if}
</div>
{/if}
</div>
</div>
{#if header.column.getCanResize()}
<button
type="button"
aria-label={`Resize ${header.column.id} column`}
ondblclick={() => header.column.resetSize()}
onmousedown={header.getResizeHandler()}
ontouchstart={header.getResizeHandler()}
class={`resizer ${header.column.getIsResizing() ? 'isResizing' : ''}`}
></button>
{/if}
{/if}
</th>
{/each}
</tr>
{/each}
</thead>
<tbody>
{#each table.getTopRows() as row (row.id)}
<tr class="pinned-row" style={pinnedRowStyle(row)}>
{#each row.getVisibleCells() as cell (cell.id)}
<td style={cellStyle(cell)} class={cellClass(cell)}>
<FlexRender cell={cell} />
</td>
{/each}
</tr>
{/each}
{#each table.getCenterRows() as row (row.id)}
<tr>
{#each row.getVisibleCells() as cell (cell.id)}
<td style={cellStyle(cell)} class={cellClass(cell)}>
{#if cell.column.id === 'select'}
<div class="column-toggle-row">
<input
type="checkbox"
checked={cell.row.getIsSelected()}
disabled={!cell.row.getCanSelect()}
indeterminate={cell.row.getIsSomeSelected()}
onchange={cell.row.getToggleSelectedHandler()}
/>{' '}
<button
class="pin-button"
onclick={() =>
cell.row.pin(
cell.row.getIsPinned() === 'top' ? false : 'top',
)}
>
{cell.row.getIsPinned() === 'top' ? 'Pinned' : 'Pin'}
</button>
</div>
{:else if cell.column.id === 'firstName'}
<div style={`padding-left: ${cell.row.depth * 1.5}rem`}>
{#if cell.row.getCanExpand()}
<button
onclick={cell.row.getToggleExpandedHandler()}
style="cursor: pointer; margin-right: 0.25rem"
>
{cell.row.getIsExpanded() ? 'v' : '>'}
</button>
{:else}
<span style="margin-right: 0.25rem">-</span>
{/if}
<FlexRender cell={cell} />
</div>
{:else if cell.getIsGrouped()}
<button
onclick={cell.row.getToggleExpandedHandler()}
style:cursor={cell.row.getCanExpand() ? 'pointer' : 'normal'}
>
{cell.row.getIsExpanded() ? 'v' : '>'}
<FlexRender cell={cell} />
({cell.row.subRows.length.toLocaleString()})
</button>
{:else}
<FlexRender cell={cell} />
{/if}
</td>
{/each}
</tr>
{/each}
{#each table.getBottomRows() as row (row.id)}
<tr class="pinned-row" style={pinnedRowStyle(row)}>
{#each row.getVisibleCells() as cell (cell.id)}
<td style={cellStyle(cell)} class={cellClass(cell)}>
<FlexRender cell={cell} />
</td>
{/each}
</tr>
{/each}
</tbody>
</table>
</div>
<div class="spacer-sm"></div>
<div class="controls">
<button class="demo-button demo-button-sm" onclick={() => table.setPageIndex(0)} disabled={!table.getCanPreviousPage()}>{'<<'}</button>
<button class="demo-button demo-button-sm" onclick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>{'<'}</button>
<button class="demo-button demo-button-sm" onclick={() => table.nextPage()} disabled={!table.getCanNextPage()}>{'>'}</button>
<button class="demo-button demo-button-sm" onclick={() => table.setPageIndex(table.getPageCount() - 1)} disabled={!table.getCanNextPage()}>{'>>'}</button>
<span class="inline-controls">
<div>Page</div>
<strong>
{(table.state.pagination.pageIndex + 1).toLocaleString()} of{' '}
{table.getPageCount().toLocaleString()}
</strong>
</span>
<span class="inline-controls">
| Go to page:
<input
type="number"
min="1"
max={table.getPageCount()}
value={table.state.pagination.pageIndex + 1}
oninput={(event) => {
const page = (event.target as HTMLInputElement).value
? Number((event.target as HTMLInputElement).value) - 1
: 0
table.setPageIndex(page)
}}
class="page-size-input"
/>
</span>
<select
value={table.state.pagination.pageSize}
onchange={(event) =>
table.setPageSize(Number((event.target as HTMLSelectElement).value))}
>
{#each [10, 20, 30, 50, 100] as pageSize}
<option value={pageSize}>Show {pageSize}</option>
{/each}
</select>
</div>
<div class="spacer-sm"></div>
<div class="nowrap">
{table.getRowModel().rows.length.toLocaleString()} rows on this page (
{table.getFilteredRowModel().rows.length.toLocaleString()} filtered of{' '}
{table.getCoreRowModel().rows.length.toLocaleString()} total)
</div>
<div class="spacer-md"></div>
<details>
<summary>Table state (live)</summary>
<pre class="state-dump">{JSON.stringify(table.state, null, 2)}</pre>
</details>
</div>