feat(data-table): use iterate differ to optimize rendering (#4823)

* feat(data-table): use iterate differ to optimize rendering

* minor changes

* let -> const

* add generic to cdktable

* readonly
This commit is contained in:
Andrew Seguin
2017-06-05 14:42:47 -07:00
committed by GitHub
parent a9ad62d0fa
commit 181820e8eb
2 changed files with 79 additions and 19 deletions
+39 -7
View File
@@ -2,7 +2,6 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
import {Component, ViewChild} from '@angular/core';
import {CdkTable} from './data-table';
import {CollectionViewer, DataSource} from './data-source';
import {CommonModule} from '@angular/common';
import {Observable} from 'rxjs/Observable';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {customMatchers} from '../testing/jasmine-matchers';
@@ -13,7 +12,7 @@ describe('CdkTable', () => {
let component: SimpleCdkTableApp;
let dataSource: FakeDataSource;
let table: CdkTable;
let table: CdkTable<any>;
let tableElement: HTMLElement;
beforeEach(async(() => {
@@ -97,14 +96,47 @@ describe('CdkTable', () => {
});
});
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) => {
row.setAttribute('initialIndex', index.toString());
});
// Prove that the attributes match their indicies
const initialRows = getRows(tableElement);
expect(initialRows[0].getAttribute('initialIndex')).toBe('0');
expect(initialRows[1].getAttribute('initialIndex')).toBe('1');
expect(initialRows[2].getAttribute('initialIndex')).toBe('2');
// Swap first and second data in data array
const copiedData = component.dataSource.data.slice();
const temp = copiedData[0];
copiedData[0] = copiedData[1];
copiedData[1] = temp;
// Remove the third element
copiedData.splice(2, 1);
// Add new data
component.dataSource.data = copiedData;
component.dataSource.addData();
// Expect that the first and second rows were swapped and that the last row is new
const changedRows = getRows(tableElement);
expect(changedRows.length).toBe(3);
expect(changedRows[0].getAttribute('initialIndex')).toBe('1');
expect(changedRows[1].getAttribute('initialIndex')).toBe('0');
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', () => {
let initialDataLength = dataSource.data.length;
const initialDataLength = dataSource.data.length;
expect(dataSource.data.length).toBe(3);
let headerContent = ['Column A', 'Column B', 'Column C'];
const headerContent = ['Column A', 'Column B', 'Column C'];
let initialTableContent = [headerContent];
const initialTableContent = [headerContent];
dataSource.data.forEach(rowData => initialTableContent.push([rowData.a, rowData.b, rowData.c]));
expect(tableElement).toMatchTableContent(initialTableContent);
@@ -114,7 +146,7 @@ describe('CdkTable', () => {
fixture.detectChanges();
fixture.detectChanges();
let changedTableContent = [headerContent];
const changedTableContent = [headerContent];
dataSource.data.forEach(rowData => changedTableContent.push([rowData.a, rowData.b, rowData.c]));
expect(tableElement).toMatchTableContent(changedTableContent);
});
@@ -190,7 +222,7 @@ class SimpleCdkTableApp {
dataSource: FakeDataSource = new FakeDataSource();
columnsToRender = ['column_a', 'column_b', 'column_c'];
@ViewChild(CdkTable) table: CdkTable;
@ViewChild(CdkTable) table: CdkTable<TestData>;
}
function getElements(element: Element, query: string): Element[] {
+40 -12
View File
@@ -6,6 +6,10 @@ import {
ContentChildren,
Directive,
Input,
IterableChangeRecord,
IterableDiffer,
IterableDiffers,
NgIterable,
QueryList,
ViewChild,
ViewContainerRef,
@@ -38,7 +42,7 @@ export class HeaderRowPlaceholder {
}
/**
* A data table that connects with a data source to retrieve data and renders
* 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.
*/
@Component({
@@ -54,12 +58,12 @@ export class HeaderRowPlaceholder {
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class CdkTable implements CollectionViewer {
export class CdkTable<T> implements CollectionViewer {
/**
* 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).
*/
@Input() dataSource: DataSource<any>;
@Input() dataSource: DataSource<T>;
// TODO(andrewseguin): Remove max value as the end index
// and instead calculate the view on init and scroll.
@@ -76,6 +80,9 @@ export class CdkTable implements CollectionViewer {
*/
private _columnDefinitionsByName = new Map<string, CdkColumnDef>();
/** Differ used to find the changes in the data provided by the data source. */
private _dataDiffer: IterableDiffer<T> = null;
// Placeholders within the table's template where the header and data rows will be inserted.
@ViewChild(RowPlaceholder) _rowPlaceholder: RowPlaceholder;
@ViewChild(HeaderRowPlaceholder) _headerRowPlaceholder: HeaderRowPlaceholder;
@@ -92,9 +99,14 @@ export class CdkTable implements CollectionViewer {
/** Set of templates that used as the data row containers. */
@ContentChildren(CdkRowDef) _rowDefinitions: QueryList<CdkRowDef>;
constructor(private _changeDetectorRef: ChangeDetectorRef) {
constructor(private readonly _differs: IterableDiffers,
private readonly _changeDetectorRef: ChangeDetectorRef) {
console.warn('The data table is still in active development ' +
'and should be considered unstable.');
// 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();
}
ngOnDestroy() {
@@ -122,12 +134,8 @@ export class CdkTable implements CollectionViewer {
// 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: any[]) => {
// TODO(andrewseguin): Add a differ that will check if the data has changed,
// rather than re-rendering all rows
this._rowPlaceholder.viewContainer.clear();
rowsData.forEach(rowData => this.insertRow(rowData));
this._changeDetectorRef.markForCheck();
this.dataSource.connect(this).subscribe((rowsData: NgIterable<T>) => {
this.renderRowChanges(rowsData);
});
}
@@ -146,11 +154,31 @@ export class CdkTable implements CollectionViewer {
CdkCellOutlet.mostRecentCellOutlet.context = {};
}
/** Check for changes made in the data and render each change (row added/removed/moved). */
renderRowChanges(dataRows: NgIterable<T>) {
const changes = this._dataDiffer.diff(dataRows);
if (!changes) { return; }
changes.forEachOperation(
(item: IterableChangeRecord<any>, adjustedPreviousIndex: number, currentIndex: number) => {
if (item.previousIndex == null) {
this.insertRow(dataRows[currentIndex], currentIndex);
} else if (currentIndex == null) {
this._rowPlaceholder.viewContainer.remove(adjustedPreviousIndex);
} else {
const view = this._rowPlaceholder.viewContainer.get(adjustedPreviousIndex);
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: any) {
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.
@@ -161,7 +189,7 @@ export class CdkTable implements CollectionViewer {
// TODO(andrewseguin): add some code to enforce that exactly one
// CdkCellOutlet was instantiated as a result of `createEmbeddedView`.
this._rowPlaceholder.viewContainer.createEmbeddedView(row.template, context);
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) : [];