diff --git a/src/cdk/table/table.spec.ts b/src/cdk/table/table.spec.ts index 6e5d8d806c33..b156bb49daa5 100644 --- a/src/cdk/table/table.spec.ts +++ b/src/cdk/table/table.spec.ts @@ -73,6 +73,7 @@ describe('CdkTable', () => { it('with a connected data source', () => { expect(table.dataSource).toBe(dataSource); expect(dataSource.isConnected).toBe(true); + expect(component.contentChangedCount).toBe(1); }); it('with a rendered header with the right number of header cells', () => { @@ -135,6 +136,9 @@ describe('CdkTable', () => { dataSource.addData(); fixture.detectChanges(); + // Expect it to have emitted once on init, and once again for the data changes. + expect(component.contentChangedCount).toBe(2); + expect(getRows(tableElement).length).toBe(dataSource.data.length); // Check that the number of cells is correct @@ -303,6 +307,9 @@ describe('CdkTable', () => { dataSource.data = originalData; fixture.detectChanges(); + // Expect it to have emitted once on init, once when empty, and again with original data. + expect(component.contentChangedCount).toBe(3); + expect(tableElement.textContent!.trim()).not.toContain('No data'); }); @@ -313,6 +320,7 @@ describe('CdkTable', () => { fixture.detectChanges(); tableElement = fixture.nativeElement.querySelector('.cdk-table'); expect(tableElement.textContent!.trim()).toContain('No data'); + expect(component.contentChangedCount).toBe(1); }); }); @@ -321,6 +329,9 @@ describe('CdkTable', () => { fixture.detectChanges(); expect(getRows(tableElement).length).toBe(0); + + // Emits that the data rows are changed even when the result is empty. + expect(component.contentChangedCount).toBe(1); })); it('should be able to render multiple header and footer rows', () => { @@ -1850,23 +1861,24 @@ class BooleanDataSource extends DataSource { @Component({ template: ` - + - Column A + Column A {{row.a}} - Footer A + Footer A - Column B + Column B {{row.b}} - Footer B + Footer B - Column C + Column C {{row.c}} - Footer C + Footer C { class SimpleCdkTableApp { dataSource: FakeDataSource | undefined = new FakeDataSource(); columnsToRender = ['column_a', 'column_b', 'column_c']; + contentChangedCount = 0; @ViewChild(CdkTable) table: CdkTable; } @@ -1937,7 +1950,8 @@ class BooleanRowCdkTableApp { @Component({ template: ` - + {{data}} @@ -1950,6 +1964,7 @@ class BooleanRowCdkTableApp { }) class NullDataCdkTableApp { dataSource = observableOf(null); + contentChangedCount = 0; } diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts index 610c81b83dcd..a5a0b29cb71a 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -34,6 +34,7 @@ import { Directive, ElementRef, EmbeddedViewRef, + EventEmitter, Inject, Input, IterableChangeRecord, @@ -42,6 +43,7 @@ import { OnDestroy, OnInit, Optional, + Output, QueryList, SkipSelf, TemplateRef, @@ -455,6 +457,13 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes } private _fixedLayout: boolean = false; + /** + * Emits when the table completes rendering a set of data rows based on the latest data from the + * data source, even if the set of rows is empty. + */ + @Output() + readonly contentChanged = new EventEmitter(); + // TODO(andrewseguin): Remove max value as the end index // and instead calculate the view on init and scroll. /** @@ -612,6 +621,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes const changes = this._dataDiffer.diff(this._renderRows); if (!changes) { this._updateNoDataRow(); + this.contentChanged.next(); return; } const viewContainer = this._rowOutlet.viewContainer; @@ -639,6 +649,8 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes this._updateNoDataRow(); this.updateStickyColumnStyles(); + + this.contentChanged.next(); } /** Adds a column definition that was not included as part of the content children. */ diff --git a/tools/public_api_guard/cdk/table.d.ts b/tools/public_api_guard/cdk/table.d.ts index f0df73bbf0c0..8aa3352d84ea 100644 --- a/tools/public_api_guard/cdk/table.d.ts +++ b/tools/public_api_guard/cdk/table.d.ts @@ -211,6 +211,7 @@ export declare class CdkTable implements AfterContentChecked, CollectionViewe _rowOutlet: DataRowOutlet; protected readonly _stickyPositioningListener: StickyPositioningListener; protected readonly _viewRepeater: _ViewRepeater, RowContext>; + readonly contentChanged: EventEmitter; get dataSource(): CdkTableDataSourceInput; set dataSource(dataSource: CdkTableDataSourceInput); get fixedLayout(): boolean; @@ -247,7 +248,7 @@ export declare class CdkTable implements AfterContentChecked, CollectionViewe updateStickyHeaderRowStyles(): void; static ngAcceptInputType_fixedLayout: BooleanInput; static ngAcceptInputType_multiTemplateDataRows: BooleanInput; - static ɵcmp: i0.ɵɵComponentDeclaration, "cdk-table, table[cdk-table]", ["cdkTable"], { "trackBy": "trackBy"; "dataSource": "dataSource"; "multiTemplateDataRows": "multiTemplateDataRows"; "fixedLayout": "fixedLayout"; }, {}, ["_noDataRow", "_contentColumnDefs", "_contentRowDefs", "_contentHeaderRowDefs", "_contentFooterRowDefs"], ["caption", "colgroup, col"]>; + static ɵcmp: i0.ɵɵComponentDeclaration, "cdk-table, table[cdk-table]", ["cdkTable"], { "trackBy": "trackBy"; "dataSource": "dataSource"; "multiTemplateDataRows": "multiTemplateDataRows"; "fixedLayout": "fixedLayout"; }, { "contentChanged": "contentChanged"; }, ["_noDataRow", "_contentColumnDefs", "_contentRowDefs", "_contentHeaderRowDefs", "_contentFooterRowDefs"], ["caption", "colgroup, col"]>; static ɵfac: i0.ɵɵFactoryDeclaration, [null, null, null, { attribute: "role"; }, { optional: true; }, null, null, null, null, null, { optional: true; skipSelf: true; }]>; }