From b8147505a9af0ae34f333b67a15928bacb93e4c1 Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Wed, 21 Jan 2026 14:11:04 +0100 Subject: [PATCH] fix(cdk/table): throw when multiple row templates are used with virtual scrolling Since we reuse rows when virtual scrolling is enabled, supporting multiple row templates is tricky. These changes throw an error and update the docs. Fixes #32670. --- src/cdk/table/table.md | 4 +++ src/cdk/table/table.spec.ts | 66 +++++++++++++++++++++++++++++-------- src/cdk/table/table.ts | 20 +++++++---- src/material/table/table.md | 4 +++ 4 files changed, 74 insertions(+), 20 deletions(-) diff --git a/src/cdk/table/table.md b/src/cdk/table/table.md index db12c29f913f..ad7bdd942f37 100644 --- a/src/cdk/table/table.md +++ b/src/cdk/table/table.md @@ -173,6 +173,10 @@ If you're showing a large amount of data in your table, you can use virtual scro smooth experience for the user. To enable virtual scrolling, you have to wrap the CDK table in a `cdk-virtual-scroll-viewport` element and add some CSS to make it scrollable. +**Note:** tables with virtual scrolling have the following limitations: +* `fixedLayout` is always enabled, in order to prevent jumping when rows are swapped out. +* Conditional templates via the `when` input are not supported. + ### Alternate HTML to using native table diff --git a/src/cdk/table/table.spec.ts b/src/cdk/table/table.spec.ts index 842033adad87..2bc5582bab80 100644 --- a/src/cdk/table/table.spec.ts +++ b/src/cdk/table/table.spec.ts @@ -2007,11 +2007,11 @@ describe('CdkTable', () => { }); describe('virtual scrolling', () => { - let fixture: ComponentFixture; - let table: HTMLTableElement; - - beforeEach(fakeAsync(() => { - fixture = TestBed.createComponent(TableWithVirtualScroll); + function createVirtualScroll(component: Type): { + fixture: ComponentFixture; + table: HTMLTableElement; + } { + const fixture = TestBed.createComponent(component); // Init logic copied from the virtual scroll tests. fixture.detectChanges(); @@ -2021,10 +2021,17 @@ describe('CdkTable', () => { tick(16); flush(); fixture.detectChanges(); - table = fixture.nativeElement.querySelector('table'); - })); - function triggerScroll(offset: number) { + return { + fixture, + table: fixture.nativeElement.querySelector('table'), + }; + } + + function triggerScroll( + fixture: ComponentFixture<{viewport: CdkVirtualScrollViewport}>, + offset: number, + ) { const viewport = fixture.componentInstance.viewport; viewport.scrollToOffset(offset); dispatchFakeEvent(viewport.scrollable!.getElementRef().nativeElement, 'scroll'); @@ -2032,24 +2039,28 @@ describe('CdkTable', () => { } it('should not render the full data set when using virtual scrolling', fakeAsync(() => { + const {fixture, table} = createVirtualScroll(TableWithVirtualScroll); expect(fixture.componentInstance.dataSource.data.length).toBeGreaterThan(2000); expect(getRows(table).length).toBe(10); })); it('should maintain a limited amount of data as the user is scrolling', fakeAsync(() => { + const {fixture, table} = createVirtualScroll(TableWithVirtualScroll); expect(getRows(table).length).toBe(10); - triggerScroll(500); + triggerScroll(fixture, 500); expect(getRows(table).length).toBe(13); - triggerScroll(500); + triggerScroll(fixture, 500); expect(getRows(table).length).toBe(13); - triggerScroll(1000); + triggerScroll(fixture, 1000); expect(getRows(table).length).toBe(12); })); it('should update the table data as the user is scrolling', fakeAsync(() => { + const {fixture, table} = createVirtualScroll(TableWithVirtualScroll); + expectTableToMatchContent(table, [ ['Column A', 'Column B', 'Column C'], ['a_1', 'b_1', 'c_1'], @@ -2065,7 +2076,7 @@ describe('CdkTable', () => { ['Footer A', 'Footer B', 'Footer C'], ]); - triggerScroll(1000); + triggerScroll(fixture, 1000); expectTableToMatchContent(table, [ ['Column A', 'Column B', 'Column C'], @@ -2086,17 +2097,19 @@ describe('CdkTable', () => { })); it('should update the position of sticky cells as the user is scrolling', fakeAsync(() => { + const {fixture, table} = createVirtualScroll(TableWithVirtualScroll); const assertStickyOffsets = (position: number) => { getHeaderCells(table).forEach(cell => expect(cell.style.top).toBe(`${position * -1}px`)); getFooterCells(table).forEach(cell => expect(cell.style.bottom).toBe(`${position}px`)); }; assertStickyOffsets(0); - triggerScroll(1000); + triggerScroll(fixture, 1000); assertStickyOffsets(884); })); it('should force tables with virtual scrolling to have a fixed layout', fakeAsync(() => { + const {fixture, table} = createVirtualScroll(TableWithVirtualScroll); expect(fixture.componentInstance.isFixedLayout()).toBe(true); expect(table.classList).toContain('cdk-table-fixed-layout'); @@ -2105,6 +2118,14 @@ describe('CdkTable', () => { expect(table.classList).toContain('cdk-table-fixed-layout'); })); + + it('should throw if multiple row templates are used with virtual scrolling', fakeAsync(() => { + expect(() => { + createVirtualScroll(TableWithVirtualScrollAndMultipleDefinitions); + }).toThrowError( + /Conditional row definitions via the `when` input are not supported when virtual scrolling is enabled/, + ); + })); }); }); @@ -3338,6 +3359,25 @@ class TableWithVirtualScroll { } } +@Component({ + template: ` + + + + + + + + +
{{row.a}}
+
+ `, + imports: [CdkTableModule, ScrollingModule], +}) +class TableWithVirtualScrollAndMultipleDefinitions extends TableWithVirtualScroll { + predicate = () => true; +} + 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 8d1781ae5be4..51cfef98f38e 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -1147,12 +1147,18 @@ export class CdkTable // After all row definitions are determined, find the row definition to be considered default. const defaultRowDefs = this._rowDefs.filter(def => !def.when); - if ( - !this.multiTemplateDataRows && - defaultRowDefs.length > 1 && - (typeof ngDevMode === 'undefined' || ngDevMode) - ) { - throw getTableMultipleDefaultRowDefsError(); + + if (typeof ngDevMode === 'undefined' || ngDevMode) { + if (this._virtualScrollEnabled() && this._rowDefs.some(def => def.when)) { + throw new Error( + 'Conditional row definitions via the `when` input are not ' + + 'supported when virtual scrolling is enabled.', + ); + } + + if (!this.multiTemplateDataRows && defaultRowDefs.length > 1) { + throw getTableMultipleDefaultRowDefsError(); + } } this._defaultRowDef = defaultRowDefs[0]; } @@ -1317,7 +1323,7 @@ export class CdkTable * definition. */ _getRowDefs(data: T, dataIndex: number): CdkRowDef[] { - if (this._rowDefs.length == 1) { + if (this._rowDefs.length === 1) { return [this._rowDefs[0]]; } diff --git a/src/material/table/table.md b/src/material/table/table.md index 2f9aba0001b1..5c3b8c9c0055 100644 --- a/src/material/table/table.md +++ b/src/material/table/table.md @@ -183,6 +183,10 @@ virtual scrolling which will only render the the visible rows in the DOM as the To enable virtual scrolling you have to wrap the Material table in a `` element and add CSS to make the viewport scrollable. +**Note:** tables with virtual scrolling have the following limitations: +* `fixedLayout` is always enabled, in order to prevent jumping when rows are swapped out. +* Conditional templates via the `when` input are not supported. + #### Sorting