diff --git a/.changeset/clean-aggregate-fallback.md b/.changeset/clean-aggregate-fallback.md new file mode 100644 index 0000000000..c2570082bf --- /dev/null +++ b/.changeset/clean-aggregate-fallback.md @@ -0,0 +1,15 @@ +--- +'@tanstack/alpine-table': patch +'@tanstack/angular-table': patch +'@tanstack/ember-table': patch +'@tanstack/lit-table': patch +'@tanstack/octane-table': patch +'@tanstack/preact-table': patch +'@tanstack/react-table': patch +'@tanstack/solid-table': patch +'@tanstack/svelte-table': patch +'@tanstack/table-core': patch +'@tanstack/vue-table': patch +--- + +Allow aggregated cells without an `aggregatedCell` renderer to fall back to the column `cell` renderer before using the default aggregate formatter. diff --git a/packages/alpine-table/src/flexRender.ts b/packages/alpine-table/src/flexRender.ts index f113a6ba9f..79403c5b6a 100644 --- a/packages/alpine-table/src/flexRender.ts +++ b/packages/alpine-table/src/flexRender.ts @@ -1,3 +1,4 @@ +import { getAggregatedCellRender } from '@tanstack/table-core/flex-render' import type { Cell, CellData, @@ -85,15 +86,9 @@ export function FlexRender< getIsAggregated?: () => boolean getIsPlaceholder?: () => boolean } - const groupingDefinition = definition as typeof definition & { - aggregatedCell?: typeof definition.cell - } if (groupingCell.getIsAggregated?.()) { - return flexRender( - groupingDefinition.aggregatedCell ?? definition.cell, - cell.getContext(), - ) + return flexRender(getAggregatedCellRender(cell), cell.getContext()) } if (groupingCell.getIsPlaceholder?.()) { diff --git a/packages/angular-table/src/helpers/flexRenderCell.ts b/packages/angular-table/src/helpers/flexRenderCell.ts index b96f02952d..0997b4e270 100644 --- a/packages/angular-table/src/helpers/flexRenderCell.ts +++ b/packages/angular-table/src/helpers/flexRenderCell.ts @@ -15,6 +15,7 @@ import { RowData, TableFeatures, } from '@tanstack/table-core' +import { getAggregatedCellRender } from '@tanstack/table-core/flex-render' import { FlexViewRenderer } from '../flex-render/renderer' import type { FlexRenderInputContent } from '../flex-render/renderer' import type { CellContext, HeaderContext } from '@tanstack/table-core' @@ -94,16 +95,12 @@ export class FlexRenderCell< const header = this.header() const footer = this.footer() if (cell) { - const def = cell.column.columnDef const groupingCell = cell as typeof cell & { getIsAggregated?: () => boolean getIsPlaceholder?: () => boolean } - const groupingDef = def as typeof def & { - aggregatedCell?: typeof def.cell - } if (groupingCell.getIsAggregated?.()) { - return [groupingDef.aggregatedCell ?? def.cell, cell.getContext()] + return [getAggregatedCellRender(cell), cell.getContext()] } if (groupingCell.getIsPlaceholder?.()) { return [null, null] diff --git a/packages/ember-table/src/FlexRender.gts b/packages/ember-table/src/FlexRender.gts index 4544b3083c..4cdf61cc84 100644 --- a/packages/ember-table/src/FlexRender.gts +++ b/packages/ember-table/src/FlexRender.gts @@ -1,7 +1,10 @@ import Component from '@glimmer/component' import { cached } from '@glimmer/tracking' import { FlexRenderComponentConfig } from './flex-render-helpers.ts' -import { flexRender } from '@tanstack/table-core/flex-render' +import { + flexRender, + getAggregatedCellRender, +} from '@tanstack/table-core/flex-render' import type { Cell_Core, CellContext, @@ -83,13 +86,10 @@ export class FlexRenderCell< getIsAggregated?: () => boolean getIsPlaceholder?: () => boolean } - const groupingDefinition = definition as typeof definition & { - aggregatedCell?: typeof definition.cell - } if (groupingCell.getIsAggregated?.()) { return flexRender( - groupingDefinition.aggregatedCell ?? definition.cell, + getAggregatedCellRender(cell), cell.getContext(), ) as CellRenderResult } diff --git a/packages/lit-table/src/flexRender.ts b/packages/lit-table/src/flexRender.ts index 35b8b5bec8..ff22d81239 100644 --- a/packages/lit-table/src/flexRender.ts +++ b/packages/lit-table/src/flexRender.ts @@ -1,3 +1,4 @@ +import { getAggregatedCellRender } from '@tanstack/table-core/flex-render' import type { Cell, CellData, @@ -110,15 +111,9 @@ export function FlexRender< getIsAggregated?: () => boolean getIsPlaceholder?: () => boolean } - const groupingColumnDef = columnDef as typeof columnDef & { - aggregatedCell?: typeof columnDef.cell - } if (groupingCell.getIsAggregated?.()) { - return flexRender( - groupingColumnDef.aggregatedCell ?? columnDef.cell, - cell.getContext(), - ) + return flexRender(getAggregatedCellRender(cell), cell.getContext()) } if (groupingCell.getIsPlaceholder?.()) { diff --git a/packages/octane-table/src/FlexRender.ts b/packages/octane-table/src/FlexRender.ts index b4a8a9f71e..d98dac344c 100644 --- a/packages/octane-table/src/FlexRender.ts +++ b/packages/octane-table/src/FlexRender.ts @@ -4,6 +4,7 @@ // builds the descriptor directly), so this file stays a normal `.ts` module and // needs no `.tsrx.d.ts` sidecar. import { createElement } from 'octane' +import { getAggregatedCellRender } from '@tanstack/table-core/flex-render' import type { CellData, RowData, TableFeatures } from '@tanstack/table-core' import type { OctaneNode } from 'octane' import type { FlexRenderProps, Renderable } from './types' @@ -65,14 +66,8 @@ export function FlexRender< getIsAggregated?: () => boolean getIsPlaceholder?: () => boolean } - const groupingDef = def as typeof def & { - aggregatedCell?: typeof def.cell - } if (groupingCell.getIsAggregated?.()) { - return flexRender( - groupingDef.aggregatedCell ?? def.cell, - cell.getContext(), - ) + return flexRender(getAggregatedCellRender(cell), cell.getContext()) } if (groupingCell.getIsPlaceholder?.()) { return null diff --git a/packages/preact-table/src/FlexRender.tsx b/packages/preact-table/src/FlexRender.tsx index 7d1e512903..b4bcbfb56f 100644 --- a/packages/preact-table/src/FlexRender.tsx +++ b/packages/preact-table/src/FlexRender.tsx @@ -1,3 +1,4 @@ +import { getAggregatedCellRender } from '@tanstack/table-core/flex-render' import type { Cell, CellData, @@ -121,14 +122,8 @@ export function FlexRender< getIsAggregated?: () => boolean getIsPlaceholder?: () => boolean } - const groupingDef = def as typeof def & { - aggregatedCell?: typeof def.cell - } if (groupingCell.getIsAggregated?.()) { - return flexRender( - groupingDef.aggregatedCell ?? def.cell, - cell.getContext(), - ) + return flexRender(getAggregatedCellRender(cell), cell.getContext()) } if (groupingCell.getIsPlaceholder?.()) { return null diff --git a/packages/react-table/src/FlexRender.tsx b/packages/react-table/src/FlexRender.tsx index 80662f348a..76be5e7af1 100644 --- a/packages/react-table/src/FlexRender.tsx +++ b/packages/react-table/src/FlexRender.tsx @@ -1,4 +1,5 @@ import React from 'react' +import { getAggregatedCellRender } from '@tanstack/table-core/flex-render' import type { Cell, CellData, @@ -106,14 +107,8 @@ export function FlexRender< getIsAggregated?: () => boolean getIsPlaceholder?: () => boolean } - const groupingDef = def as typeof def & { - aggregatedCell?: typeof def.cell - } if (groupingCell.getIsAggregated?.()) { - return flexRender( - groupingDef.aggregatedCell ?? def.cell, - cell.getContext(), - ) + return flexRender(getAggregatedCellRender(cell), cell.getContext()) } if (groupingCell.getIsPlaceholder?.()) { return null diff --git a/packages/solid-table/src/FlexRender.tsx b/packages/solid-table/src/FlexRender.tsx index 7109255129..54c1919d45 100644 --- a/packages/solid-table/src/FlexRender.tsx +++ b/packages/solid-table/src/FlexRender.tsx @@ -1,4 +1,5 @@ import { Match, Show, Switch, createComponent } from 'solid-js' +import { getAggregatedCellRender } from '@tanstack/table-core/flex-render' import type { JSX } from 'solid-js' import type { Cell, @@ -102,9 +103,6 @@ export function FlexRender< getIsAggregated?: () => boolean getIsPlaceholder?: () => boolean } - const groupingDef = def as typeof def & { - aggregatedCell?: typeof def.cell - } return ( } > - {flexRender( - groupingDef.aggregatedCell ?? def.cell, - c.getContext(), - )} + {flexRender(getAggregatedCellRender(c), c.getContext())} ) }} diff --git a/packages/svelte-table/src/FlexRender.svelte b/packages/svelte-table/src/FlexRender.svelte index 23ee661391..2318059908 100644 --- a/packages/svelte-table/src/FlexRender.svelte +++ b/packages/svelte-table/src/FlexRender.svelte @@ -3,6 +3,7 @@ generics="TFeatures extends TableFeatures, TData extends RowData, TValue extends CellData" > import { isFunction } from '@tanstack/table-core' + import { getAggregatedCellRender } from '@tanstack/table-core/flex-render' import { RenderComponentConfig, RenderSnippetConfig, @@ -66,11 +67,8 @@ getIsAggregated?: () => boolean getIsPlaceholder?: () => boolean } - const groupingColumnDef = columnDef as typeof columnDef & { - aggregatedCell?: typeof columnDef.cell - } const content = groupingCell.getIsAggregated?.() - ? (groupingColumnDef.aggregatedCell ?? columnDef.cell) + ? getAggregatedCellRender(props.cell) : groupingCell.getIsPlaceholder?.() ? undefined : columnDef.cell diff --git a/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts b/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts index 7a6ea4bed7..48796efefa 100644 --- a/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts +++ b/packages/table-core/src/core/columns/coreColumnsFeature.utils.ts @@ -1,6 +1,7 @@ import { callMemoOrStaticFn, makeObjectMap } from '../../utils' import { table_getOrderColumnsFn } from '../../features/column-ordering/columnOrderingFeature.utils' import { constructColumn } from './constructColumn' +import { defaultColumnCell } from './defaultColumnCell' import type { Table_Internal } from '../../types/Table' import type { CellData, RowData } from '../../types/type-utils' import type { TableFeatures } from '../../types/TableFeatures' @@ -98,7 +99,7 @@ export function table_getDefaultColumnDef< return null }, - cell: (props) => props.renderValue()?.toString?.() ?? null, + cell: defaultColumnCell, ...Object.values(table._features).reduce((obj, feature) => { return Object.assign(obj, feature.getDefaultColumnDef?.()) }, {}), diff --git a/packages/table-core/src/core/columns/defaultColumnCell.ts b/packages/table-core/src/core/columns/defaultColumnCell.ts new file mode 100644 index 0000000000..2336a6d9a1 --- /dev/null +++ b/packages/table-core/src/core/columns/defaultColumnCell.ts @@ -0,0 +1,3 @@ +export const defaultColumnCell = (props: { + renderValue: () => TTValue +}) => props.renderValue()?.toString?.() ?? null diff --git a/packages/table-core/src/features/row-aggregation/rowAggregationFeature.ts b/packages/table-core/src/features/row-aggregation/rowAggregationFeature.ts index 3c69aba06f..32a8af66b5 100644 --- a/packages/table-core/src/features/row-aggregation/rowAggregationFeature.ts +++ b/packages/table-core/src/features/row-aggregation/rowAggregationFeature.ts @@ -4,7 +4,6 @@ import { column_getAggregationFns, column_getAggregationValue, column_getAutoAggregationFn, - formatAggregatedCellValue, } from './rowAggregationFeature.utils' import type { TableFeature } from '../../types/TableFeatures' @@ -13,8 +12,6 @@ import type { TableFeature } from '../../types/TableFeatures' */ export const rowAggregationFeature: TableFeature = { getDefaultColumnDef: () => ({ - aggregatedCell: ({ column, getValue }: any) => - formatAggregatedCellValue(getValue(), column.columnDef.aggregationFn), aggregationFn: 'auto', maxAggregationDepth: 0, }), diff --git a/packages/table-core/src/flex-render.ts b/packages/table-core/src/flex-render.ts index 74d17bef8d..6b53f02c12 100644 --- a/packages/table-core/src/flex-render.ts +++ b/packages/table-core/src/flex-render.ts @@ -1,8 +1,25 @@ +import { defaultColumnCell } from './core/columns/defaultColumnCell' +import { formatAggregatedCellValue } from './features/row-aggregation/rowAggregationFeature.utils' +import type { CellContext } from './core/cells/coreCellsFeature.types' import type { Cell } from './types/Cell' +import type { ColumnDefTemplate } from './types/ColumnDef' import type { Header } from './types/Header' import type { TableFeatures } from './types/TableFeatures' import type { CellData, RowData } from './types/type-utils' +interface AggregatedCellRenderCell< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData, +> { + column: { + columnDef: { + cell?: ColumnDefTemplate> + } + } + getContext: () => CellContext +} + /** * Renders a static value or render function with the provided props. * @@ -21,6 +38,36 @@ export function flexRender( return comp } +export function getAggregatedCellRender< + TFeatures extends TableFeatures, + TData extends RowData, + TValue extends CellData = CellData, +>( + cell: AggregatedCellRenderCell, +): ColumnDefTemplate> { + const def = cell.column.columnDef + const groupingDef = def as typeof def & { + aggregatedCell?: ColumnDefTemplate> + } + const customCell = def.cell === defaultColumnCell ? undefined : def.cell + + return ( + groupingDef.aggregatedCell ?? + customCell ?? + ((context: CellContext) => { + const columnDef = context.column + .columnDef as typeof context.column.columnDef & { + aggregationFn?: unknown + } + + return formatAggregatedCellValue( + context.getValue(), + columnDef.aggregationFn, + ) + }) + ) +} + export type FlexRenderProps< TFeatures extends TableFeatures, TData extends RowData, @@ -65,14 +112,8 @@ export function FlexRender< getIsAggregated?: () => boolean getIsPlaceholder?: () => boolean } - const groupingDef = def as typeof def & { - aggregatedCell?: typeof def.cell - } if (groupingCell.getIsAggregated?.()) { - return flexRender( - groupingDef.aggregatedCell ?? def.cell, - cell.getContext(), - ) + return flexRender(getAggregatedCellRender(cell), cell.getContext()) } if (groupingCell.getIsPlaceholder?.()) { return null diff --git a/packages/table-core/tests/implementation/features/row-aggregation/rowAggregationFeature.test.ts b/packages/table-core/tests/implementation/features/row-aggregation/rowAggregationFeature.test.ts index 5549395328..a9259da23c 100644 --- a/packages/table-core/tests/implementation/features/row-aggregation/rowAggregationFeature.test.ts +++ b/packages/table-core/tests/implementation/features/row-aggregation/rowAggregationFeature.test.ts @@ -15,6 +15,7 @@ import { rowExpandingFeature, rowSelectionFeature, } from '../../../../src' +import { FlexRender } from '../../../../src/flex-render' import { testFeatures } from '../../../fixtures/features' import type { ColumnDef } from '../../../../src' @@ -601,12 +602,7 @@ describe('aggregation and grouping integration', () => { .getAllCells() .find((cell) => cell.column.id === 'amount')! expect(amountCell.getIsAggregated()).toBe(true) - const defaultAggregatedCell = amountCell.column.columnDef.aggregatedCell - expect( - typeof defaultAggregatedCell === 'function' - ? defaultAggregatedCell(amountCell.getContext()) - : defaultAggregatedCell, - ).toContain('sum: 130') + expect(FlexRender({ cell: amountCell })).toContain('sum: 130') expect( region .getAllCells() diff --git a/packages/table-core/tests/unit/flex-render.test.ts b/packages/table-core/tests/unit/flex-render.test.ts index aa46dc9da1..8261aaa2c5 100644 --- a/packages/table-core/tests/unit/flex-render.test.ts +++ b/packages/table-core/tests/unit/flex-render.test.ts @@ -188,10 +188,7 @@ describe('FlexRender with the column grouping feature', () => { expect(FlexRender({ cell })).toBe('Region Europe') }) - // `rowAggregationFeature` supplies a default `aggregatedCell`, so a column - // that declares none still renders the formatted aggregate rather than - // falling through to its `cell` template. - it('should use the feature default when a column defines no aggregatedCell', () => { + it('should render aggregated cells with cell when no aggregatedCell is defined', () => { const table = constructTable({ features: groupingFeatures, data, @@ -209,6 +206,27 @@ describe('FlexRender with the column grouping feature', () => { const row = table.getRowModel().rows[0]! const cell = row.getAllCells().find((c) => c.column.id === 'amount')! + expect(cell.getIsAggregated()).toBe(true) + expect(FlexRender({ cell })).toBe('Amount 3') + }) + + it('should format aggregated cells when no cell renderers are defined', () => { + const table = constructTable({ + features: groupingFeatures, + data, + columns: [ + { id: 'region', accessorKey: 'region' }, + { + id: 'amount', + accessorKey: 'amount', + aggregationFn: 'sum', + }, + ], + initialState: { grouping: ['region'] }, + }) + const row = table.getRowModel().rows[0]! + const cell = row.getAllCells().find((c) => c.column.id === 'amount')! + expect(cell.getIsAggregated()).toBe(true) expect(FlexRender({ cell })).toBe('3') }) diff --git a/packages/vue-table/src/FlexRender.ts b/packages/vue-table/src/FlexRender.ts index b257a2dd4a..aac359c959 100644 --- a/packages/vue-table/src/FlexRender.ts +++ b/packages/vue-table/src/FlexRender.ts @@ -1,4 +1,5 @@ import { defineComponent, h, isVNode } from 'vue' +import { getAggregatedCellRender } from '@tanstack/table-core/flex-render' import type { PropType } from 'vue' export interface FlexRenderCell { @@ -117,7 +118,7 @@ export const FlexRender = defineComponent({ // a custom group header typically branch on `cell.getIsGrouped()` // themselves first if (cell.getIsAggregated?.()) { - return flexRender(def.aggregatedCell ?? def.cell, cell.getContext()) + return flexRender(getAggregatedCellRender(cell), cell.getContext()) } if (cell.getIsPlaceholder?.()) { return null