Skip to content

Commit

Permalink
feat(table): add the ability to show a data row when no data is avail…
Browse files Browse the repository at this point in the history
…able (angular#18041)

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.
  • Loading branch information
crisbeto authored Apr 20, 2020
1 parent c67337b commit e512581
Show file tree
Hide file tree
Showing 15 changed files with 239 additions and 19 deletions.
8 changes: 8 additions & 0 deletions src/cdk/table/row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>) {}
}
7 changes: 5 additions & 2 deletions src/cdk/table/table-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -38,6 +39,8 @@ const EXPORTED_DECLARATIONS = [
HeaderRowOutlet,
FooterRowOutlet,
CdkTextColumn,
CdkNoDataRow,
NoDataRowOutlet,
];

@NgModule({
Expand Down
43 changes: 43 additions & 0 deletions src/cdk/table/table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(() => {
Expand Down Expand Up @@ -513,6 +529,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');
Expand Down Expand Up @@ -1490,6 +1528,8 @@ class BooleanDataSource extends DataSource<boolean> {
*cdkRowDef="let row; columns: columnsToRender"></cdk-row>
<cdk-footer-row class="customFooterRowClass"
*cdkFooterRowDef="columnsToRender"></cdk-footer-row>
<div *cdkNoDataRow>No data</div>
</cdk-table>
`
})
Expand Down Expand Up @@ -2297,6 +2337,9 @@ class OuterTableApp {
<tr cdk-header-row *cdkHeaderRowDef="columnsToRender"></tr>
<tr cdk-row *cdkRowDef="let row; columns: columnsToRender" class="customRowClass"></tr>
<tr *cdkNoDataRow>
<td>No data</td>
</tr>
</table>
`
})
Expand Down
51 changes: 45 additions & 6 deletions src/cdk/table/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ import {
TrackByFunction,
ViewChild,
ViewContainerRef,
ViewEncapsulation
ViewEncapsulation,
ContentChild
} from '@angular/core';
import {
BehaviorSubject,
Expand All @@ -54,7 +55,8 @@ import {
CdkCellOutletRowContext,
CdkFooterRowDef,
CdkHeaderRowDef,
CdkRowDef
CdkRowDef,
CdkNoDataRow
} from './row';
import {StickyStyler} from './sticky-styler';
import {
Expand Down Expand Up @@ -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.
Expand All @@ -119,6 +131,7 @@ export const CDK_TABLE_TEMPLATE =
<ng-content select="colgroup, col"></ng-content>
<ng-container headerRowOutlet></ng-container>
<ng-container rowOutlet></ng-container>
<ng-container noDataRowOutlet></ng-container>
<ng-container footerRowOutlet></ng-container>
`;

Expand Down Expand Up @@ -293,6 +306,9 @@ export class CdkTable<T> 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
Expand Down Expand Up @@ -379,6 +395,7 @@ export class CdkTable<T> 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
Expand All @@ -399,6 +416,9 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
descendants: true
}) _contentFooterRowDefs: QueryList<CdkFooterRowDef>;

/** 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,
Expand Down Expand Up @@ -464,6 +484,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes

ngOnDestroy() {
this._rowOutlet.viewContainer.clear();
this._noDataRowOutlet.viewContainer.clear();
this._headerRowOutlet.viewContainer.clear();
this._footerRowOutlet.viewContainer.clear();

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

this._updateNoDataRow();
this.updateStickyColumnStyles();
}

Expand Down Expand Up @@ -1017,15 +1039,19 @@ export class CdkTable<T> 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);
}

Expand Down Expand Up @@ -1094,6 +1120,19 @@ export class CdkTable<T> 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;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<mat-form-field>
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. ium">
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. ium" #input>
</mat-form-field>

<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
Expand Down Expand Up @@ -31,4 +31,9 @@

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>

<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="4">No data matching the filter "{{input.value}}"</td>
</tr>
</table>
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<mat-form-field>
<mat-label>Filter</mat-label>
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. Mia">
<input matInput (keyup)="applyFilter($event)" placeholder="Ex. Mia" #input>
</mat-form-field>

<div class="mat-elevation-z8">
Expand Down Expand Up @@ -31,7 +31,11 @@
</ng-container>

<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;">
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>

<!-- Row shown when there is no matching data. -->
<tr class="mat-row" *matNoDataRow>
<td class="mat-cell" colspan="4">No data matching the filter "{{input.value}}"</td>
</tr>
</table>

Expand Down
4 changes: 3 additions & 1 deletion src/material-experimental/mdc-table/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ import {
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef
MatRowDef,
MatNoDataRow
} from './row';

const EXPORTED_DECLARATIONS = [
Expand All @@ -50,6 +51,7 @@ const EXPORTED_DECLARATIONS = [
MatHeaderRow,
MatRow,
MatFooterRow,
MatNoDataRow,
];

@NgModule({
Expand Down
11 changes: 10 additions & 1 deletion src/material-experimental/mdc-table/row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
CdkHeaderRow,
CdkHeaderRowDef,
CdkRow,
CdkRowDef
CdkRowDef,
CdkNoDataRow
} from '@angular/cdk/table';
import {ChangeDetectionStrategy, Component, Directive, ViewEncapsulation} from '@angular/core';

Expand Down Expand Up @@ -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 {
}
24 changes: 24 additions & 0 deletions src/material-experimental/mdc-table/table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down Expand Up @@ -555,6 +576,9 @@ class FakeDataSource extends DataSource<TestData> {
<tr mat-header-row *matHeaderRowDef="columnsToRender"></tr>
<tr mat-row *matRowDef="let row; columns: columnsToRender"></tr>
<tr mat-row *matRowDef="let row; columns: ['special_column']; when: isFourthRow"></tr>
<tr *matNoDataRow>
<td>No data</td>
</tr>
<tr mat-footer-row *matFooterRowDef="columnsToRender"></tr>
</table>
`
Expand Down
11 changes: 10 additions & 1 deletion src/material/table/row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ import {
CdkHeaderRow,
CdkHeaderRowDef,
CdkRow,
CdkRowDef
CdkRowDef,
CdkNoDataRow
} from '@angular/cdk/table';
import {ChangeDetectionStrategy, Component, Directive, ViewEncapsulation} from '@angular/core';

Expand Down Expand Up @@ -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 {
}
4 changes: 3 additions & 1 deletion src/material/table/table-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ import {
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef
MatRowDef,
MatNoDataRow
} from './row';
import {MatTextColumn} from './text-column';
import {MatCommonModule} from '@angular/material/core';
Expand All @@ -51,6 +52,7 @@ const EXPORTED_DECLARATIONS = [
MatHeaderRow,
MatRow,
MatFooterRow,
MatNoDataRow,

MatTextColumn,
];
Expand Down
Loading

0 comments on commit e512581

Please sign in to comment.