diff --git a/packages/ember-table/src/FlexRender.gts b/packages/ember-table/src/FlexRender.gts index 66c8089f29..98215dd829 100644 --- a/packages/ember-table/src/FlexRender.gts +++ b/packages/ember-table/src/FlexRender.gts @@ -1,4 +1,5 @@ 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 type { @@ -74,6 +75,7 @@ export class FlexRenderCell< TData extends RowData, TValue extends CellData = CellData, > extends Component> { + @cached get result(): CellRenderResult { const cell = this.args.cell return flexRender( @@ -142,6 +144,7 @@ export class FlexRenderHeader< TData extends RowData, TValue extends CellData = CellData, > extends Component> { + @cached get result(): HeaderRenderResult { const header = this.args.header if (header.isPlaceholder) return null @@ -213,6 +216,7 @@ export class FlexRenderFooter< TData extends RowData, TValue extends CellData = CellData, > extends Component> { + @cached get result(): HeaderRenderResult { const footer = this.args.footer if (footer.isPlaceholder) return null diff --git a/packages/ember-table/tests/integration/flex-render-invocation-count.test.gts b/packages/ember-table/tests/integration/flex-render-invocation-count.test.gts new file mode 100644 index 0000000000..e538e54dec --- /dev/null +++ b/packages/ember-table/tests/integration/flex-render-invocation-count.test.gts @@ -0,0 +1,107 @@ +import { module, test } from 'qunit' +import { render } from '@ember/test-helpers' +import { setupRenderingTest } from 'ember-qunit' +import Component from '@glimmer/component' +import { tracked } from '@glimmer/tracking' +import { + useTable, + FlexRenderCell, + FlexRenderHeader, + stockFeatures, + type Row, + type Cell, + type ColumnDef, +} from '#src/index.ts' + +type Person = { id: string; firstName: string } + +const getVisibleCells = ( + row: Row, +): Array> => row.getVisibleCells() + +// The result of a columnDef `cell`/`header` render function is read by the +// FlexRender template more than once (branch check + content). Those reads +// must not re-invoke the user's render function: it should run exactly once +// per cell per render pass. +module('Integration | FlexRender | invocation count', function (hooks) { + setupRenderingTest(hooks) + + test('cell and header render functions run once per render pass', async (assert) => { + const calls = { cell: 0, header: 0 } + + const columns: ColumnDef[] = [ + { + id: 'firstName', + accessorFn: (row: Person) => row.firstName, + header: () => { + calls.header++ + return 'First name' + }, + cell: (info) => { + calls.cell++ + return info.getValue() + }, + }, + ] + + class TableComponent extends Component { + @tracked data: Array = [ + { id: '1', firstName: 'Alice' }, + { id: '2', firstName: 'Bob' }, + ] + + table = useTable(() => ({ + data: this.data, + features: stockFeatures, + columns, + getRowId: (row: Person) => row.id, + })) + + get rows() { + return this.table.getRowModel().rows + } + + get headers() { + return this.table.getHeaderGroups() + } + + + } + + await render() + + assert.dom('td').exists({ count: 2 }) + assert.dom('[data-test-row="1"] td').hasText('Alice') + assert.strictEqual( + calls.cell, + 2, + 'cell fn ran exactly once per cell on initial render', + ) + assert.strictEqual( + calls.header, + 1, + 'header fn ran exactly once on initial render', + ) + }) +})