diff --git a/src/cdk/table/row.ts b/src/cdk/table/row.ts index 3567b229f570..e054851fcbc7 100644 --- a/src/cdk/table/row.ts +++ b/src/cdk/table/row.ts @@ -306,3 +306,11 @@ export class CdkFooterRow { }) export class CdkRow { } + +/** Row that can be used to display a message when no data is shown in the table. */ +@Directive({ + selector: 'ng-template[cdkNoDataRow]' +}) +export class CdkNoDataRow { + constructor(public templateRef: TemplateRef) {} +} diff --git a/src/cdk/table/table-module.ts b/src/cdk/table/table-module.ts index 882e4c96ff93..45d3b3142978 100644 --- a/src/cdk/table/table-module.ts +++ b/src/cdk/table/table-module.ts @@ -7,10 +7,11 @@ */ import {NgModule} from '@angular/core'; -import {HeaderRowOutlet, DataRowOutlet, CdkTable, FooterRowOutlet} from './table'; +import {HeaderRowOutlet, DataRowOutlet, CdkTable, FooterRowOutlet, NoDataRowOutlet} from './table'; import { CdkCellOutlet, CdkFooterRow, CdkFooterRowDef, CdkHeaderRow, CdkHeaderRowDef, CdkRow, - CdkRowDef + CdkRowDef, + CdkNoDataRow } from './row'; import { CdkColumnDef, CdkHeaderCellDef, CdkHeaderCell, CdkCell, CdkCellDef, @@ -38,6 +39,8 @@ const EXPORTED_DECLARATIONS = [ HeaderRowOutlet, FooterRowOutlet, CdkTextColumn, + CdkNoDataRow, + NoDataRowOutlet, ]; @NgModule({ diff --git a/src/cdk/table/table.spec.ts b/src/cdk/table/table.spec.ts index 562f1c05c9a1..882c199a98b0 100644 --- a/src/cdk/table/table.spec.ts +++ b/src/cdk/table/table.spec.ts @@ -285,6 +285,22 @@ describe('CdkTable', () => { ['Footer C', 'Footer B'], ]); }); + + it('should be able to show a message when no data is being displayed', () => { + expect(tableElement.textContent!.trim()).not.toContain('No data'); + + const originalData = dataSource.data; + dataSource.data = []; + fixture.detectChanges(); + + expect(tableElement.textContent!.trim()).toContain('No data'); + + dataSource.data = originalData; + fixture.detectChanges(); + + expect(tableElement.textContent!.trim()).not.toContain('No data'); + }); + }); it('should render no rows when the data is null', fakeAsync(() => { @@ -497,6 +513,28 @@ describe('CdkTable', () => { expect(innerRows.map(row => row.cells.length)).toEqual([3, 3, 3]); }); + it('should be able to show a message when no data is being displayed in a native table', () => { + const thisFixture = createComponent(NativeHtmlTableApp); + thisFixture.detectChanges(); + + // Assert that the data is inside the tbody specifically. + const tbody = thisFixture.nativeElement.querySelector('tbody'); + const dataSource = thisFixture.componentInstance.dataSource!; + const originalData = dataSource.data; + + expect(tbody.textContent!.trim()).not.toContain('No data'); + + dataSource.data = []; + thisFixture.detectChanges(); + + expect(tbody.textContent!.trim()).toContain('No data'); + + dataSource.data = originalData; + thisFixture.detectChanges(); + + expect(tbody.textContent!.trim()).not.toContain('No data'); + }); + it('should apply correct roles for native table elements', () => { const thisFixture = createComponent(NativeHtmlTableApp); const thisTableElement: HTMLTableElement = thisFixture.nativeElement.querySelector('table'); @@ -1474,6 +1512,8 @@ class BooleanDataSource extends DataSource { *cdkRowDef="let row; columns: columnsToRender"> + +
No data
` }) @@ -2281,6 +2321,9 @@ class OuterTableApp { + + No data + ` }) diff --git a/src/cdk/table/table.ts b/src/cdk/table/table.ts index 2fef631e7b9b..9b004c3eb768 100644 --- a/src/cdk/table/table.ts +++ b/src/cdk/table/table.ts @@ -35,7 +35,8 @@ import { TrackByFunction, ViewChild, ViewContainerRef, - ViewEncapsulation + ViewEncapsulation, + ContentChild } from '@angular/core'; import { BehaviorSubject, @@ -54,7 +55,8 @@ import { CdkCellOutletRowContext, CdkFooterRowDef, CdkHeaderRowDef, - CdkRowDef + CdkRowDef, + CdkNoDataRow } from './row'; import {StickyStyler} from './sticky-styler'; import { @@ -106,6 +108,16 @@ export class FooterRowOutlet implements RowOutlet { constructor(public viewContainer: ViewContainerRef, public elementRef: ElementRef) {} } +/** + * Provides a handle for the table to grab the view + * container's ng-container to insert the no data row. + * @docs-private + */ +@Directive({selector: '[noDataRowOutlet]'}) +export class NoDataRowOutlet implements RowOutlet { + constructor(public viewContainer: ViewContainerRef, public elementRef: ElementRef) {} +} + /** * The table template that can be used by the mat-table. Should not be used outside of the * material library. @@ -118,6 +130,7 @@ export const CDK_TABLE_TEMPLATE = + `; @@ -292,6 +305,9 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes */ protected stickyCssClass: string = 'cdk-table-sticky'; + /** Whether the no data row is currently showing anything. */ + private _isShowingNoDataRow = false; + /** * Tracking function that will be used to check the differences in data changes. Used similarly * to `ngFor` `trackBy` function. Optimize row operations by identifying a row based on its data @@ -378,6 +394,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes @ViewChild(DataRowOutlet, {static: true}) _rowOutlet: DataRowOutlet; @ViewChild(HeaderRowOutlet, {static: true}) _headerRowOutlet: HeaderRowOutlet; @ViewChild(FooterRowOutlet, {static: true}) _footerRowOutlet: FooterRowOutlet; + @ViewChild(NoDataRowOutlet, {static: true}) _noDataRowOutlet: NoDataRowOutlet; /** * The column definitions provided by the user that contain what the header, data, and footer @@ -398,6 +415,9 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes descendants: true }) _contentFooterRowDefs: QueryList; + /** Row definition that will only be rendered if there's no data in the table. */ + @ContentChild(CdkNoDataRow) _noDataRow: CdkNoDataRow; + constructor( protected readonly _differs: IterableDiffers, protected readonly _changeDetectorRef: ChangeDetectorRef, @@ -463,6 +483,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes ngOnDestroy() { this._rowOutlet.viewContainer.clear(); + this._noDataRowOutlet.viewContainer.clear(); this._headerRowOutlet.viewContainer.clear(); this._footerRowOutlet.viewContainer.clear(); @@ -518,6 +539,7 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes rowView.context.$implicit = record.item.data; }); + this._updateNoDataRow(); this.updateStickyColumnStyles(); } @@ -1016,15 +1038,19 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes private _applyNativeTableSections() { const documentFragment = this._document.createDocumentFragment(); const sections = [ - {tag: 'thead', outlet: this._headerRowOutlet}, - {tag: 'tbody', outlet: this._rowOutlet}, - {tag: 'tfoot', outlet: this._footerRowOutlet}, + {tag: 'thead', outlets: [this._headerRowOutlet]}, + {tag: 'tbody', outlets: [this._rowOutlet, this._noDataRowOutlet]}, + {tag: 'tfoot', outlets: [this._footerRowOutlet]}, ]; for (const section of sections) { const element = this._document.createElement(section.tag); element.setAttribute('role', 'rowgroup'); - element.appendChild(section.outlet.elementRef.nativeElement); + + for (const outlet of section.outlets) { + element.appendChild(outlet.elementRef.nativeElement); + } + documentFragment.appendChild(element); } @@ -1093,6 +1119,19 @@ export class CdkTable implements AfterContentChecked, CollectionViewer, OnDes return items.filter(item => !item._table || item._table === this); } + /** Creates or removes the no data row, depending on whether any data is being shown. */ + private _updateNoDataRow() { + if (this._noDataRow) { + const shouldShow = this._rowOutlet.viewContainer.length === 0; + + if (shouldShow !== this._isShowingNoDataRow) { + const container = this._noDataRowOutlet.viewContainer; + shouldShow ? container.createEmbeddedView(this._noDataRow.templateRef) : container.clear(); + this._isShowingNoDataRow = shouldShow; + } + } + } + static ngAcceptInputType_multiTemplateDataRows: BooleanInput; } diff --git a/src/components-examples/material/table/table-filtering/table-filtering-example.html b/src/components-examples/material/table/table-filtering/table-filtering-example.html index d3f566eca289..288e7e4306d9 100644 --- a/src/components-examples/material/table/table-filtering/table-filtering-example.html +++ b/src/components-examples/material/table/table-filtering/table-filtering-example.html @@ -1,6 +1,6 @@ Filter - + @@ -31,4 +31,9 @@ + + + + +
No data matching the filter "{{input.value}}"
diff --git a/src/components-examples/material/table/table-overview/table-overview-example.html b/src/components-examples/material/table/table-overview/table-overview-example.html index e94ece60f58a..7046567851e5 100644 --- a/src/components-examples/material/table/table-overview/table-overview-example.html +++ b/src/components-examples/material/table/table-overview/table-overview-example.html @@ -1,6 +1,6 @@ Filter - +
@@ -31,7 +31,11 @@ - + + + + + No data matching the filter "{{input.value}}" diff --git a/src/material-experimental/mdc-table/module.ts b/src/material-experimental/mdc-table/module.ts index af5da1ef19d8..d1fcc6991ef2 100644 --- a/src/material-experimental/mdc-table/module.ts +++ b/src/material-experimental/mdc-table/module.ts @@ -25,7 +25,8 @@ import { MatHeaderRow, MatHeaderRowDef, MatRow, - MatRowDef + MatRowDef, + MatNoDataRow } from './row'; const EXPORTED_DECLARATIONS = [ @@ -50,6 +51,7 @@ const EXPORTED_DECLARATIONS = [ MatHeaderRow, MatRow, MatFooterRow, + MatNoDataRow, ]; @NgModule({ diff --git a/src/material-experimental/mdc-table/row.ts b/src/material-experimental/mdc-table/row.ts index 4c953e3610d2..09d11e899962 100644 --- a/src/material-experimental/mdc-table/row.ts +++ b/src/material-experimental/mdc-table/row.ts @@ -14,7 +14,8 @@ import { CdkHeaderRow, CdkHeaderRowDef, CdkRow, - CdkRowDef + CdkRowDef, + CdkNoDataRow } from '@angular/cdk/table'; import {ChangeDetectionStrategy, Component, Directive, ViewEncapsulation} from '@angular/core'; @@ -110,3 +111,11 @@ export class MatFooterRow extends CdkFooterRow { }) export class MatRow extends CdkRow { } + +/** Row that can be used to display a message when no data is shown in the table. */ +@Directive({ + selector: 'ng-template[matNoDataRow]', + providers: [{provide: CdkNoDataRow, useExisting: MatNoDataRow}], +}) +export class MatNoDataRow extends CdkNoDataRow { +} diff --git a/src/material-experimental/mdc-table/table.spec.ts b/src/material-experimental/mdc-table/table.spec.ts index 5e8c48509afc..8a95ab53574c 100644 --- a/src/material-experimental/mdc-table/table.spec.ts +++ b/src/material-experimental/mdc-table/table.spec.ts @@ -96,6 +96,27 @@ describe('MDC-based MatTable', () => { expect(innerRows.map(row => row.cells.length)).toEqual([3, 3, 3, 3]); }); + it('should be able to show a message when no data is being displayed', () => { + const fixture = TestBed.createComponent(MatTableApp); + fixture.detectChanges(); + + // Assert that the data is inside the tbody specifically. + const tbody = fixture.nativeElement.querySelector('tbody')!; + const initialData = fixture.componentInstance.dataSource!.data; + + expect(tbody.textContent.trim()).not.toContain('No data'); + + fixture.componentInstance.dataSource!.data = []; + fixture.detectChanges(); + + expect(tbody.textContent.trim()).toContain('No data'); + + fixture.componentInstance.dataSource!.data = initialData; + fixture.detectChanges(); + + expect(tbody.textContent.trim()).not.toContain('No data'); + }); + }); it('should render with MatTableDataSource and sort', () => { @@ -555,6 +576,9 @@ class FakeDataSource extends DataSource { + + No data + ` diff --git a/src/material/table/row.ts b/src/material/table/row.ts index 6664738b2be3..09b3bda4b2cd 100644 --- a/src/material/table/row.ts +++ b/src/material/table/row.ts @@ -14,7 +14,8 @@ import { CdkHeaderRow, CdkHeaderRowDef, CdkRow, - CdkRowDef + CdkRowDef, + CdkNoDataRow } from '@angular/cdk/table'; import {ChangeDetectionStrategy, Component, Directive, ViewEncapsulation} from '@angular/core'; @@ -110,3 +111,11 @@ export class MatFooterRow extends CdkFooterRow { }) export class MatRow extends CdkRow { } + +/** Row that can be used to display a message when no data is shown in the table. */ +@Directive({ + selector: 'ng-template[matNoDataRow]', + providers: [{provide: CdkNoDataRow, useExisting: MatNoDataRow}], +}) +export class MatNoDataRow extends CdkNoDataRow { +} diff --git a/src/material/table/table-module.ts b/src/material/table/table-module.ts index 4814b6255a4d..031e93ec83c3 100644 --- a/src/material/table/table-module.ts +++ b/src/material/table/table-module.ts @@ -24,7 +24,8 @@ import { MatHeaderRow, MatHeaderRowDef, MatRow, - MatRowDef + MatRowDef, + MatNoDataRow } from './row'; import {MatTextColumn} from './text-column'; import {MatCommonModule} from '@angular/material/core'; @@ -51,6 +52,7 @@ const EXPORTED_DECLARATIONS = [ MatHeaderRow, MatRow, MatFooterRow, + MatNoDataRow, MatTextColumn, ]; diff --git a/src/material/table/table.md b/src/material/table/table.md index 43e4d7bb8b28..cb5d8af44229 100644 --- a/src/material/table/table.md +++ b/src/material/table/table.md @@ -210,6 +210,9 @@ it is contained in the reduced string, and the row would be displayed in the tab To override the default filtering behavior, a custom `filterPredicate` function can be set which takes a data object and filter string and returns true if the data object is considered a match. +If you want to show a message when not data matches the filter, you can use the `*matNoDataRow` +directive. + #### Selection diff --git a/src/material/table/table.spec.ts b/src/material/table/table.spec.ts index d66968f35039..a64d1b7dc0d0 100644 --- a/src/material/table/table.spec.ts +++ b/src/material/table/table.spec.ts @@ -83,6 +83,27 @@ describe('MatTable', () => { ['Footer A'], ]); }); + + it('should be able to show a message when no data is being displayed', () => { + const fixture = TestBed.createComponent(MatTableApp); + fixture.detectChanges(); + + const table = fixture.nativeElement.querySelector('.mat-table')!; + const initialData = fixture.componentInstance.dataSource!.data; + + expect(table.textContent.trim()).not.toContain('No data'); + + fixture.componentInstance.dataSource!.data = []; + fixture.detectChanges(); + + expect(table.textContent.trim()).toContain('No data'); + + fixture.componentInstance.dataSource!.data = initialData; + fixture.detectChanges(); + + expect(table.textContent.trim()).not.toContain('No data'); + }); + }); it('should be able to render a table correctly with native elements', () => { @@ -115,6 +136,28 @@ describe('MatTable', () => { expect(innerRows.map(row => row.cells.length)).toEqual([3, 3, 3, 3]); }); + it('should be able to show a message when no data is being displayed in a native table', () => { + const fixture = TestBed.createComponent(NativeHtmlTableApp); + fixture.detectChanges(); + + // Assert that the data is inside the tbody specifically. + const tbody = fixture.nativeElement.querySelector('tbody')!; + const dataSource = fixture.componentInstance.dataSource!; + const initialData = dataSource.data; + + expect(tbody.textContent.trim()).not.toContain('No data'); + + dataSource.data = []; + fixture.detectChanges(); + + expect(tbody.textContent.trim()).toContain('No data'); + + dataSource.data = initialData; + fixture.detectChanges(); + + expect(tbody.textContent.trim()).not.toContain('No data'); + }); + it('should render with MatTableDataSource and sort', () => { let fixture = TestBed.createComponent(MatTableWithSortApp); fixture.detectChanges(); @@ -572,6 +615,7 @@ class FakeDataSource extends DataSource { +
No data
` @@ -604,6 +648,9 @@ class MatTableApp { + + No data + ` }) diff --git a/tools/public_api_guard/cdk/table.d.ts b/tools/public_api_guard/cdk/table.d.ts index 04f98f066198..54dacbc1e2e7 100644 --- a/tools/public_api_guard/cdk/table.d.ts +++ b/tools/public_api_guard/cdk/table.d.ts @@ -27,7 +27,7 @@ export declare const CDK_ROW_TEMPLATE = "; -export declare const CDK_TABLE_TEMPLATE = "\n \n \n \n \n"; +export declare const CDK_TABLE_TEMPLATE = "\n \n \n \n \n \n"; export declare class CdkCell extends BaseCdkCell { constructor(columnDef: CdkColumnDef, elementRef: ElementRef); @@ -147,6 +147,13 @@ export declare class CdkHeaderRowDef extends _CdkHeaderRowDefBase implements Can static ɵfac: i0.ɵɵFactoryDef; } +export declare class CdkNoDataRow { + templateRef: TemplateRef; + constructor(templateRef: TemplateRef); + static ɵdir: i0.ɵɵDirectiveDefWithMeta; + static ɵfac: i0.ɵɵFactoryDef; +} + export declare class CdkRow { static ɵcmp: i0.ɵɵComponentDefWithMeta; static ɵfac: i0.ɵɵFactoryDef; @@ -173,6 +180,8 @@ export declare class CdkTable implements AfterContentChecked, CollectionViewe _footerRowOutlet: FooterRowOutlet; _headerRowOutlet: HeaderRowOutlet; _multiTemplateDataRows: boolean; + _noDataRow: CdkNoDataRow; + _noDataRowOutlet: NoDataRowOutlet; _rowOutlet: DataRowOutlet; get dataSource(): CdkTableDataSourceInput; set dataSource(dataSource: CdkTableDataSourceInput); @@ -206,13 +215,13 @@ export declare class CdkTable implements AfterContentChecked, CollectionViewe updateStickyFooterRowStyles(): void; updateStickyHeaderRowStyles(): void; static ngAcceptInputType_multiTemplateDataRows: BooleanInput; - static ɵcmp: i0.ɵɵComponentDefWithMeta, "cdk-table, table[cdk-table]", ["cdkTable"], { "trackBy": "trackBy"; "dataSource": "dataSource"; "multiTemplateDataRows": "multiTemplateDataRows"; }, {}, ["_contentColumnDefs", "_contentRowDefs", "_contentHeaderRowDefs", "_contentFooterRowDefs"], ["caption"]>; + static ɵcmp: i0.ɵɵComponentDefWithMeta, "cdk-table, table[cdk-table]", ["cdkTable"], { "trackBy": "trackBy"; "dataSource": "dataSource"; "multiTemplateDataRows": "multiTemplateDataRows"; }, {}, ["_noDataRow", "_contentColumnDefs", "_contentRowDefs", "_contentHeaderRowDefs", "_contentFooterRowDefs"], ["caption"]>; static ɵfac: i0.ɵɵFactoryDef, [null, null, null, { attribute: "role"; }, { optional: true; }, null, null]>; } export declare class CdkTableModule { static ɵinj: i0.ɵɵInjectorDef; - static ɵmod: i0.ɵɵNgModuleDefWithMeta; + static ɵmod: i0.ɵɵNgModuleDefWithMeta; } export declare class CdkTextColumn implements OnDestroy, OnInit { @@ -265,6 +274,14 @@ export declare class HeaderRowOutlet implements RowOutlet { export declare function mixinHasStickyInput>(base: T): CanStickCtor & T; +export declare class NoDataRowOutlet implements RowOutlet { + elementRef: ElementRef; + viewContainer: ViewContainerRef; + constructor(viewContainer: ViewContainerRef, elementRef: ElementRef); + static ɵdir: i0.ɵɵDirectiveDefWithMeta; + static ɵfac: i0.ɵɵFactoryDef; +} + export interface RenderRow { data: T; dataIndex: number; diff --git a/tools/public_api_guard/material/table.d.ts b/tools/public_api_guard/material/table.d.ts index 0e73f53c8f9d..1c00106a3857 100644 --- a/tools/public_api_guard/material/table.d.ts +++ b/tools/public_api_guard/material/table.d.ts @@ -60,6 +60,11 @@ export declare class MatHeaderRowDef extends CdkHeaderRowDef { static ɵfac: i0.ɵɵFactoryDef; } +export declare class MatNoDataRow extends CdkNoDataRow { + static ɵdir: i0.ɵɵDirectiveDefWithMeta; + static ɵfac: i0.ɵɵFactoryDef; +} + export declare class MatRow extends CdkRow { static ɵcmp: i0.ɵɵComponentDefWithMeta; static ɵfac: i0.ɵɵFactoryDef; @@ -102,7 +107,7 @@ export declare class MatTableDataSource extends DataSource { export declare class MatTableModule { static ɵinj: i0.ɵɵInjectorDef; - static ɵmod: i0.ɵɵNgModuleDefWithMeta; + static ɵmod: i0.ɵɵNgModuleDefWithMeta; } export declare class MatTextColumn extends CdkTextColumn {