import {
createRoot,
useEffect,
useMemo,
useRef,
useState,
} from 'octane'
import { useCreateAtom } from '@tanstack/octane-store'
import {
Subscribe,
columnFilteringFeature,
createColumnHelper,
createFilteredRowModel,
createPaginatedRowModel,
filterFn_inNumberRange,
filterFn_includesString,
globalFilteringFeature,
rowPaginationFeature,
rowSelectionFeature,
tableFeatures,
useTable,
} from '@tanstack/octane-table'
import { makeData } from './makeData'
import type {
Column,
OctaneTable,
PaginationState,
Row,
RowSelectionState,
TableState,
} from '@tanstack/octane-table'
import type { Person } from './makeData'
import './index.css'
const features = tableFeatures({
rowPaginationFeature,
rowSelectionFeature,
columnFilteringFeature,
globalFilteringFeature,
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
filterFns: {
includesString: filterFn_includesString,
inNumberRange: filterFn_inNumberRange,
},
})
type TableInstance = OctaneTable<typeof features, Person, null>
const columnHelper = createColumnHelper<typeof features, Person>()
const pageSizes = [10, 20, 30, 40, 50]
function App() @{
const columns = useMemo(
() =>
columnHelper.columns([
columnHelper.display({
id: 'select',
header: ({ table }) => <Subscribe
source={table.store}
selector={(state) => state.rowSelection}
>
{() => <IndeterminateCheckbox
ariaLabel="Select all rows"
checked={table.getIsAllRowsSelected()}
indeterminate={table.getIsSomeRowsSelected()}
onChange={table.getToggleAllRowsSelectedHandler()}
/>}
</Subscribe>,
cell: ({ row, table }) => <RowCheckbox row={row} table={table as TableInstance} />,
}),
columnHelper.accessor('firstName', {
header: 'First Name',
cell: (info) => String(info.getValue()),
}),
columnHelper.accessor('lastName', {
header: 'Last Name',
cell: (info) => String(info.getValue()),
}),
columnHelper.accessor('age', { header: 'Age' }),
columnHelper.accessor('visits', { header: 'Visits' }),
columnHelper.accessor('status', { header: 'Status' }),
columnHelper.accessor('progress', { header: 'Profile Progress' }),
]),
[],
)
const [data, setData] = useState(() => makeData(1_000))
const refreshData = () => setData(makeData(1_000))
const stressTest = () => setData(makeData(1_000_000))
const rowSelectionAtom = useCreateAtom<RowSelectionState>({})
const table = useTable(
{
debugTable: true,
features,
atoms: { rowSelection: rowSelectionAtom },
columns,
data,
getRowId: (row) => row.id,
enableRowSelection: true,
},
() => null,
)
<div className="demo-root">
<div>
<button className="demo-button demo-button-spaced" onClick={refreshData}>
Regenerate Data
</button>
<button className="demo-button demo-button-spaced" onClick={stressTest}>
Stress Test (1M rows)
</button>
</div>
{}
<table.Subscribe source={table.atoms.globalFilter}>
{(globalFilter: string | undefined) => <DebouncedInput
ariaLabel="Global filter"
value={globalFilter ?? ''}
onChange={(value) => table.setGlobalFilter(value)}
className="summary-panel"
placeholder="Search all columns..."
/>}
</table.Subscribe>
<div className="spacer-sm" />
<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 : <>
<table.FlexRender header={header} />
{header.column.getCanFilter()
? <div><Filter column={header.column} table={table} /></div>
: null}
</>}
</th>
}
</tr>
}
</thead>
<tbody data-testid="row-model-subscription">
{}
<table.Subscribe
selector={(state) => ({
columnFilters: state.columnFilters,
globalFilter: state.globalFilter,
pagination: state.pagination,
})}
>
{() => <SubscribedRows table={table} />}
</table.Subscribe>
</tbody>
<tfoot>
<tr>
<td className="cell-padding">
<table.Subscribe source={table.atoms.rowSelection}>
{() => <IndeterminateCheckbox
ariaLabel="Select all page rows"
checked={table.getIsAllPageRowsSelected()}
indeterminate={table.getIsSomePageRowsSelected()}
onChange={table.getToggleAllPageRowsSelectedHandler()}
/>}
</table.Subscribe>
</td>
<td colSpan={20}>
Page Rows ({String(table.getRowModel().rows.length)})
</td>
</tr>
</tfoot>
</table>
<div className="spacer-sm" />
<table.Subscribe selector={(state) => state.pagination}>
{(pagination: PaginationState) => <div className="controls" data-testid="pagination-subscription">
<button className="demo-button demo-button-sm" onClick={() => table.firstPage()} disabled={!table.getCanPreviousPage()}>{'<<'}</button>
<button className="demo-button demo-button-sm" onClick={() => table.previousPage()} disabled={!table.getCanPreviousPage()}>{'<'}</button>
<button className="demo-button demo-button-sm" onClick={() => table.nextPage()} disabled={!table.getCanNextPage()}>{'>'}</button>
<button className="demo-button demo-button-sm" onClick={() => table.lastPage()} disabled={!table.getCanNextPage()}>{'>>'}</button>
<span className="inline-controls">
<div>Page</div>
<strong>{String(pagination.pageIndex + 1)} of {String(table.getPageCount())}</strong>
</span>
<span className="inline-controls">
| Go to page:
<input
type="number"
min="1"
max={table.getPageCount()}
defaultValue={pagination.pageIndex + 1}
onInput={(event) => {
const value = event.currentTarget.value
table.setPageIndex(value ? Number(value) - 1 : 0)
}}
className="page-size-input"
/>
</span>
<select
value={pagination.pageSize}
onChange={(event) => table.setPageSize(Number(event.currentTarget.value))}
>
@for (const pageSize of pageSizes; key pageSize) {
<option value={pageSize}>Show {String(pageSize)}</option>
}
</select>
</div>}
</table.Subscribe>
{}
<table.Subscribe source={table.atoms.rowSelection}>
{(rowSelection: RowSelectionState) => <div data-testid="identity-source-subscription">
{String(Object.keys(rowSelection).length)} of {String(table.getPreFilteredRowModel().rows.length)} Total Rows Selected
</div>}
</table.Subscribe>
<hr />
<br />
<div>
<button
className="demo-button demo-button-spaced"
onClick={() => console.info(
'table.getSelectedRowModel().flatRows',
table.getSelectedRowModel().flatRows,
)}
>
Log table.getSelectedRowModel().flatRows
</button>
</div>
<div>
<label>Table State:</label>
{}
<Subscribe source={table.store} selector={(state) => state}>
{(state: TableState<typeof features>) => <pre data-testid="whole-store-subscription">{JSON.stringify(state, null, 2)}</pre>}
</Subscribe>
</div>
</div>
}
function SubscribedRows({ table }: { table: TableInstance }) @{
<>
@for (const row of table.getRowModel().rows; key row.id) {
<tr>
@for (const cell of row.getAllCells(); key cell.id) {
<td><table.FlexRender cell={cell} /></td>
}
</tr>
}
</>
}
function Filter({
column,
table,
}: {
column: Column<typeof features, Person>
table: TableInstance
}) @{
const firstValue = table
.getPreFilteredRowModel()
.flatRows[0]?.getValue(column.id)
<table.Subscribe source={table.atoms.columnFilters}>
{() => typeof firstValue === 'number'
? <div className="filter-row">
<DebouncedInput
ariaLabel={`Minimum ${column.id}`}
type="number"
value={(column.getFilterValue() as [number, number] | undefined)?.[0] ?? ''}
onChange={(value) => column.setFilterValue(
(old: [number, number] | undefined) => [value, old?.[1]],
)}
placeholder="Min"
className="filter-input"
/>
<DebouncedInput
ariaLabel={`Maximum ${column.id}`}
type="number"
value={(column.getFilterValue() as [number, number] | undefined)?.[1] ?? ''}
onChange={(value) => column.setFilterValue(
(old: [number, number] | undefined) => [old?.[0], value],
)}
placeholder="Max"
className="filter-input"
/>
</div>
: <DebouncedInput
ariaLabel={`Filter ${column.id}`}
type="text"
value={String(column.getFilterValue() ?? '')}
onChange={(value) => column.setFilterValue(value)}
placeholder="Search..."
className="filter-select"
/>}
</table.Subscribe>
}
function RowCheckbox({
row,
table,
}: {
row: Row<typeof features, Person>
table: TableInstance
}) @{
<table.Subscribe
source={table.atoms.rowSelection}
selector={(rowSelection) => Boolean(rowSelection[row.id])}
>
{(selected: boolean) => <IndeterminateCheckbox
ariaLabel={`Select row ${row.id}`}
checked={selected}
disabled={!row.getCanSelect()}
indeterminate={row.getIsSomeSelected()}
onChange={row.getToggleSelectedHandler()}
/>}
</table.Subscribe>
}
function DebouncedInput({
value: initialValue,
onChange,
debounce = 500,
placeholder,
className,
ariaLabel,
type = 'text',
}: {
value: string | number
onChange: (value: string) => void
debounce?: number
placeholder?: string
className?: string
ariaLabel: string
type?: 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}
aria-label={ariaLabel}
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,
)
}}
/>
}
function IndeterminateCheckbox({
indeterminate,
checked,
disabled,
onChange,
ariaLabel,
}: {
indeterminate?: boolean
checked?: boolean
disabled?: boolean
onChange?: (event: Event) => void
ariaLabel: string
}) @{
const ref = useRef<HTMLInputElement | null | null>(null)
useEffect(() => {
if (ref.current) ref.current.indeterminate = !checked && Boolean(indeterminate)
}, [indeterminate, checked])
<input
ref={ref}
type="checkbox"
aria-label={ariaLabel}
className="sortable-header"
checked={checked}
disabled={disabled}
onChange={onChange}
/>
}
const rootElement = document.getElementById('root')
if (!rootElement) throw new Error('Failed to find the root element')
createRoot(rootElement).render(App)