Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/cdk/table/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<!-- example(cdk-table-virtual-scroll) -->

### Alternate HTML to using native table
Expand Down
66 changes: 53 additions & 13 deletions src/cdk/table/table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2007,11 +2007,11 @@ describe('CdkTable', () => {
});

describe('virtual scrolling', () => {
let fixture: ComponentFixture<TableWithVirtualScroll>;
let table: HTMLTableElement;

beforeEach(fakeAsync(() => {
fixture = TestBed.createComponent(TableWithVirtualScroll);
function createVirtualScroll<T>(component: Type<T>): {
fixture: ComponentFixture<T>;
table: HTMLTableElement;
} {
const fixture = TestBed.createComponent(component);

// Init logic copied from the virtual scroll tests.
fixture.detectChanges();
Expand All @@ -2021,35 +2021,46 @@ 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');
tick(16);
}

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'],
Expand All @@ -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'],
Expand All @@ -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');

Expand All @@ -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/,
);
}));
});
});

Expand Down Expand Up @@ -3338,6 +3359,25 @@ class TableWithVirtualScroll {
}
}

@Component({
template: `
<cdk-virtual-scroll-viewport [itemSize]="52">
<table cdk-table [dataSource]="dataSource" [fixedLayout]="isFixedLayout()">
<ng-container cdkColumnDef="column_a">
<td cdk-cell *cdkCellDef="let row"> {{row.a}}</td>
</ng-container>

<tr cdk-row *cdkRowDef="let row; columns: ['column_a']"></tr>
<tr cdk-row *cdkRowDef="let row; columns: ['column_b']; when: predicate"></tr>
</table>
</cdk-virtual-scroll-viewport>
`,
imports: [CdkTableModule, ScrollingModule],
})
class TableWithVirtualScrollAndMultipleDefinitions extends TableWithVirtualScroll {
predicate = () => true;
}

function getElements(element: Element, query: string): HTMLElement[] {
return [].slice.call(element.querySelectorAll(query));
}
Expand Down
20 changes: 13 additions & 7 deletions src/cdk/table/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1147,12 +1147,18 @@ export class CdkTable<T>

// 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];
}
Expand Down Expand Up @@ -1317,7 +1323,7 @@ export class CdkTable<T>
* definition.
*/
_getRowDefs(data: T, dataIndex: number): CdkRowDef<T>[] {
if (this._rowDefs.length == 1) {
if (this._rowDefs.length === 1) {
return [this._rowDefs[0]];
}

Expand Down
4 changes: 4 additions & 0 deletions src/material/table/table.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<cdk-virtual-scroll-viewport>`
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.

<!-- example(table-virtual-scroll) -->

#### Sorting
Expand Down
Loading