Skip to content

Commit fa9f419

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 695dde6 commit fa9f419

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(() => {
@@ -513,6 +529,28 @@ describe('CdkTable', () => {
513529
expect(innerRows.map(row => row.cells.length)).toEqual([3, 3, 3]);
514530
});
515531

532+
it('should be able to show a message when no data is being displayed in a native table', () => {
533+
const thisFixture = createComponent(NativeHtmlTableApp);
534+
thisFixture.detectChanges();
535+
536+
// Assert that the data is inside the tbody specifically.
537+
const tbody = thisFixture.nativeElement.querySelector('tbody');
538+
const dataSource = thisFixture.componentInstance.dataSource!;
539+
const originalData = dataSource.data;
540+
541+
expect(tbody.textContent!.trim()).not.toContain('No data');
542+
543+
dataSource.data = [];
544+
thisFixture.detectChanges();
545+
546+
expect(tbody.textContent!.trim()).toContain('No data');
547+
548+
dataSource.data = originalData;
549+
thisFixture.detectChanges();
550+
551+
expect(tbody.textContent!.trim()).not.toContain('No data');
552+
});
553+
516554
it('should apply correct roles for native table elements', () => {
517555
const thisFixture = createComponent(NativeHtmlTableApp);
518556
const thisTableElement: HTMLTableElement = thisFixture.nativeElement.querySelector('table');
@@ -1490,6 +1528,8 @@ class BooleanDataSource extends DataSource<boolean> {
14901528
*cdkRowDef="let row; columns: columnsToRender"></cdk-row>
14911529
<cdk-footer-row class="customFooterRowClass"
14921530
*cdkFooterRowDef="columnsToRender"></cdk-footer-row>
1531+
1532+
<div *cdkNoDataRow>No data</div>
14931533
</cdk-table>
14941534
`
14951535
})
@@ -2297,6 +2337,9 @@ class OuterTableApp {
22972337
22982338
<tr cdk-header-row *cdkHeaderRowDef="columnsToRender"></tr>
22992339
<tr cdk-row *cdkRowDef="let row; columns: columnsToRender" class="customRowClass"></tr>
2340+
<tr *cdkNoDataRow>
2341+
<td>No data</td>
2342+
</tr>
23002343
</table>
23012344
`
23022345
})

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.
@@ -119,6 +131,7 @@ export const CDK_TABLE_TEMPLATE =
119131
<ng-content select="colgroup, col"></ng-content>
120132
<ng-container headerRowOutlet></ng-container>
121133
<ng-container rowOutlet></ng-container>
134+
<ng-container noDataRowOutlet></ng-container>
122135
<ng-container footerRowOutlet></ng-container>
123136
`;
124137

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

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

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

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

465485
ngOnDestroy() {
466486
this._rowOutlet.viewContainer.clear();
487+
this._noDataRowOutlet.viewContainer.clear();
467488
this._headerRowOutlet.viewContainer.clear();
468489
this._footerRowOutlet.viewContainer.clear();
469490

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

543+
this._updateNoDataRow();
522544
this.updateStickyColumnStyles();
523545
}
524546

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

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

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

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

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)