diff --git a/examples/angular/composable-tables/src/app/components/cell-components.ts b/examples/angular/composable-tables/src/app/components/cell-components.ts index 3cca4ec1d2..f8a4622e39 100644 --- a/examples/angular/composable-tables/src/app/components/cell-components.ts +++ b/examples/angular/composable-tables/src/app/components/cell-components.ts @@ -1,4 +1,4 @@ -import { Component, computed } from '@angular/core' +import { ChangeDetectionStrategy, Component, computed } from '@angular/core' import { injectFlexRenderContext } from '@tanstack/angular-table' import { CurrencyPipe } from '@angular/common' import { injectTableCellContext } from '../table' @@ -20,6 +20,27 @@ export class TextCell { injectFlexRenderContext>() } +@Component({ + selector: 'table-select-cell', + host: { + class: 'selection-cell', + }, + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SelectCell { + readonly cell = injectTableCellContext() + readonly row = computed(() => this.cell().row) +} + @Component({ selector: 'span', host: { diff --git a/examples/angular/composable-tables/src/app/components/header-components.ts b/examples/angular/composable-tables/src/app/components/header-components.ts index 7afeaacb8e..65e195dd0b 100644 --- a/examples/angular/composable-tables/src/app/components/header-components.ts +++ b/examples/angular/composable-tables/src/app/components/header-components.ts @@ -9,7 +9,7 @@ // ) // } -import { Component, computed } from '@angular/core' +import { ChangeDetectionStrategy, Component, computed } from '@angular/core' import { flexRenderComponent } from '@tanstack/angular-table' import { FormsModule } from '@angular/forms' import { injectTableHeaderContext } from '../table' @@ -24,6 +24,26 @@ export function SortIndicator(): string | null { return `${sorted === 'asc' ? '🔼' : '🔽'}` } +@Component({ + selector: 'table-select-header', + host: { + class: 'selection-cell', + }, + template: ` + + `, + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class SelectHeader { + readonly header = injectTableHeaderContext() + readonly table = computed(() => this.header().getContext().table) +} + export function ColumnFilter(): FlexRenderComponent | null { const header = injectTableHeaderContext() if (!header().column.getCanFilter()) return null diff --git a/examples/angular/composable-tables/src/app/components/products-table/products-table.html b/examples/angular/composable-tables/src/app/components/products-table/products-table.html index ad0643dbea..f4eb639602 100644 --- a/examples/angular/composable-tables/src/app/components/products-table/products-table.html +++ b/examples/angular/composable-tables/src/app/components/products-table/products-table.html @@ -6,65 +6,67 @@ " /> - - - @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { - - @for (_header of headerGroup.headers; track _header.id) { - @let header = table.appHeader(_header); - @if (!header.isPlaceholder) { - + @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { + + @for (footer of footerGroup.headers; track footer.id) { + + } + + } + +
- - {{ header }} - +
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (_header of headerGroup.headers; track _header.id) { + @let header = table.appHeader(_header); + @if (!header.isPlaceholder) { + + } + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + } - } - - } - - - @for (row of table.getRowModel().rows; track row.id) { - - @for (cell of row.getAllCells(); track cell.id) { - - } - - } - + + } + - - @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { - - @for (footer of footerGroup.headers; track footer.id) { - - } - - } - -
+ + {{ header }} + - -
-
+ +
+
- -
+ +
+
+
+ + {{ cell }} - +
- - {{ cell }} - -
- @if (!footer.isPlaceholder) { - - {{ footer }} - - } -
+
+ @if (!footer.isPlaceholder) { + + {{ footer }} + + } +
+ diff --git a/examples/angular/composable-tables/src/app/components/products-table/products-table.ts b/examples/angular/composable-tables/src/app/components/products-table/products-table.ts index c8bad25b6d..303580cdd0 100644 --- a/examples/angular/composable-tables/src/app/components/products-table/products-table.ts +++ b/examples/angular/composable-tables/src/app/components/products-table/products-table.ts @@ -5,6 +5,7 @@ import { TanStackTable, TanStackTableCell, TanStackTableHeader, + flexRenderComponent, } from '@tanstack/angular-table' import { injectTanStackTableDevtools } from '@tanstack/angular-table-devtools' import { makeProductData } from '../../makeData' @@ -34,6 +35,11 @@ export class ProductsTable { readonly data = signal(makeProductData(1_000)) readonly columns = productColumnHelper.columns([ + productColumnHelper.display({ + id: 'select', + header: ({ header }) => flexRenderComponent(header.SelectHeader), + cell: ({ cell }) => flexRenderComponent(cell.SelectCell), + }), productColumnHelper.accessor('name', { header: 'Product Name', footer: (props) => props.column.id, @@ -66,6 +72,7 @@ export class ProductsTable { columns: this.columns, data: this.data(), getRowId: (row) => row.id, + enableRowSelection: true, // more table options })) diff --git a/examples/angular/composable-tables/src/app/components/users-table/users-table.html b/examples/angular/composable-tables/src/app/components/users-table/users-table.html index 3744b5b0f7..f08f9499c2 100644 --- a/examples/angular/composable-tables/src/app/components/users-table/users-table.html +++ b/examples/angular/composable-tables/src/app/components/users-table/users-table.html @@ -6,65 +6,67 @@ " /> - - - @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { - - @for (_header of headerGroup.headers; track _header.id) { - @let header = table.appHeader(_header); - @if (!header.isPlaceholder) { - + @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { + + @for (footer of footerGroup.headers; track footer.id) { + + } + + } + +
- - {{ header }} - +
+ + + @for (headerGroup of table.getHeaderGroups(); track headerGroup.id) { + + @for (_header of headerGroup.headers; track _header.id) { + @let header = table.appHeader(_header); + @if (!header.isPlaceholder) { + + } + } + + } + + + @for (row of table.getRowModel().rows; track row.id) { + + @for (cell of row.getAllCells(); track cell.id) { + } - } - - } - - - @for (row of table.getRowModel().rows; track row.id) { - - @for (cell of row.getAllCells(); track cell.id) { - - } - - } - + + } + - - @for (footerGroup of table.getFooterGroups(); track footerGroup.id) { - - @for (footer of footerGroup.headers; track footer.id) { - - } - - } - -
+ + {{ header }} + - -
-
+ +
+
- -
+ +
+
+
+ + {{ cell }} - +
- - {{ cell }} - -
- @if (!footer.isPlaceholder) { - - {{ footer }} - - } -
+
+ @if (!footer.isPlaceholder) { + + {{ footer }} + + } +
+ diff --git a/examples/angular/composable-tables/src/app/components/users-table/users-table.ts b/examples/angular/composable-tables/src/app/components/users-table/users-table.ts index 7441694757..ac07756c0d 100644 --- a/examples/angular/composable-tables/src/app/components/users-table/users-table.ts +++ b/examples/angular/composable-tables/src/app/components/users-table/users-table.ts @@ -35,6 +35,11 @@ export class UsersTable { readonly data = signal(makeData(1_000)) readonly columns = personColumnHelper.columns([ + personColumnHelper.display({ + id: 'select', + header: ({ header }) => flexRenderComponent(header.SelectHeader), + cell: ({ cell }) => flexRenderComponent(cell.SelectCell), + }), personColumnHelper.accessor('firstName', { header: 'First Name', footer: ({ header }) => flexRenderComponent(header.FooterColumnId), @@ -77,6 +82,7 @@ export class UsersTable { columns: this.columns, data: this.data(), debugTable: true, + enableRowSelection: true, // more table options })) diff --git a/examples/angular/composable-tables/src/app/table.ts b/examples/angular/composable-tables/src/app/table.ts index cff38db8bc..5d6a897920 100644 --- a/examples/angular/composable-tables/src/app/table.ts +++ b/examples/angular/composable-tables/src/app/table.ts @@ -13,6 +13,7 @@ import { createTableHook, filterFns, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, @@ -31,6 +32,7 @@ import { PriceCell, ProgressCell, RowActionsCell, + SelectCell, StatusCell, TextCell, } from './components/cell-components' @@ -39,6 +41,7 @@ import { ColumnFilter, FooterColumnId, FooterSum, + SelectHeader, SortIndicator, } from './components/header-components' @@ -66,6 +69,7 @@ export const { features: tableFeatures({ columnFilteringFeature, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortedRowModel: createSortedRowModel(), filteredRowModel: createFilteredRowModel(), @@ -86,6 +90,7 @@ export const { // Register cell-level components (accessible via cell.ComponentName in AppCell) cellComponents: { + SelectCell, TextCell, NumberCell, ProgressCell, @@ -97,6 +102,7 @@ export const { // Register header/footer-level components (accessible via header.ComponentName in AppHeader/AppFooter) headerComponents: { + SelectHeader, SortIndicator, ColumnFilter, FooterColumnId, diff --git a/examples/angular/composable-tables/src/styles.css b/examples/angular/composable-tables/src/styles.css index 4d8fef0142..b171dde3f0 100644 --- a/examples/angular/composable-tables/src/styles.css +++ b/examples/angular/composable-tables/src/styles.css @@ -35,6 +35,18 @@ tfoot th { font-weight: normal; } +.table-scroll { + max-height: 400px; + overflow: auto; + border: 1px solid lightgray; +} + +.table-scroll thead th { + position: sticky; + top: 0; + z-index: 1; +} + .table-container { padding: 16px; max-width: 1200px; diff --git a/examples/lit/composable-tables/src/components/products-table.ts b/examples/lit/composable-tables/src/components/products-table.ts index 96d75df3c4..1c40109b92 100644 --- a/examples/lit/composable-tables/src/components/products-table.ts +++ b/examples/lit/composable-tables/src/components/products-table.ts @@ -11,6 +11,26 @@ const productColumnHelper = createAppColumnHelper() // Define columns — different structure than Users table, same reusable components const columns = productColumnHelper.columns([ + productColumnHelper.display({ + id: 'select', + header: ({ table }) => html` + + `, + cell: ({ row }) => html` + + `, + }), productColumnHelper.accessor('name', { header: 'Product Name', footer: (props) => props.column.id, @@ -55,11 +75,13 @@ export class ProductsTable extends LitElement { get data() { return host.data }, + enableRowSelection: true, }, (state) => ({ pagination: state.pagination, sorting: state.sorting, columnFilters: state.columnFilters, + rowSelection: state.rowSelection, }), ) })() @@ -88,104 +110,109 @@ export class ProductsTable extends LitElement { > - - - ${table.getHeaderGroups().map( - (headerGroup) => html` - - ${headerGroup.headers.map((h) => - table.AppHeader( - h, - (header) => html` - - `, - ), - )} - - `, - )} - - - ${table.getRowModel().rows.map( - (row) => html` - - ${row - .getAllCells() - .map((c) => - table.AppCell( - c, - (cell) => html` `, - ), - )} - - `, - )} - - - ${table.getFooterGroups().map( - (footerGroup) => html` - - ${footerGroup.headers.map((f) => - table.AppFooter(f, (footer) => { - const columnId = footer.column.id - const hasFilter = columnFilters.some( - (cf) => cf.id === columnId, - ) - return html` - + `, + )} + + + ${table.getRowModel().rows.map( + (row) => html` + + ${row + .getAllCells() + .map((c) => + table.AppCell( + c, + (cell) => html` `, + ), + )} + + `, + )} + + + ${table.getFooterGroups().map( + (footerGroup) => html` + + ${footerGroup.headers.map((f) => + table.AppFooter(f, (footer) => { + const columnId = footer.column.id + const hasFilter = columnFilters.some( + (cf) => cf.id === columnId, + ) + return html` + + ` + }), + )} + + `, + )} + +
- ${header.isPlaceholder - ? nothing - : html` - ${header.FlexRender()} ${header.SortIndicator()} - ${header.ColumnFilter()} - ${sorting.length > 1 && - sorting.findIndex( - (s) => s.id === header.column.id, - ) > -1 - ? html`${sorting.findIndex( - (s) => s.id === header.column.id, - ) + 1}` - : nothing} - `} -
${cell.FlexRender()}
- ${footer.isPlaceholder - ? nothing - : columnId === 'price' || - columnId === 'stock' || - columnId === 'rating' - ? html` - ${footer.FooterSum()} - ${hasFilter - ? html` - (filtered)` - : nothing} - ` +
+ + + ${table.getHeaderGroups().map( + (headerGroup) => html` + + ${headerGroup.headers.map((h) => + table.AppHeader( + h, + (header) => html` + - `, - )} - -
+ ${header.isPlaceholder + ? nothing : html` - ${footer.FooterColumnId()} - ${hasFilter - ? html` - ✓ 1 && + sorting.findIndex( + (s) => s.id === header.column.id, + ) > -1 + ? html`${sorting.findIndex( + (s) => s.id === header.column.id, + ) + 1}` : nothing} `} - - ` - }), - )} -
+ + `, + ), + )} +
${cell.FlexRender()}
+ ${footer.isPlaceholder + ? nothing + : columnId === 'price' || + columnId === 'stock' || + columnId === 'rating' + ? html` + ${footer.FooterSum()} + ${hasFilter + ? html` + (filtered)` + : nothing} + ` + : columnId === 'select' + ? nothing + : html` + ${footer.FooterColumnId()} + ${hasFilter + ? html` + ✓` + : nothing} + `} +
+ diff --git a/examples/lit/composable-tables/src/components/users-table.ts b/examples/lit/composable-tables/src/components/users-table.ts index 737ba0f5a2..7c43bf5112 100644 --- a/examples/lit/composable-tables/src/components/users-table.ts +++ b/examples/lit/composable-tables/src/components/users-table.ts @@ -13,6 +13,26 @@ const personColumnHelper = createAppColumnHelper() // NOTE: Use createAppColumnHelper (not createColumnHelper) to get pre-bound // component types on cell/header objects (e.g., cell.TextCell, header.SortIndicator). const columns = personColumnHelper.columns([ + personColumnHelper.display({ + id: 'select', + header: ({ table }) => html` + + `, + cell: ({ row }) => html` + + `, + }), personColumnHelper.accessor('firstName', { header: 'First Name', footer: (props) => props.column.id, @@ -68,12 +88,14 @@ export class UsersTable extends LitElement { get data() { return host.data }, + enableRowSelection: true, debugTable: true, }, (state) => ({ pagination: state.pagination, sorting: state.sorting, columnFilters: state.columnFilters, + rowSelection: state.rowSelection, }), ) })() @@ -102,106 +124,110 @@ export class UsersTable extends LitElement { > - - - ${table.getHeaderGroups().map( - (headerGroup) => html` - - ${headerGroup.headers.map((h) => - table.AppHeader( - h, - (header) => html` - - `, - ), - )} - - `, - )} - - - ${table.getRowModel().rows.map( - (row) => html` - - ${row - .getAllCells() - .map((c) => - table.AppCell( - c, - (cell) => html` `, - ), - )} - - `, - )} - - - ${table.getFooterGroups().map( - (footerGroup) => html` - - ${footerGroup.headers.map((f) => - table.AppFooter(f, (footer) => { - const columnId = footer.column.id - const hasFilter = columnFilters.some( - (cf) => cf.id === columnId, - ) - return html` - + ` + }), + )} + + `, + )} + +
- ${header.isPlaceholder - ? nothing - : html` - ${header.FlexRender()} ${header.SortIndicator()} - ${header.ColumnFilter()} - ${sorting.length > 1 && - sorting.findIndex( - (s) => s.id === header.column.id, - ) > -1 - ? html`${sorting.findIndex( - (s) => s.id === header.column.id, - ) + 1}` - : nothing} - `} -
${cell.FlexRender()}
- ${footer.isPlaceholder - ? nothing - : columnId === 'age' || - columnId === 'visits' || - columnId === 'progress' - ? html` - ${footer.FooterSum()} - ${hasFilter - ? html` - (filtered) + + + ${table.getHeaderGroups().map( + (headerGroup) => html` + + ${headerGroup.headers.map((h) => + table.AppHeader( + h, + (header) => html` + + `, + ), + )} + + `, + )} + + + ${table.getRowModel().rows.map( + (row) => html` + + ${row + .getAllCells() + .map((c) => + table.AppCell( + c, + (cell) => html` `, + ), + )} + + `, + )} + + + ${table.getFooterGroups().map( + (footerGroup) => html` + + ${footerGroup.headers.map((f) => + table.AppFooter(f, (footer) => { + const columnId = footer.column.id + const hasFilter = columnFilters.some( + (cf) => cf.id === columnId, + ) + return html` + - ` - }), - )} - - `, - )} - -
+ ${header.isPlaceholder + ? nothing + : html` + ${header.FlexRender()} + ${header.SortIndicator()} + ${header.ColumnFilter()} + ${sorting.length > 1 && + sorting.findIndex( + (s) => s.id === header.column.id, + ) > -1 + ? html`${sorting.findIndex( + (s) => s.id === header.column.id, + ) + 1}` : nothing} - ` - : columnId === 'actions' - ? nothing - : html` - ${footer.FooterColumnId()} + `} +
${cell.FlexRender()}
+ ${footer.isPlaceholder + ? nothing + : columnId === 'age' || + columnId === 'visits' || + columnId === 'progress' + ? html` + ${footer.FooterSum()} ${hasFilter ? html` - ✓` : nothing} - `} -
+ ` + : columnId === 'actions' || + columnId === 'select' + ? nothing + : html` + ${footer.FooterColumnId()} + ${hasFilter + ? html` + ✓` + : nothing} + `} +
+ diff --git a/examples/lit/composable-tables/src/hooks/table.ts b/examples/lit/composable-tables/src/hooks/table.ts index 68327899b6..58c0e5f4f6 100644 --- a/examples/lit/composable-tables/src/hooks/table.ts +++ b/examples/lit/composable-tables/src/hooks/table.ts @@ -6,6 +6,7 @@ import { createTableHook, filterFns, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, @@ -33,6 +34,7 @@ import { export const features = tableFeatures({ columnFilteringFeature, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortedRowModel: createSortedRowModel(), filteredRowModel: createFilteredRowModel(), diff --git a/examples/lit/composable-tables/src/index.css b/examples/lit/composable-tables/src/index.css index 4d8fef0142..d03f22d5b7 100644 --- a/examples/lit/composable-tables/src/index.css +++ b/examples/lit/composable-tables/src/index.css @@ -43,6 +43,20 @@ tfoot th { border-spacing: 0; } +/* Fixed-height, scrollable region around the table. With a page size larger + than ~12 rows the body overflows and scrolls. */ +.table-scroll { + max-height: 400px; + overflow: auto; + border: 1px solid lightgray; +} + +.table-scroll thead th { + position: sticky; + top: 0; + z-index: 1; +} + .pagination { display: flex; align-items: center; diff --git a/examples/preact/composable-tables/src/components/cell-components.tsx b/examples/preact/composable-tables/src/components/cell-components.tsx index 29c0c22ee9..ebd1d03de0 100644 --- a/examples/preact/composable-tables/src/components/cell-components.tsx +++ b/examples/preact/composable-tables/src/components/cell-components.tsx @@ -5,6 +5,29 @@ * in AppCell children, e.g., */ import { useCellContext } from '../hooks/table' +import { IndeterminateCheckbox } from './indeterminate-checkbox' + +/** + * Row-selection checkbox cell - toggles selection for the current row. + * + * Unlike React (which needs a `Subscribe` boundary to work around React + * Compiler memoization), Preact re-renders this cell when selection state + * changes via the parent table's subscription, so reading `row.getIsSelected()` + * directly is enough. + */ +export function SelectCell() { + const cell = useCellContext() + const row = cell.row + + return ( + + ) +} /** * Generic text cell renderer diff --git a/examples/preact/composable-tables/src/components/indeterminate-checkbox.tsx b/examples/preact/composable-tables/src/components/indeterminate-checkbox.tsx new file mode 100644 index 0000000000..5f0fa43f57 --- /dev/null +++ b/examples/preact/composable-tables/src/components/indeterminate-checkbox.tsx @@ -0,0 +1,39 @@ +/** + * Indeterminate checkbox used by the row-selection column + * (both the select-all header and the per-row select cell). + */ +import { useEffect, useRef } from 'preact/hooks' + +export function IndeterminateCheckbox({ + indeterminate, + className = '', + checked, + onChange, + disabled, + ...rest +}: { + indeterminate?: boolean + checked?: boolean + disabled?: boolean + onChange?: (event: Event) => void +} & Record) { + const ref = useRef(null!) + + useEffect(() => { + if (typeof indeterminate === 'boolean') { + ref.current.indeterminate = !checked && indeterminate + } + }, [ref, indeterminate, checked]) + + return ( + + ) +} diff --git a/examples/preact/composable-tables/src/hooks/table.ts b/examples/preact/composable-tables/src/hooks/table.ts index 49cc16891d..7d5a6a906f 100644 --- a/examples/preact/composable-tables/src/hooks/table.ts +++ b/examples/preact/composable-tables/src/hooks/table.ts @@ -13,6 +13,7 @@ import { createTableHook, filterFns, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, @@ -32,6 +33,7 @@ import { PriceCell, ProgressCell, RowActionsCell, + SelectCell, StatusCell, TextCell, } from '../components/cell-components' @@ -64,6 +66,7 @@ export const { features: tableFeatures({ columnFilteringFeature, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortedRowModel: createSortedRowModel(), filteredRowModel: createFilteredRowModel(), @@ -84,6 +87,7 @@ export const { // Register cell-level components (accessible via cell.ComponentName in AppCell) cellComponents: { + SelectCell, TextCell, NumberCell, StatusCell, diff --git a/examples/preact/composable-tables/src/index.css b/examples/preact/composable-tables/src/index.css index 4d8fef0142..521b9c190c 100644 --- a/examples/preact/composable-tables/src/index.css +++ b/examples/preact/composable-tables/src/index.css @@ -43,6 +43,21 @@ tfoot th { border-spacing: 0; } +/* Fixed-height, scrollable region around the table. With a page size larger + than ~12 rows the body overflows and scrolls; this is what makes any + remounting of the App wrappers visible (the scroll position would reset). */ +.table-scroll { + max-height: 400px; + overflow: auto; + border: 1px solid lightgray; +} + +.table-scroll thead th { + position: sticky; + top: 0; + z-index: 1; +} + .pagination { display: flex; align-items: center; diff --git a/examples/preact/composable-tables/src/main.tsx b/examples/preact/composable-tables/src/main.tsx index 5792f91e3c..2ebac34597 100644 --- a/examples/preact/composable-tables/src/main.tsx +++ b/examples/preact/composable-tables/src/main.tsx @@ -6,6 +6,7 @@ import { useTanStackTableDevtools, } from '@tanstack/preact-table-devtools' import { createAppColumnHelper, useAppTable } from './hooks/table' +import { IndeterminateCheckbox } from './components/indeterminate-checkbox' import { makeData, makeProductData } from './makeData' import type { Person, Product } from './makeData' import './index.css' @@ -34,6 +35,18 @@ function UsersTable() { () => // NOTE: You must use `createAppColumnHelper` instead of `createColumnHelper` when using pre-bound components like personColumnHelper.columns([ + personColumnHelper.display({ + id: 'select', + header: ({ table }) => ( + + ), + // Cell uses the pre-bound SelectCell component via AppCell + cell: ({ cell }) => , + }), personColumnHelper.accessor('firstName', { header: 'First Name', footer: (props) => props.column.id, @@ -80,6 +93,7 @@ function UsersTable() { columns, data, debugTable: true, + enableRowSelection: true, // more table options }, (state) => state, // default selector @@ -106,112 +120,118 @@ function UsersTable() { onStressTest={stressTest} /> - {/* Table element */} - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((h) => ( - - {(header) => ( - - )} - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((c) => ( - - {(cell) => ( - - )} - - ))} - - ))} - - - {table.getFooterGroups().map((footerGroup) => ( - - {footerGroup.headers.map((f) => ( - - {(footer) => { - const columnId = footer.column.id - const hasFilter = columnFilters.some( - (cf) => cf.id === columnId, - ) - - return ( - + ))} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = columnFilters.some( + (cf) => cf.id === columnId, + ) + + return ( + + ) + }} + + ))} + + ))} + +
- {header.isPlaceholder ? null : ( - <> - - - - {/* Show sort order number when multiple columns sorted */} - {sorting.length > 1 && - sorting.findIndex( - (s) => s.id === header.column.id, - ) > -1 && ( - - {sorting.findIndex( - (s) => s.id === header.column.id, - ) + 1} - - )} - - )} -
- {/* Cell components are pre-bound via AppCell */} - -
- {footer.isPlaceholder ? null : ( + {/* Scroll container with a fixed max-height so larger page sizes + scroll. If the App wrappers remount on state updates, the scroll + position jumps back to the top on every keystroke/selection. */} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((h) => ( + + {(header) => ( + + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((c) => ( + + {(cell) => ( + - ) - }} - - ))} - - ))} - -
+ {header.isPlaceholder ? null : ( <> - {/* Use FooterSum for numeric columns, FooterColumnId for others */} - {columnId === 'age' || - columnId === 'visits' || - columnId === 'progress' ? ( - <> - - {hasFilter && ( - - {' '} - (filtered) - - )} - - ) : columnId === 'actions' ? null : ( - <> - - {hasFilter && ( - - {' '} - ✓ - - )} - - )} + + + + {/* Show sort order number when multiple columns sorted */} + {sorting.length > 1 && + sorting.findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {sorting.findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} )} +
+ {/* Cell components are pre-bound via AppCell */} +
+ )} + + ))} +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'age' || + columnId === 'visits' || + columnId === 'progress' ? ( + <> + + {hasFilter && ( + + {' '} + (filtered) + + )} + + ) : columnId === 'actions' ? null : ( + <> + + {hasFilter && ( + + {' '} + ✓ + + )} + + )} + + )} +
+ {/* Pagination using pre-bound component */} @@ -242,6 +262,18 @@ function ProductsTable() { const columns = useMemo( () => productColumnHelper.columns([ + productColumnHelper.display({ + id: 'select', + header: ({ table }) => ( + + ), + // Cell uses the pre-bound SelectCell component via AppCell + cell: ({ cell }) => , + }), productColumnHelper.accessor('name', { header: 'Product Name', footer: (props) => props.column.id, @@ -279,6 +311,7 @@ function ProductsTable() { columns, data, getRowId: (row) => row.id, + enableRowSelection: true, }, (state) => state, // default selector ) @@ -302,111 +335,117 @@ function ProductsTable() { onStressTest={stressTest} /> - {/* Table element */} - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((h) => ( - - {(header) => ( - - )} - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((c) => ( - - {(cell) => ( - - )} - - ))} - - ))} - - - {table.getFooterGroups().map((footerGroup) => ( - - {footerGroup.headers.map((f) => ( - - {(footer) => { - const columnId = footer.column.id - const hasFilter = columnFilters.some( - (cf) => cf.id === columnId, - ) - - return ( - + ))} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = columnFilters.some( + (cf) => cf.id === columnId, + ) + + return ( + + ) + }} + + ))} + + ))} + +
- {header.isPlaceholder ? null : ( - <> - - - - {sorting.length > 1 && - sorting.findIndex( - (s) => s.id === header.column.id, - ) > -1 && ( - - {sorting.findIndex( - (s) => s.id === header.column.id, - ) + 1} - - )} - - )} -
- {/* Cell components are pre-bound via AppCell */} - -
- {footer.isPlaceholder ? null : ( + {/* Scroll container with a fixed max-height so larger page sizes + scroll. If the App wrappers remount on state updates, the scroll + position jumps back to the top on every keystroke/selection. */} +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((h) => ( + + {(header) => ( + + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((c) => ( + + {(cell) => ( + - ) - }} - - ))} - - ))} - -
+ {header.isPlaceholder ? null : ( <> - {/* Use FooterSum for numeric columns, FooterColumnId for others */} - {columnId === 'price' || - columnId === 'stock' || - columnId === 'rating' ? ( - <> - - {hasFilter && ( - - {' '} - (filtered) - - )} - - ) : ( - <> - - {hasFilter && ( - - {' '} - ✓ - - )} - - )} + + + + {sorting.length > 1 && + sorting.findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {sorting.findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} )} +
+ {/* Cell components are pre-bound via AppCell */} +
+ )} + + ))} +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'price' || + columnId === 'stock' || + columnId === 'rating' ? ( + <> + + {hasFilter && ( + + {' '} + (filtered) + + )} + + ) : ( + <> + + {hasFilter && ( + + {' '} + ✓ + + )} + + )} + + )} +
+ {/* Pagination using the same pre-bound component */} diff --git a/examples/react/composable-tables/src/components/cell-components.tsx b/examples/react/composable-tables/src/components/cell-components.tsx index 29c0c22ee9..fb84eeb668 100644 --- a/examples/react/composable-tables/src/components/cell-components.tsx +++ b/examples/react/composable-tables/src/components/cell-components.tsx @@ -4,7 +4,36 @@ * These components can be used via the pre-bound cellComponents * in AppCell children, e.g., */ -import { useCellContext } from '../hooks/table' +import { Subscribe } from '@tanstack/react-table' +import { useCellContext, useTableContext } from '../hooks/table' +import { IndeterminateCheckbox } from './indeterminate-checkbox' + +/** + * Row-selection checkbox cell - toggles selection for the current row. + * + * The `Subscribe` boundary is required to work around React Compiler + * memoization: the checkbox reads `row.getIsSelected()` (a table API call, not + * a prop or hook the compiler can track), so without an explicit subscription + * to the row-selection state it would never re-render when selection changes. + */ +export function SelectCell() { + const cell = useCellContext() + const table = useTableContext() + const row = cell.row + + return ( + + {() => ( + + )} + + ) +} /** * Generic text cell renderer diff --git a/examples/react/composable-tables/src/components/indeterminate-checkbox.tsx b/examples/react/composable-tables/src/components/indeterminate-checkbox.tsx new file mode 100644 index 0000000000..b3cd414cc7 --- /dev/null +++ b/examples/react/composable-tables/src/components/indeterminate-checkbox.tsx @@ -0,0 +1,22 @@ +/** + * Indeterminate checkbox used by the row-selection column + * (both the select-all header and the per-row select cell). + */ +import { useEffect, useRef } from 'react' +import type { HTMLProps } from 'react' + +export function IndeterminateCheckbox({ + indeterminate, + className = '', + ...rest +}: { indeterminate?: boolean } & HTMLProps) { + const ref = useRef(null!) + + useEffect(() => { + if (typeof indeterminate === 'boolean') { + ref.current.indeterminate = !rest.checked && indeterminate + } + }, [ref, indeterminate, rest.checked]) + + return +} diff --git a/examples/react/composable-tables/src/hooks/table.ts b/examples/react/composable-tables/src/hooks/table.ts index e5c218c8f6..028ed4348a 100644 --- a/examples/react/composable-tables/src/hooks/table.ts +++ b/examples/react/composable-tables/src/hooks/table.ts @@ -13,6 +13,7 @@ import { createTableHook, filterFns, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, @@ -32,6 +33,7 @@ import { PriceCell, ProgressCell, RowActionsCell, + SelectCell, StatusCell, TextCell, } from '../components/cell-components' @@ -64,6 +66,7 @@ export const { features: tableFeatures({ columnFilteringFeature, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortedRowModel: createSortedRowModel(), filteredRowModel: createFilteredRowModel(), @@ -84,6 +87,7 @@ export const { // Register cell-level components (accessible via cell.ComponentName in AppCell) cellComponents: { + SelectCell, TextCell, NumberCell, StatusCell, diff --git a/examples/react/composable-tables/src/index.css b/examples/react/composable-tables/src/index.css index 4d8fef0142..521b9c190c 100644 --- a/examples/react/composable-tables/src/index.css +++ b/examples/react/composable-tables/src/index.css @@ -43,6 +43,21 @@ tfoot th { border-spacing: 0; } +/* Fixed-height, scrollable region around the table. With a page size larger + than ~12 rows the body overflows and scrolls; this is what makes any + remounting of the App wrappers visible (the scroll position would reset). */ +.table-scroll { + max-height: 400px; + overflow: auto; + border: 1px solid lightgray; +} + +.table-scroll thead th { + position: sticky; + top: 0; + z-index: 1; +} + .pagination { display: flex; align-items: center; diff --git a/examples/react/composable-tables/src/main.tsx b/examples/react/composable-tables/src/main.tsx index 3afef5e1a6..20c03c9342 100644 --- a/examples/react/composable-tables/src/main.tsx +++ b/examples/react/composable-tables/src/main.tsx @@ -6,9 +6,12 @@ import { tableDevtoolsPlugin, useTanStackTableDevtools, } from '@tanstack/react-table-devtools' +import { Subscribe } from '@tanstack/react-table' import { createAppColumnHelper, useAppTable } from './hooks/table' +import { IndeterminateCheckbox } from './components/indeterminate-checkbox' import { makeData, makeProductData } from './makeData' import type { Person, Product } from './makeData' +import './index.css' // Import cell components directly - they use useCellContext internally // Create column helpers with TFeatures already bound - only need TData! @@ -34,6 +37,23 @@ function UsersTable() { () => // NOTE: You must use `createAppColumnHelper` instead of `createColumnHelper` when using pre-bound components like personColumnHelper.columns([ + personColumnHelper.display({ + id: 'select', + header: ({ table }) => ( + // Subscribe works around React Compiler memoization (see SelectCell) + + {() => ( + + )} + + ), + // Cell uses the pre-bound SelectCell component via AppCell + cell: ({ cell }) => , + }), personColumnHelper.accessor('firstName', { header: 'First Name', footer: (props) => props.column.id, @@ -80,6 +100,7 @@ function UsersTable() { columns, data, debugTable: true, + enableRowSelection: true, // more table options }, (state) => state, // default selector @@ -105,113 +126,115 @@ function UsersTable() { onRefresh={refreshData} onStressTest={stressTest} /> - - {/* Table element */} - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((h) => ( - - {(header) => ( - - )} - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((c) => ( - - {(cell) => ( - - )} - - ))} - - ))} - - - {table.getFooterGroups().map((footerGroup) => ( - - {footerGroup.headers.map((f) => ( - - {(footer) => { - const columnId = footer.column.id - const hasFilter = columnFilters.some( - (cf) => cf.id === columnId, - ) - - return ( - + ))} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = columnFilters.some( + (cf) => cf.id === columnId, + ) + + return ( + + ) + }} + + ))} + + ))} + +
- {header.isPlaceholder ? null : ( - <> - - - - {/* Show sort order number when multiple columns sorted */} - {sorting.length > 1 && - sorting.findIndex( - (s) => s.id === header.column.id, - ) > -1 && ( - - {sorting.findIndex( - (s) => s.id === header.column.id, - ) + 1} - - )} - - )} -
- {/* Cell components are pre-bound via AppCell */} - -
- {footer.isPlaceholder ? null : ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((h) => ( + + {(header) => ( + + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((c) => ( + + {(cell) => ( + - ) - }} - - ))} - - ))} - -
+ {header.isPlaceholder ? null : ( <> - {/* Use FooterSum for numeric columns, FooterColumnId for others */} - {columnId === 'age' || - columnId === 'visits' || - columnId === 'progress' ? ( - <> - - {hasFilter && ( - - {' '} - (filtered) - - )} - - ) : columnId === 'actions' ? null : ( - <> - - {hasFilter && ( - - {' '} - ✓ - - )} - - )} + + + + {/* Show sort order number when multiple columns sorted */} + {sorting.length > 1 && + sorting.findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {sorting.findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} )} +
+ {/* Cell components are pre-bound via AppCell */} +
+ )} + + ))} +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'age' || + columnId === 'visits' || + columnId === 'progress' ? ( + <> + + {hasFilter && ( + + {' '} + (filtered) + + )} + + ) : columnId === 'actions' ? null : ( + <> + + {hasFilter && ( + + {' '} + ✓ + + )} + + )} + + )} +
+ {/* Pagination using pre-bound component */} @@ -242,6 +265,23 @@ function ProductsTable() { const columns = useMemo( () => productColumnHelper.columns([ + productColumnHelper.display({ + id: 'select', + header: ({ table }) => ( + // Subscribe works around React Compiler memoization (see SelectCell) + + {() => ( + + )} + + ), + // Cell uses the pre-bound SelectCell component via AppCell + cell: ({ cell }) => , + }), productColumnHelper.accessor('name', { header: 'Product Name', footer: (props) => props.column.id, @@ -279,6 +319,7 @@ function ProductsTable() { columns, data, getRowId: (row) => row.id, + enableRowSelection: true, }, (state) => state, // default selector ) @@ -301,112 +342,114 @@ function ProductsTable() { onRefresh={refreshData} onStressTest={stressTest} /> - - {/* Table element */} - - - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((h) => ( - - {(header) => ( - - )} - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - - {row.getAllCells().map((c) => ( - - {(cell) => ( - - )} - - ))} - - ))} - - - {table.getFooterGroups().map((footerGroup) => ( - - {footerGroup.headers.map((f) => ( - - {(footer) => { - const columnId = footer.column.id - const hasFilter = columnFilters.some( - (cf) => cf.id === columnId, - ) - - return ( - + ))} + + + {table.getFooterGroups().map((footerGroup) => ( + + {footerGroup.headers.map((f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = columnFilters.some( + (cf) => cf.id === columnId, + ) + + return ( + + ) + }} + + ))} + + ))} + +
- {header.isPlaceholder ? null : ( - <> - - - - {sorting.length > 1 && - sorting.findIndex( - (s) => s.id === header.column.id, - ) > -1 && ( - - {sorting.findIndex( - (s) => s.id === header.column.id, - ) + 1} - - )} - - )} -
- {/* Cell components are pre-bound via AppCell */} - -
- {footer.isPlaceholder ? null : ( +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((h) => ( + + {(header) => ( + + )} + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + + {row.getAllCells().map((c) => ( + + {(cell) => ( + - ) - }} - - ))} - - ))} - -
+ {header.isPlaceholder ? null : ( <> - {/* Use FooterSum for numeric columns, FooterColumnId for others */} - {columnId === 'price' || - columnId === 'stock' || - columnId === 'rating' ? ( - <> - - {hasFilter && ( - - {' '} - (filtered) - - )} - - ) : ( - <> - - {hasFilter && ( - - {' '} - ✓ - - )} - - )} + + + + {sorting.length > 1 && + sorting.findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {sorting.findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} )} +
+ {/* Cell components are pre-bound via AppCell */} +
+ )} + + ))} +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'price' || + columnId === 'stock' || + columnId === 'rating' ? ( + <> + + {hasFilter && ( + + {' '} + (filtered) + + )} + + ) : ( + <> + + {hasFilter && ( + + {' '} + ✓ + + )} + + )} + + )} +
+ {/* Pagination using the same pre-bound component */} diff --git a/examples/solid/composable-tables/src/App.tsx b/examples/solid/composable-tables/src/App.tsx index c7c83e0d03..3a84f00366 100644 --- a/examples/solid/composable-tables/src/App.tsx +++ b/examples/solid/composable-tables/src/App.tsx @@ -1,6 +1,7 @@ import { For, createSignal } from 'solid-js' import { useTanStackTableDevtools } from '@tanstack/solid-table-devtools' import { createAppColumnHelper, createAppTable } from './hooks/table' +import { IndeterminateCheckbox } from './components/indeterminate-checkbox' import { makeData, makeProductData } from './makeData' import type { Person, Product } from './makeData' // Import cell components directly - they use useCellContext internally @@ -26,6 +27,19 @@ function UsersTable() { const columns = // NOTE: You must use `createAppColumnHelper` instead of `createColumnHelper` when using pre-bound components like personColumnHelper.columns([ + personColumnHelper.display({ + id: 'select', + header: ({ table }) => ( + // Solid tracks these calls natively, so no Subscribe is needed. + + ), + // Cell uses the pre-bound SelectCell component via AppCell + cell: ({ cell }) => , + }), personColumnHelper.accessor('firstName', { header: 'First Name', footer: (props) => props.column.id, @@ -71,6 +85,7 @@ function UsersTable() { return data() }, debugTable: true, + enableRowSelection: true, // more table options }) @@ -99,124 +114,128 @@ function UsersTable() { /> {/* Table element */} - - - - {(headerGroup) => ( - - - {(h) => ( - - {(header) => ( - - )} - - )} - - - )} - - - - - {(row) => ( - - - {(c) => ( - - {(cell) => ( - - )} - - )} - - - )} - - - - - {(footerGroup) => ( - - - {(f) => ( - - {(footer) => { - const columnId = footer.column.id - const hasFilter = () => - columnFilters().some((cf) => cf.id === columnId) - - return ( - + )} + + + + + {(footerGroup) => ( + + + {(f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = () => + columnFilters().some( + (cf) => cf.id === columnId, + ) + + return ( + + ) + }} + + )} + + + )} + + +
- {header.isPlaceholder ? null : ( - <> - - - - {/* Show sort order number when multiple columns sorted */} - {sorting().length > 1 && - sorting().findIndex( - (s) => s.id === header.column.id, - ) > -1 && ( - - {sorting().findIndex( - (s) => s.id === header.column.id, - ) + 1} - - )} - - )} -
- {/* Cell components are pre-bound via AppCell */} - -
- {footer.isPlaceholder ? null : ( +
+ + + + {(headerGroup) => ( + + + {(h) => ( + + {(header) => ( + + )} + + )} + + + )} + + + + + {(row) => ( + + + {(c) => ( + + {(cell) => ( + - ) - }} - - )} - - - )} - - -
+ {header.isPlaceholder ? null : ( <> - {/* Use FooterSum for numeric columns, FooterColumnId for others */} - {columnId === 'age' || - columnId === 'visits' || - columnId === 'progress' ? ( - <> - - {hasFilter() && ( - - {' '} - (filtered) - - )} - - ) : columnId === 'actions' ? null : ( - <> - - {hasFilter() && ( - - {' '} - ✓ - - )} - - )} + + + + {/* Show sort order number when multiple columns sorted */} + {sorting().length > 1 && + sorting().findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {sorting().findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} )} +
+ {/* Cell components are pre-bound via AppCell */} +
+ )} + + )} + +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'age' || + columnId === 'visits' || + columnId === 'progress' ? ( + <> + + {hasFilter() && ( + + {' '} + (filtered) + + )} + + ) : columnId === 'actions' ? null : ( + <> + + {hasFilter() && ( + + {' '} + ✓ + + )} + + )} + + )} +
+ {/* Pagination using pre-bound component */} @@ -245,6 +264,19 @@ function ProductsTable() { // Define columns using the column helper - different structure than Users table const columns = productColumnHelper.columns([ + productColumnHelper.display({ + id: 'select', + header: ({ table }) => ( + // Solid tracks these calls natively, so no Subscribe is needed. + + ), + // Cell uses the pre-bound SelectCell component via AppCell + cell: ({ cell }) => , + }), productColumnHelper.accessor('name', { header: 'Product Name', footer: (props) => props.column.id, @@ -281,6 +313,7 @@ function ProductsTable() { return data() }, getRowId: (row) => row.id, + enableRowSelection: true, }) useTanStackTableDevtools(table) @@ -306,123 +339,127 @@ function ProductsTable() { /> {/* Table element */} - - - - {(headerGroup) => ( - - - {(h) => ( - - {(header) => ( - - )} - - )} - - - )} - - - - - {(row) => ( - - - {(c) => ( - - {(cell) => ( - - )} - - )} - - - )} - - - - - {(footerGroup) => ( - - - {(f) => ( - - {(footer) => { - const columnId = footer.column.id - const hasFilter = () => - columnFilters().some((cf) => cf.id === columnId) - - return ( - + )} + + + + + {(footerGroup) => ( + + + {(f) => ( + + {(footer) => { + const columnId = footer.column.id + const hasFilter = () => + columnFilters().some( + (cf) => cf.id === columnId, + ) + + return ( + + ) + }} + + )} + + + )} + + +
- {header.isPlaceholder ? null : ( - <> - - - - {sorting().length > 1 && - sorting().findIndex( - (s) => s.id === header.column.id, - ) > -1 && ( - - {sorting().findIndex( - (s) => s.id === header.column.id, - ) + 1} - - )} - - )} -
- {/* Cell components are pre-bound via AppCell */} - -
- {footer.isPlaceholder ? null : ( +
+ + + + {(headerGroup) => ( + + + {(h) => ( + + {(header) => ( + + )} + + )} + + + )} + + + + + {(row) => ( + + + {(c) => ( + + {(cell) => ( + - ) - }} - - )} - - - )} - - -
+ {header.isPlaceholder ? null : ( <> - {/* Use FooterSum for numeric columns, FooterColumnId for others */} - {columnId === 'price' || - columnId === 'stock' || - columnId === 'rating' ? ( - <> - - {hasFilter() && ( - - {' '} - (filtered) - - )} - - ) : ( - <> - - {hasFilter() && ( - - {' '} - ✓ - - )} - - )} + + + + {sorting().length > 1 && + sorting().findIndex( + (s) => s.id === header.column.id, + ) > -1 && ( + + {sorting().findIndex( + (s) => s.id === header.column.id, + ) + 1} + + )} )} +
+ {/* Cell components are pre-bound via AppCell */} +
+ )} + + )} + +
+ {footer.isPlaceholder ? null : ( + <> + {/* Use FooterSum for numeric columns, FooterColumnId for others */} + {columnId === 'price' || + columnId === 'stock' || + columnId === 'rating' ? ( + <> + + {hasFilter() && ( + + {' '} + (filtered) + + )} + + ) : ( + <> + + {hasFilter() && ( + + {' '} + ✓ + + )} + + )} + + )} +
+ {/* Pagination using the same pre-bound component */} diff --git a/examples/solid/composable-tables/src/components/cell-components.tsx b/examples/solid/composable-tables/src/components/cell-components.tsx index af78520679..1cb6b35c20 100644 --- a/examples/solid/composable-tables/src/components/cell-components.tsx +++ b/examples/solid/composable-tables/src/components/cell-components.tsx @@ -5,6 +5,27 @@ * in AppCell children, e.g., */ import { useCellContext } from '../hooks/table' +import { IndeterminateCheckbox } from './indeterminate-checkbox' + +/** + * Row-selection checkbox cell - toggles selection for the current row. + * + * Solid tracks the `row.getIsSelected()` etc. calls natively, so passing them + * as accessors keeps the checkbox reactive without any Subscribe boundary. + */ +export function SelectCell() { + const cell = useCellContext() + const row = cell.row + + return ( + + ) +} /** * Generic text cell renderer diff --git a/examples/solid/composable-tables/src/components/indeterminate-checkbox.tsx b/examples/solid/composable-tables/src/components/indeterminate-checkbox.tsx new file mode 100644 index 0000000000..ed760d1f7d --- /dev/null +++ b/examples/solid/composable-tables/src/components/indeterminate-checkbox.tsx @@ -0,0 +1,36 @@ +/** + * Indeterminate checkbox used by the row-selection column + * (both the select-all header and the per-row select cell). + * + * Solid handles reactivity natively, so `checked`/`indeterminate` are read + * from props (kept reactive by the callers) and the indeterminate DOM property + * is synced via a ref in createEffect. + */ +import { createEffect } from 'solid-js' + +export function IndeterminateCheckbox(props: { + indeterminate?: boolean + class?: string + checked?: boolean + disabled?: boolean + onChange?: (event: Event) => void +}) { + let ref: HTMLInputElement | undefined + + createEffect(() => { + if (typeof props.indeterminate === 'boolean' && ref) { + ref.indeterminate = !props.checked && props.indeterminate + } + }) + + return ( + + ) +} diff --git a/examples/solid/composable-tables/src/hooks/table.ts b/examples/solid/composable-tables/src/hooks/table.ts index d8379b071c..377aed21e8 100644 --- a/examples/solid/composable-tables/src/hooks/table.ts +++ b/examples/solid/composable-tables/src/hooks/table.ts @@ -13,6 +13,7 @@ import { createTableHook, filterFns, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, @@ -32,6 +33,7 @@ import { PriceCell, ProgressCell, RowActionsCell, + SelectCell, StatusCell, TextCell, } from '../components/cell-components' @@ -64,6 +66,7 @@ export const { features: tableFeatures({ columnFilteringFeature, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortedRowModel: createSortedRowModel(), filteredRowModel: createFilteredRowModel(), @@ -84,6 +87,7 @@ export const { // Register cell-level components (accessible via cell.ComponentName in AppCell) cellComponents: { + SelectCell, TextCell, NumberCell, StatusCell, diff --git a/examples/solid/composable-tables/src/index.css b/examples/solid/composable-tables/src/index.css index 4d8fef0142..8dd0d1ca90 100644 --- a/examples/solid/composable-tables/src/index.css +++ b/examples/solid/composable-tables/src/index.css @@ -603,3 +603,17 @@ tfoot th { font-size: 1.875rem; font-weight: 700; } + +/* Fixed-height scroll container for the table body. When the page size exceeds + ~12 rows the body overflows and scrolls, with sticky headers. */ +.table-scroll { + max-height: 400px; + overflow: auto; + border: 1px solid lightgray; +} + +.table-scroll thead th { + position: sticky; + top: 0; + z-index: 1; +} diff --git a/examples/svelte/composable-tables/src/components/ProductsTable.svelte b/examples/svelte/composable-tables/src/components/ProductsTable.svelte index 596db98a56..1aa036e8c6 100644 --- a/examples/svelte/composable-tables/src/components/ProductsTable.svelte +++ b/examples/svelte/composable-tables/src/components/ProductsTable.svelte @@ -24,6 +24,11 @@ // Define columns using the column helper - different structure than Users table const columns = productColumnHelper.columns([ + productColumnHelper.display({ + id: 'select', + header: ({ header }) => renderComponent(header.SelectHeader), + cell: ({ cell }) => renderComponent(cell.SelectCell), + }), productColumnHelper.accessor('name', { header: 'Product Name', footer: (props) => props.column.id, @@ -59,6 +64,7 @@ return data }, getRowId: (row) => row.id, + enableRowSelection: true, }) // Reactive derived values from table state @@ -84,6 +90,7 @@ /> +
{#each table.getHeaderGroups() as headerGroup (headerGroup.id) @@ -166,6 +173,7 @@ {/each}
+
diff --git a/examples/svelte/composable-tables/src/components/SelectCell.svelte b/examples/svelte/composable-tables/src/components/SelectCell.svelte new file mode 100644 index 0000000000..6b144f1a07 --- /dev/null +++ b/examples/svelte/composable-tables/src/components/SelectCell.svelte @@ -0,0 +1,31 @@ + + + + diff --git a/examples/svelte/composable-tables/src/components/SelectHeader.svelte b/examples/svelte/composable-tables/src/components/SelectHeader.svelte new file mode 100644 index 0000000000..c19be36e72 --- /dev/null +++ b/examples/svelte/composable-tables/src/components/SelectHeader.svelte @@ -0,0 +1,31 @@ + + + + + + e.stopPropagation()} +/> diff --git a/examples/svelte/composable-tables/src/components/UsersTable.svelte b/examples/svelte/composable-tables/src/components/UsersTable.svelte index a09cab1763..95fe06bc76 100644 --- a/examples/svelte/composable-tables/src/components/UsersTable.svelte +++ b/examples/svelte/composable-tables/src/components/UsersTable.svelte @@ -25,6 +25,11 @@ // NOTE: You must use `createAppColumnHelper` instead of `createColumnHelper` // when using pre-bound components like cell.TextCell const columns = personColumnHelper.columns([ + personColumnHelper.display({ + id: 'select', + header: ({ header }) => renderComponent(header.SelectHeader), + cell: ({ cell }) => renderComponent(cell.SelectCell), + }), personColumnHelper.accessor('firstName', { header: 'First Name', footer: (props) => props.column.id, @@ -68,6 +73,7 @@ get data() { return data }, + enableRowSelection: true, debugTable: true, }) @@ -96,6 +102,7 @@ /> +
{#each table.getHeaderGroups() as headerGroup (headerGroup.id) @@ -180,6 +187,7 @@ {/each}
+
diff --git a/examples/svelte/composable-tables/src/hooks/table.ts b/examples/svelte/composable-tables/src/hooks/table.ts index 9694a6b1cf..fcff4aca29 100644 --- a/examples/svelte/composable-tables/src/hooks/table.ts +++ b/examples/svelte/composable-tables/src/hooks/table.ts @@ -13,6 +13,7 @@ import { createTableHook, filterFns, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, @@ -29,6 +30,7 @@ import NumberCell from '../components/NumberCell.svelte' import PriceCell from '../components/PriceCell.svelte' import ProgressCell from '../components/ProgressCell.svelte' import RowActionsCell from '../components/RowActionsCell.svelte' +import SelectCell from '../components/SelectCell.svelte' import StatusCell from '../components/StatusCell.svelte' import TextCell from '../components/TextCell.svelte' @@ -36,6 +38,7 @@ import TextCell from '../components/TextCell.svelte' import ColumnFilter from '../components/ColumnFilter.svelte' import FooterColumnId from '../components/FooterColumnId.svelte' import FooterSum from '../components/FooterSum.svelte' +import SelectHeader from '../components/SelectHeader.svelte' import SortIndicator from '../components/SortIndicator.svelte' /** @@ -58,6 +61,7 @@ export const { features: tableFeatures({ columnFilteringFeature, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortedRowModel: createSortedRowModel(), filteredRowModel: createFilteredRowModel(), @@ -78,6 +82,7 @@ export const { // Register cell-level components (accessible via cell.ComponentName in AppCell) cellComponents: { + SelectCell, TextCell, NumberCell, StatusCell, @@ -89,6 +94,7 @@ export const { // Register header/footer-level components (accessible via header.ComponentName in AppHeader/AppFooter) headerComponents: { + SelectHeader, SortIndicator, ColumnFilter, FooterColumnId, diff --git a/examples/svelte/composable-tables/src/index.css b/examples/svelte/composable-tables/src/index.css index 4d8fef0142..09ce02888c 100644 --- a/examples/svelte/composable-tables/src/index.css +++ b/examples/svelte/composable-tables/src/index.css @@ -43,6 +43,18 @@ tfoot th { border-spacing: 0; } +.table-scroll { + max-height: 400px; + overflow: auto; + border: 1px solid lightgray; +} + +.table-scroll thead th { + position: sticky; + top: 0; + z-index: 1; +} + .pagination { display: flex; align-items: center; diff --git a/examples/vue/composable-tables/src/components/IndeterminateCheckbox.vue b/examples/vue/composable-tables/src/components/IndeterminateCheckbox.vue new file mode 100644 index 0000000000..7ea24e9f0d --- /dev/null +++ b/examples/vue/composable-tables/src/components/IndeterminateCheckbox.vue @@ -0,0 +1,28 @@ + + + diff --git a/examples/vue/composable-tables/src/components/ProductsTable.vue b/examples/vue/composable-tables/src/components/ProductsTable.vue index 35d811cbd2..58eaa61cad 100644 --- a/examples/vue/composable-tables/src/components/ProductsTable.vue +++ b/examples/vue/composable-tables/src/components/ProductsTable.vue @@ -10,6 +10,11 @@ const columnHelper = createAppColumnHelper() const data = ref(makeProductData(1_000)) const columns = columnHelper.columns([ + columnHelper.display({ + id: 'select', + header: ({ header }) => header.SelectAllHeader, + cell: ({ cell }) => cell.SelectCell, + }), columnHelper.accessor('name', { header: 'Product', footer: (props) => props.column.id, @@ -48,6 +53,7 @@ function stressTest() { const table = useAppTable({ key: 'products-table', // needed for devtools debugTable: true, + enableRowSelection: true, columns, data, initialState: { @@ -71,75 +77,79 @@ useTanStackTableDevtools(table) :onStressTest="stressTest" /> - - - - +
+ + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - -
- -
- -
+ +
- -
+ + + + + + + + + diff --git a/examples/vue/composable-tables/src/components/UsersTable.vue b/examples/vue/composable-tables/src/components/UsersTable.vue index 9295bed4dd..1ed3ec5acb 100644 --- a/examples/vue/composable-tables/src/components/UsersTable.vue +++ b/examples/vue/composable-tables/src/components/UsersTable.vue @@ -10,6 +10,11 @@ const columnHelper = createAppColumnHelper() const data = ref(makeData(1_000)) const columns = columnHelper.columns([ + columnHelper.display({ + id: 'select', + header: ({ header }) => header.SelectAllHeader, + cell: ({ cell }) => cell.SelectCell, + }), columnHelper.accessor('firstName', { header: 'First Name', footer: (props) => props.column.id, @@ -58,6 +63,7 @@ function stressTest() { const table = useAppTable({ key: 'users-table', // needed for devtools debugTable: true, + enableRowSelection: true, columns, data, initialState: { @@ -89,85 +95,90 @@ function tableSelector(state: ReturnType) { :onStressTest="stressTest" /> - - - - +
+ + - - - - - - - - - - - - - - + + + + + + + + + + + + + + - - - - -
- -
- -
+ +
- -
+ + + + + + + + + diff --git a/examples/vue/composable-tables/src/components/cell-components.ts b/examples/vue/composable-tables/src/components/cell-components.ts index bacd92252e..b39e149cbb 100644 --- a/examples/vue/composable-tables/src/components/cell-components.ts +++ b/examples/vue/composable-tables/src/components/cell-components.ts @@ -1,5 +1,27 @@ import { defineComponent, h } from 'vue' import { useCellContext } from '../hooks/table' +import IndeterminateCheckbox from './IndeterminateCheckbox.vue' + +// Row-selection checkbox cell - toggles selection for the current row. +// Vue tracks the reactive table state natively, so no Subscribe boundary is +// needed for the checkbox to update when the row-selection state changes. +export const SelectCell = defineComponent({ + name: 'SelectCell', + setup() { + const cell = useCellContext() + return () => { + const row = cell.row + return h('div', { class: 'column-toggle-row' }, [ + h(IndeterminateCheckbox, { + checked: row.getIsSelected(), + disabled: !row.getCanSelect(), + indeterminate: row.getIsSomeSelected(), + onChange: row.getToggleSelectedHandler(), + }), + ]) + } + }, +}) export const TextCell = defineComponent({ name: 'TextCell', diff --git a/examples/vue/composable-tables/src/components/header-components.ts b/examples/vue/composable-tables/src/components/header-components.ts index 5cb64d8667..af6d826783 100644 --- a/examples/vue/composable-tables/src/components/header-components.ts +++ b/examples/vue/composable-tables/src/components/header-components.ts @@ -1,5 +1,22 @@ import { computed, defineComponent, h } from 'vue' import { useHeaderContext } from '../hooks/table' +import IndeterminateCheckbox from './IndeterminateCheckbox.vue' + +// Select-all checkbox used in the header of the row-selection column. +export const SelectAllHeader = defineComponent({ + name: 'SelectAllHeader', + setup() { + const header = useHeaderContext() + return () => { + const table = header.getContext().table + return h(IndeterminateCheckbox, { + checked: table.getIsAllRowsSelected(), + indeterminate: table.getIsSomeRowsSelected(), + onChange: table.getToggleAllRowsSelectedHandler(), + }) + } + }, +}) export const SortIndicator = defineComponent({ name: 'SortIndicator', diff --git a/examples/vue/composable-tables/src/hooks/table.ts b/examples/vue/composable-tables/src/hooks/table.ts index 3fd438e13d..a94114613e 100644 --- a/examples/vue/composable-tables/src/hooks/table.ts +++ b/examples/vue/composable-tables/src/hooks/table.ts @@ -7,6 +7,7 @@ import { filterFns, globalFilteringFeature, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, sortFns, tableFeatures, @@ -17,6 +18,7 @@ import { PriceCell, ProgressCell, RowActionsCell, + SelectCell, StatusCell, TextCell, } from '../components/cell-components' @@ -24,6 +26,7 @@ import { ColumnFilter, FooterColumnId, FooterSum, + SelectAllHeader, SortIndicator, } from '../components/header-components' import { @@ -47,6 +50,7 @@ const features = tableFeatures({ columnFilteringFeature, globalFilteringFeature, rowPaginationFeature, + rowSelectionFeature, rowSortingFeature, filteredRowModel: createFilteredRowModel(), paginatedRowModel: createPaginatedRowModel(), @@ -71,12 +75,14 @@ const _hook = createTableHook({ RowActionsCell, PriceCell, CategoryCell, + SelectCell, }, headerComponents: { SortIndicator, ColumnFilter, FooterColumnId, FooterSum, + SelectAllHeader, }, }) diff --git a/examples/vue/composable-tables/src/index.css b/examples/vue/composable-tables/src/index.css index 4d8fef0142..b171dde3f0 100644 --- a/examples/vue/composable-tables/src/index.css +++ b/examples/vue/composable-tables/src/index.css @@ -35,6 +35,18 @@ tfoot th { font-weight: normal; } +.table-scroll { + max-height: 400px; + overflow: auto; + border: 1px solid lightgray; +} + +.table-scroll thead th { + position: sticky; + top: 0; + z-index: 1; +} + .table-container { padding: 16px; max-width: 1200px; diff --git a/packages/preact-table/src/createTableHook.tsx b/packages/preact-table/src/createTableHook.tsx index ee8fadbd0c..9b3e57d057 100644 --- a/packages/preact-table/src/createTableHook.tsx +++ b/packages/preact-table/src/createTableHook.tsx @@ -1,6 +1,6 @@ 'use client' import { createContext } from 'preact' -import { useContext, useMemo } from 'preact/hooks' +import { useContext, useMemo, useRef } from 'preact/hooks' import { createColumnHelper as coreCreateColumnHelper } from '@tanstack/table-core' import { useTable } from './useTable' import { FlexRender } from './FlexRender' @@ -819,6 +819,16 @@ export function createTableHook< selector, ) + // `useTable` returns a fresh `table` reference on every render (required for + // the React Compiler). The App wrapper components below must NOT depend on + // that reference, or they would be recreated each render and Preact would + // remount their entire subtree on every state update (e.g. a controlled + // input in a toolbar would lose focus on each keystroke). Instead we keep + // the components stable (created once) and read the current table from a + // ref that we refresh on every render. + const tableRef = useRef(table) + tableRef.current = table + // AppTable - Root wrapper that provides table context with optional Subscribe const AppTable = useMemo(() => { function AppTableImpl( @@ -833,17 +843,18 @@ export function createTableHook< | AppTablePropsWithSelector, ): ComponentChildren { const { children, selector: appTableSelector } = props as any + const currentTable = tableRef.current return ( - + {appTableSelector ? ( - + {(state: TAppTableSelected) => (children as (state: TAppTableSelected) => ComponentChildren)( state, ) } - + ) : ( children )} @@ -851,7 +862,7 @@ export function createTableHook< ) } return AppTableImpl as AppTableComponent - }, [table]) + }, []) // AppCell - Wraps cell with context, pre-bound cellComponents, and optional Subscribe const AppCell = useMemo(() => { @@ -895,6 +906,7 @@ export function createTableHook< >, ): ComponentChildren { const { cell, children, selector: appCellSelector } = props as any + const currentTable = tableRef.current const extendedCell = Object.assign(cell, { FlexRender: CellFlexRender, ...cellComponents, @@ -903,7 +915,7 @@ export function createTableHook< return ( {appCellSelector ? ( - + {(state: TAppCellSelected) => ( children as ( @@ -915,7 +927,7 @@ export function createTableHook< ) => ComponentChildren )(extendedCell, state) } - + ) : ( ( children as ( @@ -928,7 +940,7 @@ export function createTableHook< ) } return AppCellImpl as AppCellComponent - }, [table]) + }, []) // AppHeader - Wraps header with context, pre-bound headerComponents, and optional Subscribe const AppHeader = useMemo(() => { @@ -972,6 +984,7 @@ export function createTableHook< >, ): ComponentChildren { const { header, children, selector: appHeaderSelector } = props as any + const currentTable = tableRef.current const extendedHeader = Object.assign(header, { FlexRender: HeaderFlexRender, ...headerComponents, @@ -980,7 +993,7 @@ export function createTableHook< return ( {appHeaderSelector ? ( - + {(state: TAppHeaderSelected) => ( children as ( @@ -990,7 +1003,7 @@ export function createTableHook< ) => ComponentChildren )(extendedHeader, state) } - + ) : ( ( children as ( @@ -1007,7 +1020,7 @@ export function createTableHook< TData, THeaderComponents > - }, [table]) + }, []) // AppFooter - Same as AppHeader (footers use Header type) const AppFooter = useMemo(() => { @@ -1051,6 +1064,7 @@ export function createTableHook< >, ): ComponentChildren { const { header, children, selector: appFooterSelector } = props as any + const currentTable = tableRef.current const extendedHeader = Object.assign(header, { FlexRender: FooterFlexRender, ...headerComponents, @@ -1059,7 +1073,7 @@ export function createTableHook< return ( {appFooterSelector ? ( - + {(state: TAppFooterSelected) => ( children as ( @@ -1069,7 +1083,7 @@ export function createTableHook< ) => ComponentChildren )(extendedHeader, state) } - + ) : ( ( children as ( @@ -1086,7 +1100,7 @@ export function createTableHook< TData, THeaderComponents > - }, [table]) + }, []) // Combine everything into the extended table API const extendedTable = useMemo(() => { diff --git a/packages/react-table/src/createTableHook.tsx b/packages/react-table/src/createTableHook.tsx index 7f976a2959..5bd8a55be1 100644 --- a/packages/react-table/src/createTableHook.tsx +++ b/packages/react-table/src/createTableHook.tsx @@ -1,6 +1,6 @@ 'use client' /* eslint-disable @eslint-react/no-context-provider */ -import React, { createContext, useContext, useMemo } from 'react' +import React, { createContext, useContext, useMemo, useRef } from 'react' import { createColumnHelper as coreCreateColumnHelper } from '@tanstack/table-core' import { useTable } from './useTable' import { FlexRender } from './FlexRender' @@ -823,6 +823,16 @@ export function createTableHook< selector, ) + // `useTable` returns a fresh `table` reference on every render (required for + // the React Compiler). The App wrapper components below must NOT depend on + // that reference, or they would be recreated each render and React would + // remount their entire subtree on every state update (e.g. a controlled + // input in a toolbar would lose focus on each keystroke). Instead we keep + // the components stable (created once) and read the current table from a + // ref that we refresh on every render. + const tableRef = useRef(table) + tableRef.current = table + // AppTable - Root wrapper that provides table context with optional Subscribe const AppTable = useMemo(() => { function AppTableImpl(props: AppTablePropsWithoutSelector): ReactNode @@ -835,15 +845,16 @@ export function createTableHook< | AppTablePropsWithSelector, ): ReactNode { const { children, selector: appTableSelector } = props + const currentTable = tableRef.current return ( - + {appTableSelector ? ( - + {(state: TAppTableSelected) => (children as (state: TAppTableSelected) => ReactNode)(state) } - + ) : ( children )} @@ -851,7 +862,7 @@ export function createTableHook< ) } return AppTableImpl as AppTableComponent - }, [table]) + }, []) // AppCell - Wraps cell with context, pre-bound cellComponents, and optional Subscribe const AppCell = useMemo(() => { @@ -895,6 +906,7 @@ export function createTableHook< >, ): ReactNode { const { cell, children, selector: appCellSelector } = props as any + const currentTable = tableRef.current const extendedCell = Object.assign(cell, { FlexRender: CellFlexRender, ...cellComponents, @@ -903,7 +915,7 @@ export function createTableHook< return ( {appCellSelector ? ( - + {(state: TAppCellSelected) => ( children as ( @@ -913,7 +925,7 @@ export function createTableHook< ) => ReactNode )(extendedCell, state) } - + ) : ( ( children as ( @@ -926,7 +938,7 @@ export function createTableHook< ) } return AppCellImpl as AppCellComponent - }, [table]) + }, []) // AppHeader - Wraps header with context, pre-bound headerComponents, and optional Subscribe const AppHeader = useMemo(() => { @@ -970,6 +982,7 @@ export function createTableHook< >, ): ReactNode { const { header, children, selector: appHeaderSelector } = props as any + const currentTable = tableRef.current const extendedHeader = Object.assign(header, { FlexRender: HeaderFlexRender, ...headerComponents, @@ -978,7 +991,7 @@ export function createTableHook< return ( {appHeaderSelector ? ( - + {(state: TAppHeaderSelected) => ( children as ( @@ -988,7 +1001,7 @@ export function createTableHook< ) => ReactNode )(extendedHeader, state) } - + ) : ( ( children as ( @@ -1005,7 +1018,7 @@ export function createTableHook< TData, THeaderComponents > - }, [table]) + }, []) // AppFooter - Same as AppHeader (footers use Header type) const AppFooter = useMemo(() => { @@ -1049,6 +1062,7 @@ export function createTableHook< >, ): ReactNode { const { header, children, selector: appFooterSelector } = props as any + const currentTable = tableRef.current const extendedHeader = Object.assign(header, { FlexRender: FooterFlexRender, ...headerComponents, @@ -1057,7 +1071,7 @@ export function createTableHook< return ( {appFooterSelector ? ( - + {(state: TAppFooterSelected) => ( children as ( @@ -1067,7 +1081,7 @@ export function createTableHook< ) => ReactNode )(extendedHeader, state) } - + ) : ( ( children as ( @@ -1084,7 +1098,7 @@ export function createTableHook< TData, THeaderComponents > - }, [table]) + }, []) // Combine everything into the extended table API const extendedTable = useMemo(() => {