Skip to content

Commit b814750

Browse files
committed
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.
1 parent e397ecb commit b814750

4 files changed

Lines changed: 74 additions & 20 deletions

File tree

src/cdk/table/table.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -173,6 +173,10 @@ If you're showing a large amount of data in your table, you can use virtual scro
173173
smooth experience for the user. To enable virtual scrolling, you have to wrap the CDK table in a
174174
`cdk-virtual-scroll-viewport` element and add some CSS to make it scrollable.
175175

176+
**Note:** tables with virtual scrolling have the following limitations:
177+
* `fixedLayout` is always enabled, in order to prevent jumping when rows are swapped out.
178+
* Conditional templates via the `when` input are not supported.
179+
176180
<!-- example(cdk-table-virtual-scroll) -->
177181

178182
### Alternate HTML to using native table

src/cdk/table/table.spec.ts

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2007,11 +2007,11 @@ describe('CdkTable', () => {
20072007
});
20082008

20092009
describe('virtual scrolling', () => {
2010-
let fixture: ComponentFixture<TableWithVirtualScroll>;
2011-
let table: HTMLTableElement;
2012-
2013-
beforeEach(fakeAsync(() => {
2014-
fixture = TestBed.createComponent(TableWithVirtualScroll);
2010+
function createVirtualScroll<T>(component: Type<T>): {
2011+
fixture: ComponentFixture<T>;
2012+
table: HTMLTableElement;
2013+
} {
2014+
const fixture = TestBed.createComponent(component);
20152015

20162016
// Init logic copied from the virtual scroll tests.
20172017
fixture.detectChanges();
@@ -2021,35 +2021,46 @@ describe('CdkTable', () => {
20212021
tick(16);
20222022
flush();
20232023
fixture.detectChanges();
2024-
table = fixture.nativeElement.querySelector('table');
2025-
}));
20262024

2027-
function triggerScroll(offset: number) {
2025+
return {
2026+
fixture,
2027+
table: fixture.nativeElement.querySelector('table'),
2028+
};
2029+
}
2030+
2031+
function triggerScroll(
2032+
fixture: ComponentFixture<{viewport: CdkVirtualScrollViewport}>,
2033+
offset: number,
2034+
) {
20282035
const viewport = fixture.componentInstance.viewport;
20292036
viewport.scrollToOffset(offset);
20302037
dispatchFakeEvent(viewport.scrollable!.getElementRef().nativeElement, 'scroll');
20312038
tick(16);
20322039
}
20332040

20342041
it('should not render the full data set when using virtual scrolling', fakeAsync(() => {
2042+
const {fixture, table} = createVirtualScroll(TableWithVirtualScroll);
20352043
expect(fixture.componentInstance.dataSource.data.length).toBeGreaterThan(2000);
20362044
expect(getRows(table).length).toBe(10);
20372045
}));
20382046

20392047
it('should maintain a limited amount of data as the user is scrolling', fakeAsync(() => {
2048+
const {fixture, table} = createVirtualScroll(TableWithVirtualScroll);
20402049
expect(getRows(table).length).toBe(10);
20412050

2042-
triggerScroll(500);
2051+
triggerScroll(fixture, 500);
20432052
expect(getRows(table).length).toBe(13);
20442053

2045-
triggerScroll(500);
2054+
triggerScroll(fixture, 500);
20462055
expect(getRows(table).length).toBe(13);
20472056

2048-
triggerScroll(1000);
2057+
triggerScroll(fixture, 1000);
20492058
expect(getRows(table).length).toBe(12);
20502059
}));
20512060

20522061
it('should update the table data as the user is scrolling', fakeAsync(() => {
2062+
const {fixture, table} = createVirtualScroll(TableWithVirtualScroll);
2063+
20532064
expectTableToMatchContent(table, [
20542065
['Column A', 'Column B', 'Column C'],
20552066
['a_1', 'b_1', 'c_1'],
@@ -2065,7 +2076,7 @@ describe('CdkTable', () => {
20652076
['Footer A', 'Footer B', 'Footer C'],
20662077
]);
20672078

2068-
triggerScroll(1000);
2079+
triggerScroll(fixture, 1000);
20692080

20702081
expectTableToMatchContent(table, [
20712082
['Column A', 'Column B', 'Column C'],
@@ -2086,17 +2097,19 @@ describe('CdkTable', () => {
20862097
}));
20872098

20882099
it('should update the position of sticky cells as the user is scrolling', fakeAsync(() => {
2100+
const {fixture, table} = createVirtualScroll(TableWithVirtualScroll);
20892101
const assertStickyOffsets = (position: number) => {
20902102
getHeaderCells(table).forEach(cell => expect(cell.style.top).toBe(`${position * -1}px`));
20912103
getFooterCells(table).forEach(cell => expect(cell.style.bottom).toBe(`${position}px`));
20922104
};
20932105

20942106
assertStickyOffsets(0);
2095-
triggerScroll(1000);
2107+
triggerScroll(fixture, 1000);
20962108
assertStickyOffsets(884);
20972109
}));
20982110

20992111
it('should force tables with virtual scrolling to have a fixed layout', fakeAsync(() => {
2112+
const {fixture, table} = createVirtualScroll(TableWithVirtualScroll);
21002113
expect(fixture.componentInstance.isFixedLayout()).toBe(true);
21012114
expect(table.classList).toContain('cdk-table-fixed-layout');
21022115

@@ -2105,6 +2118,14 @@ describe('CdkTable', () => {
21052118

21062119
expect(table.classList).toContain('cdk-table-fixed-layout');
21072120
}));
2121+
2122+
it('should throw if multiple row templates are used with virtual scrolling', fakeAsync(() => {
2123+
expect(() => {
2124+
createVirtualScroll(TableWithVirtualScrollAndMultipleDefinitions);
2125+
}).toThrowError(
2126+
/Conditional row definitions via the `when` input are not supported when virtual scrolling is enabled/,
2127+
);
2128+
}));
21082129
});
21092130
});
21102131

@@ -3338,6 +3359,25 @@ class TableWithVirtualScroll {
33383359
}
33393360
}
33403361

3362+
@Component({
3363+
template: `
3364+
<cdk-virtual-scroll-viewport [itemSize]="52">
3365+
<table cdk-table [dataSource]="dataSource" [fixedLayout]="isFixedLayout()">
3366+
<ng-container cdkColumnDef="column_a">
3367+
<td cdk-cell *cdkCellDef="let row"> {{row.a}}</td>
3368+
</ng-container>
3369+
3370+
<tr cdk-row *cdkRowDef="let row; columns: ['column_a']"></tr>
3371+
<tr cdk-row *cdkRowDef="let row; columns: ['column_b']; when: predicate"></tr>
3372+
</table>
3373+
</cdk-virtual-scroll-viewport>
3374+
`,
3375+
imports: [CdkTableModule, ScrollingModule],
3376+
})
3377+
class TableWithVirtualScrollAndMultipleDefinitions extends TableWithVirtualScroll {
3378+
predicate = () => true;
3379+
}
3380+
33413381
function getElements(element: Element, query: string): HTMLElement[] {
33423382
return [].slice.call(element.querySelectorAll(query));
33433383
}

src/cdk/table/table.ts

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,12 +1147,18 @@ export class CdkTable<T>
11471147

11481148
// After all row definitions are determined, find the row definition to be considered default.
11491149
const defaultRowDefs = this._rowDefs.filter(def => !def.when);
1150-
if (
1151-
!this.multiTemplateDataRows &&
1152-
defaultRowDefs.length > 1 &&
1153-
(typeof ngDevMode === 'undefined' || ngDevMode)
1154-
) {
1155-
throw getTableMultipleDefaultRowDefsError();
1150+
1151+
if (typeof ngDevMode === 'undefined' || ngDevMode) {
1152+
if (this._virtualScrollEnabled() && this._rowDefs.some(def => def.when)) {
1153+
throw new Error(
1154+
'Conditional row definitions via the `when` input are not ' +
1155+
'supported when virtual scrolling is enabled.',
1156+
);
1157+
}
1158+
1159+
if (!this.multiTemplateDataRows && defaultRowDefs.length > 1) {
1160+
throw getTableMultipleDefaultRowDefsError();
1161+
}
11561162
}
11571163
this._defaultRowDef = defaultRowDefs[0];
11581164
}
@@ -1317,7 +1323,7 @@ export class CdkTable<T>
13171323
* definition.
13181324
*/
13191325
_getRowDefs(data: T, dataIndex: number): CdkRowDef<T>[] {
1320-
if (this._rowDefs.length == 1) {
1326+
if (this._rowDefs.length === 1) {
13211327
return [this._rowDefs[0]];
13221328
}
13231329

src/material/table/table.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,10 @@ virtual scrolling which will only render the the visible rows in the DOM as the
183183
To enable virtual scrolling you have to wrap the Material table in a `<cdk-virtual-scroll-viewport>`
184184
element and add CSS to make the viewport scrollable.
185185

186+
**Note:** tables with virtual scrolling have the following limitations:
187+
* `fixedLayout` is always enabled, in order to prevent jumping when rows are swapped out.
188+
* Conditional templates via the `when` input are not supported.
189+
186190
<!-- example(table-virtual-scroll) -->
187191

188192
#### Sorting

0 commit comments

Comments
 (0)