+
+
+
diff --git a/src/demo-app/data-table/data-table-demo.scss b/src/demo-app/data-table/data-table-demo.scss
index bfbf160f52e..a6f5c6cd082 100644
--- a/src/demo-app/data-table/data-table-demo.scss
+++ b/src/demo-app/data-table/data-table-demo.scss
@@ -3,6 +3,7 @@
display: flex;
flex-direction: column;
max-height: 800px;
+ background: white;
// Table fills in the remaining area with a scroll
.cdk-table {
@@ -17,7 +18,6 @@
*/
.cdk-table {
display: block;
- background: white;
}
.cdk-row, .cdk-header-row {
diff --git a/src/demo-app/data-table/data-table-demo.ts b/src/demo-app/data-table/data-table-demo.ts
index f72e6fb232f..0f9e29fb7e8 100644
--- a/src/demo-app/data-table/data-table-demo.ts
+++ b/src/demo-app/data-table/data-table-demo.ts
@@ -2,6 +2,8 @@ import {Component} from '@angular/core';
import {PeopleDatabase} from './people-database';
import {PersonDataSource} from './person-data-source';
+type UserProperties = 'userId' | 'userName' | 'progress' | 'color';
+
@Component({
moduleId: module.id,
selector: 'data-table-demo',
@@ -10,7 +12,7 @@ import {PersonDataSource} from './person-data-source';
})
export class DataTableDemo {
dataSource: PersonDataSource;
- propertiesToDisplay = ['userId', 'userName', 'progress', 'color'];
+ propertiesToDisplay: UserProperties[] = ['userId', 'userName', 'progress', 'color'];
constructor(private _peopleDatabase: PeopleDatabase) { }
@@ -22,4 +24,13 @@ export class DataTableDemo {
let distanceFromMiddle = Math.abs(50 - progress);
return distanceFromMiddle / 50 + .3;
}
+
+ toggleColorColumn() {
+ let colorColumnIndex = this.propertiesToDisplay.indexOf('color');
+ if (colorColumnIndex == -1) {
+ this.propertiesToDisplay.push('color');
+ } else {
+ this.propertiesToDisplay.splice(colorColumnIndex, 1);
+ }
+ }
}
diff --git a/src/demo-app/data-table/person-data-source.ts b/src/demo-app/data-table/person-data-source.ts
index c21eade02d4..18904b2b3d1 100644
--- a/src/demo-app/data-table/person-data-source.ts
+++ b/src/demo-app/data-table/person-data-source.ts
@@ -10,7 +10,7 @@ export class PersonDataSource extends DataSource {
}
connect(collectionViewer: CollectionViewer): Observable {
- return collectionViewer.viewChanged.map((view: {start: number, end: number}) => {
+ return collectionViewer.viewChange.map((view: {start: number, end: number}) => {
// Set the rendered rows length to the virtual page size. Fill in the data provided
// from the index start until the end index or pagination size, whichever is smaller.
this._renderedData.length = this._peopleDatabase.data.length;
diff --git a/src/demo-app/data-table/table-header-demo.html b/src/demo-app/data-table/table-header-demo.html
new file mode 100644
index 00000000000..e03ccb1e682
--- /dev/null
+++ b/src/demo-app/data-table/table-header-demo.html
@@ -0,0 +1,19 @@
+
+ Users
+
+
+
+
+
+
+
+
+
diff --git a/src/demo-app/data-table/table-header-demo.scss b/src/demo-app/data-table/table-header-demo.scss
new file mode 100644
index 00000000000..7304532ab81
--- /dev/null
+++ b/src/demo-app/data-table/table-header-demo.scss
@@ -0,0 +1,15 @@
+:host {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ min-height: 64px;
+ padding: 0 16px;
+}
+
+.title {
+ font-size: 20px;
+}
+
+.actions {
+ color: rgba(0, 0, 0, 0.54);
+}
diff --git a/src/demo-app/data-table/table-header-demo.ts b/src/demo-app/data-table/table-header-demo.ts
new file mode 100644
index 00000000000..3c6d1b5afc9
--- /dev/null
+++ b/src/demo-app/data-table/table-header-demo.ts
@@ -0,0 +1,12 @@
+import {Component, EventEmitter, Output} from '@angular/core';
+
+@Component({
+ moduleId: module.id,
+ selector: 'table-header-demo',
+ templateUrl: 'table-header-demo.html',
+ styleUrls: ['table-header-demo.css'],
+})
+export class TableHeaderDemo {
+ @Output() shiftColumns = new EventEmitter();
+ @Output() toggleColorColumn = new EventEmitter();
+}
diff --git a/src/demo-app/demo-app-module.ts b/src/demo-app/demo-app-module.ts
index 38ec54b7ce1..86fd2322361 100644
--- a/src/demo-app/demo-app-module.ts
+++ b/src/demo-app/demo-app-module.ts
@@ -75,6 +75,7 @@ import {
MdTooltipModule,
OverlayContainer
} from '@angular/material';
+import {TableHeaderDemo} from './data-table/table-header-demo';
/**
* NgModule that includes all Material modules that are required to serve the demo-app.
@@ -165,6 +166,7 @@ export class DemoMaterialModule {}
SlideToggleDemo,
SpagettiPanel,
StyleDemo,
+ TableHeaderDemo,
ToolbarDemo,
TooltipDemo,
TabsDemo,
diff --git a/src/lib/core/data-table/data-source.ts b/src/lib/core/data-table/data-source.ts
index 8314d1299bd..b0a57593ef4 100644
--- a/src/lib/core/data-table/data-source.ts
+++ b/src/lib/core/data-table/data-source.ts
@@ -1,7 +1,7 @@
import {Observable} from 'rxjs/Observable';
export interface CollectionViewer {
- viewChanged: Observable<{start: number, end: number}>;
+ viewChange: Observable<{start: number, end: number}>;
}
export abstract class DataSource {
diff --git a/src/lib/core/data-table/data-table.spec.ts b/src/lib/core/data-table/data-table.spec.ts
index ff0d7a36b15..b463faa77a0 100644
--- a/src/lib/core/data-table/data-table.spec.ts
+++ b/src/lib/core/data-table/data-table.spec.ts
@@ -96,6 +96,8 @@ describe('CdkTable', () => {
});
});
+ // TODO(andrewseguin): Add test for dynamic classes on header/rows
+
it('should use differ to add/remove/move rows', () => {
// Each row receives an attribute 'initialIndex' the element's original place
getRows(tableElement).forEach((row: Element, index: number) => {
@@ -129,26 +131,57 @@ describe('CdkTable', () => {
expect(changedRows[2].getAttribute('initialIndex')).toBe(null);
});
- // TODO(andrewseguin): Add test for dynamic classes on header/rows
-
it('should match the right table content with dynamic data', () => {
const initialDataLength = dataSource.data.length;
expect(dataSource.data.length).toBe(3);
- const headerContent = ['Column A', 'Column B', 'Column C'];
- const initialTableContent = [headerContent];
- dataSource.data.forEach(rowData => initialTableContent.push([rowData.a, rowData.b, rowData.c]));
- expect(tableElement).toMatchTableContent(initialTableContent);
+ let data = dataSource.data;
+ expect(tableElement).toMatchTableContent([
+ ['Column A', 'Column B', 'Column C'],
+ [data[0].a, data[0].b, data[0].c],
+ [data[1].a, data[1].b, data[1].c],
+ [data[2].a, data[2].b, data[2].c],
+ ]);
// Add data to the table and recreate what the rendered output should be.
dataSource.addData();
expect(dataSource.data.length).toBe(initialDataLength + 1); // Make sure data was added
- fixture.detectChanges();
+
+ data = dataSource.data;
+ expect(tableElement).toMatchTableContent([
+ ['Column A', 'Column B', 'Column C'],
+ [data[0].a, data[0].b, data[0].c],
+ [data[1].a, data[1].b, data[1].c],
+ [data[2].a, data[2].b, data[2].c],
+ [data[3].a, data[3].b, data[3].c],
+ ]);
+ });
+
+ it('should be able to dynamically change the columns for header and rows', () => {
+ expect(dataSource.data.length).toBe(3);
+
+ let data = dataSource.data;
+ expect(tableElement).toMatchTableContent([
+ ['Column A', 'Column B', 'Column C'],
+ [data[0].a, data[0].b, data[0].c],
+ [data[1].a, data[1].b, data[1].c],
+ [data[2].a, data[2].b, data[2].c],
+ ]);
+
+ // Remove column_a and swap column_b/column_c.
+ component.columnsToRender = ['column_c', 'column_b'];
fixture.detectChanges();
- const changedTableContent = [headerContent];
- dataSource.data.forEach(rowData => changedTableContent.push([rowData.a, rowData.b, rowData.c]));
- expect(tableElement).toMatchTableContent(changedTableContent);
+ let changedTableContent = [['Column C', 'Column B']];
+ dataSource.data.forEach(rowData => changedTableContent.push([rowData.c, rowData.b]));
+
+ data = dataSource.data;
+ expect(tableElement).toMatchTableContent([
+ ['Column C', 'Column B'],
+ [data[0].c, data[0].b],
+ [data[1].c, data[1].b],
+ [data[2].c, data[2].b],
+ ]);
});
});
@@ -172,11 +205,8 @@ class FakeDataSource extends DataSource {
connect(collectionViewer: CollectionViewer): Observable {
this.isConnected = true;
- const streams = [collectionViewer.viewChanged, this._dataChange];
- return Observable.combineLatest(streams).map((results: any[]) => {
- const [view, data] = results;
- return data;
- });
+ const streams = [this._dataChange, collectionViewer.viewChange];
+ return Observable.combineLatest(streams).map(([data]) => data);
}
addData() {
diff --git a/src/lib/core/data-table/data-table.ts b/src/lib/core/data-table/data-table.ts
index 2533e360e89..fbe26f7b0b2 100644
--- a/src/lib/core/data-table/data-table.ts
+++ b/src/lib/core/data-table/data-table.ts
@@ -15,13 +15,14 @@ import {
ViewContainerRef,
ViewEncapsulation
} from '@angular/core';
+import {CollectionViewer, DataSource} from './data-source';
+import {BaseRowDef, CdkCellOutlet, CdkHeaderRowDef, CdkRowDef} from './row';
+import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef} from './cell';
+import {Observable} from 'rxjs/Observable';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/let';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/observable/combineLatest';
-import {CollectionViewer, DataSource} from './data-source';
-import {CdkCellOutlet, CdkHeaderRowDef, CdkRowDef} from './row';
-import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef} from './cell';
/**
* Provides a handle for the table to grab the view container's ng-container to insert data rows.
@@ -66,14 +67,17 @@ export class CdkTable implements CollectionViewer {
@Input() dataSource: DataSource;
// TODO(andrewseguin): Remove max value as the end index
- // and instead calculate the view on init and scroll.
+ // and instead calculate the view on init and scroll.
/**
* Stream containing the latest information on what rows are being displayed on screen.
* Can be used by the data source to as a heuristic of what data should be provided.
*/
- viewChanged =
+ viewChange =
new BehaviorSubject<{start: number, end: number}>({start: 0, end: Number.MAX_VALUE});
+ /** Stream that emits when a row def has a change to its array of columns to render. */
+ _columnsChange = new Observable();
+
/**
* Map of all the user's defined columns identified by name.
* Contains the header and data-cell templates.
@@ -115,12 +119,12 @@ export class CdkTable implements CollectionViewer {
ngOnDestroy() {
// TODO(andrewseguin): Disconnect from the data source so
- // that it can unsubscribe from its streams.
+ // that it can unsubscribe from its streams.
}
ngOnInit() {
// TODO(andrewseguin): Setup a listener for scroll events
- // and emit the calculated view to this.viewChanged
+ // and emit the calculated view to this.viewChange
}
ngAfterContentInit() {
@@ -128,17 +132,32 @@ export class CdkTable implements CollectionViewer {
this._columnDefinitions.forEach(columnDef => {
this._columnDefinitionsByName.set(columnDef.name, columnDef);
});
+
+ // Get and merge the streams for column changes made to the row defs
+ const rowDefs = [...this._rowDefinitions.toArray(), this._headerDefinition];
+ const columnChangeStreams =
+ rowDefs.map((rowDef: BaseRowDef) => rowDef.columnsChange);
+ this._columnsChange = Observable.merge(...columnChangeStreams);
}
ngAfterViewInit() {
- // TODO(andrewseguin): Re-render the header when the header's columns change.
this.renderHeaderRow();
- // TODO(andrewseguin): Re-render rows when their list of columns change.
+ // Re-render the header row if the columns changed.
+ this._columnsChange.subscribe(() => {
+ this._headerRowPlaceholder.viewContainer.clear();
+ this.renderHeaderRow();
+
+ // Reset the data to an empty array so that renderRowChanges will re-render all new rows.
+ this._rowPlaceholder.viewContainer.clear();
+ this._dataDiffer.diff([]);
+ });
+
// TODO(andrewseguin): If the data source is not
// present after view init, connect it when it is defined.
// TODO(andrewseguin): Unsubscribe from this on destroy.
- this.dataSource.connect(this).subscribe((rowsData: NgIterable) => {
+ const streams = [this.dataSource.connect(this), this._columnsChange];
+ Observable.combineLatest(streams).subscribe(([rowsData]) => {
this.renderRowChanges(rowsData);
});
}
@@ -150,8 +169,8 @@ export class CdkTable implements CollectionViewer {
const cells = this.getHeaderCellTemplatesForRow(this._headerDefinition);
// TODO(andrewseguin): add some code to enforce that exactly
- // one CdkCellOutlet was instantiated as a result
- // of `createEmbeddedView`.
+ // one CdkCellOutlet was instantiated as a result
+ // of `createEmbeddedView`.
this._headerRowPlaceholder.viewContainer
.createEmbeddedView(this._headerDefinition.template, {cells});
CdkCellOutlet.mostRecentCellOutlet.cells = cells;
@@ -206,6 +225,7 @@ export class CdkTable implements CollectionViewer {
*/
getHeaderCellTemplatesForRow(headerDef: CdkHeaderRowDef): CdkHeaderCellDef[] {
return headerDef.columns.map(columnId => {
+ // TODO(andrewseguin): Throw an error if there is no column with this columnId
return this._columnDefinitionsByName.get(columnId).headerCell;
});
}
@@ -216,6 +236,7 @@ export class CdkTable implements CollectionViewer {
*/
getCellTemplatesForRow(rowDef: CdkRowDef): CdkCellDef[] {
return rowDef.columns.map(columnId => {
+ // TODO(andrewseguin): Throw an error if there is no column with this columnId
return this._columnDefinitionsByName.get(columnId).cell;
});
}
diff --git a/src/lib/core/data-table/row.ts b/src/lib/core/data-table/row.ts
index 79baa7bfb21..5c794b04733 100644
--- a/src/lib/core/data-table/row.ts
+++ b/src/lib/core/data-table/row.ts
@@ -2,35 +2,83 @@ import {
ChangeDetectionStrategy,
Component,
Directive,
- Input,
+ IterableDiffer,
+ IterableDiffers,
+ SimpleChanges,
TemplateRef,
ViewContainerRef
} from '@angular/core';
import {CdkCellDef} from './cell';
+import {Subject} from 'rxjs/Subject';
+
+/**
+ * Base class for the CdkHeaderRowDef and CdkRowDef that handles checking their columns inputs
+ * for changes and notifying the table.
+ */
+export abstract class BaseRowDef {
+ /** The columns to be displayed on this row. */
+ columns: string[];
+
+ /** Event stream that emits when changes are made to the columns. */
+ columnsChange: Subject = new Subject();
+
+ /** Differ used to check if any changes were made to the columns. */
+ protected _columnsDiffer: IterableDiffer;
+
+ private viewInitialized = false;
+
+ constructor(public template: TemplateRef,
+ protected _differs: IterableDiffers) { }
+
+ ngAfterViewInit() {
+ this.viewInitialized = true;
+ }
+
+ ngOnChanges(changes: SimpleChanges): void {
+ // Create a new columns differ if one does not yet exist. Initialize it based on initial value
+ // of the columns property.
+ if (!this._columnsDiffer) {
+ this._columnsDiffer = this._differs.find(changes['columns'].currentValue).create();
+ }
+ }
+
+ ngDoCheck(): void {
+ if (!this.viewInitialized || !this._columnsDiffer || !this.columns) { return; }
+
+ // Notify the table if there are any changes to the columns.
+ const changes = this._columnsDiffer.diff(this.columns);
+ if (changes) { this.columnsChange.next(); }
+ }
+}
/**
* Header row definition for the CDK data-table.
* Captures the header row's template and other header properties such as the columns to display.
*/
-@Directive({selector: '[cdkHeaderRowDef]'})
-export class CdkHeaderRowDef {
- @Input('cdkHeaderRowDef') columns: string[];
-
- constructor(public template: TemplateRef) { }
+@Directive({
+ selector: '[cdkHeaderRowDef]',
+ inputs: ['columns: cdkHeaderRowDef'],
+})
+export class CdkHeaderRowDef extends BaseRowDef {
+ constructor(template: TemplateRef, _differs: IterableDiffers) {
+ super(template, _differs);
+ }
}
/**
* Data row definition for the CDK data-table.
* Captures the header row's template and other row properties such as the columns to display.
*/
-@Directive({selector: '[cdkRowDef]'})
-export class CdkRowDef {
- @Input('cdkRowDefColumns') columns: string[];
-
+@Directive({
+ selector: '[cdkRowDef]',
+ inputs: ['columns: cdkRowDefColumns'],
+})
+export class CdkRowDef extends BaseRowDef {
// TODO(andrewseguin): Add an input for providing a switch function to determine
// if this template should be used.
-
- constructor(public template: TemplateRef) { }
+ constructor(template: TemplateRef, _differs: IterableDiffers) {
+ super(template, _differs);
+ }
}
/**