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

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 committed Mar 28, 2020
1 parent 29e74eb commit 386d590
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 @@ -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');
Expand Down Expand Up @@ -1474,6 +1512,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 @@ -2281,6 +2321,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 @@ -118,6 +130,7 @@ export const CDK_TABLE_TEMPLATE =
<ng-content select="caption"></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 @@ -292,6 +305,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 @@ -378,6 +394,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 @@ -398,6 +415,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 @@ -463,6 +483,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 @@ -518,6 +539,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
rowView.context.$implicit = record.item.data;
});

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

Expand Down Expand Up @@ -1016,15 +1038,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 @@ -1093,6 +1119,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 386d590

Please sign in to comment.