<script lang="ts">
import { createRawSnippet } from 'svelte'
import {
FlexRender,
columnFilteringFeature,
createColumnHelper,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
createTable,
filterFn_includesString,
globalFilteringFeature,
renderSnippet,
rowPaginationFeature,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_text,
tableFeatures,
} from '@tanstack/svelte-table'
import { debounce } from '@tanstack/pacer'
import ArrowDown from '@lucide/svelte/icons/arrow-down'
import ArrowUp from '@lucide/svelte/icons/arrow-up'
import ArrowUpDown from '@lucide/svelte/icons/arrow-up-down'
import ChevronLeft from '@lucide/svelte/icons/chevron-left'
import ChevronRight from '@lucide/svelte/icons/chevron-right'
import ChevronsLeft from '@lucide/svelte/icons/chevrons-left'
import ChevronsRight from '@lucide/svelte/icons/chevrons-right'
import Search from '@lucide/svelte/icons/search'
import { Button } from '@/lib/components/ui/button'
import { Input } from '@/lib/components/ui/input'
import * as Select from '@/lib/components/ui/select'
import * as Table from '@/lib/components/ui/table'
import { makeData } from './makeData'
import type { Person } from './makeData'
import './index.css'
const features = tableFeatures({
rowSortingFeature,
rowPaginationFeature,
columnFilteringFeature,
globalFilteringFeature,
sortedRowModel: createSortedRowModel(),
paginatedRowModel: createPaginatedRowModel(),
filteredRowModel: createFilteredRowModel(),
sortFns: {
alphanumeric: sortFn_alphanumeric,
text: sortFn_text,
},
filterFns: {
includesString: filterFn_includesString,
},
})
const italicCell = createRawSnippet<[{ value: string | undefined }]>(
(getProps) => ({
render: () => `<i>${getProps().value ?? ''}</i>`,
}),
)
const columnHelper = createColumnHelper<typeof features, Person>()
const columns = columnHelper.columns([
columnHelper.accessor('firstName', {
header: 'First Name',
cell: (info) => info.getValue(),
}),
columnHelper.accessor((row) => row.lastName, {
id: 'lastName',
header: 'Last Name',
cell: (info) => renderSnippet(italicCell, { value: info.getValue<string>() }),
}),
columnHelper.accessor((row) => Number(row.age), {
id: 'age',
header: 'Age',
cell: (info) => info.renderValue(),
}),
columnHelper.accessor('visits', {
header: 'Visits',
}),
columnHelper.accessor('status', {
header: 'Status',
}),
columnHelper.accessor('progress', {
header: 'Profile Progress',
}),
])
let data = $state(makeData(200))
const refreshData = () => (data = makeData(200))
const stressTest = () => (data = makeData(1_000_000))
const table = createTable({
debugTable: true,
features,
columns,
get data() {
return data
},
globalFilterFn: 'includesString',
})
const globalFilter = $derived(table.atoms.globalFilter.get())
const pagination = $derived(table.atoms.pagination.get())
const setGlobalFilterDebounced = debounce(
(value: string | number) => table.setGlobalFilter(String(value)),
{ wait: 300 },
)
</script>
<div class="p-4">
<div class="flex items-center justify-between gap-2 mb-4">
<div class="relative w-full max-w-sm">
<Search
class="pointer-events-none absolute left-2.5 top-1/2 size-4 -translate-y-1/2 text-muted-foreground"
/>
<Input
value={globalFilter ?? ''}
placeholder="Search all columns..."
class="pl-8"
oninput={(e) => setGlobalFilterDebounced(e.currentTarget.value)}
/>
</div>
<div class="flex gap-2">
<Button variant="outline" onclick={refreshData}>Regenerate Data</Button>
<Button variant="outline" onclick={stressTest}>
Stress Test (1M rows)
</Button>
</div>
</div>
<div class="rounded-md border">
<Table.Root>
<Table.Header>
{#each table.getHeaderGroups() as headerGroup (headerGroup.id)}
<Table.Row>
{#each headerGroup.headers as header (header.id)}
{@const sorted = header.column.getIsSorted()}
{@const Icon =
sorted === 'asc'
? ArrowUp
: sorted === 'desc'
? ArrowDown
: ArrowUpDown}
<Table.Head colspan={header.colSpan}>
{#if !header.isPlaceholder}
{#if header.column.getCanSort()}
<Button
variant="ghost"
size="sm"
class="-ml-3 h-8 data-[state=open]:bg-accent"
onclick={header.column.getToggleSortingHandler()}
>
<FlexRender {header} />
<Icon class="ml-2" />
</Button>
{:else}
<span class="text-sm font-medium">
<FlexRender {header} />
</span>
{/if}
{/if}
</Table.Head>
{/each}
</Table.Row>
{/each}
</Table.Header>
<Table.Body>
{#each table.getRowModel().rows as row (row.id)}
<Table.Row>
{#each row.getAllCells() as cell (cell.id)}
<Table.Cell>
<FlexRender {cell} />
</Table.Cell>
{/each}
</Table.Row>
{:else}
<Table.Row>
<Table.Cell colspan={columns.length} class="h-24 text-center">
No results.
</Table.Cell>
</Table.Row>
{/each}
</Table.Body>
</Table.Root>
</div>
<div class="flex items-center justify-between gap-4 px-2 py-4">
<div class="text-sm text-muted-foreground">
{table.getPrePaginatedRowModel().rows.length.toLocaleString()} of{' '}
{data.length.toLocaleString()} rows
</div>
<div class="flex items-center gap-6 lg:gap-8">
<div class="flex items-center gap-2">
<p class="text-sm font-medium">Rows per page</p>
<Select.Root
type="single"
value={`${pagination.pageSize}`}
onValueChange={(value) => table.setPageSize(Number(value))}
>
<Select.Trigger size="sm" class="w-[70px]">
{pagination.pageSize}
</Select.Trigger>
<Select.Content side="top">
{#each [10, 20, 30, 40, 50] as pageSize (pageSize)}
<Select.Item value={`${pageSize}`} label={`${pageSize}`} />
{/each}
</Select.Content>
</Select.Root>
</div>
<div class="flex w-[100px] items-center justify-center text-sm font-medium">
Page {pagination.pageIndex + 1} of{' '}
{Math.max(1, table.getPageCount())}
</div>
<div class="flex items-center gap-2">
<Button
variant="outline"
size="icon"
class="hidden size-8 lg:flex"
disabled={!table.getCanPreviousPage()}
onclick={() => table.firstPage()}
>
<span class="sr-only">Go to first page</span>
<ChevronsLeft />
</Button>
<Button
variant="outline"
size="icon"
class="size-8"
disabled={!table.getCanPreviousPage()}
onclick={() => table.previousPage()}
>
<span class="sr-only">Go to previous page</span>
<ChevronLeft />
</Button>
<Button
variant="outline"
size="icon"
class="size-8"
disabled={!table.getCanNextPage()}
onclick={() => table.nextPage()}
>
<span class="sr-only">Go to next page</span>
<ChevronRight />
</Button>
<Button
variant="outline"
size="icon"
class="hidden size-8 lg:flex"
disabled={!table.getCanNextPage()}
onclick={() => table.lastPage()}
>
<span class="sr-only">Go to last page</span>
<ChevronsRight />
</Button>
</div>
</div>
</div>
</div>