Skip to content

Commit e78587f

Browse files
authored
fix(cdk/table): throw when multiple row templates are used with virtual scrolling (#32682)
Since we reuse rows when virtual scrolling is enabled, supporting multiple row templates is tricky. These changes throw an error and update the docs.
1 parent 7cb78db commit e78587f

File tree

4 files changed

+77
-20
lines changed

4 files changed

+77
-20
lines changed

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 at the moment](https://github.com/angular/components/issues/32670).
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: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1147,12 +1147,21 @@ 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+
// At the moment of writing, it's tricky to support `when` with virtual scrolling
1153+
// because we reuse templates and they can change arbitrarily based on the `when`
1154+
// condition. We may be able to support it in the future (see #32670).
1155+
if (this._virtualScrollEnabled() && this._rowDefs.some(def => def.when)) {
1156+
throw new Error(
1157+
'Conditional row definitions via the `when` input are not ' +
1158+
'supported when virtual scrolling is enabled, at the moment.',
1159+
);
1160+
}
1161+
1162+
if (!this.multiTemplateDataRows && defaultRowDefs.length > 1) {
1163+
throw getTableMultipleDefaultRowDefsError();
1164+
}
11561165
}
11571166
this._defaultRowDef = defaultRowDefs[0];
11581167
}
@@ -1317,7 +1326,7 @@ export class CdkTable<T>
13171326
* definition.
13181327
*/
13191328
_getRowDefs(data: T, dataIndex: number): CdkRowDef<T>[] {
1320-
if (this._rowDefs.length == 1) {
1329+
if (this._rowDefs.length === 1) {
13211330
return [this._rowDefs[0]];
13221331
}
13231332

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 at the moment](https://github.com/angular/components/issues/32670).
189+
186190
<!-- example(table-virtual-scroll) -->
187191

188192
#### Sorting

0 commit comments

Comments
 (0)