diff --git a/src/cdk/collections/recycle-view-repeater-strategy.ts b/src/cdk/collections/recycle-view-repeater-strategy.ts index 735e32aec9de..f759ed1b5d7a 100644 --- a/src/cdk/collections/recycle-view-repeater-strategy.ts +++ b/src/cdk/collections/recycle-view-repeater-strategy.ts @@ -106,10 +106,12 @@ export class _RecycleViewRepeaterStrategy { expect(() => table.renderRows()).not.toThrow(); }); + describe('with recycled rows', () => { + beforeEach(() => { + setupTableTestApp(CdkTableRecycleRowsApp); + component = fixture.componentInstance as CdkTableRecycleRowsApp; + tableElement = fixture.nativeElement.querySelector('.cdk-table'); + }); + + it('should re-render cached rows when columns change', () => { + component.addExtraRows(3); + fixture.detectChanges(); + + component.setShortList(); + fixture.detectChanges(); + + const newColumns = ['column_a']; + const tableInstance = component.table as CdkTable; + const forceColumns = (defs: BaseRowDef[]) => { + defs.forEach(def => { + def.columns = newColumns; + (def as any)._columnsDiffer?.diff(newColumns); + }); + }; + + forceColumns(tableInstance['_rowDefs']); + forceColumns(tableInstance['_headerRowDefs']); + + tableInstance['_forceRenderDataRows'](); + tableInstance['_forceRenderHeaderRows'](); + fixture.detectChanges(); + + const rows = getRows(tableElement); + expect(rows.length).toBe(component.dataSource.data.length); + rows.forEach(row => { + expect(getCells(row).length).toBe(newColumns.length); + }); + }); + }); + + describe('view repeater cleanup', () => { + beforeEach(() => { + setupTableTestApp(CdkTableRecycleRowsSpyApp); + component = fixture.componentInstance as CdkTableRecycleRowsSpyApp; + }); + + it('should clear the recycled row cache when forcing a re-render', () => { + const repeater = component.table['_viewRepeater'] as _RecycleViewRepeaterStrategy< + unknown, + unknown, + _ViewRepeaterItemContext + >; + spyOn(repeater, 'clearCache'); + + component.table['_forceRenderDataRows'](); + + expect(repeater.clearCache).toHaveBeenCalledTimes(1); + }); + + it('should detach the view repeater when the table is destroyed', () => { + const destroyFixture = TestBed.createComponent(CdkTableRecycleRowsSpyApp); + destroyFixture.detectChanges(); + const table = destroyFixture.componentInstance.table; + const repeater = table['_viewRepeater'] as _RecycleViewRepeaterStrategy< + unknown, + unknown, + _ViewRepeaterItemContext + >; + spyOn(repeater, 'detach'); + + destroyFixture.destroy(); + + expect(repeater.detach).toHaveBeenCalledTimes(1); + }); + }); + describe('with different data inputs other than data source', () => { let baseData: TestData[] = [ {a: 'a_1', b: 'b_1', c: 'c_1'}, @@ -3176,6 +3255,68 @@ class WrapNativeHtmlTableAppOnPush { dataSource = new FakeDataSource(); } +@Component({ + template: ` + + + + + + + + +
Column A {{row.a}}
+ `, + imports: [CdkTableModule], +}) +class CdkTableRecycleRowsSpyApp { + dataSource = new FakeDataSource(); + + @ViewChild(CdkTable) table: CdkTable; +} + +@Component({ + template: ` + + + + + + + + + + + + + + + + + + +
Column A {{row.a}} Column B {{row.b}} Column C {{row.c}}
+ `, + imports: [CdkTableModule], +}) +class CdkTableRecycleRowsApp { + private _longColumnSet: string[] = ['column_a', 'column_b', 'column_c']; + private _shortListLength = 3; + dataSource = new FakeDataSource(); + displayedColumns = this._longColumnSet; + @ViewChild(CdkTable) table: CdkTable; + + addExtraRows(count: number) { + for (let i = 0; i < count; i++) { + this.dataSource.addData(); + } + } + + setShortList() { + this.dataSource.data = this.dataSource.data.slice(0, this._shortListLength); + } +} + function getElements(element: Element, query: string): HTMLElement[] { return [].slice.call(element.querySelectorAll(query)); } diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts index c704690e1163..f6bd2120230b 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -640,6 +640,7 @@ export class CdkTable ngOnDestroy() { this._stickyStyler?.destroy(); + this._viewRepeater.detach(); [ this._rowOutlet?.viewContainer, @@ -1330,11 +1331,19 @@ export class CdkTable * `multiTemplateDataRows` or adding/removing row definitions. */ private _forceRenderDataRows() { + this._clearReusableRowCache(); this._dataDiffer.diff([]); this._rowOutlet.viewContainer.clear(); this.renderRows(); } + /** Clears any cached data rows that were being reused. */ + private _clearReusableRowCache() { + if (this._viewRepeater instanceof _RecycleViewRepeaterStrategy) { + this._viewRepeater.clearCache(); + } + } + /** * Checks if there has been a change in sticky states since last check and applies the correct * sticky styles. Since checking resets the "dirty" state, this should only be performed once