diff --git a/packages/angular-table/tsconfig.build.json b/packages/angular-table/tsconfig.build.json index b44d5d729c..b0492fecaa 100644 --- a/packages/angular-table/tsconfig.build.json +++ b/packages/angular-table/tsconfig.build.json @@ -4,7 +4,9 @@ "allowJs": true, "module": "ESNext", "moduleDetection": "force", - "moduleResolution": "Bundler" + "moduleResolution": "Bundler", + // Use a more recent lib to support ES2022 features, because we now use the index.ts as entrypoint for table-core + "lib": ["dom", "es2022"] }, "angularCompilerOptions": { "enableI18nLegacyMessageIdFormat": false, diff --git a/packages/table-core/package.json b/packages/table-core/package.json index 4d6e45c3b6..afa969b953 100644 --- a/packages/table-core/package.json +++ b/packages/table-core/package.json @@ -23,14 +23,14 @@ "datagrid" ], "type": "commonjs", - "module": "build/lib/index.esm.js", - "main": "build/lib/index.js", - "types": "build/lib/index.d.ts", + "module": "src/index.ts", + "main": "src/index.ts", + "types": "src/index.ts", "exports": { ".": { - "types": "./build/lib/index.d.ts", - "import": "./build/lib/index.mjs", - "default": "./build/lib/index.js" + "types": "./src/index.ts", + "import": "./src/index.ts", + "default": "./src/index.ts" }, "./package.json": "./package.json" }, diff --git a/packages/table-core/src/core/row.ts b/packages/table-core/src/core/row.ts index 0af28aa94c..40a17a29ec 100644 --- a/packages/table-core/src/core/row.ts +++ b/packages/table-core/src/core/row.ts @@ -6,6 +6,7 @@ export interface CoreRow { _getAllCellsByColumnId: () => Record> _uniqueValuesCache: Record _valuesCache: Record + clone: () => Row /** * The depth of the row (if nested or grouped) relative to the root row array. * @link [API Docs](https://tanstack.com/table/v8/docs/api/core/row#depth) @@ -92,27 +93,32 @@ export interface CoreRow { subRows: Row[] } -export const createRow = ( - table: Table, - id: string, - original: TData, - rowIndex: number, - depth: number, - subRows?: Row[], - parentId?: string -): Row => { - let row: CoreRow = { - id, - index: rowIndex, - original, - depth, - parentId, - _valuesCache: {}, - _uniqueValuesCache: {}, - getValue: columnId => { - if (row._valuesCache.hasOwnProperty(columnId)) { - return row._valuesCache[columnId] +const rowProtosByTable = new WeakMap, any>() + +/** + * Creates a table-specific row prototype object to hold shared row methods, including from all the + * features that have been registered on the table. + */ +export function getRowProto(table: Table) { + let rowProto = rowProtosByTable.get(table) + + if (!rowProto) { + const proto = {} as CoreRow + + proto.clone = function () { + return Object.assign(Object.create(Object.getPrototypeOf(this)), this) + } + + // Make the default fallback value available on the proto itself to avoid duplicating it on every row instance + // even if it's not used. This is safe as long as we don't mutate the value directly. + proto.subRows = [] as const + + proto.getValue = function (columnId: string) { + /* + if (this._valuesCache.hasOwnProperty(columnId)) { + return this._valuesCache[columnId] } + */ const column = table.getColumn(columnId) @@ -120,16 +126,28 @@ export const createRow = ( return undefined } - row._valuesCache[columnId] = column.accessorFn( - row.original as TData, - rowIndex + return column.accessorFn( + this.original as TData, + this.index + ) as any + /* + this._valuesCache[columnId] = column.accessorFn( + this.original as TData, + this.index ) - return row._valuesCache[columnId] as any - }, - getUniqueValues: columnId => { - if (row._uniqueValuesCache.hasOwnProperty(columnId)) { - return row._uniqueValuesCache[columnId] + return this._valuesCache[columnId] as any + */ + } + + proto.getUniqueValues = function (columnId: string) { + if (!this.hasOwnProperty('_uniqueValuesCache')) { + // lazy-init cache on the instance + this._uniqueValuesCache = {} + } + + if (this._uniqueValuesCache.hasOwnProperty(columnId)) { + return this._uniqueValuesCache[columnId] } const column = table.getColumn(columnId) @@ -139,26 +157,38 @@ export const createRow = ( } if (!column.columnDef.getUniqueValues) { - row._uniqueValuesCache[columnId] = [row.getValue(columnId)] - return row._uniqueValuesCache[columnId] + // Avoid unnecessary caching for unique values + // @TODO @Michael @Moritz Is this the correct place to fix this? + // the .flat() is required because this.getValue() cann return an array, resulting in a nested array. But getUniqueValues is expected + // to always return a flat array of unique values. + return [this.getValue(columnId)].flat(); + // this._uniqueValuesCache[columnId] = [this.getValue(columnId)] + // return this._uniqueValuesCache[columnId] } - row._uniqueValuesCache[columnId] = column.columnDef.getUniqueValues( - row.original as TData, - rowIndex + this._uniqueValuesCache[columnId] = column.columnDef.getUniqueValues( + this.original as TData, + this.index ) - return row._uniqueValuesCache[columnId] as any - }, - renderValue: columnId => - row.getValue(columnId) ?? table.options.renderFallbackValue, - subRows: subRows ?? [], - getLeafRows: () => flattenBy(row.subRows, d => d.subRows), - getParentRow: () => - row.parentId ? table.getRow(row.parentId, true) : undefined, - getParentRows: () => { + return this._uniqueValuesCache[columnId] as any + } + + proto.renderValue = function (columnId: string) { + return this.getValue(columnId) ?? table.options.renderFallbackValue + } + + proto.getLeafRows = function () { + return flattenBy(this.subRows, d => d.subRows) + } + + proto.getParentRow = function () { + return this.parentId ? table.getRow(this.parentId, true) : undefined + } + + proto.getParentRows = function () { let parentRows: Row[] = [] - let currentRow = row + let currentRow = this while (true) { const parentRow = currentRow.getParentRow() if (!parentRow) break @@ -166,20 +196,28 @@ export const createRow = ( currentRow = parentRow } return parentRows.reverse() - }, - getAllCells: memo( - () => [table.getAllLeafColumns()], - leafColumns => { + } + + proto.getAllCells = memo( + function (this: Row) { + return [this, table.getAllLeafColumns()] + }, + (row, leafColumns) => { return leafColumns.map(column => { - return createCell(table, row as Row, column, column.id) + return createCell(table, row, column, column.id) }) }, getMemoOptions(table.options, 'debugRows', 'getAllCells') - ), + ) - _getAllCellsByColumnId: memo( - () => [row.getAllCells()], - allCells => { + proto._getAllCellsByColumnId = memo( + function (this: Row) { + // return [this.getAllCells()] + return [] + }, + () => { + throw new Error('Row._getAllCellsByColumnId not implemented because it is a memory leak') + /* return allCells.reduce( (acc, cell) => { acc[cell.column.id] = cell @@ -187,9 +225,39 @@ export const createRow = ( }, {} as Record> ) + */ }, getMemoOptions(table.options, 'debugRows', 'getAllCellsByColumnId') - ), + ) + + rowProtosByTable.set(table, proto) + rowProto = proto + } + + return rowProto as CoreRow +} + +export const createRow = ( + table: Table, + id: string, + original: TData, + rowIndex: number, + depth: number, + subRows?: Row[], + parentId?: string +): Row => { + const row: CoreRow = Object.create(getRowProto(table)) + Object.assign(row, { + id, + index: rowIndex, + original, + depth, + parentId, + // _valuesCache: {}, + }) + + if (subRows) { + row.subRows = subRows } for (let i = 0; i < table._features.length; i++) { diff --git a/packages/table-core/src/core/table.ts b/packages/table-core/src/core/table.ts index 00d62da98f..0825922643 100644 --- a/packages/table-core/src/core/table.ts +++ b/packages/table-core/src/core/table.ts @@ -323,6 +323,22 @@ export function createTable( const queued: (() => void)[] = [] let queuedTimeout = false + let getColumnCache = new WeakMap< + ColumnDef[], + Record> + >() + + const getColumnFromCache = (columnId: string) => { + const columnDefs = table.options.columns + let columnsById = getColumnCache.get(columnDefs) + + if (!columnsById) { + columnsById = table._getAllFlatColumnsById() + getColumnCache.set(columnDefs, columnsById) + } + + return columnsById[columnId] + } const coreInstance: CoreInstance = { _features, @@ -506,7 +522,7 @@ export function createTable( ), getColumn: columnId => { - const column = table._getAllFlatColumnsById()[columnId] + const column = getColumnFromCache(columnId) if (process.env.NODE_ENV !== 'production' && !column) { console.error(`[Table] Column with id '${columnId}' does not exist.`) diff --git a/packages/table-core/src/features/ColumnFiltering.ts b/packages/table-core/src/features/ColumnFiltering.ts index 24ebaedaf6..18e558f004 100644 --- a/packages/table-core/src/features/ColumnFiltering.ts +++ b/packages/table-core/src/features/ColumnFiltering.ts @@ -1,4 +1,4 @@ -import { RowModel } from '..' +import { getRowProto, RowModel } from '..' import { BuiltInFilterFn, filterFns } from '../filterFns' import { Column, @@ -362,14 +362,6 @@ export const ColumnFiltering: TableFeature = { } }, - createRow: ( - row: Row, - _table: Table - ): void => { - row.columnFilters = {} - row.columnFiltersMeta = {} - }, - createTable: (table: Table): void => { table.setColumnFilters = (updater: Updater) => { const leafColumns = table.getAllLeafColumns() @@ -411,6 +403,28 @@ export const ColumnFiltering: TableFeature = { return table._getFilteredRowModel() } + + // Lazy-init the backing caches on the instance so we don't take up memory for rows that don't need it + Object.defineProperties(getRowProto(table), { + columnFilters: { + get() { + return (this._columnFilters ??= {}) + }, + set(value) { + this._columnFilters = value + }, + enumerable: true, + }, + columnFiltersMeta: { + get() { + return (this._columnFiltersMeta ??= {}) + }, + set(value) { + this._columnFiltersMeta = value + }, + enumerable: true, + }, + }) }, } diff --git a/packages/table-core/src/features/ColumnGrouping.ts b/packages/table-core/src/features/ColumnGrouping.ts index 21e8781cc2..0240dfe25d 100644 --- a/packages/table-core/src/features/ColumnGrouping.ts +++ b/packages/table-core/src/features/ColumnGrouping.ts @@ -1,4 +1,4 @@ -import { RowModel } from '..' +import { getRowProto, RowModel } from '..' import { BuiltInAggregationFn, aggregationFns } from '../aggregationFns' import { AggregationFns, @@ -353,31 +353,37 @@ export const ColumnGrouping: TableFeature = { return table._getGroupedRowModel() } - }, - createRow: ( - row: Row, - table: Table - ): void => { - row.getIsGrouped = () => !!row.groupingColumnId - row.getGroupingValue = columnId => { - if (row._groupingValuesCache.hasOwnProperty(columnId)) { - return row._groupingValuesCache[columnId] - } + Object.defineProperty(getRowProto(table), '_groupingValuesCache', { + get() { + // Lazy-init the backing cache on the instance so we don't take up memory for rows that don't need it + return (this.__groupingValuesCache ??= {}) + }, + enumerable: true, + }) + + Object.assign(getRowProto(table), { + getIsGrouped() { + return !!this.groupingColumnId + }, + getGroupingValue(columnId) { + if (this._groupingValuesCache.hasOwnProperty(columnId)) { + return this._groupingValuesCache[columnId] + } - const column = table.getColumn(columnId) + const column = table.getColumn(columnId) - if (!column?.columnDef.getGroupingValue) { - return row.getValue(columnId) - } + if (!column?.columnDef.getGroupingValue) { + return this.getValue(columnId) + } - row._groupingValuesCache[columnId] = column.columnDef.getGroupingValue( - row.original - ) + this._groupingValuesCache[columnId] = column.columnDef.getGroupingValue( + this.original + ) - return row._groupingValuesCache[columnId] - } - row._groupingValuesCache = {} + return this._groupingValuesCache[columnId] + }, + } as GroupingRow & Row) }, createCell: ( diff --git a/packages/table-core/src/features/ColumnPinning.ts b/packages/table-core/src/features/ColumnPinning.ts index 1c0ef70799..d3487409c6 100644 --- a/packages/table-core/src/features/ColumnPinning.ts +++ b/packages/table-core/src/features/ColumnPinning.ts @@ -1,3 +1,4 @@ +import { getRowProto } from '..' import { OnChangeFn, Updater, @@ -9,6 +10,7 @@ import { TableFeature, } from '../types' import { getMemoOptions, makeStateUpdater, memo } from '../utils' +import { RowPinningRow } from './RowPinning' export type ColumnPinningPosition = false | 'left' | 'right' @@ -236,49 +238,6 @@ export const ColumnPinning: TableFeature = { } }, - createRow: ( - row: Row, - table: Table - ): void => { - row.getCenterVisibleCells = memo( - () => [ - row._getAllVisibleCells(), - table.getState().columnPinning.left, - table.getState().columnPinning.right, - ], - (allCells, left, right) => { - const leftAndRight: string[] = [...(left ?? []), ...(right ?? [])] - - return allCells.filter(d => !leftAndRight.includes(d.column.id)) - }, - getMemoOptions(table.options, 'debugRows', 'getCenterVisibleCells') - ) - row.getLeftVisibleCells = memo( - () => [row._getAllVisibleCells(), table.getState().columnPinning.left], - (allCells, left) => { - const cells = (left ?? []) - .map(columnId => allCells.find(cell => cell.column.id === columnId)!) - .filter(Boolean) - .map(d => ({ ...d, position: 'left' }) as Cell) - - return cells - }, - getMemoOptions(table.options, 'debugRows', 'getLeftVisibleCells') - ) - row.getRightVisibleCells = memo( - () => [row._getAllVisibleCells(), table.getState().columnPinning.right], - (allCells, right) => { - const cells = (right ?? []) - .map(columnId => allCells.find(cell => cell.column.id === columnId)!) - .filter(Boolean) - .map(d => ({ ...d, position: 'right' }) as Cell) - - return cells - }, - getMemoOptions(table.options, 'debugRows', 'getRightVisibleCells') - ) - }, - createTable: (table: Table): void => { table.setColumnPinning = updater => table.options.onColumnPinningChange?.(updater) @@ -332,5 +291,63 @@ export const ColumnPinning: TableFeature = { }, getMemoOptions(table.options, 'debugColumns', 'getCenterLeafColumns') ) + + Object.assign(getRowProto(table), { + getCenterVisibleCells: memo( + function (this: Row) { + return [ + this._getAllVisibleCells(), + table.getState().columnPinning.left, + table.getState().columnPinning.right, + ] + }, + (allCells, left, right) => { + const leftAndRight: string[] = [...(left ?? []), ...(right ?? [])] + + return allCells.filter(d => !leftAndRight.includes(d.column.id)) + }, + getMemoOptions(table.options, 'debugRows', 'getCenterVisibleCells') + ), + + getLeftVisibleCells: memo( + function (this: Row) { + return [ + this._getAllVisibleCells(), + table.getState().columnPinning.left, + ] + }, + (allCells, left) => { + const cells = (left ?? []) + .map( + columnId => allCells.find(cell => cell.column.id === columnId)! + ) + .filter(Boolean) + .map(d => ({ ...d, position: 'left' }) as Cell) + + return cells + }, + getMemoOptions(table.options, 'debugRows', 'getLeftVisibleCells') + ), + + getRightVisibleCells: memo( + function (this: Row) { + return [ + this._getAllVisibleCells(), + table.getState().columnPinning.right, + ] + }, + (allCells, right) => { + const cells = (right ?? []) + .map( + columnId => allCells.find(cell => cell.column.id === columnId)! + ) + .filter(Boolean) + .map(d => ({ ...d, position: 'right' }) as Cell) + + return cells + }, + getMemoOptions(table.options, 'debugRows', 'getRightVisibleCells') + ), + } as RowPinningRow & Row) }, } diff --git a/packages/table-core/src/features/ColumnVisibility.ts b/packages/table-core/src/features/ColumnVisibility.ts index f37e57be8e..ed8f5e900a 100644 --- a/packages/table-core/src/features/ColumnVisibility.ts +++ b/packages/table-core/src/features/ColumnVisibility.ts @@ -1,4 +1,4 @@ -import { ColumnPinningPosition } from '..' +import { ColumnPinningPosition, getRowProto } from '..' import { Cell, Column, @@ -201,28 +201,6 @@ export const ColumnVisibility: TableFeature = { } }, - createRow: ( - row: Row, - table: Table - ): void => { - row._getAllVisibleCells = memo( - () => [row.getAllCells(), table.getState().columnVisibility], - cells => { - return cells.filter(cell => cell.column.getIsVisible()) - }, - getMemoOptions(table.options, 'debugRows', '_getAllVisibleCells') - ) - row.getVisibleCells = memo( - () => [ - row.getLeftVisibleCells(), - row.getCenterVisibleCells(), - row.getRightVisibleCells(), - ], - (left, center, right) => [...left, ...center, ...right], - getMemoOptions(table.options, 'debugRows', 'getVisibleCells') - ) - }, - createTable: (table: Table): void => { const makeVisibleColumnsMethod = ( key: string, @@ -300,6 +278,30 @@ export const ColumnVisibility: TableFeature = { ) } } + + Object.assign(getRowProto(table), { + _getAllVisibleCells: memo( + function (this: Row) { + return [this.getAllCells(), table.getState().columnVisibility] + }, + cells => { + return cells.filter(cell => cell.column.getIsVisible()) + }, + getMemoOptions(table.options, 'debugRows', '_getAllVisibleCells') + ), + + getVisibleCells: memo( + function (this: Row) { + return [ + this.getLeftVisibleCells(), + this.getCenterVisibleCells(), + this.getRightVisibleCells(), + ] + }, + (left, center, right) => [...left, ...center, ...right], + getMemoOptions(table.options, 'debugRows', 'getVisibleCells') + ), + }) }, } diff --git a/packages/table-core/src/features/RowExpanding.ts b/packages/table-core/src/features/RowExpanding.ts index 15da45e0ea..7614ea81e7 100644 --- a/packages/table-core/src/features/RowExpanding.ts +++ b/packages/table-core/src/features/RowExpanding.ts @@ -1,4 +1,4 @@ -import { RowModel } from '..' +import { getRowProto, RowModel } from '..' import { OnChangeFn, Table, @@ -281,75 +281,76 @@ export const RowExpanding: TableFeature = { return table._getExpandedRowModel() } - }, - createRow: ( - row: Row, - table: Table - ): void => { - row.toggleExpanded = expanded => { - table.setExpanded(old => { - const exists = old === true ? true : !!old?.[row.id] - - let oldExpanded: ExpandedStateList = {} - - if (old === true) { - Object.keys(table.getRowModel().rowsById).forEach(rowId => { - oldExpanded[rowId] = true - }) - } else { - oldExpanded = old - } + Object.assign(getRowProto(table), { + toggleExpanded(expanded) { + table.setExpanded(old => { + const exists = old === true ? true : !!old?.[this.id] - expanded = expanded ?? !exists + let oldExpanded: ExpandedStateList = {} - if (!exists && expanded) { - return { - ...oldExpanded, - [row.id]: true, + if (old === true) { + Object.keys(table.getRowModel().rowsById).forEach(rowId => { + oldExpanded[rowId] = true + }) + } else { + oldExpanded = old } - } - if (exists && !expanded) { - const { [row.id]: _, ...rest } = oldExpanded - return rest - } + expanded = expanded ?? !exists - return old - }) - } - row.getIsExpanded = () => { - const expanded = table.getState().expanded + if (!exists && expanded) { + return { + ...oldExpanded, + [this.id]: true, + } + } - return !!( - table.options.getIsRowExpanded?.(row) ?? - (expanded === true || expanded?.[row.id]) - ) - } - row.getCanExpand = () => { - return ( - table.options.getRowCanExpand?.(row) ?? - ((table.options.enableExpanding ?? true) && !!row.subRows?.length) - ) - } - row.getIsAllParentsExpanded = () => { - let isFullyExpanded = true - let currentRow = row + if (exists && !expanded) { + const { [this.id]: _, ...rest } = oldExpanded + return rest + } - while (isFullyExpanded && currentRow.parentId) { - currentRow = table.getRow(currentRow.parentId, true) - isFullyExpanded = currentRow.getIsExpanded() - } + return old + }) + }, - return isFullyExpanded - } - row.getToggleExpandedHandler = () => { - const canExpand = row.getCanExpand() + getIsExpanded() { + const expanded = table.getState().expanded - return () => { - if (!canExpand) return - row.toggleExpanded() - } - } + return !!( + table.options.getIsRowExpanded?.(this) ?? + (expanded === true || expanded?.[this.id]) + ) + }, + + getCanExpand() { + return ( + table.options.getRowCanExpand?.(this) ?? + ((table.options.enableExpanding ?? true) && !!this.subRows?.length) + ) + }, + + getIsAllParentsExpanded() { + let isFullyExpanded = true + let currentRow = this + + while (isFullyExpanded && currentRow.parentId) { + currentRow = table.getRow(currentRow.parentId, true) + isFullyExpanded = currentRow.getIsExpanded() + } + + return isFullyExpanded + }, + + getToggleExpandedHandler() { + const canExpand = this.getCanExpand() + + return () => { + if (!canExpand) return + this.toggleExpanded() + } + }, + } as ExpandedRow & Row) }, } diff --git a/packages/table-core/src/features/RowPinning.ts b/packages/table-core/src/features/RowPinning.ts index 02288ab8d1..123dd8ddfd 100644 --- a/packages/table-core/src/features/RowPinning.ts +++ b/packages/table-core/src/features/RowPinning.ts @@ -1,3 +1,4 @@ +import { getRowProto } from '..' import { OnChangeFn, Updater, @@ -142,75 +143,6 @@ export const RowPinning: TableFeature = { } }, - createRow: ( - row: Row, - table: Table - ): void => { - row.pin = (position, includeLeafRows, includeParentRows) => { - const leafRowIds = includeLeafRows - ? row.getLeafRows().map(({ id }) => id) - : [] - const parentRowIds = includeParentRows - ? row.getParentRows().map(({ id }) => id) - : [] - const rowIds = new Set([...parentRowIds, row.id, ...leafRowIds]) - - table.setRowPinning(old => { - if (position === 'bottom') { - return { - top: (old?.top ?? []).filter(d => !rowIds?.has(d)), - bottom: [ - ...(old?.bottom ?? []).filter(d => !rowIds?.has(d)), - ...Array.from(rowIds), - ], - } - } - - if (position === 'top') { - return { - top: [ - ...(old?.top ?? []).filter(d => !rowIds?.has(d)), - ...Array.from(rowIds), - ], - bottom: (old?.bottom ?? []).filter(d => !rowIds?.has(d)), - } - } - - return { - top: (old?.top ?? []).filter(d => !rowIds?.has(d)), - bottom: (old?.bottom ?? []).filter(d => !rowIds?.has(d)), - } - }) - } - row.getCanPin = () => { - const { enableRowPinning, enablePinning } = table.options - if (typeof enableRowPinning === 'function') { - return enableRowPinning(row) - } - return enableRowPinning ?? enablePinning ?? true - } - row.getIsPinned = () => { - const rowIds = [row.id] - - const { top, bottom } = table.getState().rowPinning - - const isTop = rowIds.some(d => top?.includes(d)) - const isBottom = rowIds.some(d => bottom?.includes(d)) - - return isTop ? 'top' : isBottom ? 'bottom' : false - } - row.getPinnedIndex = () => { - const position = row.getIsPinned() - if (!position) return -1 - - const visiblePinnedRowIds = ( - position === 'top' ? table.getTopRows() : table.getBottomRows() - )?.map(({ id }) => id) - - return visiblePinnedRowIds?.indexOf(row.id) ?? -1 - } - }, - createTable: (table: Table): void => { table.setRowPinning = updater => table.options.onRowPinningChange?.(updater) @@ -273,5 +205,74 @@ export const RowPinning: TableFeature = { }, getMemoOptions(table.options, 'debugRows', 'getCenterRows') ) + + Object.assign(getRowProto(table), { + pin(position, includeLeafRows, includeParentRows) { + const leafRowIds = includeLeafRows + ? this.getLeafRows().map(({ id }) => id) + : [] + const parentRowIds = includeParentRows + ? this.getParentRows().map(({ id }) => id) + : [] + const rowIds = new Set([...parentRowIds, this.id, ...leafRowIds]) + + table.setRowPinning(old => { + if (position === 'bottom') { + return { + top: (old?.top ?? []).filter(d => !rowIds?.has(d)), + bottom: [ + ...(old?.bottom ?? []).filter(d => !rowIds?.has(d)), + ...Array.from(rowIds), + ], + } + } + + if (position === 'top') { + return { + top: [ + ...(old?.top ?? []).filter(d => !rowIds?.has(d)), + ...Array.from(rowIds), + ], + bottom: (old?.bottom ?? []).filter(d => !rowIds?.has(d)), + } + } + + return { + top: (old?.top ?? []).filter(d => !rowIds?.has(d)), + bottom: (old?.bottom ?? []).filter(d => !rowIds?.has(d)), + } + }) + }, + + getCanPin() { + const { enableRowPinning, enablePinning } = table.options + if (typeof enableRowPinning === 'function') { + return enableRowPinning(this) + } + return enableRowPinning ?? enablePinning ?? true + }, + + getIsPinned() { + const rowIds = [this.id] + + const { top, bottom } = table.getState().rowPinning + + const isTop = rowIds.some(d => top?.includes(d)) + const isBottom = rowIds.some(d => bottom?.includes(d)) + + return isTop ? 'top' : isBottom ? 'bottom' : false + }, + + getPinnedIndex() { + const position = this.getIsPinned() + if (!position) return -1 + + const visiblePinnedRowIds = ( + position === 'top' ? table.getTopRows() : table.getBottomRows() + )?.map(({ id }) => id) + + return visiblePinnedRowIds?.indexOf(this.id) ?? -1 + }, + } as RowPinningRow & Row) }, } diff --git a/packages/table-core/src/features/RowSelection.ts b/packages/table-core/src/features/RowSelection.ts index 90166823aa..8284c789eb 100644 --- a/packages/table-core/src/features/RowSelection.ts +++ b/packages/table-core/src/features/RowSelection.ts @@ -1,3 +1,4 @@ +import { getRowProto } from '..' import { OnChangeFn, Table, @@ -464,83 +465,82 @@ export const RowSelection: TableFeature = { ) } } - }, - createRow: ( - row: Row, - table: Table - ): void => { - row.toggleSelected = (value, opts) => { - const isSelected = row.getIsSelected() + Object.assign(getRowProto(table), { + toggleSelected(value, opts) { + const isSelected = this.getIsSelected() - table.setRowSelection(old => { - value = typeof value !== 'undefined' ? value : !isSelected + table.setRowSelection(old => { + value = typeof value !== 'undefined' ? value : !isSelected - if (row.getCanSelect() && isSelected === value) { - return old - } + if (this.getCanSelect() && isSelected === value) { + return old + } - const selectedRowIds = { ...old } + const selectedRowIds = { ...old } - mutateRowIsSelected( - selectedRowIds, - row.id, - value, - opts?.selectChildren ?? true, - table - ) + mutateRowIsSelected( + selectedRowIds, + this.id, + value, + opts?.selectChildren ?? true, + table + ) - return selectedRowIds - }) - } - row.getIsSelected = () => { - const { rowSelection } = table.getState() - return isRowSelected(row, rowSelection) - } + return selectedRowIds + }) + }, - row.getIsSomeSelected = () => { - const { rowSelection } = table.getState() - return isSubRowSelected(row, rowSelection, table) === 'some' - } + getIsSelected() { + const { rowSelection } = table.getState() + return isRowSelected(this, rowSelection) + }, - row.getIsAllSubRowsSelected = () => { - const { rowSelection } = table.getState() - return isSubRowSelected(row, rowSelection, table) === 'all' - } + getIsSomeSelected() { + const { rowSelection } = table.getState() + return isSubRowSelected(this, rowSelection, table) === 'some' + }, - row.getCanSelect = () => { - if (typeof table.options.enableRowSelection === 'function') { - return table.options.enableRowSelection(row) - } + getIsAllSubRowsSelected() { + const { rowSelection } = table.getState() + return isSubRowSelected(this, rowSelection, table) === 'all' + }, - return table.options.enableRowSelection ?? true - } + getCanSelect() { + if (typeof table.options.enableRowSelection === 'function') { + return table.options.enableRowSelection(this) + } - row.getCanSelectSubRows = () => { - if (typeof table.options.enableSubRowSelection === 'function') { - return table.options.enableSubRowSelection(row) - } + return table.options.enableRowSelection ?? true + }, - return table.options.enableSubRowSelection ?? true - } + getCanSelectSubRows() { + if (typeof table.options.enableSubRowSelection === 'function') { + return table.options.enableSubRowSelection(this) + } - row.getCanMultiSelect = () => { - if (typeof table.options.enableMultiRowSelection === 'function') { - return table.options.enableMultiRowSelection(row) - } + return table.options.enableSubRowSelection ?? true + }, - return table.options.enableMultiRowSelection ?? true - } - row.getToggleSelectedHandler = () => { - const canSelect = row.getCanSelect() + getCanMultiSelect() { + if (typeof table.options.enableMultiRowSelection === 'function') { + return table.options.enableMultiRowSelection(this) + } - return (e: unknown) => { - if (!canSelect) return - row.toggleSelected( - ((e as MouseEvent).target as HTMLInputElement)?.checked - ) - } - } + return table.options.enableMultiRowSelection ?? true + }, + + getToggleSelectedHandler() { + const canSelect = this.getCanSelect() + + return (e: unknown) => { + if (!canSelect) return + this.toggleSelected( + ((e as MouseEvent).target as HTMLInputElement)?.checked + ) + } + }, + } as RowSelectionRow & Row) }, } @@ -599,10 +599,8 @@ export function selectRowsFn( } if (row.subRows?.length) { - row = { - ...row, - subRows: recurseRows(row.subRows, depth + 1), - } + row = row.clone() + row.subRows = recurseRows(row.subRows, depth + 1) } if (isSelected) { diff --git a/packages/table-core/src/index.ts b/packages/table-core/src/index.ts index 1186d27202..b4f7ab5bab 100755 --- a/packages/table-core/src/index.ts +++ b/packages/table-core/src/index.ts @@ -27,6 +27,7 @@ export * from './features/RowSorting' //utils export * from './utils' +export * from './utils/filterRowsUtils' export * from './utils/getCoreRowModel' export * from './utils/getExpandedRowModel' export * from './utils/getFacetedMinMaxValues' diff --git a/packages/table-core/src/utils.ts b/packages/table-core/src/utils.ts index 77be57d750..98dad4d4fd 100755 --- a/packages/table-core/src/utils.ts +++ b/packages/table-core/src/utils.ts @@ -145,11 +145,11 @@ export function memo( let deps: any[] = [] let result: TResult | undefined - return depArgs => { + return function (this: any, depArgs) { let depTime: number if (opts.key && opts.debug) depTime = Date.now() - const newDeps = getDeps(depArgs) + const newDeps = getDeps.call(this, depArgs) const depsChanged = newDeps.length !== deps.length || @@ -164,7 +164,7 @@ export function memo( let resultTime: number if (opts.key && opts.debug) resultTime = Date.now() - result = fn(...newDeps) + result = fn.apply(this, newDeps) opts?.onChange?.(result) if (opts.key && opts.debug) { diff --git a/packages/table-core/src/utils/getFilteredRowModel.ts b/packages/table-core/src/utils/getFilteredRowModel.ts index c04341ad2a..6354218cf0 100644 --- a/packages/table-core/src/utils/getFilteredRowModel.ts +++ b/packages/table-core/src/utils/getFilteredRowModel.ts @@ -1,9 +1,151 @@ -import { ResolvedColumnFilter } from '../features/ColumnFiltering' +import { ColumnFiltersState, ResolvedColumnFilter } from '../features/ColumnFiltering' import { Table, RowModel, Row, RowData } from '../types' import { getMemoOptions, memo } from '../utils' import { filterRows } from './filterRowsUtils' -export function getFilteredRowModel(): ( +export function getFilteredRowModelUnmemoized( + table: Table, + columnFilters: ColumnFiltersState, + globalFilter: any, + rowModel: RowModel + +) { + if ( + !rowModel.rows.length || + (!columnFilters?.length && !globalFilter) + ) { + // TODO: Does this have any consequences? Would avoid iterating the rows again + return rowModel; + /* + for (let i = 0; i < rowModel.flatRows.length; i++) { + rowModel.flatRows[i]!.columnFilters = {} + rowModel.flatRows[i]!.columnFiltersMeta = {} + } + return rowModel + */ + } + + const resolvedColumnFilters: ResolvedColumnFilter[] = [] + const resolvedGlobalFilters: ResolvedColumnFilter[] = [] + + ; (columnFilters ?? []).forEach(d => { + const column = table.getColumn(d.id) + + if (!column) { + return + } + + const filterFn = column.getFilterFn() + + if (!filterFn) { + if (process.env.NODE_ENV !== 'production') { + console.warn( + `Could not find a valid 'column.filterFn' for column with the ID: ${column.id}.` + ) + } + return + } + + resolvedColumnFilters.push({ + id: d.id, + filterFn, + resolvedValue: filterFn.resolveFilterValue?.(d.value) ?? d.value, + }) + }) + + const filterableIds = (columnFilters ?? []).map(d => d.id) + + const globalFilterFn = table.getGlobalFilterFn() + + const globallyFilterableColumns = table + .getAllLeafColumns() + .filter(column => column.getCanGlobalFilter()) + + if ( + globalFilter && + globalFilterFn && + globallyFilterableColumns.length + ) { + filterableIds.push('__global__') + + globallyFilterableColumns.forEach(column => { + resolvedGlobalFilters.push({ + id: column.id, + filterFn: globalFilterFn, + resolvedValue: + globalFilterFn.resolveFilterValue?.(globalFilter) ?? + globalFilter, + }) + }) + } + + let currentColumnFilter + let currentGlobalFilter + + // Flag the prefiltered row model with each filter state + for (let j = 0; j < rowModel.flatRows.length; j++) { + const row = rowModel.flatRows[j]! + + row.columnFilters = {} + + if (resolvedColumnFilters.length) { + for (let i = 0; i < resolvedColumnFilters.length; i++) { + currentColumnFilter = resolvedColumnFilters[i]! + const id = currentColumnFilter.id + + // Tag the row with the column filter state + row.columnFilters[id] = currentColumnFilter.filterFn( + row, + id, + currentColumnFilter.resolvedValue, + filterMeta => { + row.columnFiltersMeta[id] = filterMeta + } + ) + } + } + + if (resolvedGlobalFilters.length) { + for (let i = 0; i < resolvedGlobalFilters.length; i++) { + currentGlobalFilter = resolvedGlobalFilters[i]! + const id = currentGlobalFilter.id + // Tag the row with the first truthy global filter state + if ( + currentGlobalFilter.filterFn( + row, + id, + currentGlobalFilter.resolvedValue, + filterMeta => { + row.columnFiltersMeta[id] = filterMeta + } + ) + ) { + row.columnFilters.__global__ = true + break + } + } + + if (row.columnFilters.__global__ !== true) { + row.columnFilters.__global__ = false + } + } + } + + const filterRowsImpl = (row: Row) => { + // Horizontally filter rows through each column + for (let i = 0; i < filterableIds.length; i++) { + if (row.columnFilters[filterableIds[i]!] === false) { + return false + } + } + return true + } + + // Filter final rows using all of the active filters + return filterRows(rowModel.rows, filterRowsImpl, table) +} + +export function getFilteredRowModel(middleware?: (rowModel: RowModel) => RowModel): ( table: Table ) => () => RowModel { return table => @@ -14,135 +156,13 @@ export function getFilteredRowModel(): ( table.getState().globalFilter, ], (rowModel, columnFilters, globalFilter) => { - if ( - !rowModel.rows.length || - (!columnFilters?.length && !globalFilter) - ) { - for (let i = 0; i < rowModel.flatRows.length; i++) { - rowModel.flatRows[i]!.columnFilters = {} - rowModel.flatRows[i]!.columnFiltersMeta = {} - } - return rowModel - } - - const resolvedColumnFilters: ResolvedColumnFilter[] = [] - const resolvedGlobalFilters: ResolvedColumnFilter[] = [] - - ;(columnFilters ?? []).forEach(d => { - const column = table.getColumn(d.id) - - if (!column) { - return - } - - const filterFn = column.getFilterFn() - - if (!filterFn) { - if (process.env.NODE_ENV !== 'production') { - console.warn( - `Could not find a valid 'column.filterFn' for column with the ID: ${column.id}.` - ) - } - return - } - - resolvedColumnFilters.push({ - id: d.id, - filterFn, - resolvedValue: filterFn.resolveFilterValue?.(d.value) ?? d.value, - }) - }) - - const filterableIds = (columnFilters ?? []).map(d => d.id) - - const globalFilterFn = table.getGlobalFilterFn() - - const globallyFilterableColumns = table - .getAllLeafColumns() - .filter(column => column.getCanGlobalFilter()) - - if ( - globalFilter && - globalFilterFn && - globallyFilterableColumns.length - ) { - filterableIds.push('__global__') - - globallyFilterableColumns.forEach(column => { - resolvedGlobalFilters.push({ - id: column.id, - filterFn: globalFilterFn, - resolvedValue: - globalFilterFn.resolveFilterValue?.(globalFilter) ?? - globalFilter, - }) - }) - } - - let currentColumnFilter - let currentGlobalFilter - - // Flag the prefiltered row model with each filter state - for (let j = 0; j < rowModel.flatRows.length; j++) { - const row = rowModel.flatRows[j]! - - row.columnFilters = {} - - if (resolvedColumnFilters.length) { - for (let i = 0; i < resolvedColumnFilters.length; i++) { - currentColumnFilter = resolvedColumnFilters[i]! - const id = currentColumnFilter.id - - // Tag the row with the column filter state - row.columnFilters[id] = currentColumnFilter.filterFn( - row, - id, - currentColumnFilter.resolvedValue, - filterMeta => { - row.columnFiltersMeta[id] = filterMeta - } - ) - } - } - - if (resolvedGlobalFilters.length) { - for (let i = 0; i < resolvedGlobalFilters.length; i++) { - currentGlobalFilter = resolvedGlobalFilters[i]! - const id = currentGlobalFilter.id - // Tag the row with the first truthy global filter state - if ( - currentGlobalFilter.filterFn( - row, - id, - currentGlobalFilter.resolvedValue, - filterMeta => { - row.columnFiltersMeta[id] = filterMeta - } - ) - ) { - row.columnFilters.__global__ = true - break - } - } - - if (row.columnFilters.__global__ !== true) { - row.columnFilters.__global__ = false - } - } - } - - const filterRowsImpl = (row: Row) => { - // Horizontally filter rows through each column - for (let i = 0; i < filterableIds.length; i++) { - if (row.columnFilters[filterableIds[i]!] === false) { - return false - } - } - return true - } - - // Filter final rows using all of the active filters - return filterRows(rowModel.rows, filterRowsImpl, table) + const newRowModel = getFilteredRowModelUnmemoized( + table, + columnFilters, + globalFilter, + rowModel + ) + return middleware ? middleware(newRowModel) : newRowModel }, getMemoOptions(table.options, 'debugTable', 'getFilteredRowModel', () => table._autoResetPageIndex() diff --git a/packages/table-core/src/utils/getGroupedRowModel.ts b/packages/table-core/src/utils/getGroupedRowModel.ts index da97631913..87fd9dc835 100644 --- a/packages/table-core/src/utils/getGroupedRowModel.ts +++ b/packages/table-core/src/utils/getGroupedRowModel.ts @@ -3,151 +3,137 @@ import { Row, RowData, RowModel, Table } from '../types' import { flattenBy, getMemoOptions, memo } from '../utils' import { GroupingState } from '../features/ColumnGrouping' -export function getGroupedRowModel(): ( - table: Table -) => () => RowModel { - return table => - memo( - () => [table.getState().grouping, table.getPreGroupedRowModel()], - (grouping, rowModel) => { - if (!rowModel.rows.length || !grouping.length) { - rowModel.rows.forEach(row => { - row.depth = 0 - row.parentId = undefined - }) - return rowModel +export function getGroupedRowModelUnmemoized( + table: Table, + grouping: GroupingState, + rowModel: RowModel +) { + if (!rowModel.rows.length || !grouping.length) { + // TODO: Does this have any consequences? Would avoid iterating the rows again + return rowModel; + /* + rowModel.rows.forEach(row => { + row.depth = 0 + row.parentId = undefined + }) + return rowModel + */ + } + + // Filter the grouping list down to columns that exist + const existingGrouping = grouping.filter(columnId => + table.getColumn(columnId) + ) + + const groupedFlatRows: Row[] = [] + const groupedRowsById: Record> = {} + // const onlyGroupedFlatRows: Row[] = []; + // const onlyGroupedRowsById: Record = {}; + // const nonGroupedFlatRows: Row[] = []; + // const nonGroupedRowsById: Record = {}; + + // Recursively group the data + const groupUpRecursively = ( + rows: Row[], + depth = 0, + parentId?: string + ) => { + // Grouping depth has been been met + // Stop grouping and simply rewrite thd depth and row relationships + if (depth >= existingGrouping.length) { + return rows.map(row => { + row.depth = depth + + groupedFlatRows.push(row) + groupedRowsById[row.id] = row + + if (row.subRows) { + row.subRows = groupUpRecursively(row.subRows, depth + 1, row.id) } - // Filter the grouping list down to columns that exist - const existingGrouping = grouping.filter(columnId => - table.getColumn(columnId) + return row + }) + } + + const columnId: string = existingGrouping[depth]! + + // Group the rows together for this level + const rowGroupsMap = groupBy(rows, columnId) + + // Perform aggregations for each group + const aggregatedGroupedRows = Array.from(rowGroupsMap.entries()).map( + ([groupingValue, groupedRows], index) => { + let id = `${columnId}:${groupingValue}` + id = parentId ? `${parentId}>${id}` : id + + // First, Recurse to group sub rows before aggregation + const subRows = groupUpRecursively(groupedRows, depth + 1, id) + + subRows.forEach(subRow => { + subRow.parentId = id + }) + + // Flatten the leaf rows of the rows in this group + const leafRows = depth + ? flattenBy(groupedRows, row => row.subRows) + : groupedRows + + const row = createRow( + table, + id, + leafRows[0]!.original, + index, + depth, + undefined, + parentId ) - const groupedFlatRows: Row[] = [] - const groupedRowsById: Record> = {} - // const onlyGroupedFlatRows: Row[] = []; - // const onlyGroupedRowsById: Record = {}; - // const nonGroupedFlatRows: Row[] = []; - // const nonGroupedRowsById: Record = {}; - - // Recursively group the data - const groupUpRecursively = ( - rows: Row[], - depth = 0, - parentId?: string - ) => { - // Grouping depth has been been met - // Stop grouping and simply rewrite thd depth and row relationships - if (depth >= existingGrouping.length) { - return rows.map(row => { - row.depth = depth - - groupedFlatRows.push(row) - groupedRowsById[row.id] = row - - if (row.subRows) { - row.subRows = groupUpRecursively(row.subRows, depth + 1, row.id) + Object.assign(row, { + groupingColumnId: columnId, + groupingValue, + subRows, + leafRows, + getValue: (columnId: string) => { + // Don't aggregate columns that are in the grouping + if (existingGrouping.includes(columnId)) { + /* + if (row._valuesCache.hasOwnProperty(columnId)) { + return row._valuesCache[columnId] + } + */ + if (groupedRows[0]) { + return groupedRows[0].getValue(columnId) ?? undefined + /* + row._valuesCache[columnId] = + groupedRows[0].getValue(columnId) ?? undefined + */ } - return row - }) - } - - const columnId: string = existingGrouping[depth]! - - // Group the rows together for this level - const rowGroupsMap = groupBy(rows, columnId) - - // Perform aggregations for each group - const aggregatedGroupedRows = Array.from(rowGroupsMap.entries()).map( - ([groupingValue, groupedRows], index) => { - let id = `${columnId}:${groupingValue}` - id = parentId ? `${parentId}>${id}` : id - - // First, Recurse to group sub rows before aggregation - const subRows = groupUpRecursively(groupedRows, depth + 1, id) - - subRows.forEach(subRow => { - subRow.parentId = id - }) - - // Flatten the leaf rows of the rows in this group - const leafRows = depth - ? flattenBy(groupedRows, row => row.subRows) - : groupedRows - - const row = createRow( - table, - id, - leafRows[0]!.original, - index, - depth, - undefined, - parentId - ) + return undefined; + // return row._valuesCache[columnId] + } - Object.assign(row, { - groupingColumnId: columnId, - groupingValue, - subRows, - leafRows, - getValue: (columnId: string) => { - // Don't aggregate columns that are in the grouping - if (existingGrouping.includes(columnId)) { - if (row._valuesCache.hasOwnProperty(columnId)) { - return row._valuesCache[columnId] - } - - if (groupedRows[0]) { - row._valuesCache[columnId] = - groupedRows[0].getValue(columnId) ?? undefined - } - - return row._valuesCache[columnId] - } - - if (row._groupingValuesCache.hasOwnProperty(columnId)) { - return row._groupingValuesCache[columnId] - } - - // Aggregate the values - const column = table.getColumn(columnId) - const aggregateFn = column?.getAggregationFn() - - if (aggregateFn) { - row._groupingValuesCache[columnId] = aggregateFn( - columnId, - leafRows, - groupedRows - ) - - return row._groupingValuesCache[columnId] - } - }, - }) - - subRows.forEach(subRow => { - groupedFlatRows.push(subRow) - groupedRowsById[subRow.id] = subRow - // if (subRow.getIsGrouped?.()) { - // onlyGroupedFlatRows.push(subRow); - // onlyGroupedRowsById[subRow.id] = subRow; - // } else { - // nonGroupedFlatRows.push(subRow); - // nonGroupedRowsById[subRow.id] = subRow; - // } - }) - - return row + if (row._groupingValuesCache.hasOwnProperty(columnId)) { + return row._groupingValuesCache[columnId] } - ) - return aggregatedGroupedRows - } + // Aggregate the values + const column = table.getColumn(columnId) + const aggregateFn = column?.getAggregationFn() - const groupedRows = groupUpRecursively(rowModel.rows, 0) + if (aggregateFn) { + row._groupingValuesCache[columnId] = aggregateFn( + columnId, + leafRows, + groupedRows + ) - groupedRows.forEach(subRow => { + return row._groupingValuesCache[columnId] + } + }, + }) + + subRows.forEach(subRow => { groupedFlatRows.push(subRow) groupedRowsById[subRow.id] = subRow // if (subRow.getIsGrouped?.()) { @@ -159,11 +145,43 @@ export function getGroupedRowModel(): ( // } }) - return { - rows: groupedRows, - flatRows: groupedFlatRows, - rowsById: groupedRowsById, - } + return row + } + ) + + return aggregatedGroupedRows + } + + const groupedRows = groupUpRecursively(rowModel.rows, 0) + + groupedRows.forEach(subRow => { + groupedFlatRows.push(subRow) + groupedRowsById[subRow.id] = subRow + // if (subRow.getIsGrouped?.()) { + // onlyGroupedFlatRows.push(subRow); + // onlyGroupedRowsById[subRow.id] = subRow; + // } else { + // nonGroupedFlatRows.push(subRow); + // nonGroupedRowsById[subRow.id] = subRow; + // } + }) + + return { + rows: groupedRows, + flatRows: groupedFlatRows, + rowsById: groupedRowsById, + } +} + +export function getGroupedRowModel(middleware?: (rowModel: RowModel) => RowModel): ( + table: Table +) => () => RowModel { + return table => + memo( + () => [table.getState().grouping, table.getPreGroupedRowModel()], + (grouping, rowModel) => { + const newRowModel = getGroupedRowModelUnmemoized(table, grouping, rowModel) + return middleware ? middleware(newRowModel) : newRowModel }, getMemoOptions(table.options, 'debugTable', 'getGroupedRowModel', () => { table._queue(() => { diff --git a/packages/table-core/src/utils/getSortedRowModel.ts b/packages/table-core/src/utils/getSortedRowModel.ts index 928c26448f..52bc9bbe27 100644 --- a/packages/table-core/src/utils/getSortedRowModel.ts +++ b/packages/table-core/src/utils/getSortedRowModel.ts @@ -1,118 +1,127 @@ import { Table, Row, RowModel, RowData } from '../types' -import { SortingFn } from '../features/RowSorting' +import { SortingFn, SortingState } from '../features/RowSorting' import { getMemoOptions, memo } from '../utils' -export function getSortedRowModel(): ( - table: Table -) => () => RowModel { - return table => - memo( - () => [table.getState().sorting, table.getPreSortedRowModel()], - (sorting, rowModel) => { - if (!rowModel.rows.length || !sorting?.length) { - return rowModel +export function getSortedRowModelUnmemoized( + table: Table, + sorting: SortingState, + rowModel: RowModel +) { + if (!rowModel.rows.length || !sorting?.length) { + return rowModel + } + + const sortingState = table.getState().sorting + + const sortedFlatRows: Row[] = [] + + // Filter out sortings that correspond to non existing columns + const availableSorting = sortingState.filter(sort => + table.getColumn(sort.id)?.getCanSort() + ) + + const columnInfoById: Record< + string, + { + sortUndefined?: false | -1 | 1 | 'first' | 'last' + invertSorting?: boolean + sortingFn: SortingFn + } + > = {} + + availableSorting.forEach(sortEntry => { + const column = table.getColumn(sortEntry.id) + if (!column) return + + columnInfoById[sortEntry.id] = { + sortUndefined: column.columnDef.sortUndefined, + invertSorting: column.columnDef.invertSorting, + sortingFn: column.getSortingFn(), + } + }) + + const sortData = (rows: Row[]) => { + // This will also perform a stable sorting using the row index + // if needed. + const sortedData = rows.map(row => row.clone()) + + sortedData.sort((rowA, rowB) => { + for (let i = 0; i < availableSorting.length; i += 1) { + const sortEntry = availableSorting[i]! + const columnInfo = columnInfoById[sortEntry.id]! + const sortUndefined = columnInfo.sortUndefined + const isDesc = sortEntry?.desc ?? false + + let sortInt = 0 + + // All sorting ints should always return in ascending order + if (sortUndefined) { + const aValue = rowA.getValue(sortEntry.id) + const bValue = rowB.getValue(sortEntry.id) + + const aUndefined = aValue === undefined + const bUndefined = bValue === undefined + + if (aUndefined || bUndefined) { + if (sortUndefined === 'first') return aUndefined ? -1 : 1 + if (sortUndefined === 'last') return aUndefined ? 1 : -1 + sortInt = + aUndefined && bUndefined + ? 0 + : aUndefined + ? sortUndefined + : -sortUndefined + } } - const sortingState = table.getState().sorting - - const sortedFlatRows: Row[] = [] - - // Filter out sortings that correspond to non existing columns - const availableSorting = sortingState.filter(sort => - table.getColumn(sort.id)?.getCanSort() - ) + if (sortInt === 0) { + sortInt = columnInfo.sortingFn(rowA, rowB, sortEntry.id) + } - const columnInfoById: Record< - string, - { - sortUndefined?: false | -1 | 1 | 'first' | 'last' - invertSorting?: boolean - sortingFn: SortingFn + // If sorting is non-zero, take care of desc and inversion + if (sortInt !== 0) { + if (isDesc) { + sortInt *= -1 } - > = {} - availableSorting.forEach(sortEntry => { - const column = table.getColumn(sortEntry.id) - if (!column) return - - columnInfoById[sortEntry.id] = { - sortUndefined: column.columnDef.sortUndefined, - invertSorting: column.columnDef.invertSorting, - sortingFn: column.getSortingFn(), + if (columnInfo.invertSorting) { + sortInt *= -1 } - }) - - const sortData = (rows: Row[]) => { - // This will also perform a stable sorting using the row index - // if needed. - const sortedData = rows.map(row => ({ ...row })) - - sortedData.sort((rowA, rowB) => { - for (let i = 0; i < availableSorting.length; i += 1) { - const sortEntry = availableSorting[i]! - const columnInfo = columnInfoById[sortEntry.id]! - const sortUndefined = columnInfo.sortUndefined - const isDesc = sortEntry?.desc ?? false - - let sortInt = 0 - - // All sorting ints should always return in ascending order - if (sortUndefined) { - const aValue = rowA.getValue(sortEntry.id) - const bValue = rowB.getValue(sortEntry.id) - - const aUndefined = aValue === undefined - const bUndefined = bValue === undefined - - if (aUndefined || bUndefined) { - if (sortUndefined === 'first') return aUndefined ? -1 : 1 - if (sortUndefined === 'last') return aUndefined ? 1 : -1 - sortInt = - aUndefined && bUndefined - ? 0 - : aUndefined - ? sortUndefined - : -sortUndefined - } - } - - if (sortInt === 0) { - sortInt = columnInfo.sortingFn(rowA, rowB, sortEntry.id) - } - - // If sorting is non-zero, take care of desc and inversion - if (sortInt !== 0) { - if (isDesc) { - sortInt *= -1 - } - - if (columnInfo.invertSorting) { - sortInt *= -1 - } - - return sortInt - } - } - - return rowA.index - rowB.index - }) - - // If there are sub-rows, sort them - sortedData.forEach(row => { - sortedFlatRows.push(row) - if (row.subRows?.length) { - row.subRows = sortData(row.subRows) - } - }) - - return sortedData - } - return { - rows: sortData(rowModel.rows), - flatRows: sortedFlatRows, - rowsById: rowModel.rowsById, + return sortInt } + } + + return rowA.index - rowB.index + }) + + // If there are sub-rows, sort them + sortedData.forEach(row => { + sortedFlatRows.push(row) + if (row.subRows?.length) { + row.subRows = sortData(row.subRows) + } + }) + + return sortedData + } + + return { + rows: sortData(rowModel.rows), + flatRows: sortedFlatRows, + rowsById: rowModel.rowsById, + } +} + +export function getSortedRowModel(middleware?: (rowModel: RowModel) => RowModel): ( + table: Table +) => () => RowModel { + return table => + memo( + () => [table.getState().sorting, table.getPreSortedRowModel()], + (sorting, rowModel) => { + const newRowModel = getSortedRowModelUnmemoized(table, sorting, rowModel) + return middleware ? middleware(newRowModel) : newRowModel }, getMemoOptions(table.options, 'debugTable', 'getSortedRowModel', () => table._autoResetPageIndex() diff --git a/packages/table-core/tests/coreTable.test.ts b/packages/table-core/tests/coreTable.test.ts new file mode 100644 index 0000000000..48a480ac75 --- /dev/null +++ b/packages/table-core/tests/coreTable.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it } from 'vitest' +import { ColumnDef, createTable, getCoreRowModel } from '../src' + +type Person = { + firstName: string + lastName: string +} + +describe('CoreTable', () => { + it('refreshes the column lookup cache when columns change without data changing', () => { + const data: Person[] = [{ firstName: 'Ada', lastName: 'Lovelace' }] + const firstNameColumns: ColumnDef[] = [ + { + id: 'name', + accessorFn: row => row.firstName, + }, + ] + const lastNameColumns: ColumnDef[] = [ + { + id: 'name', + accessorFn: row => row.lastName, + }, + ] + + const table = createTable({ + onStateChange() {}, + renderFallbackValue: '', + data, + state: {}, + columns: firstNameColumns, + getCoreRowModel: getCoreRowModel(), + }) + + const firstColumn = table.getColumn('name') + const row = table.getCoreRowModel().rows[0]! + + expect(row.getValue('name')).toBe('Ada') + + table.setOptions(old => ({ + ...old, + columns: lastNameColumns, + })) + + expect(table.getCoreRowModel().rows[0]).toBe(row) + expect(table.getColumn('name')).not.toBe(firstColumn) + expect(row.getValue('name')).toBe('Lovelace') + }) +})