import { For, Show, createSignal } from 'solid-js'
import {
FlexRender,
cellSelectionFeature,
cellSpanningFeature,
columnFilteringFeature,
columnVisibilityFeature,
createColumnHelper,
createFilteredRowModel,
createPaginatedRowModel,
createSortedRowModel,
createTable,
filterFn_includesString,
rowPaginationFeature,
rowSortingFeature,
sortFn_alphanumeric,
sortFn_basic,
tableFeatures,
} from '@tanstack/solid-table'
import { makeData, makeSummaryData } from './makeData'
import type { Shift, SummaryRow } from './makeData'
const features = tableFeatures({
cellSelectionFeature,
cellSpanningFeature,
columnFilteringFeature,
columnVisibilityFeature,
rowPaginationFeature,
rowSortingFeature,
filteredRowModel: createFilteredRowModel(),
paginatedRowModel: createPaginatedRowModel(),
sortedRowModel: createSortedRowModel(),
filterFns: { includesString: filterFn_includesString },
sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic },
})
const columnHelper = createColumnHelper<typeof features, Shift>()
const columns = columnHelper.columns([
columnHelper.accessor('region', {
header: 'Region',
sortFn: 'alphanumeric',
spanRows: true,
}),
columnHelper.accessor('team', {
header: 'Team',
sortFn: 'alphanumeric',
spanRows: true,
}),
columnHelper.accessor('shift', {
header: 'Shift',
sortFn: 'alphanumeric',
spanRows: ({ column, value, anchorValue }) =>
column.getIsSorted() !== false && value === anchorValue,
}),
columnHelper.accessor('employee', {
header: 'Employee',
sortFn: 'alphanumeric',
filterFn: 'includesString',
}),
columnHelper.accessor('hours', {
header: 'Hours',
sortFn: 'basic',
}),
columnHelper.accessor('status', {
header: 'Status',
sortFn: 'alphanumeric',
filterFn: 'includesString',
}),
])
const summaryFeatures = tableFeatures({
cellSpanningFeature,
columnVisibilityFeature,
})
const summaryColumnHelper = createColumnHelper<
typeof summaryFeatures,
SummaryRow
>()
const summaryColumns = summaryColumnHelper.columns([
summaryColumnHelper.accessor('label', {
header: 'Shift',
spanColumns: ({ row }) => (row.original.kind === 'subtotal' ? Infinity : 1),
}),
summaryColumnHelper.accessor('region', {
header: 'Region',
}),
summaryColumnHelper.accessor('hours', {
header: 'Hours',
}),
])
function getCellClassName(cell: {
getRowSpan: () => number
getIsSelected: () => boolean
getIsFocused: () => boolean
getSelectionEdges: () => {
top: boolean
right: boolean
bottom: boolean
left: boolean
}
}): string {
const base =
cell.getRowSpan() > 1 ? 'cell-selectable span-cell' : 'cell-selectable'
if (!cell.getIsSelected()) {
return cell.getIsFocused() ? `${base} cell-focused` : base
}
const edges = cell.getSelectionEdges()
return [
base,
'cell-selected',
cell.getIsFocused() && 'cell-focused',
edges.top && 'cell-edge-top',
edges.right && 'cell-edge-right',
edges.bottom && 'cell-edge-bottom',
edges.left && 'cell-edge-left',
]
.filter(Boolean)
.join(' ')
}
function App() {
const [data, setData] = createSignal(makeData())
const [summaryData] = createSignal(makeSummaryData())
const [spanningEnabled, setSpanningEnabled] = createSignal(true)
const refreshData = () => setData(makeData())
const table = createTable({
debugTable: true,
features,
columns,
get data() {
return data()
},
get enableCellSpanning() {
return spanningEnabled()
},
initialState: {
pagination: { pageIndex: 0, pageSize: 12 },
},
})
const summaryTable = createTable({
debugTable: true,
features: summaryFeatures,
columns: summaryColumns,
get data() {
return summaryData()
},
})
const renderHead = () => (
<thead>
<For each={table.getHeaderGroups()}>
{(headerGroup) => (
<tr>
<For each={headerGroup.headers}>
{(header) => (
<th colSpan={header.colSpan}>
<button
type="button"
class="sortable-header header-sort-button"
onClick={header.column.getToggleSortingHandler()}
>
<FlexRender header={header} />
{{ asc: ' 🔼', desc: ' 🔽' }[
header.column.getIsSorted() as string
] ?? null}
</button>
</th>
)}
</For>
</tr>
)}
</For>
</thead>
)
return (
<div class="demo-root">
<div class="controls">
<button onClick={() => refreshData()} class="demo-button">
Regenerate Data
</button>
<label>
<input
type="checkbox"
checked={spanningEnabled()}
onChange={(event) =>
setSpanningEnabled(event.currentTarget.checked)
}
/>{' '}
Row spanning
</label>
<For each={['team', 'shift']}>
{(columnId) => (
<label>
<input
type="checkbox"
checked={table.getColumn(columnId)!.getIsVisible()}
onChange={table
.getColumn(columnId)!
.getToggleVisibilityHandler()}
/>{' '}
{String(table.getColumn(columnId)!.columnDef.header)}
</label>
)}
</For>
<select
data-testid="status-filter"
value={
(table.getColumn('status')!.getFilterValue() as
string | undefined) ?? ''
}
onChange={(event) =>
table
.getColumn('status')!
.setFilterValue(event.currentTarget.value || undefined)
}
>
<option value="">All statuses</option>
<option value="Approved">Approved</option>
<option value="Pending">Pending</option>
<option value="Rejected">Rejected</option>
</select>
<input
data-testid="employee-filter"
class="filter-input"
placeholder="Filter employees..."
value={
(table.getColumn('employee')!.getFilterValue() as
string | undefined) ?? ''
}
onInput={(event) =>
table
.getColumn('employee')!
.setFilterValue(event.currentTarget.value || undefined)
}
/>
</div>
<div class="spacer-sm" />
<div class="controls">
<button
class="demo-button-sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
{'<'}
</button>
<button
class="demo-button-sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
{'>'}
</button>
<span>
Page {table.store.get().pagination.pageIndex + 1} of{' '}
{table.getPageCount()}
</span>
<select
data-testid="page-size"
value={table.store.get().pagination.pageSize}
onChange={(event) =>
table.setPageSize(Number(event.currentTarget.value))
}
>
<For each={[10, 12, 36]}>
{(pageSize) => <option value={pageSize}>Show {pageSize}</option>}
</For>
</select>
<span>
Visible columns:{' '}
<span data-testid="visible-leaf-count">
{table.getVisibleLeafColumns().length}
</span>
</span>
<span>
Selected cells:{' '}
<span data-testid="selected-count">
{table.getSelectedCellCount()}
</span>
</span>
</div>
<div class="spacer-md" />
{}
<div class="example-grid">
<section class="example-panel">
<h2 class="section-title">Row Spanning</h2>
<table data-testid="span-table">
{renderHead()}
<tbody>
<For each={table.getRowModel().rows}>
{(row) => (
<tr>
<For each={row.getVisibleCells()}>
{(cell) => (
<Show
when={
cell.getRowSpan() !== 0 && cell.getColSpan() !== 0
}
>
<td
rowSpan={cell.getRowSpan()}
colSpan={cell.getColSpan()}
class={getCellClassName(cell)}
onMouseDown={cell.getSelectionStartHandler()}
onMouseEnter={cell.getSelectionExtendHandler()}
>
<FlexRender cell={cell} />
</td>
</Show>
)}
</For>
</tr>
)}
</For>
</tbody>
</table>
</section>
<section class="example-panel">
<h2 class="section-title">Reference (no spanning)</h2>
{}
<table data-testid="reference-table">
{renderHead()}
<tbody>
<For each={table.getRowModel().rows}>
{(row) => (
<tr>
<For each={row.getVisibleCells()}>
{(cell) => (
<td>
<FlexRender cell={cell} />
</td>
)}
</For>
</tr>
)}
</For>
</tbody>
</table>
</section>
<section class="example-panel">
<h2 class="section-title">Summary Rows (colSpan)</h2>
<table data-testid="summary-table">
<thead>
<For each={summaryTable.getHeaderGroups()}>
{(headerGroup) => (
<tr>
<For each={headerGroup.headers}>
{(header) => (
<th colSpan={header.colSpan}>
<FlexRender header={header} />
</th>
)}
</For>
</tr>
)}
</For>
</thead>
<tbody>
<For each={summaryTable.getRowModel().rows}>
{(row) => (
<tr
class={
row.original.kind === 'subtotal'
? 'subtotal-row'
: undefined
}
>
<For each={row.getVisibleCells()}>
{(cell) => (
<Show
when={
cell.getRowSpan() !== 0 && cell.getColSpan() !== 0
}
>
<td
rowSpan={cell.getRowSpan()}
colSpan={cell.getColSpan()}
>
<FlexRender cell={cell} />
</td>
</Show>
)}
</For>
</tr>
)}
</For>
</tbody>
</table>
</section>
</div>
<div class="spacer-md" />
<pre data-testid="table-state">
{JSON.stringify(table.store.get(), null, 2)}
</pre>
</div>
)
}
export default App