diff --git a/.changeset/legacy-column-helper-fn-names.md b/.changeset/legacy-column-helper-fn-names.md new file mode 100644 index 0000000000..9363dd676e --- /dev/null +++ b/.changeset/legacy-column-helper-fn-names.md @@ -0,0 +1,5 @@ +--- +'@tanstack/react-table': patch +--- + +Column defs built with `legacyCreateColumnHelper` now accept the built-in `filterFn`, `sortFn`, and `aggregationFn` names, matching the registries `useLegacyTable` registers at runtime. diff --git a/packages/react-table/src/useLegacyTable.ts b/packages/react-table/src/useLegacyTable.ts index 8c72899c0e..ec07bf25f9 100644 --- a/packages/react-table/src/useLegacyTable.ts +++ b/packages/react-table/src/useLegacyTable.ts @@ -26,6 +26,7 @@ import type { FilterFns, Header, HeaderGroup, + Prettify, Row, RowData, RowModel, @@ -157,26 +158,39 @@ export function getCoreRowModel< // Type definitions // ============================================================================= +/** + * Feature set registered by `useLegacyTable`. + * + * Extends the stock features with the built-in filter, sort, and aggregation + * registries so column definitions accept the v8 string identifiers such as + * `'mean'` and `'includesString'`. + */ +export interface LegacyFeatures extends StockFeatures { + aggregationFns: Prettify + filterFns: Prettify + sortFns: Prettify +} + /** * Row model factory function type from v8 API */ export type RowModelFactory = ( - table: Table, -) => () => RowModel + table: Table, +) => () => RowModel /** * Faceted row model factory function type from v8 API */ export type FacetedRowModelFactory = ( - table: Table, + table: Table, columnId: string, -) => () => RowModel +) => () => RowModel /** * Faceted min/max values factory function type from v8 API */ export type FacetedMinMaxValuesFactory = ( - table: Table, + table: Table, columnId: string, ) => () => undefined | [number, number] @@ -184,7 +198,7 @@ export type FacetedMinMaxValuesFactory = ( * Faceted unique values factory function type from v8 API */ export type FacetedUniqueValuesFactory = ( - table: Table, + table: Table, columnId: string, ) => () => Map @@ -263,7 +277,7 @@ export interface LegacyRowModelOptions { * @deprecated This is a compatibility layer for migrating from v8. Use `useTable` with an explicit `features` option instead. */ export type LegacyTableOptions = Omit< - TableOptions, + TableOptions, 'features' > & LegacyRowModelOptions @@ -274,53 +288,53 @@ export type LegacyTableOptions = Omit< * @deprecated Use `useTable` with explicit state selection instead. */ export type LegacyReactTable = ReactTable< - StockFeatures, + LegacyFeatures, TData, - TableState + TableState > & { /** * Returns the current table state. * @deprecated In v9, access state directly via `table.state` or use `table.state` for the full state. */ - getState: () => TableState + getState: () => TableState /** * Sets the current table state. * @deprecated In v9, access state directly via `table.baseAtoms` */ - setState: (state: TableState) => void + setState: (state: TableState) => void } // ============================================================================= -// Legacy type aliases - StockFeatures hardcoded for simpler prop typing with useLegacyTable +// Legacy type aliases - LegacyFeatures hardcoded for simpler prop typing with useLegacyTable // ============================================================================= /** @deprecated Use Column with useTable instead. */ export type LegacyColumn = Column< - StockFeatures, + LegacyFeatures, TData, TValue > /** @deprecated Use Row with useTable instead. */ -export type LegacyRow = Row +export type LegacyRow = Row /** @deprecated Use Cell with useTable instead. */ export type LegacyCell = Cell< - StockFeatures, + LegacyFeatures, TData, TValue > /** @deprecated Use Header with useTable instead. */ export type LegacyHeader = Header< - StockFeatures, + LegacyFeatures, TData, TValue > /** @deprecated Use HeaderGroup with useTable instead. */ export type LegacyHeaderGroup = HeaderGroup< - StockFeatures, + LegacyFeatures, TData > @@ -328,26 +342,26 @@ export type LegacyHeaderGroup = HeaderGroup< export type LegacyColumnDef< TData extends RowData, TValue = unknown, -> = ColumnDef +> = ColumnDef /** @deprecated Use Table with useTable instead. */ -export type LegacyTable = Table +export type LegacyTable = Table // ============================================================================= -// Legacy column helper - StockFeatures hardcoded +// Legacy column helper - LegacyFeatures hardcoded // ============================================================================= /** * @deprecated Use `createColumnHelper()` with useTable instead. * - * A column helper with StockFeatures pre-bound for use with useLegacyTable. + * A column helper with LegacyFeatures pre-bound for use with useLegacyTable. * Only requires TData—no need to specify TFeatures. */ export function legacyCreateColumnHelper(): ColumnHelper< - StockFeatures, + LegacyFeatures, TData > { - return createColumnHelper() + return createColumnHelper() } // ============================================================================= @@ -409,11 +423,16 @@ export function useLegacyTable( const [features] = useState(() => { // Legacy row model options are setup-only. Capture the first render's // marker options to match the table instance lifecycle. - const legacyFeatures: StockFeatures & Partial = { + const legacyFeatures: LegacyFeatures & Partial = { ...stockFeatures, - filterFns: { ...filterFns, ...options.filterFns }, - sortFns: { ...sortFns, ...options.sortFns }, - aggregationFns: { ...aggregationFns, ...options.aggregationFns }, + // Declaration-merged names are required members of the registries, so the + // optional options are asserted; spreading `undefined` yields no keys. + filterFns: { ...filterFns, ...(options.filterFns as FilterFns) }, + sortFns: { ...sortFns, ...(options.sortFns as SortFns) }, + aggregationFns: { + ...aggregationFns, + ...(options.aggregationFns as AggregationFns), + }, } if (getFilteredRowModel) { @@ -452,7 +471,7 @@ export function useLegacyTable( }) // Call useTable with the v9 API, subscribing to all state changes - const table = useTable>( + const table = useTable>( { ...restOptions, features, @@ -465,7 +484,7 @@ export function useLegacyTable( }, [table]) const setState = useCallback( - (state: TableState) => { + (state: TableState) => { Object.entries(state).forEach(([key, value]) => { // @ts-expect-error - baseAtoms is indexed by dynamic string keys table.baseAtoms[key].set(value) diff --git a/packages/react-table/tests/useLegacyTable.test.tsx b/packages/react-table/tests/useLegacyTable.test.tsx new file mode 100644 index 0000000000..827ddc8416 --- /dev/null +++ b/packages/react-table/tests/useLegacyTable.test.tsx @@ -0,0 +1,167 @@ +// @vitest-environment jsdom + +import { cleanup, renderHook } from '@testing-library/react' +import { afterEach, describe, expect, it } from 'vitest' +import { + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + legacyCreateColumnHelper, + useLegacyTable, +} from '../src/useLegacyTable' +import type { + AggregationFnDef, + FilterFn, + RowData, + SortFn, + TableFeatures, +} from '@tanstack/table-core' + +type Person = { + firstName: string + age: number +} + +// v8-style declaration merging: the way custom fn names became valid string +// values before the registry slots existed. `useLegacyTable` still supports it. +declare module '@tanstack/table-core' { + interface FilterFns { + startsWithLetter: FilterFn + } + interface SortFns { + byNameLength: SortFn + } + interface AggregationFns { + range: AggregationFnDef + } +} + +const startsWithLetter: FilterFn = ( + row, + columnId, + filterValue, +) => String(row.getValue(columnId)).startsWith(String(filterValue)) + +const byNameLength: SortFn = (rowA, rowB, columnId) => + String(rowA.getValue(columnId)).length - + String(rowB.getValue(columnId)).length + +const range: AggregationFnDef = { + aggregate: (context) => { + const values = context.rows.map((row) => Number(context.getValue(row))) + return Math.max(...values) - Math.min(...values) + }, +} + +const columnHelper = legacyCreateColumnHelper() + +const columns = columnHelper.columns([ + columnHelper.accessor('firstName', { filterFn: 'includesString' }), + columnHelper.accessor('age', { aggregationFn: 'mean', sortFn: 'basic' }), +]) + +const customFnColumns = columnHelper.columns([ + columnHelper.accessor('firstName', { + filterFn: 'startsWithLetter', + sortFn: 'byNameLength', + }), + columnHelper.accessor('age', { aggregationFn: 'range' }), +]) + +const data: ReadonlyArray = [ + { firstName: 'Tanner', age: 20 }, + { firstName: 'Kevin', age: 40 }, +] + +// Name length runs opposite to alphabetical order, so a sorted result can only +// come from `byNameLength`. +const customFnData: ReadonlyArray = [ + { firstName: 'Alexander', age: 20 }, + { firstName: 'Bo', age: 50 }, +] + +afterEach(() => { + cleanup() +}) + +describe('useLegacyTable', () => { + it('accepts built-in fn names on helper-built column defs', () => { + const { result } = renderHook(() => + useLegacyTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + }), + ) + + expect(result.current.getColumn('age')!.getAggregationValue()).toBe(30) + }) + + it('applies the named filter fn to the filtered row model', () => { + const { result } = renderHook(() => + useLegacyTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + initialState: { columnFilters: [{ id: 'firstName', value: 'ann' }] }, + }), + ) + + expect( + result.current.getRowModel().rows.map((row) => row.getValue('firstName')), + ).toEqual(['Tanner']) + }) + + it('applies the named sort fn to the sorted row model', () => { + const { result } = renderHook(() => + useLegacyTable({ + columns, + data, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + initialState: { sorting: [{ id: 'age', desc: true }] }, + }), + ) + + expect( + result.current.getRowModel().rows.map((row) => row.getValue('age')), + ).toEqual([40, 20]) + }) + + it('applies declaration-merged sort and aggregation fn names', () => { + const { result } = renderHook(() => + useLegacyTable({ + columns: customFnColumns, + data: customFnData, + getCoreRowModel: getCoreRowModel(), + getSortedRowModel: getSortedRowModel(), + sortFns: { byNameLength }, + aggregationFns: { range }, + initialState: { sorting: [{ id: 'firstName', desc: false }] }, + }), + ) + + expect( + result.current.getRowModel().rows.map((row) => row.getValue('firstName')), + ).toEqual(['Bo', 'Alexander']) + expect(result.current.getColumn('age')!.getAggregationValue()).toBe(30) + }) + + it('applies the declaration-merged filter fn name', () => { + const { result } = renderHook(() => + useLegacyTable({ + columns: customFnColumns, + data: customFnData, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + filterFns: { startsWithLetter }, + initialState: { columnFilters: [{ id: 'firstName', value: 'A' }] }, + }), + ) + + expect( + result.current.getRowModel().rows.map((row) => row.getValue('firstName')), + ).toEqual(['Alexander']) + }) +})