Skip to content

Commit 386d590

Browse files
committed
feat(table): add the ability to show a data row when no data is available
As the table is set up at the moment, there's no convenient way to show the user something when their filtered table didn't match any data. These changes add a new directive that renders out a single row when no other data is available which can be used to show a message.
1 parent 29e74eb commit 386d590

File tree

15 files changed

+239
-19
lines changed

15 files changed

+239
-19
lines changed

src/cdk/table/row.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -306,3 +306,11 @@ export class CdkFooterRow {
306306
})
307307
export class CdkRow {
308308
}
309+
310+
/** Row that can be used to display a message when no data is shown in the table. */
311+
@Directive({
312+
selector: 'ng-template[cdkNoDataRow]'
313+
})
314+
export class CdkNoDataRow {
315+
constructor(public templateRef: TemplateRef<any>) {}
316+
}

src/cdk/table/table-module.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
*/
88

99
import {NgModule} from '@angular/core';
10-
import {HeaderRowOutlet, DataRowOutlet, CdkTable, FooterRowOutlet} from './table';
10+
import {HeaderRowOutlet, DataRowOutlet, CdkTable, FooterRowOutlet, NoDataRowOutlet} from './table';
1111
import {
1212
CdkCellOutlet, CdkFooterRow, CdkFooterRowDef, CdkHeaderRow, CdkHeaderRowDef, CdkRow,
13-
CdkRowDef
13+
CdkRowDef,
14+
CdkNoDataRow
1415
} from './row';
1516
import {
1617
CdkColumnDef, CdkHeaderCellDef, CdkHeaderCell, CdkCell, CdkCellDef,
@@ -38,6 +39,8 @@ const EXPORTED_DECLARATIONS = [
3839
HeaderRowOutlet,
3940
FooterRowOutlet,
4041
CdkTextColumn,
42+
CdkNoDataRow,
43+
NoDataRowOutlet,
4144
];
4245

4346
@NgModule({

src/cdk/table/table.spec.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -285,6 +285,22 @@ describe('CdkTable', () => {
285285
['Footer C', 'Footer B'],
286286
]);
287287
});
288+
289+
it('should be able to show a message when no data is being displayed', () => {
290+
expect(tableElement.textContent!.trim()).not.toContain('No data');
291+
292+
const originalData = dataSource.data;
293+
dataSource.data = [];
294+
fixture.detectChanges();
295+
296+
expect(tableElement.textContent!.trim()).toContain('No data');
297+
298+
dataSource.data = originalData;
299+
fixture.detectChanges();
300+
301+
expect(tableElement.textContent!.trim()).not.toContain('No data');
302+
});
303+
288304
});
289305

290306
it('should render no rows when the data is null', fakeAsync(() => {
@@ -497,6 +513,28 @@ describe('CdkTable', () => {
497513
expect(innerRows.map(row => row.cells.length)).toEqual([3, 3, 3]);
498514
});
499515

516+
it('should be able to show a message when no data is being displayed in a native table', () => {
517+
const thisFixture = createComponent(NativeHtmlTableApp);
518+
thisFixture.detectChanges();
519+
520+
// Assert that the data is inside the tbody specifically.
521+
const tbody = thisFixture.nativeElement.querySelector('tbody');
522+
const dataSource = thisFixture.componentInstance.dataSource!;
523+
const originalData = dataSource.data;
524+
525+
expect(tbody.textContent!.trim()).not.toContain('No data');
526+
527+
dataSource.data = [];
528+
thisFixture.detectChanges();
529+
530+
expect(tbody.textContent!.trim()).toContain('No data');
531+
532+
dataSource.data = originalData;
533+
thisFixture.detectChanges();
534+
535+
expect(tbody.textContent!.trim()).not.toContain('No data');
536+
});
537+
500538
it('should apply correct roles for native table elements', () => {
501539
const thisFixture = createComponent(NativeHtmlTableApp);
502540
const thisTableElement: HTMLTableElement = thisFixture.nativeElement.querySelector('table');
@@ -1474,6 +1512,8 @@ class BooleanDataSource extends DataSource<boolean> {
14741512
*cdkRowDef="let row; columns: columnsToRender"></cdk-row>
14751513
<cdk-footer-row class="customFooterRowClass"
14761514
*cdkFooterRowDef="columnsToRender"></cdk-footer-row>
1515+
1516+
<div *cdkNoDataRow>No data</div>
14771517
</cdk-table>
14781518
`
14791519
})
@@ -2281,6 +2321,9 @@ class OuterTableApp {
22812321
22822322
<tr cdk-header-row *cdkHeaderRowDef="columnsToRender"></tr>
22832323
<tr cdk-row *cdkRowDef="let row; columns: columnsToRender" class="customRowClass"></tr>
2324+
<tr *cdkNoDataRow>
2325+
<td>No data</td>
2326+
</tr>
22842327
</table>
22852328
`
22862329
})

src/cdk/table/table.ts

Lines changed: 45 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@ import {
3535
TrackByFunction,
3636
ViewChild,
3737
ViewContainerRef,
38-
ViewEncapsulation
38+
ViewEncapsulation,
39+
ContentChild
3940
} from '@angular/core';
4041
import {
4142
BehaviorSubject,
@@ -54,7 +55,8 @@ import {
5455
CdkCellOutletRowContext,
5556
CdkFooterRowDef,
5657
CdkHeaderRowDef,
57-
CdkRowDef
58+
CdkRowDef,
59+
CdkNoDataRow
5860
} from './row';
5961
import {StickyStyler} from './sticky-styler';
6062
import {
@@ -106,6 +108,16 @@ export class FooterRowOutlet implements RowOutlet {
106108
constructor(public viewContainer: ViewContainerRef, public elementRef: ElementRef) {}
107109
}
108110

111+
/**
112+
* Provides a handle for the table to grab the view
113+
* container's ng-container to insert the no data row.
114+
* @docs-private
115+
*/
116+
@Directive({selector: '[noDataRowOutlet]'})
117+
export class NoDataRowOutlet implements RowOutlet {
118+
constructor(public viewContainer: ViewContainerRef, public elementRef: ElementRef) {}
119+
}
120+
109121
/**
110122
* The table template that can be used by the mat-table. Should not be used outside of the
111123
* material library.
@@ -118,6 +130,7 @@ export const CDK_TABLE_TEMPLATE =
118130
<ng-content select="caption"></ng-content>
119131
<ng-container headerRowOutlet></ng-container>
120132
<ng-container rowOutlet></ng-container>
133+
<ng-container noDataRowOutlet></ng-container>
121134
<ng-container footerRowOutlet></ng-container>
122135
`;
123136

@@ -292,6 +305,9 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
292305
*/
293306
protected stickyCssClass: string = 'cdk-table-sticky';
294307

308+
/** Whether the no data row is currently showing anything. */
309+
private _isShowingNoDataRow = false;
310+
295311
/**
296312
* Tracking function that will be used to check the differences in data changes. Used similarly
297313
* to `ngFor` `trackBy` function. Optimize row operations by identifying a row based on its data
@@ -378,6 +394,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
378394
@ViewChild(DataRowOutlet, {static: true}) _rowOutlet: DataRowOutlet;
379395
@ViewChild(HeaderRowOutlet, {static: true}) _headerRowOutlet: HeaderRowOutlet;
380396
@ViewChild(FooterRowOutlet, {static: true}) _footerRowOutlet: FooterRowOutlet;
397+
@ViewChild(NoDataRowOutlet, {static: true}) _noDataRowOutlet: NoDataRowOutlet;
381398

382399
/**
383400
* The column definitions provided by the user that contain what the header, data, and footer
@@ -398,6 +415,9 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
398415
descendants: true
399416
}) _contentFooterRowDefs: QueryList<CdkFooterRowDef>;
400417

418+
/** Row definition that will only be rendered if there's no data in the table. */
419+
@ContentChild(CdkNoDataRow) _noDataRow: CdkNoDataRow;
420+
401421
constructor(
402422
protected readonly _differs: IterableDiffers,
403423
protected readonly _changeDetectorRef: ChangeDetectorRef,
@@ -463,6 +483,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
463483

464484
ngOnDestroy() {
465485
this._rowOutlet.viewContainer.clear();
486+
this._noDataRowOutlet.viewContainer.clear();
466487
this._headerRowOutlet.viewContainer.clear();
467488
this._footerRowOutlet.viewContainer.clear();
468489

@@ -518,6 +539,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
518539
rowView.context.$implicit = record.item.data;
519540
});
520541

542+
this._updateNoDataRow();
521543
this.updateStickyColumnStyles();
522544
}
523545

@@ -1016,15 +1038,19 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
10161038
private _applyNativeTableSections() {
10171039
const documentFragment = this._document.createDocumentFragment();
10181040
const sections = [
1019-
{tag: 'thead', outlet: this._headerRowOutlet},
1020-
{tag: 'tbody', outlet: this._rowOutlet},
1021-
{tag: 'tfoot', outlet: this._footerRowOutlet},
1041+
{tag: 'thead', outlets: [this._headerRowOutlet]},
1042+
{tag: 'tbody', outlets: [this._rowOutlet, this._noDataRowOutlet]},
1043+
{tag: 'tfoot', outlets: [this._footerRowOutlet]},
10221044
];
10231045

10241046
for (const section of sections) {
10251047
const element = this._document.createElement(section.tag);
10261048
element.setAttribute('role', 'rowgroup');
1027-
element.appendChild(section.outlet.elementRef.nativeElement);
1049+
1050+
for (const outlet of section.outlets) {
1051+
element.appendChild(outlet.elementRef.nativeElement);
1052+
}
1053+
10281054
documentFragment.appendChild(element);
10291055
}
10301056

@@ -1093,6 +1119,19 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
10931119
return items.filter(item => !item._table || item._table === this);
10941120
}
10951121

1122+
/** Creates or removes the no data row, depending on whether any data is being shown. */
1123+
private _updateNoDataRow() {
1124+
if (this._noDataRow) {
1125+
const shouldShow = this._rowOutlet.viewContainer.length === 0;
1126+
1127+
if (shouldShow !== this._isShowingNoDataRow) {
1128+
const container = this._noDataRowOutlet.viewContainer;
1129+
shouldShow ? container.createEmbeddedView(this._noDataRow.templateRef) : container.clear();
1130+
this._isShowingNoDataRow = shouldShow;
1131+
}
1132+
}
1133+
}
1134+
10961135
static ngAcceptInputType_multiTemplateDataRows: BooleanInput;
10971136
}
10981137

src/components-examples/material/table/table-filtering/table-filtering-example.html

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<mat-form-field>
22
<mat-label>Filter</mat-label>
3-
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. ium">
3+
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. ium" #input>
44
</mat-form-field>
55

66
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
@@ -31,4 +31,9 @@
3131

3232
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
3333
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
34+
35+
<!-- Row shown when there is no matching data. -->
36+
<tr class="mat-row" *matNoDataRow>
37+
<td class="mat-cell" colspan="4">No data matching the filter "{{input.value}}"</td>
38+
</tr>
3439
</table>

src/components-examples/material/table/table-overview/table-overview-example.html

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<mat-form-field>
22
<mat-label>Filter</mat-label>
3-
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. Mia">
3+
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. Mia" #input>
44
</mat-form-field>
55

66
<div class="mat-elevation-z8">
@@ -31,7 +31,11 @@
3131
</ng-container>
3232

3333
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
34-
<tr mat-row *matRowDef="let row; columns: displayedColumns;">
34+
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
35+
36+
<!-- Row shown when there is no matching data. -->
37+
<tr class="mat-row" *matNoDataRow>
38+
<td class="mat-cell" colspan="4">No data matching the filter "{{input.value}}"</td>
3539
</tr>
3640
</table>
3741

src/material-experimental/mdc-table/module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {
2525
MatHeaderRow,
2626
MatHeaderRowDef,
2727
MatRow,
28-
MatRowDef
28+
MatRowDef,
29+
MatNoDataRow
2930
} from './row';
3031

3132
const EXPORTED_DECLARATIONS = [
@@ -50,6 +51,7 @@ const EXPORTED_DECLARATIONS = [
5051
MatHeaderRow,
5152
MatRow,
5253
MatFooterRow,
54+
MatNoDataRow,
5355
];
5456

5557
@NgModule({

src/material-experimental/mdc-table/row.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
CdkHeaderRow,
1515
CdkHeaderRowDef,
1616
CdkRow,
17-
CdkRowDef
17+
CdkRowDef,
18+
CdkNoDataRow
1819
} from '@angular/cdk/table';
1920
import {ChangeDetectionStrategy, Component, Directive, ViewEncapsulation} from '@angular/core';
2021

@@ -110,3 +111,11 @@ export class MatFooterRow extends CdkFooterRow {
110111
})
111112
export class MatRow extends CdkRow {
112113
}
114+
115+
/** Row that can be used to display a message when no data is shown in the table. */
116+
@Directive({
117+
selector: 'ng-template[matNoDataRow]',
118+
providers: [{provide: CdkNoDataRow, useExisting: MatNoDataRow}],
119+
})
120+
export class MatNoDataRow extends CdkNoDataRow {
121+
}

src/material-experimental/mdc-table/table.spec.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,27 @@ describe('MDC-based MatTable', () => {
9696
expect(innerRows.map(row => row.cells.length)).toEqual([3, 3, 3, 3]);
9797
});
9898

99+
it('should be able to show a message when no data is being displayed', () => {
100+
const fixture = TestBed.createComponent(MatTableApp);
101+
fixture.detectChanges();
102+
103+
// Assert that the data is inside the tbody specifically.
104+
const tbody = fixture.nativeElement.querySelector('tbody')!;
105+
const initialData = fixture.componentInstance.dataSource!.data;
106+
107+
expect(tbody.textContent.trim()).not.toContain('No data');
108+
109+
fixture.componentInstance.dataSource!.data = [];
110+
fixture.detectChanges();
111+
112+
expect(tbody.textContent.trim()).toContain('No data');
113+
114+
fixture.componentInstance.dataSource!.data = initialData;
115+
fixture.detectChanges();
116+
117+
expect(tbody.textContent.trim()).not.toContain('No data');
118+
});
119+
99120
});
100121

101122
it('should render with MatTableDataSource and sort', () => {
@@ -555,6 +576,9 @@ class FakeDataSource extends DataSource<TestData> {
555576
<tr mat-header-row *matHeaderRowDef="columnsToRender"></tr>
556577
<tr mat-row *matRowDef="let row; columns: columnsToRender"></tr>
557578
<tr mat-row *matRowDef="let row; columns: ['special_column']; when: isFourthRow"></tr>
579+
<tr *matNoDataRow>
580+
<td>No data</td>
581+
</tr>
558582
<tr mat-footer-row *matFooterRowDef="columnsToRender"></tr>
559583
</table>
560584
`

src/material/table/row.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ import {
1414
CdkHeaderRow,
1515
CdkHeaderRowDef,
1616
CdkRow,
17-
CdkRowDef
17+
CdkRowDef,
18+
CdkNoDataRow
1819
} from '@angular/cdk/table';
1920
import {ChangeDetectionStrategy, Component, Directive, ViewEncapsulation} from '@angular/core';
2021

@@ -110,3 +111,11 @@ export class MatFooterRow extends CdkFooterRow {
110111
})
111112
export class MatRow extends CdkRow {
112113
}
114+
115+
/** Row that can be used to display a message when no data is shown in the table. */
116+
@Directive({
117+
selector: 'ng-template[matNoDataRow]',
118+
providers: [{provide: CdkNoDataRow, useExisting: MatNoDataRow}],
119+
})
120+
export class MatNoDataRow extends CdkNoDataRow {
121+
}

src/material/table/table-module.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import {
2424
MatHeaderRow,
2525
MatHeaderRowDef,
2626
MatRow,
27-
MatRowDef
27+
MatRowDef,
28+
MatNoDataRow
2829
} from './row';
2930
import {MatTextColumn} from './text-column';
3031
import {MatCommonModule} from '@angular/material/core';
@@ -51,6 +52,7 @@ const EXPORTED_DECLARATIONS = [
5152
MatHeaderRow,
5253
MatRow,
5354
MatFooterRow,
55+
MatNoDataRow,
5456

5557
MatTextColumn,
5658
];

0 commit comments

Comments
 (0)