From 84568d31509357cce11685743b13435f3d30422e Mon Sep 17 00:00:00 2001 From: Kevin Van Cott Date: Sat, 20 Jun 2026 15:18:21 -0500 Subject: [PATCH] fix: add checks to dictionaries for ssr --- .../src/app/density/density-feature.ts | 8 +- examples/preact/custom-plugin/src/main.tsx | 8 +- examples/react/custom-plugin/src/main.tsx | 8 +- .../core/columns/coreColumnsFeature.utils.ts | 6 +- .../src/core/row-models/createCoreRowModel.ts | 4 +- .../table-core/src/core/rows/constructRow.ts | 5 +- .../src/core/rows/coreRowsFeature.utils.ts | 8 +- .../columnFilteringFeature.ts | 5 +- .../createFilteredRowModel.ts | 23 +- .../column-filtering/filterRowsUtils.ts | 5 +- .../column-grouping/columnGroupingFeature.ts | 3 +- .../columnGroupingFeature.utils.ts | 4 +- .../column-grouping/createGroupedRowModel.ts | 15 +- .../columnResizingFeature.utils.ts | 11 +- .../columnSizingFeature.utils.ts | 32 ++- .../columnVisibilityFeature.utils.ts | 46 ++-- .../rowExpandingFeature.utils.ts | 51 +++- .../rowSelectionFeature.utils.ts | 48 +++- .../row-sorting/createSortedRowModel.ts | 15 +- packages/table-core/src/utils.ts | 26 +- .../prototypeSafeDictionaries.test.ts | 227 ++++++++++++++++++ 21 files changed, 456 insertions(+), 102 deletions(-) create mode 100644 packages/table-core/tests/implementation/prototypeSafeDictionaries.test.ts diff --git a/examples/angular/custom-plugin/src/app/density/density-feature.ts b/examples/angular/custom-plugin/src/app/density/density-feature.ts index 319fe5de03..b6730bfcee 100644 --- a/examples/angular/custom-plugin/src/app/density/density-feature.ts +++ b/examples/angular/custom-plugin/src/app/density/density-feature.ts @@ -84,7 +84,9 @@ export const densityPlugin: TableFeature = { const newState = functionalUpdate(updater, old) return newState } - return table.options.onDensityChange?.(safeUpdater) + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) }, }, table_toggleDensity: { @@ -93,7 +95,9 @@ export const densityPlugin: TableFeature = { if (value) return value return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' // cycle through the 3 options } - return table.options.onDensityChange?.(safeUpdater) + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) }, }, }) diff --git a/examples/preact/custom-plugin/src/main.tsx b/examples/preact/custom-plugin/src/main.tsx index 79db294618..fc5e567a94 100644 --- a/examples/preact/custom-plugin/src/main.tsx +++ b/examples/preact/custom-plugin/src/main.tsx @@ -102,7 +102,9 @@ export const densityPlugin: TableFeature = { const newState = functionalUpdate(updater, old) return newState } - return table.options.onDensityChange?.(safeUpdater) + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) }, }, table_toggleDensity: { @@ -111,7 +113,9 @@ export const densityPlugin: TableFeature = { if (value) return value return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' // cycle through the 3 options } - return table.options.onDensityChange?.(safeUpdater) + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) }, }, }) diff --git a/examples/react/custom-plugin/src/main.tsx b/examples/react/custom-plugin/src/main.tsx index 7e3a5a86c2..d110aebe7a 100644 --- a/examples/react/custom-plugin/src/main.tsx +++ b/examples/react/custom-plugin/src/main.tsx @@ -103,7 +103,9 @@ export const densityPlugin: TableFeature = { const newState = functionalUpdate(updater, old) return newState } - return table.options.onDensityChange?.(safeUpdater) + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) }, }, table_toggleDensity: { @@ -112,7 +114,9 @@ export const densityPlugin: TableFeature = { if (value) return value return old === 'lg' ? 'md' : old === 'md' ? 'sm' : 'lg' // cycle through the 3 options } - return table.options.onDensityChange?.(safeUpdater) + return (table.options as TableOptions_Density).onDensityChange?.( + safeUpdater, + ) }, }, }) diff --git a/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts b/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts index 5901595f1c..2fae8a1fd7 100644 --- a/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts +++ b/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts @@ -1,4 +1,4 @@ -import { callMemoOrStaticFn } from '../../utils' +import { callMemoOrStaticFn, makeObjectMap } from '../../utils' import { table_getOrderColumnsFn } from '../../features/column-ordering/columnOrderingFeature.utils' import { constructColumn } from './constructColumn' import type { Table_Internal } from '../../types/Table' @@ -185,7 +185,7 @@ export function table_getAllFlatColumnsById< >( table: Table_Internal, ): Record> { - const result: Record> = {} + const result = makeObjectMap>() const flatColumns = table.getAllFlatColumns() for (let i = 0; i < flatColumns.length; i++) { const column = flatColumns[i]! @@ -238,7 +238,7 @@ export function table_getAllLeafColumnsById< >( table: Table_Internal, ): Record> { - const result: Record> = {} + const result = makeObjectMap>() const leafColumns = table.getAllLeafColumns() for (let i = 0; i < leafColumns.length; i++) { const column = leafColumns[i]! diff --git a/packages/table-core/src/core/row-models/createCoreRowModel.ts b/packages/table-core/src/core/row-models/createCoreRowModel.ts index 417893961a..1876b066d5 100644 --- a/packages/table-core/src/core/row-models/createCoreRowModel.ts +++ b/packages/table-core/src/core/row-models/createCoreRowModel.ts @@ -1,5 +1,5 @@ import { constructRow } from '../rows/constructRow' -import { tableMemo } from '../../utils' +import { makeObjectMap, tableMemo } from '../../utils' import { table_autoResetPageIndex } from '../../features/row-pagination/rowPaginationFeature.utils' import type { Table_Internal } from '../../types/Table' import type { RowModel } from './coreRowModelsFeature.types' @@ -44,7 +44,7 @@ function _createCoreRowModel< const rowModel: RowModel = { rows: [], flatRows: [], - rowsById: {}, + rowsById: makeObjectMap(), } const accessRows = ( diff --git a/packages/table-core/src/core/rows/constructRow.ts b/packages/table-core/src/core/rows/constructRow.ts index 5c9e707e18..4a1a56ae3d 100644 --- a/packages/table-core/src/core/rows/constructRow.ts +++ b/packages/table-core/src/core/rows/constructRow.ts @@ -1,3 +1,4 @@ +import { makeObjectMap } from '../../utils' import type { Table_Internal } from '../../types/Table' import type { RowData } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' @@ -47,8 +48,8 @@ export const constructRow = < > // Only assign instance-specific properties - row._uniqueValuesCache = {} - row._valuesCache = {} + row._uniqueValuesCache = makeObjectMap() + row._valuesCache = makeObjectMap() row.depth = depth row.id = id row.index = rowIndex diff --git a/packages/table-core/src/core/rows/coreRowsFeature.utils.ts b/packages/table-core/src/core/rows/coreRowsFeature.utils.ts index ce568a28a1..a4318bf15c 100644 --- a/packages/table-core/src/core/rows/coreRowsFeature.utils.ts +++ b/packages/table-core/src/core/rows/coreRowsFeature.utils.ts @@ -1,4 +1,4 @@ -import { flattenBy } from '../../utils' +import { flattenBy, hasOwn, makeObjectMap } from '../../utils' import { constructCell } from '../cells/constructCell' import type { Table_Internal } from '../../types/Table' import type { RowData } from '../../types/type-utils' @@ -21,7 +21,7 @@ export function row_getValue< TFeatures extends TableFeatures, TData extends RowData, >(row: Row, columnId: string) { - if (row._valuesCache.hasOwnProperty(columnId)) { + if (hasOwn(row._valuesCache, columnId)) { return row._valuesCache[columnId] } @@ -51,7 +51,7 @@ export function row_getUniqueValues< TFeatures extends TableFeatures, TData extends RowData, >(row: Row, columnId: string) { - if (row._uniqueValuesCache.hasOwnProperty(columnId)) { + if (hasOwn(row._uniqueValuesCache, columnId)) { return row._uniqueValuesCache[columnId] } @@ -192,7 +192,7 @@ export function row_getAllCellsByColumnId< TFeatures extends TableFeatures, TData extends RowData, >(row: Row) { - const result: Record> = {} + const result = makeObjectMap>() const cells = row.getAllCells() for (let i = 0; i < cells.length; i++) { const cell = cells[i]! diff --git a/packages/table-core/src/features/column-filtering/columnFilteringFeature.ts b/packages/table-core/src/features/column-filtering/columnFilteringFeature.ts index ac5d92bd6e..a6d36e3b5c 100644 --- a/packages/table-core/src/features/column-filtering/columnFilteringFeature.ts +++ b/packages/table-core/src/features/column-filtering/columnFilteringFeature.ts @@ -1,6 +1,7 @@ import { assignPrototypeAPIs, assignTableAPIs, + makeObjectMap, makeStateUpdater, } from '../../utils' import { @@ -69,8 +70,8 @@ export const columnFilteringFeature: TableFeature = { }, initRowInstanceData: (row) => { - ;(row as any).columnFilters = {} - ;(row as any).columnFiltersMeta = {} + ;(row as any).columnFilters = makeObjectMap() + ;(row as any).columnFiltersMeta = makeObjectMap() }, constructTableAPIs: (table) => { diff --git a/packages/table-core/src/features/column-filtering/createFilteredRowModel.ts b/packages/table-core/src/features/column-filtering/createFilteredRowModel.ts index 29cec6fb47..74a4b87891 100644 --- a/packages/table-core/src/features/column-filtering/createFilteredRowModel.ts +++ b/packages/table-core/src/features/column-filtering/createFilteredRowModel.ts @@ -1,4 +1,4 @@ -import { tableMemo } from '../../utils' +import { makeObjectMap, tableMemo } from '../../utils' import { table_getColumn } from '../../core/columns/coreColumnsFeature.utils' import { column_getCanGlobalFilter, @@ -60,8 +60,8 @@ function _createFilteredRowModel< > for (let i = 0; i < flatRows.length; i++) { const row = flatRows[i]! - row.columnFilters = {} - row.columnFiltersMeta = {} + row.columnFilters = makeObjectMap() + row.columnFiltersMeta = makeObjectMap() } return rowModel } @@ -115,7 +115,8 @@ function _createFilteredRowModel< > for (let i = 0; i < flatRows.length; i++) { const row = flatRows[i]! - row.columnFilters = {} + row.columnFilters = makeObjectMap() + row.columnFiltersMeta = makeObjectMap() if (resolvedColumnFilters.length) { for (let j = 0; j < resolvedColumnFilters.length; j++) { @@ -128,9 +129,10 @@ function _createFilteredRowModel< id, currentColumnFilter.resolvedValue, (filterMeta) => { - !row.columnFiltersMeta - ? (row.columnFiltersMeta = {}) - : (row.columnFiltersMeta[id] = filterMeta) + if (!row.columnFiltersMeta) { + row.columnFiltersMeta = makeObjectMap() + } + row.columnFiltersMeta[id] = filterMeta }, ) } @@ -147,9 +149,10 @@ function _createFilteredRowModel< id, currentGlobalFilter.resolvedValue, (filterMeta) => { - !row.columnFiltersMeta - ? (row.columnFiltersMeta = {}) - : (row.columnFiltersMeta[id] = filterMeta) + if (!row.columnFiltersMeta) { + row.columnFiltersMeta = makeObjectMap() + } + row.columnFiltersMeta[id] = filterMeta }, ) ) { diff --git a/packages/table-core/src/features/column-filtering/filterRowsUtils.ts b/packages/table-core/src/features/column-filtering/filterRowsUtils.ts index 1a995b4ed2..e8a701a7d6 100644 --- a/packages/table-core/src/features/column-filtering/filterRowsUtils.ts +++ b/packages/table-core/src/features/column-filtering/filterRowsUtils.ts @@ -1,4 +1,5 @@ import { constructRow } from '../../core/rows/constructRow' +import { makeObjectMap } from '../../utils' import type { Row_ColumnFiltering } from './columnFilteringFeature.types' import type { RowModel } from '../../core/row-models/coreRowModelsFeature.types' import type { Row } from '../../types/Row' @@ -37,7 +38,7 @@ function filterRowModelFromLeafs< table: Table_Internal, ): RowModel { const newFilteredFlatRows: Array> = [] - const newFilteredRowsById: Record> = {} + const newFilteredRowsById = makeObjectMap>() const maxDepth = table.options.maxLeafRowFilterDepth ?? 100 const recurseFilterRows = ( @@ -109,7 +110,7 @@ function filterRowModelFromRoot< table: Table_Internal, ): RowModel { const newFilteredFlatRows: Array> = [] - const newFilteredRowsById: Record> = {} + const newFilteredRowsById = makeObjectMap>() const maxDepth = table.options.maxLeafRowFilterDepth ?? 100 // Filters top level and nested rows diff --git a/packages/table-core/src/features/column-grouping/columnGroupingFeature.ts b/packages/table-core/src/features/column-grouping/columnGroupingFeature.ts index bff57c9f16..b20aa82e20 100644 --- a/packages/table-core/src/features/column-grouping/columnGroupingFeature.ts +++ b/packages/table-core/src/features/column-grouping/columnGroupingFeature.ts @@ -1,6 +1,7 @@ import { assignPrototypeAPIs, assignTableAPIs, + makeObjectMap, makeStateUpdater, } from '../../utils' import { @@ -99,7 +100,7 @@ export const columnGroupingFeature: TableFeature = { }, initRowInstanceData: (row) => { - ;(row as any)._groupingValuesCache = {} + ;(row as any)._groupingValuesCache = makeObjectMap() }, constructTableAPIs: (table) => { diff --git a/packages/table-core/src/features/column-grouping/columnGroupingFeature.utils.ts b/packages/table-core/src/features/column-grouping/columnGroupingFeature.utils.ts index 7b96c2ceac..49c26083b3 100644 --- a/packages/table-core/src/features/column-grouping/columnGroupingFeature.utils.ts +++ b/packages/table-core/src/features/column-grouping/columnGroupingFeature.utils.ts @@ -1,4 +1,4 @@ -import { cloneState, isFunction } from '../../utils' +import { cloneState, hasOwn, isFunction } from '../../utils' import type { Column_Internal } from '../../types/Column' import type { CellData, RowData, Updater } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' @@ -268,7 +268,7 @@ export function row_getGroupingValue< TFeatures extends TableFeatures, TData extends RowData, >(row: Row & Partial, columnId: string) { - if (row._groupingValuesCache?.hasOwnProperty(columnId)) { + if (row._groupingValuesCache && hasOwn(row._groupingValuesCache, columnId)) { return row._groupingValuesCache[columnId] } diff --git a/packages/table-core/src/features/column-grouping/createGroupedRowModel.ts b/packages/table-core/src/features/column-grouping/createGroupedRowModel.ts index 35d74464fc..e370593148 100644 --- a/packages/table-core/src/features/column-grouping/createGroupedRowModel.ts +++ b/packages/table-core/src/features/column-grouping/createGroupedRowModel.ts @@ -1,4 +1,4 @@ -import { flattenBy, tableMemo } from '../../utils' +import { flattenBy, hasOwn, makeObjectMap, tableMemo } from '../../utils' import { constructRow } from '../../core/rows/constructRow' import { table_getColumn } from '../../core/columns/coreColumnsFeature.utils' import { table_autoResetExpanded } from '../row-expanding/rowExpandingFeature.utils' @@ -69,7 +69,7 @@ function _createGroupedRowModel< const groupedFlatRows: Array> & Partial = [] - const groupedRowsById: Record> = {} + const groupedRowsById = makeObjectMap>() // Recursively group the data const groupUpRecursively = ( @@ -135,7 +135,7 @@ function _createGroupedRowModel< getValue: (colId: string) => { // Don't aggregate columns that are in the grouping if (existingGrouping.includes(colId)) { - if (row._valuesCache.hasOwnProperty(colId)) { + if (hasOwn(row._valuesCache, colId)) { return row._valuesCache[colId] } @@ -147,7 +147,10 @@ function _createGroupedRowModel< return row._valuesCache[colId] } - if (row._groupingValuesCache?.hasOwnProperty(colId)) { + if ( + row._groupingValuesCache && + hasOwn(row._groupingValuesCache, colId) + ) { return row._groupingValuesCache[colId] } @@ -157,7 +160,9 @@ function _createGroupedRowModel< column as Column, ) - if (!row._groupingValuesCache) row._groupingValuesCache = {} + if (!row._groupingValuesCache) { + row._groupingValuesCache = makeObjectMap() + } if (aggregateFn) { row._groupingValuesCache[colId] = aggregateFn( diff --git a/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts b/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts index 75c278c23c..8d241dbccb 100644 --- a/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts +++ b/packages/table-core/src/features/column-resizing/columnResizingFeature.utils.ts @@ -3,7 +3,7 @@ import { header_getSize, table_setColumnSizing, } from '../column-sizing/columnSizingFeature.utils' -import { cloneState } from '../../utils' +import { cloneState, makeObjectMap } from '../../utils' import type { CellData, RowData, Updater } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' import type { Table_Internal } from '../../types/Table' @@ -123,7 +123,7 @@ export function header_getResizeHandler< ? Math.round(event.touches[0]!.clientX) : (event as MouseEvent).clientX - const newColumnSizing: ColumnSizingState = {} + const newColumnSizing: ColumnSizingState = makeObjectMap() const updateOffset = (eventType: 'move' | 'end', clientXPos?: number) => { if (typeof clientXPos !== 'number') { @@ -164,10 +164,9 @@ export function header_getResizeHandler< column.table.options.columnResizeMode === 'onChange' || eventType === 'end' ) { - table_setColumnSizing(column.table, (old) => ({ - ...old, - ...newColumnSizing, - })) + table_setColumnSizing(column.table, (old) => + Object.assign(makeObjectMap(), old, newColumnSizing), + ) } } diff --git a/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts b/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts index 89ce6ce02c..25dc833674 100644 --- a/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts +++ b/packages/table-core/src/features/column-sizing/columnSizingFeature.utils.ts @@ -5,7 +5,12 @@ import { table_getRightHeaderGroups, } from '../column-pinning/columnPinningFeature.utils' import { column_getIndex } from '../column-ordering/columnOrderingFeature.utils' -import { callMemoOrStaticFn, cloneState } from '../../utils' +import { + callMemoOrStaticFn, + cloneState, + hasOwn, + makeObjectMap, +} from '../../utils' import type { ColumnPinningPosition } from '../column-pinning/columnPinningFeature.types' import type { CellData, RowData, Updater } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' @@ -26,7 +31,7 @@ import type { ColumnSizingState } from './columnSizingFeature.types' * ``` */ export function getDefaultColumnSizingState(): ColumnSizingState { - return {} + return makeObjectMap() } /** @@ -66,7 +71,11 @@ export function column_getSize< TValue extends CellData = CellData, >(column: Column_Internal): number { const defaultSizes = getDefaultColumnSizingColumnDef() - const columnSize = column.table.atoms.columnSizing?.get()?.[column.id] + const columnSizing = column.table.atoms.columnSizing?.get() + const columnSize = + columnSizing && hasOwn(columnSizing, column.id) + ? columnSizing[column.id] + : undefined return Math.min( Math.max( @@ -174,7 +183,15 @@ export function column_resetSize< TData extends RowData, TValue extends CellData = CellData, >(column: Column_Internal) { - table_setColumnSizing(column.table, ({ [column.id]: _, ...rest }) => { + table_setColumnSizing(column.table, (old) => { + const rest = makeObjectMap() + const columnIds = Object.keys(old) + for (let i = 0; i < columnIds.length; i++) { + const columnId = columnIds[i]! + if (columnId !== column.id) { + rest[columnId] = old[columnId]! + } + } return rest }) } @@ -280,7 +297,12 @@ export function table_resetColumnSizing< >(table: Table_Internal, defaultState?: boolean) { table_setColumnSizing( table, - defaultState ? {} : cloneState(table.initialState.columnSizing ?? {}), + defaultState + ? makeObjectMap() + : Object.assign( + makeObjectMap(), + cloneState(table.initialState.columnSizing ?? {}), + ), ) } diff --git a/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts b/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts index 7c669d871b..5d1aede3b8 100644 --- a/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts +++ b/packages/table-core/src/features/column-visibility/columnVisibilityFeature.utils.ts @@ -1,4 +1,9 @@ -import { callMemoOrStaticFn, cloneState } from '../../utils' +import { + callMemoOrStaticFn, + cloneState, + hasOwn, + makeObjectMap, +} from '../../utils' import { getDefaultColumnPinningState } from '../column-pinning/columnPinningFeature.utils' import type { CellData, RowData, Updater } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' @@ -20,7 +25,7 @@ import type { Row } from '../../types/Row' * ``` */ export function getDefaultColumnVisibilityState(): ColumnVisibilityState { - return {} + return makeObjectMap() } /** @@ -40,12 +45,13 @@ export function column_toggleVisibility< TValue extends CellData = CellData, >(column: Column_Internal, visible?: boolean): void { if (column_getCanHide(column)) { - table_setColumnVisibility(column.table, (old) => ({ - ...old, - [column.id]: + table_setColumnVisibility(column.table, (old) => { + const next = Object.assign(makeObjectMap(), old) + next[column.id] = visible ?? - !callMemoOrStaticFn(column, 'getIsVisible', column_getIsVisible), - })) + !callMemoOrStaticFn(column, 'getIsVisible', column_getIsVisible) + return next + }) } } @@ -67,12 +73,17 @@ export function column_getIsVisible< TValue extends CellData = CellData, >(column: Column_Internal): boolean { const childColumns = column.columns + if (childColumns.length) { + return childColumns.some((childColumn) => + callMemoOrStaticFn(childColumn, 'getIsVisible', column_getIsVisible), + ) + } + + const columnVisibility = column.table.atoms.columnVisibility?.get() return ( - (childColumns.length - ? childColumns.some((childColumn) => - callMemoOrStaticFn(childColumn, 'getIsVisible', column_getIsVisible), - ) - : column.table.atoms.columnVisibility?.get()?.[column.id]) ?? true + (columnVisibility && hasOwn(columnVisibility, column.id) + ? columnVisibility[column.id] + : undefined) ?? true ) } @@ -192,7 +203,7 @@ export function row_getVisibleCellsByColumnId< TFeatures extends TableFeatures, TData extends RowData, >(row: Row): Record> { - const result: Record> = {} + const result = makeObjectMap>() const allCells = row.getAllCells() for (let i = 0; i < allCells.length; i++) { const cell = allCells[i]! @@ -286,7 +297,12 @@ export function table_resetColumnVisibility< >(table: Table_Internal, defaultState?: boolean) { table_setColumnVisibility( table, - defaultState ? {} : cloneState(table.initialState.columnVisibility ?? {}), + defaultState + ? makeObjectMap() + : Object.assign( + makeObjectMap(), + cloneState(table.initialState.columnVisibility ?? {}), + ), ) } @@ -306,7 +322,7 @@ export function table_toggleAllColumnsVisible< >(table: Table_Internal, value?: boolean) { value = value ?? !table_getIsAllColumnsVisible(table) - const visibility: Record = {} + const visibility = makeObjectMap() const leafColumns = table.getAllLeafColumns() for (let i = 0; i < leafColumns.length; i++) { const column = leafColumns[i]! diff --git a/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts b/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts index cb99ad56f2..887316ec59 100644 --- a/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts +++ b/packages/table-core/src/features/row-expanding/rowExpandingFeature.utils.ts @@ -1,4 +1,4 @@ -import { cloneState } from '../../utils' +import { cloneState, hasOwn, makeObjectMap } from '../../utils' import type { RowData, Updater } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' import type { Table_Internal } from '../../types/Table' @@ -20,7 +20,7 @@ import type { * ``` */ export function getDefaultExpandedState(): ExpandedState { - return {} + return makeObjectMap() } /** @@ -85,7 +85,7 @@ export function table_toggleAllRowsExpanded< if (expanded ?? !table_getIsAllRowsExpanded(table)) { table_setExpanded(table, true) } else { - table_setExpanded(table, {}) + table_setExpanded(table, makeObjectMap()) } } @@ -105,9 +105,17 @@ export function table_resetExpanded< TFeatures extends TableFeatures, TData extends RowData, >(table: Table_Internal, defaultState?: boolean) { + const initialExpanded = table.initialState.expanded table_setExpanded( table, - defaultState ? {} : cloneState(table.initialState.expanded ?? {}), + defaultState + ? makeObjectMap() + : initialExpanded === true + ? true + : Object.assign( + makeObjectMap(), + cloneState(initialExpanded ?? {}), + ), ) } @@ -252,29 +260,34 @@ export function row_toggleExpanded< TData extends RowData, >(row: Row, expanded?: boolean) { table_setExpanded(row.table, (old) => { - const exists = old === true ? true : !!old[row.id] + const exists = old === true ? true : isExpandedRowId(old, row.id) - let oldExpanded: ExpandedStateList = {} + let oldExpanded: ExpandedStateList = makeObjectMap() if (old === true) { Object.keys(row.table.getRowModel().rowsById).forEach((rowId) => { oldExpanded[rowId] = true }) } else { - oldExpanded = old + oldExpanded = Object.assign(makeObjectMap(), old) } expanded = expanded ?? !exists if (!exists && expanded) { - return { - ...oldExpanded, - [row.id]: true, - } + oldExpanded[row.id] = true + return oldExpanded } if (exists && !expanded) { - const { [row.id]: _, ...rest } = oldExpanded + const rest: ExpandedStateList = makeObjectMap() + const rowIds = Object.keys(oldExpanded) + for (let i = 0; i < rowIds.length; i++) { + const rowId = rowIds[i]! + if (rowId !== row.id && oldExpanded[rowId]) { + rest[rowId] = true + } + } return rest } @@ -301,7 +314,19 @@ export function row_getIsExpanded< return !!( row.table.options.getIsRowExpanded?.(row) ?? - (expanded === true || expanded[row.id]) + (expanded === true || isExpandedRowId(expanded, row.id)) + ) +} + +function isExpandedRowId( + expanded: ExpandedState | undefined, + rowId: string, +): boolean { + return !!( + expanded && + expanded !== true && + hasOwn(expanded, rowId) && + expanded[rowId] ) } diff --git a/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts b/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts index a04e84510e..7279af6241 100644 --- a/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts +++ b/packages/table-core/src/features/row-selection/rowSelectionFeature.utils.ts @@ -1,4 +1,4 @@ -import { cloneState } from '../../utils' +import { cloneState, hasOwn, makeObjectMap } from '../../utils' import type { RowData, Updater } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' import type { RowModel } from '../../core/row-models/coreRowModelsFeature.types' @@ -20,7 +20,7 @@ import type { RowSelectionState } from './rowSelectionFeature.types' * ``` */ export function getDefaultRowSelectionState(): RowSelectionState { - return {} + return makeObjectMap() } /** @@ -62,7 +62,12 @@ export function table_resetRowSelection< >(table: Table_Internal, defaultState?: boolean) { table_setRowSelection( table, - defaultState ? {} : cloneState(table.initialState.rowSelection ?? {}), + defaultState + ? makeObjectMap() + : Object.assign( + makeObjectMap(), + cloneState(table.initialState.rowSelection ?? {}), + ), ) } @@ -87,7 +92,10 @@ export function table_toggleAllRowsSelected< value = typeof value !== 'undefined' ? value : !table_getIsAllRowsSelected(table) - const rowSelection = { ...old } + const rowSelection = Object.assign( + makeObjectMap(), + old, + ) const preGroupedFlatRows = table.getPreGroupedRowModel().flatRows @@ -131,7 +139,10 @@ export function table_toggleAllPageRowsSelected< ? value : !table_getIsAllPageRowsSelected(table) - const rowSelection: RowSelectionState = { ...old } + const rowSelection: RowSelectionState = Object.assign( + makeObjectMap(), + old, + ) table.getRowModel().rows.forEach((row) => { mutateRowIsSelected(rowSelection, row.id, resolvedValue, true, table) @@ -180,7 +191,7 @@ export function table_getSelectedRowModel< return { rows: [], flatRows: [], - rowsById: {}, + rowsById: makeObjectMap(), } } @@ -208,7 +219,7 @@ export function table_getFilteredSelectedRowModel< return { rows: [], flatRows: [], - rowsById: {}, + rowsById: makeObjectMap(), } } @@ -236,7 +247,7 @@ export function table_getGroupedSelectedRowModel< return { rows: [], flatRows: [], - rowsById: {}, + rowsById: makeObjectMap(), } } @@ -268,7 +279,8 @@ export function table_getIsAllRowsSelected< if (isAllRowsSelected) { if ( preGroupedFlatRows.some( - (row) => row_getCanSelect(row) && !rowSelection[row.id], + (row) => + row_getCanSelect(row) && !isRowIdSelected(rowSelection, row.id), ) ) { isAllRowsSelected = false @@ -301,7 +313,7 @@ export function table_getIsAllPageRowsSelected< if ( isAllPageRowsSelected && - paginationFlatRows.some((row) => !rowSelection[row.id]) + paginationFlatRows.some((row) => !isRowIdSelected(rowSelection, row.id)) ) { isAllPageRowsSelected = false } @@ -434,7 +446,10 @@ export function row_toggleSelected< return old } - const selectedRowIds = { ...old } + const selectedRowIds = Object.assign( + makeObjectMap(), + old, + ) mutateRowIsSelected( selectedRowIds, @@ -640,7 +655,7 @@ export function selectRowsFn< TData extends RowData, >(rowModel: RowModel): RowModel { const newSelectedFlatRows: Array> = [] - const newSelectedRowsById: Record> = {} + const newSelectedRowsById = makeObjectMap>() // Filters top level and nested rows. const recurseRows = ( @@ -695,7 +710,14 @@ export function isRowSelected< TFeatures extends TableFeatures, TData extends RowData, >(row: Row): boolean { - return (row.table.atoms.rowSelection?.get() ?? {})[row.id] ?? false + return isRowIdSelected(row.table.atoms.rowSelection?.get(), row.id) +} + +function isRowIdSelected( + rowSelection: RowSelectionState | undefined, + rowId: string, +): boolean { + return !!(rowSelection && hasOwn(rowSelection, rowId) && rowSelection[rowId]) } /** diff --git a/packages/table-core/src/features/row-sorting/createSortedRowModel.ts b/packages/table-core/src/features/row-sorting/createSortedRowModel.ts index 42e8578233..87415c6efd 100644 --- a/packages/table-core/src/features/row-sorting/createSortedRowModel.ts +++ b/packages/table-core/src/features/row-sorting/createSortedRowModel.ts @@ -1,4 +1,4 @@ -import { tableMemo } from '../../utils' +import { makeObjectMap, tableMemo } from '../../utils' import { table_autoResetPageIndex } from '../row-pagination/rowPaginationFeature.utils' import { column_getCanSort, column_getSortFn } from './rowSortingFeature.utils' import type { Column_Internal } from '../../types/Column' @@ -57,14 +57,11 @@ function _createSortedRowModel< ), ) - const columnInfoById: Record< - string, - { - sortUndefined?: false | -1 | 1 | 'first' | 'last' - invertSorting?: boolean - sortFn: SortFn - } - > = {} + const columnInfoById = makeObjectMap<{ + sortUndefined?: false | -1 | 1 | 'first' | 'last' + invertSorting?: boolean + sortFn: SortFn + }>() availableSorting.forEach((sortEntry) => { const column: Column_Internal | undefined = diff --git a/packages/table-core/src/utils.ts b/packages/table-core/src/utils.ts index 4d0e1b4b0e..c5d642d2d2 100755 --- a/packages/table-core/src/utils.ts +++ b/packages/table-core/src/utils.ts @@ -31,12 +31,17 @@ export function cloneState(value: T): T { return value } - const copy: Record = {} + const copy: Record = proto === null ? makeObjectMap() : {} const keys = Object.keys(value) for (let i = 0; i < keys.length; i++) { const key = keys[i]! - copy[key] = cloneState((value as Record)[key]) + Object.defineProperty(copy, key, { + configurable: true, + enumerable: true, + value: cloneState((value as Record)[key]), + writable: true, + }) } return copy as T @@ -45,6 +50,23 @@ export function cloneState(value: T): T { return value } +/** + * Creates an object intended only for string-keyed dictionary lookups. + * + * The null prototype keeps user-controlled ids such as `__proto__` and + * `hasOwnProperty` as plain data keys. + */ +export function makeObjectMap(): Record { + return Object.create(null) as Record +} + +/** + * Checks whether an object owns a key, including null-prototype dictionaries. + */ +export function hasOwn(obj: object, key: PropertyKey): boolean { + return Object.prototype.hasOwnProperty.call(obj, key) +} + /** * Creates a table state updater for a single state slice. * diff --git a/packages/table-core/tests/implementation/prototypeSafeDictionaries.test.ts b/packages/table-core/tests/implementation/prototypeSafeDictionaries.test.ts new file mode 100644 index 0000000000..4f87a54ed1 --- /dev/null +++ b/packages/table-core/tests/implementation/prototypeSafeDictionaries.test.ts @@ -0,0 +1,227 @@ +import { describe, expect, it } from 'vitest' +import { + aggregationFns, + columnFilteringFeature, + columnGroupingFeature, + columnVisibilityFeature, + constructTable, + coreFeatures, + createFilteredRowModel, + createGroupedRowModel, + createSortedRowModel, + filterFns, + rowSelectionFeature, + rowSortingFeature, + sortFns, +} from '../../src' +import { storeReactivityBindings } from '../../src/store-reactivity-bindings' +import type { ColumnDef } from '../../src' + +type TestRow = Record + +const features = { + ...coreFeatures, + columnFilteringFeature, + columnGroupingFeature, + columnVisibilityFeature, + rowSelectionFeature, + rowSortingFeature, + coreReactivityFeature: storeReactivityBindings(), + filteredRowModel: createFilteredRowModel(), + groupedRowModel: createGroupedRowModel(), + sortedRowModel: createSortedRowModel(), + aggregationFns, + filterFns, + sortFns, +} + +const PROTOTYPE_IDS = [ + 'hasOwnProperty', + 'toString', + 'constructor', + 'valueOf', + '__proto__', + 'isPrototypeOf', +] + +const hasOwn = (obj: object, key: string) => + Object.prototype.hasOwnProperty.call(obj, key) + +function rowFromEntries(entries: Array<[string, unknown]>): TestRow { + return Object.fromEntries(entries) +} + +function prototypeNamedRow(suffix: string): TestRow { + return rowFromEntries(PROTOTYPE_IDS.map((id) => [id, `${id}-${suffix}`])) +} + +function columnsFor( + columnIds: ReadonlyArray, +): Array> { + return columnIds.map((id) => ({ + id, + accessorFn: (row) => row[id], + })) +} + +function makeTable({ + columns = columnsFor(PROTOTYPE_IDS), + data = [prototypeNamedRow('value')], + getRowId, + initialState, +}: { + columns?: Array> + data?: Array + getRowId?: (row: TestRow, index: number) => string + initialState?: Record +} = {}) { + return constructTable({ + features, + renderFallbackValue: '', + data, + columns, + getRowId, + initialState: initialState, + }) +} + +describe('prototype-safe dictionary keys', () => { + it('caches row values and unique values for prototype-named column ids', () => { + const table = makeTable() + const row = table.getCoreRowModel().rows[0]! + + for (const id of PROTOTYPE_IDS) { + expect(row.getValue(id)).toBe(`${id}-value`) + expect(row.getValue(id)).toBe(`${id}-value`) + + expect(row.getUniqueValues(id)).toEqual([`${id}-value`]) + expect(row.getUniqueValues(id)).toEqual([`${id}-value`]) + } + }) + + it('builds own-property lookup maps for prototype-named column ids', () => { + const table = makeTable() + const row = table.getCoreRowModel().rows[0]! + const flatColumnsById = table.getAllFlatColumnsById() + const leafColumnsById = table.getAllLeafColumnsById() + const cellsById = row.getAllCellsByColumnId() + const visibleCellsById = row.getVisibleCellsByColumnId() + + expect(Object.getPrototypeOf(flatColumnsById)).toBeNull() + expect(Object.getPrototypeOf(leafColumnsById)).toBeNull() + expect(Object.getPrototypeOf(cellsById)).toBeNull() + expect(Object.getPrototypeOf(visibleCellsById)).toBeNull() + + for (const id of PROTOTYPE_IDS) { + expect(hasOwn(flatColumnsById, id)).toBe(true) + expect(hasOwn(leafColumnsById, id)).toBe(true) + expect(hasOwn(cellsById, id)).toBe(true) + expect(hasOwn(visibleCellsById, id)).toBe(true) + + expect(table.getColumn(id)?.id).toBe(id) + expect(cellsById[id]?.column.id).toBe(id) + expect(visibleCellsById[id]?.column.id).toBe(id) + } + }) + + it('filters rows through a __proto__ column id', () => { + const table = makeTable({ + columns: columnsFor(['__proto__']), + data: [ + rowFromEntries([['__proto__', 'keep']]), + rowFromEntries([['__proto__', 'drop']]), + ], + initialState: { + columnFilters: [{ id: '__proto__', value: 'keep' }], + }, + }) + + expect(table.getFilteredRowModel().rows.map((row) => row.original)).toEqual( + [rowFromEntries([['__proto__', 'keep']])], + ) + }) + + it('sorts rows through a __proto__ column id', () => { + const table = makeTable({ + columns: columnsFor(['__proto__']), + data: [ + rowFromEntries([['__proto__', 'b']]), + rowFromEntries([['__proto__', 'a']]), + ], + initialState: { + sorting: [{ id: '__proto__', desc: false }], + }, + }) + + expect( + table.getSortedRowModel().rows.map((row) => row.getValue('__proto__')), + ).toEqual(['a', 'b']) + }) + + it('groups and aggregates through prototype-named column ids', () => { + const columns: Array> = [ + { + id: 'hasOwnProperty', + accessorFn: (row) => row.hasOwnProperty, + getGroupingValue: (row) => row.hasOwnProperty, + }, + { + id: 'value', + accessorFn: (row) => row.value, + aggregationFn: 'sum', + }, + ] + const table = makeTable({ + columns, + data: [ + rowFromEntries([ + ['hasOwnProperty', 'group-a'], + ['value', 1], + ]), + rowFromEntries([ + ['hasOwnProperty', 'group-b'], + ['value', 2], + ]), + ], + initialState: { + grouping: ['hasOwnProperty'], + }, + }) + const firstCoreRow = table.getCoreRowModel().rows[0]! + + expect(firstCoreRow.getGroupingValue('hasOwnProperty')).toBe('group-a') + expect(firstCoreRow.getGroupingValue('hasOwnProperty')).toBe('group-a') + + const groupedRows = table.getGroupedRowModel().rows + + expect(groupedRows.map((row) => row.groupingValue)).toEqual([ + 'group-a', + 'group-b', + ]) + expect(groupedRows[0]?.getValue('hasOwnProperty')).toBe('group-a') + expect(groupedRows[0]?.getValue('value')).toBe(1) + expect(groupedRows[0]?.getValue('value')).toBe(1) + }) + + it('stores prototype-named row ids as own keys in row models', () => { + const rowSelection = rowFromEntries([['__proto__', true]]) + const table = makeTable({ + columns: columnsFor(['value']), + data: [rowFromEntries([['value', 'selected']])], + getRowId: () => '__proto__', + initialState: { rowSelection }, + }) + const rowModel = table.getCoreRowModel() + const row = rowModel.rows[0]! + const selectedRowModel = table.getSelectedRowModel() + + expect(Object.getPrototypeOf(rowModel.rowsById)).toBeNull() + expect(hasOwn(rowModel.rowsById, '__proto__')).toBe(true) + expect(table.getRow('__proto__')).toBe(row) + expect(row.getIsSelected()).toBe(true) + + expect(Object.getPrototypeOf(selectedRowModel.rowsById)).toBeNull() + expect(hasOwn(selectedRowModel.rowsById, '__proto__')).toBe(true) + expect(selectedRowModel.rowsById.__proto__).toBe(row) + }) +})