feat(table): allow data input to be array, stream (#9489)

* feat(table): allow data input to be array, stream

* comments

* fix connect function

* fix connect function v2

* fix demo imports
This commit is contained in:
Andrew Seguin
2018-02-01 11:28:16 -08:00
committed by tinayuangao
parent 3acbf26131
commit 085d8056ae
13 changed files with 568 additions and 116 deletions
+8
View File
@@ -47,3 +47,11 @@ export function getTableMissingRowDefsError() {
return Error('Missing definitions for header and row, ' +
'cannot determine which columns should be rendered.');
}
/**
* Returns an error to be thrown when the data source does not match the compatible types.
* @docs-private
*/
export function getTableUnknownDataSourceError() {
return Error(`Provided data source did not match an array, Observable, or DataSource`);
}
+175 -2
View File
@@ -11,7 +11,8 @@ import {
getTableMissingMatchingRowDefError,
getTableMissingRowDefsError,
getTableMultipleDefaultRowDefsError,
getTableUnknownColumnError
getTableUnknownColumnError,
getTableUnknownDataSourceError
} from './table-errors';
import {CdkHeaderRowDef, CdkRowDef} from './row';
import {CdkColumnDef} from './cell';
@@ -45,6 +46,7 @@ describe('CdkTable', () => {
BooleanRowCdkTableApp,
WrapperCdkTableApp,
OuterTableApp,
CdkTableWithDifferentDataInputsApp,
],
}).compileComponents();
}));
@@ -113,6 +115,138 @@ describe('CdkTable', () => {
});
});
describe('with different data inputs other than data source', () => {
let dataInputFixture: ComponentFixture<CdkTableWithDifferentDataInputsApp>;
let dataInputComponent: CdkTableWithDifferentDataInputsApp;
let dataInputTableElement: HTMLElement;
let baseData: TestData[] = [
{a: 'a_1', b: 'b_1', c: 'c_1'},
{a: 'a_2', b: 'b_2', c: 'c_2'},
{a: 'a_3', b: 'b_3', c: 'c_3'},
];
beforeEach(() => {
dataInputFixture = TestBed.createComponent(CdkTableWithDifferentDataInputsApp);
dataInputComponent = dataInputFixture.componentInstance;
dataInputFixture.detectChanges();
dataInputTableElement = dataInputFixture.nativeElement.querySelector('cdk-table');
});
it('should render with data array input', () => {
const data = baseData.slice();
dataInputComponent.dataSource = data;
dataInputFixture.detectChanges();
const expectedRender = [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
];
expectTableToMatchContent(dataInputTableElement, expectedRender);
// Push data to the array but neglect to tell the table, should be no change
data.push({a: 'a_4', b: 'b_4', c: 'c_4'});
expectTableToMatchContent(dataInputTableElement, expectedRender);
// Notify table of the change, expect another row
dataInputComponent.table.renderRows();
dataInputFixture.detectChanges();
expectedRender.push(['a_4', 'b_4', 'c_4']);
expectTableToMatchContent(dataInputTableElement, expectedRender);
// Remove a row and expect the change in rows
data.pop();
dataInputComponent.table.renderRows();
expectedRender.pop();
expectTableToMatchContent(dataInputTableElement, expectedRender);
// Remove the data input entirely and expect no rows - just header.
dataInputComponent.dataSource = null;
dataInputFixture.detectChanges();
expectTableToMatchContent(dataInputTableElement, [expectedRender[0]]);
// Add back the data to verify that it renders rows
dataInputComponent.dataSource = data;
dataInputFixture.detectChanges();
expectTableToMatchContent(dataInputTableElement, expectedRender);
});
it('should render with data stream input', () => {
const data = baseData.slice();
const stream = new BehaviorSubject<TestData[]>(data);
dataInputComponent.dataSource = stream;
dataInputFixture.detectChanges();
const expectedRender = [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
];
expectTableToMatchContent(dataInputTableElement, expectedRender);
// Push data to the array and emit the data array on the stream
data.push({a: 'a_4', b: 'b_4', c: 'c_4'});
stream.next(data);
dataInputFixture.detectChanges();
expectedRender.push(['a_4', 'b_4', 'c_4']);
expectTableToMatchContent(dataInputTableElement, expectedRender);
// Push data to the array but rather than emitting, call renderRows.
data.push({a: 'a_5', b: 'b_5', c: 'c_5'});
dataInputComponent.table.renderRows();
dataInputFixture.detectChanges();
expectedRender.push(['a_5', 'b_5', 'c_5']);
expectTableToMatchContent(dataInputTableElement, expectedRender);
// Remove a row and expect the change in rows
data.pop();
expectedRender.pop();
stream.next(data);
expectTableToMatchContent(dataInputTableElement, expectedRender);
// Remove the data input entirely and expect no rows - just header.
dataInputComponent.dataSource = null;
dataInputFixture.detectChanges();
expectTableToMatchContent(dataInputTableElement, [expectedRender[0]]);
// Add back the data to verify that it renders rows
dataInputComponent.dataSource = stream;
dataInputFixture.detectChanges();
expectTableToMatchContent(dataInputTableElement, expectedRender);
});
it('should throw an error if the data source is not valid', () => {
dataInputComponent.dataSource = {invalid: 'dataSource'};
expect(() => dataInputFixture.detectChanges())
.toThrowError(getTableUnknownDataSourceError().message);
});
it('should throw an error if the data source is not valid', () => {
dataInputComponent.dataSource = undefined;
dataInputFixture.detectChanges();
// Expect the table to render just the header, no rows
expectTableToMatchContent(dataInputTableElement, [
['Column A', 'Column B', 'Column C']
]);
});
});
it('should render cells even if row data is falsy', () => {
const booleanRowCdkTableAppFixture = TestBed.createComponent(BooleanRowCdkTableApp);
const booleanRowCdkTableElement =
@@ -720,6 +854,36 @@ class SimpleCdkTableApp {
@ViewChild(CdkTable) table: CdkTable<TestData>;
}
@Component({
template: `
<cdk-table [dataSource]="dataSource">
<ng-container cdkColumnDef="column_a">
<cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
</ng-container>
<ng-container cdkColumnDef="column_b">
<cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
</ng-container>
<ng-container cdkColumnDef="column_c">
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
</ng-container>
<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
<cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
</cdk-table>
`
})
class CdkTableWithDifferentDataInputsApp {
dataSource: DataSource<TestData> | Observable<TestData[]> | TestData[] | any = null;
columnsToRender = ['column_a', 'column_b', 'column_c'];
@ViewChild(CdkTable) table: CdkTable<TestData>;
}
@Component({
template: `
<cdk-table [dataSource]="dataSource">
@@ -1186,6 +1350,9 @@ function expectTableToMatchContent(tableElement: Element, expectedTableContent:
}
}
// Copy the expected data array to avoid mutating the test's array
expectedTableContent = expectedTableContent.slice();
// Check header cells
const expectedHeaderContent = expectedTableContent.shift();
getHeaderCells(tableElement).forEach((cell, index) => {
@@ -1196,7 +1363,13 @@ function expectTableToMatchContent(tableElement: Element, expectedTableContent:
});
// Check data row cells
getRows(tableElement).forEach((row, rowIndex) => {
const rows = getRows(tableElement);
if (rows.length !== expectedTableContent.length) {
missedExpectations.push(
`Expected ${expectedTableContent.length} rows but found ${rows.length}`);
fail(missedExpectations.join('\n'));
}
rows.forEach((row, rowIndex) => {
getCells(row).forEach((cell, cellIndex) => {
const expected = expectedTableContent.length ?
expectedTableContent[rowIndex][cellIndex] :
+103 -53
View File
@@ -22,7 +22,6 @@ import {
IterableChangeRecord,
IterableDiffer,
IterableDiffers,
NgIterable,
OnInit,
QueryList,
TrackByFunction,
@@ -42,8 +41,11 @@ import {
getTableMissingMatchingRowDefError,
getTableMissingRowDefsError,
getTableMultipleDefaultRowDefsError,
getTableUnknownColumnError
getTableUnknownColumnError,
getTableUnknownDataSourceError
} from './table-errors';
import {Observable} from 'rxjs/Observable';
import {of as observableOf} from 'rxjs/observable/of';
/**
* Provides a handle for the table to grab the view container's ng-container to insert data rows.
@@ -78,8 +80,10 @@ export const CDK_TABLE_TEMPLATE = `
abstract class RowViewRef<T> extends EmbeddedViewRef<CdkCellOutletRowContext<T>> { }
/**
* A data table that connects with a data source to retrieve data of type `T` and renders
* a header row and data rows. Updates the rows when new data is provided by the data source.
* A data table that renders a header row and data rows. Uses the dataSource input to determine
* the data to be rendered. The data can be provided either as a data array, an Observable stream
* that emits the data array to render, or a DataSource with a connect function that will
* return an Observable stream that emits the data array to render.
*/
@Component({
moduleId: module.id,
@@ -97,8 +101,8 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
/** Subject that emits when the component has been destroyed. */
private _onDestroy = new Subject<void>();
/** Latest data provided by the data source through the connect interface. */
private _data: NgIterable<T> = [];
/** Latest data provided by the data source. */
private _data: T[];
/** Subscription that listens for the data provided by the data source. */
private _renderChangeSubscription: Subscription | null;
@@ -153,17 +157,33 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
private _trackByFn: TrackByFunction<T>;
/**
* Provides a stream containing the latest data array to render. Influenced by the table's
* stream of view window (what rows are currently on screen).
* The table's source of data, which can be provided in three ways (in order of complexity):
* - Simple data array (each object represents one table row)
* - Stream that emits a data array each time the array changes
* - `DataSource` object that implements the connect/disconnect interface.
*
* If a data array is provided, the table must be notified when the array's objects are
* added, removed, or moved. This can be done by calling the `renderRows()` function which will
* render the diff since the last table render. If the data array reference is changed, the table
* will automatically trigger an update to the rows.
*
* When providing an Observable stream, the table will trigger an update automatically when the
* stream emits a new array of data.
*
* Finally, when providing a `DataSource` object, the table will use the Observable stream
* provided by the connect function and trigger updates when that stream emits new data array
* values. During the table's ngOnDestroy or when the data source is removed from the table, the
* table will call the DataSource's `disconnect` function (may be useful for cleaning up any
* subscriptions registered during the connect process).
*/
@Input()
get dataSource(): DataSource<T> { return this._dataSource; }
set dataSource(dataSource: DataSource<T>) {
get dataSource(): DataSource<T> | Observable<T[]> | T[] { return this._dataSource; }
set dataSource(dataSource: DataSource<T> | Observable<T[]> | T[]) {
if (this._dataSource !== dataSource) {
this._switchDataSource(dataSource);
}
}
private _dataSource: DataSource<T>;
private _dataSource: DataSource<T> | Observable<T[]> | T[] | T[];
// TODO(andrewseguin): Remove max value as the end index
// and instead calculate the view on init and scroll.
@@ -247,11 +267,49 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
this._onDestroy.next();
this._onDestroy.complete();
if (this.dataSource) {
if (this.dataSource instanceof DataSource) {
this.dataSource.disconnect(this);
}
}
/**
* Renders rows based on the table's latest set of data, which was either provided directly as an
* input or retrieved through an Observable stream (directly or from a DataSource).
* Checks for differences in the data since the last diff to perform only the necessary
* changes (add/remove/move rows).
*
* If the table's data source is a DataSource or Observable, this will be invoked automatically
* each time the provided Observable stream emits a new data array. Otherwise if your data is
* an array, this function will need to be called to render any changes.
*/
renderRows() {
const changes = this._dataDiffer.diff(this._data);
if (!changes) { return; }
const viewContainer = this._rowPlaceholder.viewContainer;
changes.forEachOperation(
(record: IterableChangeRecord<T>, adjustedPreviousIndex: number, currentIndex: number) => {
if (record.previousIndex == null) {
this._insertRow(record.item, currentIndex);
} else if (currentIndex == null) {
viewContainer.remove(adjustedPreviousIndex);
} else {
const view = <RowViewRef<T>>viewContainer.get(adjustedPreviousIndex);
viewContainer.move(view!, currentIndex);
}
});
// Update the meta context of a row's context data (index, count, first, last, ...)
this._updateRowIndexContext();
// Update rows that did not get added/removed/moved but may have had their identity changed,
// e.g. if trackBy matched data on some property but the actual data reference changed.
changes.forEachIdentityChange((record: IterableChangeRecord<T>) => {
const rowView = <RowViewRef<T>>viewContainer.get(record.currentIndex!);
rowView.context.$implicit = record.item;
});
}
/**
* Sets the header row definition to be used. Overrides the header row definition gathered by
* using `ContentChild`, if one exists. Sets a flag that will re-render the header row after the
@@ -319,7 +377,7 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
this._dataDiffer.diff([]);
this._rowPlaceholder.viewContainer.clear();
this._renderRowChanges();
this.renderRows();
}
});
@@ -334,10 +392,10 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
* render change subscription if one exists. If the data source is null, interpret this by
* clearing the row placeholder. Otherwise start listening for new data.
*/
private _switchDataSource(dataSource: DataSource<T>) {
private _switchDataSource(dataSource: DataSource<T> | Observable<T[]> | T[]) {
this._data = [];
if (this.dataSource) {
if (this.dataSource instanceof DataSource) {
this.dataSource.disconnect(this);
}
@@ -347,8 +405,10 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
this._renderChangeSubscription = null;
}
// Remove the table's rows if there is now no data source
if (!dataSource) {
if (this._dataDiffer) {
this._dataDiffer.diff([]);
}
this._rowPlaceholder.viewContainer.clear();
}
@@ -357,11 +417,33 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
/** Set up a subscription for the data provided by the data source. */
private _observeRenderChanges() {
this._renderChangeSubscription = this.dataSource.connect(this).pipe(takeUntil(this._onDestroy))
.subscribe(data => {
this._data = data;
this._renderRowChanges();
});
// If no data source has been set, there is nothing to observe for changes.
if (!this.dataSource) { return; }
let dataStream: Observable<T[]> | undefined;
// Check if the datasource is a DataSource object by observing if it has a connect function.
// Cannot check this.dataSource['connect'] due to potential property renaming, nor can it
// checked as an instanceof DataSource<T> since the table should allow for data sources
// that did not explicitly extend DataSource<T>.
if ((this.dataSource as DataSource<T>).connect instanceof Function) {
dataStream = (this.dataSource as DataSource<T>).connect(this);
} else if (this.dataSource instanceof Observable) {
dataStream = this.dataSource;
} else if (Array.isArray(this.dataSource)) {
dataStream = observableOf(this.dataSource);
}
if (dataStream === undefined) {
throw getTableUnknownDataSourceError();
}
this._renderChangeSubscription = dataStream
.pipe(takeUntil(this._onDestroy))
.subscribe(data => {
this._data = data;
this.renderRows();
});
}
/**
@@ -392,38 +474,6 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
this._changeDetectorRef.markForCheck();
}
/**
* Check for changes made in the data and render each change (row added/removed/moved) and update
* row contexts.
*/
private _renderRowChanges() {
const changes = this._dataDiffer.diff(this._data);
if (!changes) { return; }
const viewContainer = this._rowPlaceholder.viewContainer;
changes.forEachOperation(
(record: IterableChangeRecord<T>, adjustedPreviousIndex: number, currentIndex: number) => {
if (record.previousIndex == null) {
this._insertRow(record.item, currentIndex);
} else if (currentIndex == null) {
viewContainer.remove(adjustedPreviousIndex);
} else {
const view = <RowViewRef<T>>viewContainer.get(adjustedPreviousIndex);
viewContainer.move(view!, currentIndex);
}
});
// Update the meta context of a row's context data (index, count, first, last, ...)
this._updateRowIndexContext();
// Update rows that did not get added/removed/moved but may have had their identity changed,
// e.g. if trackBy matched data on some property but the actual data reference changed.
changes.forEachIdentityChange((record: IterableChangeRecord<T>) => {
const rowView = <RowViewRef<T>>viewContainer.get(record.currentIndex!);
rowView.context.$implicit = record.item;
});
}
/**
* Finds the matching row definition that should be used for this row data. If there is only
* one row definition, it is returned. Otherwise, find the row definition that has a when
@@ -0,0 +1,51 @@
<div class="demo-actions">
<strong>Input type: </strong>
<mat-radio-group [value]="inputType" (change)="changeInput($event)">
<mat-radio-button value="array">Array</mat-radio-button>
<mat-radio-button value="stream">Stream</mat-radio-button>
<mat-radio-button value="source">DataSource</mat-radio-button>
</mat-radio-group>
</div>
<div class="demo-actions">
<strong>Data Changes: </strong>
<button mat-raised-button (click)="addRow()">Add Row</button>
<button mat-raised-button (click)="removeRow()">Remove Row</button>
<button mat-raised-button (click)="reassignDataClone()">Clone Data</button>
</div>
<div class="demo-actions">
<strong>Table: </strong>
<button mat-raised-button (click)="this.renderRows()">Re-render</button>
<button mat-raised-button (click)="this.removeDataSource()">Remove Data Source</button>
</div>
<mat-card class="demo-table-card">
<h3> CdkTable </h3>
<cdk-table #cdkTable [dataSource]="dataSourceInput">
<!-- Name (normal column) -->
<ng-container cdkColumnDef="{{column}}" *ngFor="let column of columnsToDisplay">
<cdk-header-cell *cdkHeaderCellDef> {{column}} </cdk-header-cell>
<cdk-cell *cdkCellDef="let element"> {{element[column]}} </cdk-cell>
</ng-container>
<cdk-header-row *cdkHeaderRowDef="columnsToDisplay"></cdk-header-row>
<cdk-row *cdkRowDef="let data; columns: columnsToDisplay;"></cdk-row>
</cdk-table>
</mat-card>
<mat-card class="demo-table-card">
<h3> MatTable </h3>
<mat-table #matTable [dataSource]="dataSourceInput">
<!-- Name (normal column) -->
<ng-container matColumnDef="{{column}}" *ngFor="let column of columnsToDisplay">
<mat-header-cell *matHeaderCellDef> {{column}} </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element[column]}} </mat-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columnsToDisplay"></mat-header-row>
<mat-row *matRowDef="let data; columns: columnsToDisplay;"></mat-row>
</mat-table>
</mat-card>
@@ -0,0 +1,20 @@
.demo-actions {
margin-top: 24px;
}
mat-card {
margin: 8px 0;
}
cdk-table {
display: table;
width: 100%;
cdk-header-row, cdk-row {
display: table-row;
}
cdk-header-cell, cdk-cell {
display: table-cell;
}
}
@@ -0,0 +1,70 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {Component, ViewChild} from '@angular/core';
import {Element, ELEMENT_DATA} from '../element-data';
import {CdkTable} from '@angular/cdk/table';
import {MatRadioChange, MatTable, MatTableDataSource} from '@angular/material';
import {Observable} from 'rxjs/Observable';
import {DataSource} from '@angular/cdk/collections';
@Component({
moduleId: module.id,
templateUrl: 'data-input-table.html',
styleUrls: ['data-input-table.css'],
})
export class DataInputTableDemo {
columnsToDisplay = ['name', 'weight', 'symbol', 'position'];
inputType: 'source' | 'stream' | 'array' | null = 'array';
data = ELEMENT_DATA.slice();
tableDataSource = new MatTableDataSource(this.data);
dataSourceInput: DataSource<Element> | Observable<Element[]> | Element[] | null = this.data;
@ViewChild(CdkTable) cdkTable: CdkTable<Element>;
@ViewChild(MatTable) matTable: MatTable<Element>;
changeInput(e: MatRadioChange) {
this.inputType = e.value;
switch (this.inputType) {
case 'array': this.dataSourceInput = this.data; break;
case 'stream': this.dataSourceInput = this.tableDataSource.connect(); break;
case 'source': this.dataSourceInput = this.tableDataSource; break;
}
}
addRow() {
this.data.push({name: 'new', weight: 0, symbol: 'New', position: 0});
this.tableDataSource.data = this.data;
}
removeRow() {
this.data.pop();
this.tableDataSource.data = this.data;
}
reassignDataClone() {
this.data = this.data.slice();
if (this.dataSourceInput instanceof Array) {
this.dataSourceInput = this.data;
}
this.tableDataSource.data = this.data;
}
renderRows() {
this.cdkTable.renderRows();
this.matTable.renderRows();
}
removeDataSource() {
this.dataSourceInput = null;
this.inputType = null;
}
}
+2
View File
@@ -9,9 +9,11 @@
import {Routes} from '@angular/router';
import {TableDemo} from './table-demo';
import {CustomTableDemo} from './custom-table/custom-table';
import {DataInputTableDemo} from './data-input-table/data-input-table';
export const TABLE_DEMO_ROUTES: Routes = [
{path: '', redirectTo: 'main-demo', pathMatch: 'full'},
{path: 'main-demo', component: TableDemo},
{path: 'custom-table', component: CustomTableDemo},
{path: 'data-input-table', component: DataInputTableDemo},
];
+2
View File
@@ -29,6 +29,7 @@ import {CommonModule} from '@angular/common';
import {RouterModule} from '@angular/router';
import {WrapperTable} from './custom-table/wrapper-table';
import {SimpleColumn} from './custom-table/simple-column';
import {DataInputTableDemo} from './data-input-table/data-input-table';
@NgModule({
imports: [
@@ -50,6 +51,7 @@ import {SimpleColumn} from './custom-table/simple-column';
],
declarations: [
CustomTableDemo,
DataInputTableDemo,
TableDemo,
TableDemoPage,
TableHeaderDemo,
+1
View File
@@ -16,5 +16,6 @@ export class TableDemoPage {
links = [
{name: 'Main Page', link: 'main-demo'},
{name: 'Custom Table', link: 'custom-table'},
{name: 'Direct Data', link: 'data-input-table'},
];
}
+2 -1
View File
@@ -25,7 +25,7 @@ import {_isNumberValue} from '@angular/cdk/coercion';
* properties are accessed. Also allows for filter customization by overriding filterTermAccessor,
* which defines how row data is converted to a string for filter matching.
*/
export class MatTableDataSource<T> implements DataSource<T> {
export class MatTableDataSource<T> extends DataSource<T> {
/** Stream that emits when a new data array is set on the data source. */
private readonly _data: BehaviorSubject<T[]>;
@@ -165,6 +165,7 @@ export class MatTableDataSource<T> implements DataSource<T> {
}
constructor(initialData: T[] = []) {
super();
this._data = new BehaviorSubject<T[]>(initialData);
this._updateChangeSubscription();
}
+123 -53
View File
@@ -1,29 +1,43 @@
The `mat-table` provides a Material Design styled data-table that can be used to display rows of
data.
The table's template consists of two parts: column definitions and row definitions. Each column
definition contains templates for that column's header and content cells. Each row definition
captures the columns used for that row and any bindings applied to the row element.
A `DataSource` provides data to the table by emitting an `Observable` stream of the items to be
rendered. Each emit includes the _entire set of items_ that should be displayed. The table,
listening to this stream, will render a row per item. Any manipulation of the data being displayed
(e.g. sorting, pagination, filtering) should be captured by the `DataSource`, ultimately emitting
a new set of items to reflect any changes.
This table builds on the foundation of the CDK data-table and uses a similar interface for its
data source input and template, except that its element and attribute selectors will be prefixed
with `mat-` instead of `cdk-`. For detailed information on the interface and how it works, see the
data input and template, except that its element and attribute selectors will be prefixed
with `mat-` instead of `cdk-`. For more information on the interface and a detailed look at how
the table is implemented, see the
[guide covering the CDK data-table](https://material.angular.io/guide/cdk-table).
### Getting Started
<!-- example(table-basic) -->
#### 1. Define the table's columns
#### 1. Write your mat-table and provide data
Start by writing your table's column definitions. Each column definition should be given a unique
name and contain the content for its header and row cells.
Begin by creating a `<mat-table>` component in your template and passing in data.
The simplest way to provide data to the table is by passing a data array to the table's `data`
input. The table will take the array and render a row for each object in the data array.
```html
<mat-table [dataSource]=”myDataArray”>
...
</mat-table>
```
Since the table optimizes for performance, it will not automatically check for changes to the data
array. Instead, when objects are added, removed, or moved on the data array, you can trigger an
update to the table's rendered rows by calling its `renderRows()` function.
While an array is the _simplest_ way to bind data into the data source, it is also
the most limited. For more complex applications, using a `DataSource` instance
is recommended. See the section "Advanced data sources" below for more information.
#### 2. Define the column templates
Next, write your table's column templates.
Each column definition should be given a unique name and contain the content for its header and row
cells.
Here's a simple column definition with the name `'userName'`. The header cell contains the text
"Name" and each row cell will render the `name` property of each row's data.
@@ -35,60 +49,96 @@ Here's a simple column definition with the name `'userName'`. The header cell co
</ng-container>
```
#### 2. Define the table's rows
#### 3. Define the row templates
After defining your columns, provide the header and data row templates that will be rendered out by
the table. Each row needs to be given a list of the columns that it should contain. The order of
the names will define the order of the cells rendered. It is not required to provide a list of all
the defined column names, but only the ones that you want to have rendered.
Finally, once you have defined your columns, you need to tell the table which columns will be
rendered in the header and data rows.
```html
<mat-header-row *matHeaderRowDef="['userName', 'age']"></mat-header-row>
<mat-row *matRowDef="let myRowData; columns: ['userName', 'age']"></mat-row>
```
#### 3. Provide data
The column and row definitions now capture _how_ data will render - all that's left is to provide
the data itself. For simple scenarios with client-side operations, `MatTableDataSource` offers a
quick and easy starting point. Simply create an instance of `MatTableDataSource` and set the items
to be displayed to the `data` property. For more advanced scenarios, applications will implement
one or more custom `DataSource` to capture more specific behaviors.
To start, create a variable in your component that contains the list of the columns you want to
render.
```ts
this.myDataSource = new MatTableDataSource();
this.myDataSource.data = dataToRender;
columnsToDisplay = ['userName', 'age'];
```
Then add `mat-header-row` and `mat-row` to the content of your `mat-table` and provide your
column list as inputs.
```html
<mat-table [dataSource]=”myDataSource”>
...
</mat-table>
<mat-header-row *matHeaderRowDef="columnsToDisplay"></mat-header-row>
<mat-row *matRowDef="let myRowData; columns: columnsToDisplay"></mat-row>
```
Note that this list of columns provided to the rows can be in any order, not necessary the order in
which you wrote the column definitions. Also, you do not necessarily have to include every column
that was defined in your template.
This means that by changing your column list provided to the rows, you can easily re-order and
include/exclude columns dynamically.
### Advanced data sources
The simplest way to provide data to your table is by passing a data array. More complex use-cases
may benefit from a more flexible approach involving an Observable stream or by encapsulating your
data source logic into a `DataSource` class.
#### Observable stream of data arrays
An alternative approach to providing data to the table is by passing an Observable stream that emits
the data array to be rendered each time it is changed. The table will listen to this stream and
automatically trigger an update to the rows each time a new data array is emitted.
#### DataSource
For most real-world applications, providing the table a DataSource instance will be the best way to
manage data. The DataSource is meant to serve a place to encapsulate any sorting, filtering,
pagination, and data retrieval logic specific to the application.
A DataSource is simply a base class that has two functions: `connect` and `disconnect`. The
`connect` function will be called by the table to receive a stream that emits the data array that
should be rendered. The table will call `disconnect` when the table is destroyed, which may be the
right time to clean up any subscriptions that may have been registered during the connect process.
### Features
The `MatTable` is focused on a single responsibility: efficiently render rows of data in a
performant and accessible way.
You'll notice that the table itself doesn't come out of the box with a lot of features, but expects
that the table will be included in a composition of components that fills out its features.
For example, you can add sorting and pagination to the table by using MatSort and MatPaginator and
mutating the data provided to the table according to their outputs.
To simplify the use case of having a table that can sort, paginate, and filter an array of data,
the Angular Material library comes with a `MatTableDataSource` that has already implemented
the logic of determining what rows should be rendered according to the current table state. To add
these feature to the table, check out their respective sections below.
#### Pagination
To paginate the table's data, add a `<mat-paginator>` after the `<mat-table>` and provide the
`MatPaginator` to the `MatTableDataSource`. The data source will automatically listen for page
changes made by the user and send the right paged data to the table.
To paginate the table's data, add a `<mat-paginator>` after the `<mat-table>`.
If you are using the `MatTableDataSource` for your table's data source, simply provide the
`MatPaginator` to your data source. It will automatically listen for page changes made by the user
and send the right paged data to the table.
Otherwise if you are implementing the logic to paginate your data, you will want to listen to the
paginator's `(page)` output and pass the right slice of data to your table.
For more information on using and configuring the `<mat-paginator>`, check out the
[mat-paginator docs](https://material.angular.io/components/paginator/overview).
The `MatPaginator` is one provided solution to paginating your table's data, but it is not the only
option. In fact, the table can work with any custom pagination UI or strategy since the `MatTable` and `DataSource` interface is not tied to any one specific implementation.
option. In fact, the table can work with any custom pagination UI or strategy since the `MatTable`
and its interface is not tied to any one specific implementation.
<!-- example(table-pagination) -->
#### Sorting
To add sorting behavior to the table, add the `matSort` directive to the `<mat-table>` and add
`mat-sort-header` to each column header cell that should trigger sorting. Provide the `MatSort` directive to the `MatTableDataSource` and it will automatically listen for sorting changes and change the order of data rendered by the table.
By default, the `MatTableDataSource` sorts with the assumption that the sorted column's name
matches the data property name that the column displays. For example, the following column definition is named `position`, which matches the name of the property displayed in the row cell.
`mat-sort-header` to each column header cell that should trigger sorting.
```html
<!-- Name Column -->
@@ -98,8 +148,22 @@ matches the data property name that the column displays. For example, the follow
</ng-container>
```
If the data properties do not match the column names, or if a more complex data property accessor
is required, then a custom `sortingDataAccessor` function can be set to override the default data accessor on the `MatTableDataSource`.
If you are using the `MatTableDataSource` for your table's data source, provide the `MatSort`
directive to the data source and it will automatically listen for sorting changes and change the
order of data rendered by the table.
By default, the `MatTableDataSource` sorts with the assumption that the sorted column's name
matches the data property name that the column displays. For example, the following column
definition is named `position`, which matches the name of the property displayed in the row cell.
Note that if the data properties do not match the column names, or if a more complex data property
accessor is required, then a custom `sortingDataAccessor` function can be set to override the
default data accessor on the `MatTableDataSource`.
If you are not using the `MatTableDataSource`, but instead implementing custom logic to sort your
data, listen to the sort's `(sortChange)` event and re-order your data according to the sort state.
If you are providing a data array directly to the table, don't forget to call `renderRows()` on the
table, since it will not automatically check the array for changes.
<!-- example(table-sorting) -->
@@ -107,7 +171,8 @@ For more information on using and configuring the sorting behavior, check out th
[matSort docs](https://material.angular.io/components/sort/overview).
The `MatSort` is one provided solution to sorting your table's data, but it is not the only option.
In fact, the table can work with any custom pagination UI or strategy since the `MatTable` and `DataSource` interface is not tied to any one specific implementation.
In fact, the table can work with any custom pagination UI or strategy since the `MatTable` and
its interface is not tied to any one specific implementation.
#### Filtering
@@ -117,11 +182,14 @@ since there is no single common approach to adding a filter UI to table data.
A general strategy is to add an input where users can type in a filter string and listen to this
input to change what data is offered from the data source to the table.
If you are using the `MatTableDataSource`, simply provide the filter string to the
`MatTableDataSource`. The data source will reduce each row data to a serialized form and will filter out the row if it does not contain the filter string. By default, the row data reducing function will concatenate all the object values and convert them to lowercase.
If you are using the `MatTableDataSource`, simply provide the filter string to the
`MatTableDataSource`. The data source will reduce each row data to a serialized form and will filter
out the row if it does not contain the filter string. By default, the row data reducing function
will concatenate all the object values and convert them to lowercase.
For example, the data object `{id: 123, name: 'Mr. Smith', favoriteColor: 'blue'}` will be reduced
to `123mr. smithblue`. If your filter string was `blue` then it would be considered a match because it is contained in the reduced string, and the row would be displayed in the table.
to `123mr. smithblue`. If your filter string was `blue` then it would be considered a match because
it is contained in the reduced string, and the row would be displayed in the table.
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.
@@ -131,7 +199,8 @@ takes a data object and filter string and returns true if the data object is con
#### Selection
Right now there is no formal support for adding a selection UI to the table, but Angular Material
does offer the right components and pieces to set this up. The following steps are one solution but it is not the only way to incorporate row selection in your table.
does offer the right components and pieces to set this up. The following steps are one solution but
it is not the only way to incorporate row selection in your table.
##### 1. Add a selection model
@@ -147,7 +216,8 @@ this.selection = new SelectionModel<MyDataType>(allowMultiSelect, initialSelecti
##### 2. Define a selection column
Add a column definition for displaying the row checkboxes, including a master toggle checkbox for
the header. The column name should be added to the list of displayed columns provided to the `<mat-header-row>` and `<mat-row>`.
the header. The column name should be added to the list of displayed columns provided to the
`<mat-header-row>` and `<mat-row>`.
```html
<ng-container matColumnDef="select">
@@ -1,5 +1,4 @@
import {Component} from '@angular/core';
import {MatTableDataSource} from '@angular/material';
/**
* @title Basic table
@@ -11,7 +10,7 @@ import {MatTableDataSource} from '@angular/material';
})
export class TableBasicExample {
displayedColumns = ['position', 'name', 'weight', 'symbol'];
dataSource = new MatTableDataSource<Element>(ELEMENT_DATA);
dataSource = ELEMENT_DATA;
}
export interface Element {
+10 -5
View File
@@ -45,6 +45,15 @@ import {
import {Overlay} from '@angular/cdk/overlay';
import {of as observableOf} from 'rxjs/observable/of';
import {Observable} from 'rxjs/Observable';
export class TableDataSource extends DataSource<any> {
connect(): Observable<any> {
return observableOf([{userId: 1}, {userId: 2}]);
}
disconnect() {}
}
@Component({
@@ -64,10 +73,7 @@ export class KitchenSink {
tableColumns = ['userId'];
/** Data source for the CDK and Material table. */
tableDataSource: DataSource<any> = {
connect: () => observableOf([{userId: 1}, {userId: 2}]),
disconnect: () => {}
};
tableDataSource = new TableDataSource();
constructor(snackBar: MatSnackBar, dialog: MatDialog, overlay: Overlay) {
// Open a snack bar to do a basic sanity check of the overlays.
@@ -77,7 +83,6 @@ export class KitchenSink {
// the fixes for the block scroll strategy get in.
dialog.open(TestDialog, {scrollStrategy: overlay.scrollStrategies.noop()});
}
}