import {
FlexRender,
columnFacetingFeature,
columnFilteringFeature,
createFacetedMinMaxValues,
createFacetedRowModel,
createFacetedUniqueValues,
createFilteredRowModel,
createSortedRowModel,
createTable,
filterFns,
metaHelper,
rowSortingFeature,
sortFns,
tableFeatures,
} from '@tanstack/solid-table'
import { For, Show, createMemo, createSignal } from 'solid-js'
import { makeData } from './makeData'
import ColumnFilter from './ColumnFilter'
import type {
ColumnDef,
FilterFn,
FilterFnOption,
SortFnOption,
} from '@tanstack/solid-table'
// 3. render a different filter component per type (see the branches in <ColumnFilter>).
export type DynamicRow = Record<string, unknown>
export type DataType = 'string' | 'number' | 'boolean' | 'date'
interface DynamicColumnMeta {
dataType: DataType
}
export const features = tableFeatures({
rowSortingFeature,
columnFilteringFeature,
columnFacetingFeature,
sortedRowModel: createSortedRowModel(),
filteredRowModel: createFilteredRowModel(),
facetedRowModel: createFacetedRowModel(),
facetedUniqueValues: createFacetedUniqueValues(),
facetedMinMaxValues: createFacetedMinMaxValues(),
sortFns,
filterFns,
columnMeta: metaHelper<DynamicColumnMeta>(),
})
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(value as string).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> {
switch (dataType) {
case 'number':
case 'boolean':
return 'basic'
case 'date':
return 'datetime'
case 'string':
default:
return 'alphanumeric'
}
}
function getFilterFn(dataType: DataType): FilterFnOption<typeof features, any> {
switch (dataType) {
case 'number':
return 'inNumberRange'
case 'boolean':
return booleanFilterFn
case 'date':
return dateRangeFilterFn
case 'string':
default:
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 as boolean) ? '✅' : '❌'
return String(value)
}
function App() {
const [data, setData] = createSignal<Array<DynamicRow>>(makeData(1_000))
const refreshData = () => setData(makeData(1_000))
const stressTest = () => setData(makeData(1_000_000))
const columns = createMemo<Array<ColumnDef<typeof features, DynamicRow>>>(
() => {
const rows = data()
if (rows.length === 0) return []
return Object.keys(rows[0]).map(
(key): ColumnDef<typeof features, DynamicRow> => {
const dataType = detectDataType(rows, key)
return {
accessorKey: key,
header: formatHeader(key),
meta: { dataType },
sortFn: getSortFn(dataType),
filterFn: getFilterFn(dataType),
cell: (info) => renderValue(info.getValue(), dataType),
}
},
)
},
)
const table = createTable({
features,
get data() {
return data()
},
get columns() {
return columns()
},
debugTable: true,
})
return (
<div class="demo-root">
<p class="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 class="button-row">
<button class="demo-button demo-button-sm" onClick={refreshData}>
Regenerate Data
</button>
<button class="demo-button demo-button-sm" onClick={stressTest}>
Stress Test (1M rows)
</button>
</div>
<div class="spacer-sm" />
<div class="scroll-container">
<table>
<thead>
<For each={table.getHeaderGroups()}>
{(headerGroup) => (
<tr>
<For each={headerGroup.headers}>
{(header) => (
<th colSpan={header.colSpan}>
<Show when={!header.isPlaceholder}>
<div
class={
header.column.getCanSort()
? 'sortable-header'
: ''
}
onClick={header.column.getToggleSortingHandler()}
title={
header.column.getCanSort()
? 'Toggle sorting'
: undefined
}
>
<FlexRender header={header} />
{{ asc: ' 🔼', desc: ' 🔽' }[
header.column.getIsSorted() as string
] ?? null}
</div>
<Show when={header.column.getCanFilter()}>
<ColumnFilter
column={header.column}
table={table}
/>
</Show>
</Show>
</th>
)}
</For>
</tr>
)}
</For>
</thead>
<tbody>
<For each={table.getRowModel().rows.slice(0, 15)}>
{(row) => (
<tr>
<For each={row.getAllCells()}>
{(cell) => (
<td>
<FlexRender cell={cell} />
</td>
)}
</For>
</tr>
)}
</For>
</tbody>
</table>
</div>
<div class="spacer-sm" />
<div>{table.getRowModel().rows.length.toLocaleString()} Rows</div>
</div>
)
}
export default App