{
TestBed.configureTestingModule({
imports: [CdkDataTableModule],
- declarations: [SimpleCdkTableApp, CustomRoleCdkTableApp],
+ declarations: [SimpleCdkTableApp, DynamicDataSourceCdkTableApp, CustomRoleCdkTableApp],
}).compileComponents();
fixture = TestBed.createComponent(SimpleCdkTableApp);
@@ -164,6 +164,40 @@ describe('CdkTable', () => {
]);
});
+ it('should match the right table content with dynamic data source', () => {
+ fixture = TestBed.createComponent(DynamicDataSourceCdkTableApp);
+ component = fixture.componentInstance;
+ tableElement = fixture.nativeElement.querySelector('cdk-table');
+
+ fixture.detectChanges(); // Let the table render the rows
+ fixture.detectChanges(); // Let the rows render their cells
+
+ // Expect that the component has no data source and the table element reflects empty data.
+ expect(component.dataSource).toBe(undefined);
+ expect(tableElement).toMatchTableContent([
+ ['Column A']
+ ]);
+
+ // Add a data source that has initialized data. Expect that the table shows this data.
+ component.dataSource = new FakeDataSource();
+ fixture.detectChanges();
+
+ let data = component.dataSource.data;
+ expect(tableElement).toMatchTableContent([
+ ['Column A'],
+ [data[0].a],
+ [data[1].a],
+ [data[2].a],
+ ]);
+
+ // Remove the data source and check to make sure the table is empty again.
+ component.dataSource = null;
+ fixture.detectChanges();
+ expect(tableElement).toMatchTableContent([
+ ['Column A']
+ ]);
+ });
+
it('should be able to dynamically change the columns for header and rows', () => {
expect(dataSource.data.length).toBe(3);
@@ -262,6 +296,26 @@ class SimpleCdkTableApp {
@ViewChild(CdkTable) table: CdkTable;
}
+@Component({
+ template: `
+
+
+ Column A
+ {{row.a}}
+
+
+
+
+
+ `
+})
+class DynamicDataSourceCdkTableApp {
+ dataSource: FakeDataSource;
+ columnsToRender = ['column_a'];
+
+ @ViewChild(CdkTable) table: CdkTable;
+}
+
@Component({
template: `
@@ -317,13 +371,19 @@ const tableCustomMatchers: jasmine.CustomMatcherFactories = {
// Check header cells
const expectedHeaderContent = expectedTableContent.shift();
getHeaderCells(tableElement).forEach((cell, index) => {
- return checkCellContent(cell, expectedHeaderContent[index]);
+ const expected = expectedHeaderContent ?
+ expectedHeaderContent[index] :
+ null;
+ checkCellContent(cell, expected);
});
// Check data row cells
getRows(tableElement).forEach((row, rowIndex) => {
getCells(row).forEach((cell, cellIndex) => {
- checkCellContent(cell, expectedTableContent[rowIndex][cellIndex]);
+ const expected = expectedHeaderContent ?
+ expectedTableContent[rowIndex][cellIndex] :
+ null;
+ checkCellContent(cell, expected);
});
});
diff --git a/src/lib/core/data-table/data-table.ts b/src/lib/core/data-table/data-table.ts
index 070f1fba1ef..5a3d5865b1c 100644
--- a/src/lib/core/data-table/data-table.ts
+++ b/src/lib/core/data-table/data-table.ts
@@ -34,6 +34,8 @@ import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import 'rxjs/add/operator/let';
import 'rxjs/add/operator/debounceTime';
import 'rxjs/add/observable/combineLatest';
+import {Subscription} from 'rxjs/Subscription';
+import {Subject} from 'rxjs/Subject';
/**
* Provides a handle for the table to grab the view container's ng-container to insert data rows.
@@ -70,11 +72,26 @@ export class HeaderRowPlaceholder {
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CdkTable implements CollectionViewer {
+ /** Subject that emits when the component has been destroyed. */
+ private _onDestroy = new Subject();
+
+ /** Flag set to true after the component has been initialized. */
+ private _isViewInitialized = false;
+
+ /** Latest data provided by the data source through the connect interface. */
+ private _data: NgIterable = [];
+
+ /** Subscription that listens for the data provided by the data source. */
+ private _renderChangeSubscription: Subscription;
+
/**
- * 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).
+ * Map of all the user's defined columns identified by name.
+ * Contains the header and data-cell templates.
*/
- @Input() dataSource: DataSource;
+ private _columnDefinitionsByName = new Map();
+
+ /** Differ used to find the changes in the data provided by the data source. */
+ private _dataDiffer: IterableDiffer = null;
// TODO(andrewseguin): Remove max value as the end index
// and instead calculate the view on init and scroll.
@@ -85,17 +102,18 @@ export class CdkTable implements CollectionViewer {
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.
+ * 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).
*/
- private _columnDefinitionsByName = new Map();
-
- /** Differ used to find the changes in the data provided by the data source. */
- private _dataDiffer: IterableDiffer = null;
+ @Input()
+ get dataSource(): DataSource { return this._dataSource; }
+ set dataSource(dataSource: DataSource) {
+ if (this._dataSource !== dataSource) {
+ this._switchDataSource(dataSource);
+ }
+ }
+ private _dataSource: DataSource;
// Placeholders within the table's template where the header and data rows will be inserted.
@ViewChild(RowPlaceholder) _rowPlaceholder: RowPlaceholder;
@@ -131,12 +149,12 @@ export class CdkTable implements CollectionViewer {
// TODO(andrewseguin): Add trackby function input.
// Find and construct an iterable differ that can be used to find the diff in an array.
- this._dataDiffer = this._differs.find([]).create();
+ this._dataDiffer = this._differs.find(this._data).create();
}
ngOnDestroy() {
- // TODO(andrewseguin): Disconnect from the data source so
- // that it can unsubscribe from its streams.
+ this._onDestroy.next();
+ this._onDestroy.complete();
}
ngOnInit() {
@@ -150,40 +168,74 @@ export class CdkTable implements CollectionViewer {
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);
+ // Re-render the rows if any of their columns change.
+ // TODO(andrewseguin): Determine how to only re-render the rows that have their columns changed.
+ Observable.merge(...this._rowDefinitions.map(rowDef => rowDef.columnsChange))
+ .takeUntil(this._onDestroy)
+ .subscribe(() => {
+ // Reset the data to an empty array so that renderRowChanges will re-render all new rows.
+ this._rowPlaceholder.viewContainer.clear();
+ this._dataDiffer.diff([]);
+ this._renderRowChanges();
+ });
+
+ // Re-render the header row if the columns change
+ this._headerDefinition.columnsChange
+ .takeUntil(this._onDestroy)
+ .subscribe(() => {
+ this._headerRowPlaceholder.viewContainer.clear();
+ this._renderHeaderRow();
+ });
}
ngAfterViewInit() {
- this.renderHeaderRow();
+ this._renderHeaderRow();
- // Re-render the header row if the columns changed.
- this._columnsChange.subscribe(() => {
- this._headerRowPlaceholder.viewContainer.clear();
- this.renderHeaderRow();
+ if (this.dataSource) {
+ this._observeRenderChanges();
+ }
- // Reset the data to an empty array so that renderRowChanges will re-render all new rows.
- this._rowPlaceholder.viewContainer.clear();
- this._dataDiffer.diff([]);
- });
+ this._isViewInitialized = true;
+ }
- // 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.
- const streams = [this.dataSource.connect(this), this._columnsChange];
- Observable.combineLatest(streams).subscribe(([rowsData]) => {
- this.renderRowChanges(rowsData);
- });
+ /**
+ * Switch to the provided data source by resetting the data and unsubscribing from the current
+ * 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) {
+ this._data = [];
+ this._dataSource = dataSource;
+
+ if (this._isViewInitialized) {
+ if (this._renderChangeSubscription) {
+ this._renderChangeSubscription.unsubscribe();
+ }
+
+ if (this._dataSource) {
+ this._observeRenderChanges();
+ } else {
+ this._rowPlaceholder.viewContainer.clear();
+ }
+ }
+ }
+
+ /** Set up a subscription for the data provided by the data source. */
+ private _observeRenderChanges() {
+ this._renderChangeSubscription = this.dataSource.connect(this)
+ .takeUntil(this._onDestroy)
+ .subscribe(data => {
+ this._data = data;
+ this._renderRowChanges();
+ });
}
/**
* Create the embedded view for the header template and place it in the header row view container.
*/
- renderHeaderRow() {
- const cells = this.getHeaderCellTemplatesForRow(this._headerDefinition);
+ private _renderHeaderRow() {
+ const cells = this._getHeaderCellTemplatesForRow(this._headerDefinition);
+ if (!cells.length) { return; }
// TODO(andrewseguin): add some code to enforce that exactly
// one CdkCellOutlet was instantiated as a result
@@ -192,17 +244,19 @@ export class CdkTable implements CollectionViewer {
.createEmbeddedView(this._headerDefinition.template, {cells});
CdkCellOutlet.mostRecentCellOutlet.cells = cells;
CdkCellOutlet.mostRecentCellOutlet.context = {};
+
+ this._changeDetectorRef.markForCheck();
}
/** Check for changes made in the data and render each change (row added/removed/moved). */
- renderRowChanges(dataRows: NgIterable) {
- const changes = this._dataDiffer.diff(dataRows);
+ private _renderRowChanges() {
+ const changes = this._dataDiffer.diff(this._data);
if (!changes) { return; }
changes.forEachOperation(
(item: IterableChangeRecord, adjustedPreviousIndex: number, currentIndex: number) => {
if (item.previousIndex == null) {
- this.insertRow(dataRows[currentIndex], currentIndex);
+ this._insertRow(this._data[currentIndex], currentIndex);
} else if (currentIndex == null) {
this._rowPlaceholder.viewContainer.remove(adjustedPreviousIndex);
} else {
@@ -210,15 +264,13 @@ export class CdkTable implements CollectionViewer {
this._rowPlaceholder.viewContainer.move(view, currentIndex);
}
});
-
- this._changeDetectorRef.markForCheck();
}
/**
* Create the embedded view for the data row template and place it in the correct index location
* within the data row view container.
*/
- insertRow(rowData: T, index: number) {
+ private _insertRow(rowData: T, index: number) {
// TODO(andrewseguin): Add when predicates to the row definitions
// to find the right template to used based on
// the data rather than choosing the first row definition.
@@ -232,15 +284,17 @@ export class CdkTable implements CollectionViewer {
this._rowPlaceholder.viewContainer.createEmbeddedView(row.template, context, index);
// Insert empty cells if there is no data to improve rendering time.
- CdkCellOutlet.mostRecentCellOutlet.cells = rowData ? this.getCellTemplatesForRow(row) : [];
+ CdkCellOutlet.mostRecentCellOutlet.cells = rowData ? this._getCellTemplatesForRow(row) : [];
CdkCellOutlet.mostRecentCellOutlet.context = context;
+
+ this._changeDetectorRef.markForCheck();
}
/**
* Returns the cell template definitions to insert into the header
* as defined by its list of columns to display.
*/
- getHeaderCellTemplatesForRow(headerDef: CdkHeaderRowDef): CdkHeaderCellDef[] {
+ private _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;
@@ -251,7 +305,7 @@ export class CdkTable implements CollectionViewer {
* Returns the cell template definitions to insert in the provided row
* as defined by its list of columns to display.
*/
- getCellTemplatesForRow(rowDef: CdkRowDef): CdkCellDef[] {
+ private _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;