diff --git a/packages/table-core/src/core/columns/constructColumn.ts b/packages/table-core/src/core/columns/constructColumn.ts index b6ca11debf..b9bb8975ac 100644 --- a/packages/table-core/src/core/columns/constructColumn.ts +++ b/packages/table-core/src/core/columns/constructColumn.ts @@ -115,5 +115,11 @@ export function constructColumn< column.id = `${String(id)}` column.parent = parent + // Initialize instance-specific data for features that need it + const features = Object.values(table._features) + for (let i = 0; i < features.length; i++) { + features[i]!.initColumnInstanceData?.(column) + } + return column as Column } diff --git a/packages/table-core/src/types/TableFeatures.ts b/packages/table-core/src/types/TableFeatures.ts index 74d4d2fbe2..fb4c3e2f00 100644 --- a/packages/table-core/src/types/TableFeatures.ts +++ b/packages/table-core/src/types/TableFeatures.ts @@ -1,5 +1,6 @@ import type { CoreFeatures } from '../core/coreFeatures' import type { CellData, RowData, UnionToIntersection } from './type-utils' +import type { Column } from './Column' import type { ColumnDefBase_All } from './ColumnDef' import type { Row } from './Row' import type { Table_Internal } from './Table' @@ -11,11 +12,31 @@ import type { FilterFn } from '../features/column-filtering/columnFilteringFeatu import type { SortFn } from '../features/row-sorting/rowSortingFeature.types' import type { AggregationFn } from '../features/column-grouping/columnGroupingFeature.types' +/** + * Detects whether a type is `any`. + * + * Several feature-map helpers need a separate `any` path so broad generic + * usage still exposes all known feature APIs instead of narrowing to no keys. + */ export type IsAny = 0 extends 1 & T ? true : false + +/** + * Converts a union to an intersection, returning `{}` for `never`. + * + * This keeps empty feature-map lookups usable as an intersection member. + */ type UnionToIntersectionOrEmpty = [T] extends [never] ? {} : UnionToIntersection & {} +/** + * Extracts the API/types contributed by the features present in `TFeatures`. + * + * `TFeatureMap` maps feature keys to the types they contribute. When + * `TFeatures` is `any`, all feature-map entries are included to preserve the + * permissive behavior expected by broad table types. Otherwise, only entries + * whose keys are present in `TFeatures` are intersected together. + */ export type ExtractFeatureMapTypes< TFeatures extends TableFeatures, TFeatureMap extends object, @@ -27,7 +48,11 @@ export type ExtractFeatureMapTypes< > /** - * This is an interface that you can delcaration-merge into to allow for custom plugins to be added to the table. + * Declaration-merge target for custom table features. + * + * Add custom feature keys here so `TableFeatures` can accept them in the + * `features` option and use the same feature-map extraction system as built-in + * features. */ export interface Plugins {} @@ -64,19 +89,61 @@ export type NonFeatureKeys = * interface to get the same validation from `tableFeatures()`. */ export interface FeatureSlotPrereqs { + /** + * Named aggregation functions are only meaningful when grouping is enabled. + */ aggregationFns: 'columnGroupingFeature' - columnResizingFeature: 'columnSizingFeature' // columnSizingFeature is required for columnResizingFeature + /** + * Column resizing builds on the column sizing state and APIs. + */ + columnResizingFeature: 'columnSizingFeature' + /** + * Expanded row-model factories require row expanding APIs and state. + */ expandedRowModel: 'rowExpandingFeature' + /** + * Faceted min/max factories require column faceting APIs. + */ facetedMinMaxValues: 'columnFacetingFeature' + /** + * Faceted row-model factories require column faceting APIs. + */ facetedRowModel: 'columnFacetingFeature' + /** + * Faceted unique-value factories require column faceting APIs. + */ facetedUniqueValues: 'columnFacetingFeature' + /** + * Filtered row-model factories require column filtering APIs and state. + */ filteredRowModel: 'columnFilteringFeature' + /** + * Named filter functions are only meaningful when column filtering is enabled. + */ filterFns: 'columnFilteringFeature' + /** + * Filter metadata types are only read and written by filtering features. + */ filterMeta: 'columnFilteringFeature' - globalFilteringFeature: 'columnFilteringFeature' // columnFilteringFeature is required for globalFilteringFeature + /** + * Global filtering builds on column filtering state and filter functions. + */ + globalFilteringFeature: 'columnFilteringFeature' + /** + * Grouped row-model factories require column grouping APIs and state. + */ groupedRowModel: 'columnGroupingFeature' + /** + * Paginated row-model factories require row pagination APIs and state. + */ paginatedRowModel: 'rowPaginationFeature' + /** + * Sorted row-model factories require row sorting APIs and state. + */ sortedRowModel: 'rowSortingFeature' + /** + * Named sorting functions are only meaningful when row sorting is enabled. + */ sortFns: 'rowSortingFeature' } @@ -102,6 +169,15 @@ export type ValidateFeatureSlots = : never } +/** + * Complete feature registry for a table. + * + * This type combines core features, stock features, declaration-merged custom + * plugins, row-model factory slots, function registries, and type-only meta + * slots. The concrete `features` object passed to `tableFeatures()` determines + * which feature APIs are included throughout table, row, column, cell, header, + * options, and state types. + */ export interface TableFeatures extends Partial, Partial, Partial { /** @@ -221,10 +297,22 @@ export interface TableFeatures tableMeta?: object } +/** + * Lifecycle hooks and defaults contributed by a table feature. + * + * Feature objects are registered in the table's `features` option. They can + * contribute default state/options, default column definitions, table APIs, + * shared prototype APIs for rows/columns/headers/cells, and per-instance row + * or column data. + */ export interface TableFeature { /** - * Assigns Cell APIs to the cell prototype for memory-efficient method sharing. - * This is called once per table to build a shared prototype for all cells. + * Adds feature methods to the shared cell prototype for a table. + * + * This runs lazily the first time a cell is constructed for a table. Methods + * assigned here are shared by every cell instance created by that table, so + * this hook should be used for APIs and memoized methods rather than + * per-cell mutable data. */ assignCellPrototype?: < TFeatures extends TableFeatures, @@ -234,8 +322,12 @@ export interface TableFeature { table: Table_Internal, ) => void /** - * Assigns Column APIs to the column prototype for memory-efficient method sharing. - * This is called once per table to build a shared prototype for all columns. + * Adds feature methods to the shared column prototype for a table. + * + * This runs lazily the first time a column is constructed for a table. + * Methods assigned here are shared by every column instance created by that + * table, so this hook should be used for APIs and memoized methods rather + * than per-column mutable data. */ assignColumnPrototype?: < TFeatures extends TableFeatures, @@ -245,8 +337,12 @@ export interface TableFeature { table: Table_Internal, ) => void /** - * Assigns Header APIs to the header prototype for memory-efficient method sharing. - * This is called once per table to build a shared prototype for all headers. + * Adds feature methods to the shared header prototype for a table. + * + * This runs lazily the first time a header is constructed for a table. + * Methods assigned here are shared by every header instance created by that + * table, so this hook should be used for APIs and memoized methods rather + * than per-header mutable data. */ assignHeaderPrototype?: < TFeatures extends TableFeatures, @@ -256,35 +352,85 @@ export interface TableFeature { table: Table_Internal, ) => void /** - * Assigns Row APIs to the row prototype for memory-efficient method sharing. - * This is called once per table to build a shared prototype for all rows. + * Adds feature methods to the shared row prototype for a table. + * + * This runs lazily the first time a row is constructed for a table. Methods + * assigned here are shared by every row instance created by that table, so + * this hook should be used for APIs and memoized methods rather than per-row + * mutable data. */ assignRowPrototype?: ( prototype: Record, table: Table_Internal, ) => void /** - * Assigns Table APIs to the table instance. - * Unlike row/cell/column/header, the table is a singleton so methods are assigned directly. + * Adds feature APIs directly to the table instance. + * + * The table is a singleton, unlike rows, columns, headers, and cells, so + * table APIs are assigned directly instead of through a shared prototype. + * This runs while the table is being constructed, after options and initial + * state have been resolved. */ constructTableAPIs?: ( table: Table_Internal, ) => void + /** + * Returns default column definition options contributed by this feature. + * + * These defaults are merged into the table's default column definition before + * `options.defaultColumn` and before each user-supplied column definition is + * resolved, so users can override values supplied here. + */ getDefaultColumnDef?: < TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData = CellData, >() => ColumnDefBase_All + /** + * Returns default table options contributed by this feature. + * + * This runs while table options are being resolved. Use it for option + * defaults such as feature enablement flags and default state-updater + * callbacks. User-supplied table options take precedence over values returned + * here. + */ getDefaultTableOptions?: < TFeatures extends TableFeatures, TData extends RowData, >( table: Table_Internal, ) => Partial> + /** + * Returns this feature's initial table state. + * + * The incoming `initialState` contains state accumulated from earlier + * features and user-provided initial state. Return a complete state object + * for this feature, preserving `initialState` so user-provided values can + * override feature defaults. + */ getInitialState?: (initialState: Partial) => TableState_All /** - * Initializes instance-specific data on each row (e.g., caches). - * Methods should be assigned via assignRowPrototype instead. + * Initializes instance-specific data on each column. + * + * This runs for every constructed column after core column fields such as + * `id`, `depth`, `parent`, `columnDef`, and `columns` have been assigned. + * Use this for per-column mutable data, caches, or annotations. Shared + * methods should be assigned via `assignColumnPrototype` instead. + */ + initColumnInstanceData?: < + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, + >( + column: Column, + ) => void + /** + * Initializes instance-specific data on each row. + * + * This runs for every constructed row after core row fields such as `id`, + * `index`, `depth`, `original`, `parentId`, and `subRows` have been assigned. + * Use this for per-row mutable data, caches, or annotations. Shared methods + * should be assigned via `assignRowPrototype` instead. */ initRowInstanceData?: < TFeatures extends TableFeatures, diff --git a/packages/table-core/tests/unit/core/columns/constructColumn.test.ts b/packages/table-core/tests/unit/core/columns/constructColumn.test.ts index bd671531f8..9278581752 100644 --- a/packages/table-core/tests/unit/core/columns/constructColumn.test.ts +++ b/packages/table-core/tests/unit/core/columns/constructColumn.test.ts @@ -5,6 +5,7 @@ import { constructTable } from '../../../../src' import { storeReactivityBindings } from '../../../../src/store-reactivity-bindings' import type { ColumnDef } from '../../../../src/types/ColumnDef' import type { Table_Internal } from '../../../../src/types/Table' +import type { TableFeature } from '../../../../src/types/TableFeatures' const features = { coreColumnsFeature, @@ -15,6 +16,17 @@ interface TestRow { 'test-accessor-key'?: string } +interface ColumnAnnotation { + id: string + depth: number + parentId: string | undefined + parentAnnotationId: string | undefined +} + +interface AnnotatedColumn { + instanceAnnotation?: ColumnAnnotation +} + describe('constructColumn', () => { it('should create a column with all core column APIs and properties', () => { const table = constructTable({ @@ -46,4 +58,84 @@ describe('constructColumn', () => { expect(column.depth).toBe(depth) expect(column.parent).toBe(parent) }) + + it('should initialize instance-specific column data for every column', () => { + const initializedColumnIds: Array = [] + const annotationFeature: TableFeature = { + initColumnInstanceData: (column) => { + const parentAnnotation = ( + column.parent as (typeof column & AnnotatedColumn) | undefined + )?.instanceAnnotation + + Object.defineProperty(column, 'instanceAnnotation', { + value: { + id: column.id, + depth: column.depth, + parentId: column.parent?.id, + parentAnnotationId: parentAnnotation?.id, + } satisfies ColumnAnnotation, + enumerable: true, + configurable: true, + }) + + initializedColumnIds.push(column.id) + }, + } + + const featuresWithAnnotations = { + ...features, + annotationFeature, + } + + const columns: Array< + ColumnDef + > = [ + { + id: 'group', + header: 'Group', + columns: [ + { + id: 'child', + accessorKey: 'test-accessor-key', + }, + ], + }, + ] + + const table = constructTable({ + features: featuresWithAnnotations, + columns, + data: [], + }) + + const flatColumns = table.getAllFlatColumns() + const groupColumn = flatColumns.find((column) => column.id === 'group')! + const childColumn = flatColumns.find((column) => column.id === 'child')! + const groupAnnotation = ( + groupColumn as typeof groupColumn & AnnotatedColumn + ).instanceAnnotation + const childAnnotation = ( + childColumn as typeof childColumn & AnnotatedColumn + ).instanceAnnotation + + expect(initializedColumnIds).toEqual(['group', 'child']) + expect( + Object.prototype.hasOwnProperty.call(groupColumn, 'instanceAnnotation'), + ).toBe(true) + expect( + Object.prototype.hasOwnProperty.call(childColumn, 'instanceAnnotation'), + ).toBe(true) + expect(groupAnnotation).toEqual({ + id: 'group', + depth: 0, + parentId: undefined, + parentAnnotationId: undefined, + }) + expect(childAnnotation).toEqual({ + id: 'child', + depth: 1, + parentId: 'group', + parentAnnotationId: 'group', + }) + }) }) diff --git a/perf.md b/perf.md index 14c4c25890..bb34479352 100644 --- a/perf.md +++ b/perf.md @@ -1753,12 +1753,12 @@ table_getIsSomeRowsSelected: { **Scale impact** (`getIsAllRowsSelected` `.some()` scans saved during scroll/render churn — dimension: renders × filtered rows, selection unchanged): -| Renders × Rows | Scan walks before | After (steady state) | Saved | -| --------------- | ----------------- | -------------------- | ---------- | -| 10 × 1,000 | 10,000 | 0 | 10,000 | -| 100 × 10,000 | 1,000,000 | 0 | 1,000,000 | -| 60 × 100,000 | 6,000,000 | 0 | 6,000,000 | -| 60 × 500,000 | 30,000,000 | 0 | 30,000,000 | +| Renders × Rows | Scan walks before | After (steady state) | Saved | +| -------------- | ----------------- | -------------------- | ---------- | +| 10 × 1,000 | 10,000 | 0 | 10,000 | +| 100 × 10,000 | 1,000,000 | 0 | 1,000,000 | +| 60 × 100,000 | 6,000,000 | 0 | 6,000,000 | +| 60 × 500,000 | 30,000,000 | 0 | 30,000,000 | **Risk:** Low. Memo deps capture every input the getters read (selection, row model, `enableRowSelection`). The `getIsSome*` semantic change is intentional and documented.