import { createRoot, useEffect, useMemo, useRef, useState } from 'octane'
import {
columnFacetingFeature,
columnFilteringFeature,
createFacetedMinMaxValues,
createFacetedRowModel,
createFacetedUniqueValues,
createFilteredRowModel,
createSortedRowModel,
filterFn_inNumberRange,
filterFn_includesString,
metaHelper,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_basic,
sortFn_datetime,
tableFeatures,
useTable,
} from '@tanstack/octane-table'
import { makeData } from './makeData'
import type {
Column,
ColumnDef,
FilterFn,
FilterFnOption,
OctaneTable,
SortFnOption,
} from '@tanstack/octane-table'
import './index.css'
type DynamicRow = Record<string, unknown>
type DataType = 'string' | 'number' | 'boolean' | 'date'
interface DynamicColumnMeta {
dataType: DataType
}
const features = tableFeatures({
rowSortingFeature,
columnFilteringFeature,
columnFacetingFeature,
sortedRowModel: createSortedRowModel(),
filteredRowModel: createFilteredRowModel(),
facetedRowModel: createFacetedRowModel(),
facetedUniqueValues: createFacetedUniqueValues(),
facetedMinMaxValues: createFacetedMinMaxValues(),
sortFns: {
alphanumeric: sortFn_alphanumeric,
basic: sortFn_basic,
datetime: sortFn_datetime,
},
filterFns: {
includesString: filterFn_includesString,
inNumberRange: filterFn_inNumberRange,
},
columnMeta: metaHelper<DynamicColumnMeta>(),
})
type DynamicTable = OctaneTable<typeof features, DynamicRow>
const booleanFilterFn: FilterFn<typeof features, any> = (
row,
columnId,
filterValue,
) => {
if (filterValue === '' || filterValue == null) return true
return String(row.getValue(columnId)) === String(filterValue)
}
const dateRangeFilterFn: FilterFn<typeof features, any> = (
row,
columnId,
filterValue,
) => {
const [min, max] = (filterValue as [string, string] | undefined) ?? ['', '']
const value = row.getValue(columnId)
const time =
value instanceof Date ? value.getTime() : new Date(String(value)).getTime()
if (min && time < new Date(min).getTime()) return false
if (max && time > new Date(max).getTime()) return false
return true
}
function formatHeader(key: string) {
const withSpaces = key
.replace(/([a-z0-9])([A-Z])/g, '$1 $2')
.replace(/[_-]+/g, ' ')
return withSpaces.charAt(0).toUpperCase() + withSpaces.slice(1)
}
function detectDataType(data: Array<DynamicRow>, key: string): DataType {
const sample = data.find((row) => row[key] != null)?.[key]
if (sample instanceof Date) return 'date'
if (typeof sample === 'boolean') return 'boolean'
if (typeof sample === 'number') return 'number'
return 'string'
}
function getSortFn(dataType: DataType): SortFnOption<typeof features, any> {
if (dataType === 'number' || dataType === 'boolean') return 'basic'
if (dataType === 'date') return 'datetime'
return 'alphanumeric'
}
function getFilterFn(dataType: DataType): FilterFnOption<typeof features, any> {
if (dataType === 'number') return 'inNumberRange'
if (dataType === 'boolean') return booleanFilterFn
if (dataType === 'date') return dateRangeFilterFn
return 'includesString'
}
function renderValue(value: unknown, dataType: DataType) {
if (value == null) return ''
if (dataType === 'date') return (value as Date).toLocaleDateString()
if (dataType === 'boolean') return value ? '✅' : '❌'
return String(value)
}
function App() @{
const [data, setData] = useState<Array<DynamicRow>>(() => makeData(1_000))
const refreshData = () => setData(makeData(1_000))
const stressTest = () => setData(makeData(1_000_000))
const columns = useMemo<Array<ColumnDef<typeof features, DynamicRow>>>(() => {
if (data.length === 0) return []
return Object.keys(data[0]!).map((key) => {
const dataType = detectDataType(data, key)
return {
accessorKey: key,
header: formatHeader(key),
meta: { dataType },
sortFn: getSortFn(dataType),
filterFn: getFilterFn(dataType),
cell: (info) => renderValue(info.getValue(), dataType),
}
})
}, [data])
const table = useTable(
{
debugTable: true,
features,
columns,
data,
},
(state) => state,
)
<div className="demo-root">
<p className="demo-note">
Columns, sort fns, filter fns, and filter components are all derived
from the data type of each field, not from a hard-coded column definition.
</p>
<div className="button-row">
<button className="demo-button demo-button-sm" onClick={refreshData}>
Regenerate Data
</button>
<button className="demo-button demo-button-sm" onClick={stressTest}>
Stress Test (1M rows)
</button>
</div>
<div className="spacer-sm" />
<div className="scroll-container">
<table>
<thead>
@for (const headerGroup of table.getHeaderGroups(); key headerGroup.id) {
<tr>
@for (const header of headerGroup.headers; key header.id) {
<th colSpan={header.colSpan}>
{header.isPlaceholder ? null : <>
<div
className={header.column.getCanSort() ? 'sortable-header' : ''}
onClick={header.column.getToggleSortingHandler()}
title={header.column.getCanSort() ? 'Toggle sorting' : undefined}
>
<table.FlexRender header={header} />
{header.column.getIsSorted() === 'asc'
? ' 🔼'
: header.column.getIsSorted() === 'desc'
? ' 🔽'
: null}
</div>
{header.column.getCanFilter()
? <Filter column={header.column} table={table} />
: null}
</>}
</th>
}
</tr>
}
</thead>
<tbody>
@for (const row of table.getRowModel().rows.slice(0, 15); key row.id) {
<tr>
@for (const cell of row.getAllCells(); key cell.id) {
<td><table.FlexRender cell={cell} /></td>
}
</tr>
}
</tbody>
</table>
</div>
<div className="spacer-sm" />
<div>{String(table.getRowModel().rows.length)} Rows</div>
</div>
}
function Filter({
column,
table,
}: {
column: Column<typeof features, DynamicRow, unknown>
table: DynamicTable
}) @{
<table.Subscribe selector={(state) => state.columnFilters}>
{() => <FilterContent column={column} />}
</table.Subscribe>
}
function FilterContent({
column,
}: {
column: Column<typeof features, DynamicRow, unknown>
}) {
const { dataType } = column.columnDef.meta ?? { dataType: 'string' }
const filterValue = column.getFilterValue()
if (dataType === 'number') {
const [min, max] = column.getFacetedMinMaxValues() ?? []
return <div className="filter-row">
<DebouncedInput
type="number"
value={(filterValue as [number, number] | undefined)?.[0] ?? ''}
onChange={(value) => column.setFilterValue((old: [number, number] | undefined) => [value, old?.[1]])}
placeholder={`Min${min !== undefined ? ` (${min})` : ''}`}
className="filter-input"
/>
<DebouncedInput
type="number"
value={(filterValue as [number, number] | undefined)?.[1] ?? ''}
onChange={(value) => column.setFilterValue((old: [number, number] | undefined) => [old?.[0], value])}
placeholder={`Max${max !== undefined ? ` (${max})` : ''}`}
className="filter-input"
/>
</div>
}
if (dataType === 'date') {
return <div className="filter-row">
<DebouncedInput
type="date"
value={(filterValue as [string, string] | undefined)?.[0] ?? ''}
onChange={(value) => column.setFilterValue((old: [string, string] | undefined) => [String(value), old?.[1] ?? ''])}
className="filter-input"
/>
<DebouncedInput
type="date"
value={(filterValue as [string, string] | undefined)?.[1] ?? ''}
onChange={(value) => column.setFilterValue((old: [string, string] | undefined) => [old?.[0] ?? '', String(value)])}
className="filter-input"
/>
</div>
}
if (dataType === 'boolean') {
return <select
className="filter-select"
value={String(filterValue ?? '')}
onChange={(event) => column.setFilterValue(event.currentTarget.value)}
>
<option value="">All</option>
<option value="true">Yes</option>
<option value="false">No</option>
</select>
}
const uniqueValues = Array.from(column.getFacetedUniqueValues().keys())
.map(String)
.sort()
if (uniqueValues.length > 0 && uniqueValues.length <= 10) {
return <select
className="filter-select"
value={String(filterValue ?? '')}
onChange={(event) => column.setFilterValue(event.currentTarget.value)}
>
<option value="">All</option>
<EnumOptions values={uniqueValues} />
</select>
}
return <DebouncedInput
type="text"
value={String(filterValue ?? '')}
onChange={(value) => column.setFilterValue(value)}
placeholder={`Search... (${column.getFacetedUniqueValues().size})`}
className="filter-input"
/>
}
function EnumOptions({ values }: { values: Array<string> }) @{
<>
@for (const value of values; key value) {
<option value={value}>{value}</option>
}
</>
}
function DebouncedInput({
value: initialValue,
onChange,
debounce = 500,
type,
placeholder,
className,
}: {
value: string | number
onChange: (value: string | number) => void
debounce?: number
type: string
placeholder?: string
className?: string
}) @{
const [value, setValue] = useState(initialValue)
const onChangeRef = useRef(onChange)
const timeoutRef = useRef<number | undefined>(undefined)
onChangeRef.current = onChange
useEffect(() => {
setValue(initialValue)
}, [initialValue])
useEffect(() => {
return () => {
if (timeoutRef.current !== undefined) {
window.clearTimeout(timeoutRef.current)
}
}
}, [])
<input
type={type}
placeholder={placeholder}
className={className}
value={value}
onInput={(event) => {
const nextValue = event.currentTarget.value
setValue(nextValue)
if (timeoutRef.current !== undefined) {
window.clearTimeout(timeoutRef.current)
}
timeoutRef.current = window.setTimeout(
() => onChangeRef.current(nextValue),
debounce,
)
}}
/>
}
const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')
createRoot(rootElement).render(App)