Skip to content

Commit 181820e

Browse files
authored
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
1 parent a9ad62d commit 181820e

File tree

2 files changed

+79
-19
lines changed

2 files changed

+79
-19
lines changed

src/lib/core/data-table/data-table.spec.ts

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import {async, ComponentFixture, TestBed} from '@angular/core/testing';
22
import {Component, ViewChild} from '@angular/core';
33
import {CdkTable} from './data-table';
44
import {CollectionViewer, DataSource} from './data-source';
5-
import {CommonModule} from '@angular/common';
65
import {Observable} from 'rxjs/Observable';
76
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
87
import {customMatchers} from '../testing/jasmine-matchers';
@@ -13,7 +12,7 @@ describe('CdkTable', () => {
1312

1413
let component: SimpleCdkTableApp;
1514
let dataSource: FakeDataSource;
16-
let table: CdkTable;
15+
let table: CdkTable<any>;
1716
let tableElement: HTMLElement;
1817

1918
beforeEach(async(() => {
@@ -97,14 +96,47 @@ describe('CdkTable', () => {
9796
});
9897
});
9998

99+
it('should use differ to add/remove/move rows', () => {
100+
// Each row receives an attribute 'initialIndex' the element's original place
101+
getRows(tableElement).forEach((row: Element, index: number) => {
102+
row.setAttribute('initialIndex', index.toString());
103+
});
104+
105+
// Prove that the attributes match their indicies
106+
const initialRows = getRows(tableElement);
107+
expect(initialRows[0].getAttribute('initialIndex')).toBe('0');
108+
expect(initialRows[1].getAttribute('initialIndex')).toBe('1');
109+
expect(initialRows[2].getAttribute('initialIndex')).toBe('2');
110+
111+
// Swap first and second data in data array
112+
const copiedData = component.dataSource.data.slice();
113+
const temp = copiedData[0];
114+
copiedData[0] = copiedData[1];
115+
copiedData[1] = temp;
116+
117+
// Remove the third element
118+
copiedData.splice(2, 1);
119+
120+
// Add new data
121+
component.dataSource.data = copiedData;
122+
component.dataSource.addData();
123+
124+
// Expect that the first and second rows were swapped and that the last row is new
125+
const changedRows = getRows(tableElement);
126+
expect(changedRows.length).toBe(3);
127+
expect(changedRows[0].getAttribute('initialIndex')).toBe('1');
128+
expect(changedRows[1].getAttribute('initialIndex')).toBe('0');
129+
expect(changedRows[2].getAttribute('initialIndex')).toBe(null);
130+
});
131+
100132
// TODO(andrewseguin): Add test for dynamic classes on header/rows
101133

102134
it('should match the right table content with dynamic data', () => {
103-
let initialDataLength = dataSource.data.length;
135+
const initialDataLength = dataSource.data.length;
104136
expect(dataSource.data.length).toBe(3);
105-
let headerContent = ['Column A', 'Column B', 'Column C'];
137+
const headerContent = ['Column A', 'Column B', 'Column C'];
106138

107-
let initialTableContent = [headerContent];
139+
const initialTableContent = [headerContent];
108140
dataSource.data.forEach(rowData => initialTableContent.push([rowData.a, rowData.b, rowData.c]));
109141
expect(tableElement).toMatchTableContent(initialTableContent);
110142

@@ -114,7 +146,7 @@ describe('CdkTable', () => {
114146
fixture.detectChanges();
115147
fixture.detectChanges();
116148

117-
let changedTableContent = [headerContent];
149+
const changedTableContent = [headerContent];
118150
dataSource.data.forEach(rowData => changedTableContent.push([rowData.a, rowData.b, rowData.c]));
119151
expect(tableElement).toMatchTableContent(changedTableContent);
120152
});
@@ -190,7 +222,7 @@ class SimpleCdkTableApp {
190222
dataSource: FakeDataSource = new FakeDataSource();
191223
columnsToRender = ['column_a', 'column_b', 'column_c'];
192224

193-
@ViewChild(CdkTable) table: CdkTable;
225+
@ViewChild(CdkTable) table: CdkTable<TestData>;
194226
}
195227

196228
function getElements(element: Element, query: string): Element[] {

src/lib/core/data-table/data-table.ts

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ import {
66
ContentChildren,
77
Directive,
88
Input,
9+
IterableChangeRecord,
10+
IterableDiffer,
11+
IterableDiffers,
12+
NgIterable,
913
QueryList,
1014
ViewChild,
1115
ViewContainerRef,
@@ -38,7 +42,7 @@ export class HeaderRowPlaceholder {
3842
}
3943

4044
/**
41-
* A data table that connects with a data source to retrieve data and renders
45+
* A data table that connects with a data source to retrieve data of type T and renders
4246
* a header row and data rows. Updates the rows when new data is provided by the data source.
4347
*/
4448
@Component({
@@ -54,12 +58,12 @@ export class HeaderRowPlaceholder {
5458
encapsulation: ViewEncapsulation.None,
5559
changeDetection: ChangeDetectionStrategy.OnPush,
5660
})
57-
export class CdkTable implements CollectionViewer {
61+
export class CdkTable<T> implements CollectionViewer {
5862
/**
5963
* Provides a stream containing the latest data array to render. Influenced by the table's
6064
* stream of view window (what rows are currently on screen).
6165
*/
62-
@Input() dataSource: DataSource<any>;
66+
@Input() dataSource: DataSource<T>;
6367

6468
// TODO(andrewseguin): Remove max value as the end index
6569
// and instead calculate the view on init and scroll.
@@ -76,6 +80,9 @@ export class CdkTable implements CollectionViewer {
7680
*/
7781
private _columnDefinitionsByName = new Map<string, CdkColumnDef>();
7882

83+
/** Differ used to find the changes in the data provided by the data source. */
84+
private _dataDiffer: IterableDiffer<T> = null;
85+
7986
// Placeholders within the table's template where the header and data rows will be inserted.
8087
@ViewChild(RowPlaceholder) _rowPlaceholder: RowPlaceholder;
8188
@ViewChild(HeaderRowPlaceholder) _headerRowPlaceholder: HeaderRowPlaceholder;
@@ -92,9 +99,14 @@ export class CdkTable implements CollectionViewer {
9299
/** Set of templates that used as the data row containers. */
93100
@ContentChildren(CdkRowDef) _rowDefinitions: QueryList<CdkRowDef>;
94101

95-
constructor(private _changeDetectorRef: ChangeDetectorRef) {
102+
constructor(private readonly _differs: IterableDiffers,
103+
private readonly _changeDetectorRef: ChangeDetectorRef) {
96104
console.warn('The data table is still in active development ' +
97105
'and should be considered unstable.');
106+
107+
// TODO(andrewseguin): Add trackby function input.
108+
// Find and construct an iterable differ that can be used to find the diff in an array.
109+
this._dataDiffer = this._differs.find([]).create();
98110
}
99111

100112
ngOnDestroy() {
@@ -122,12 +134,8 @@ export class CdkTable implements CollectionViewer {
122134
// TODO(andrewseguin): If the data source is not
123135
// present after view init, connect it when it is defined.
124136
// TODO(andrewseguin): Unsubscribe from this on destroy.
125-
this.dataSource.connect(this).subscribe((rowsData: any[]) => {
126-
// TODO(andrewseguin): Add a differ that will check if the data has changed,
127-
// rather than re-rendering all rows
128-
this._rowPlaceholder.viewContainer.clear();
129-
rowsData.forEach(rowData => this.insertRow(rowData));
130-
this._changeDetectorRef.markForCheck();
137+
this.dataSource.connect(this).subscribe((rowsData: NgIterable<T>) => {
138+
this.renderRowChanges(rowsData);
131139
});
132140
}
133141

@@ -146,11 +154,31 @@ export class CdkTable implements CollectionViewer {
146154
CdkCellOutlet.mostRecentCellOutlet.context = {};
147155
}
148156

157+
/** Check for changes made in the data and render each change (row added/removed/moved). */
158+
renderRowChanges(dataRows: NgIterable<T>) {
159+
const changes = this._dataDiffer.diff(dataRows);
160+
if (!changes) { return; }
161+
162+
changes.forEachOperation(
163+
(item: IterableChangeRecord<any>, adjustedPreviousIndex: number, currentIndex: number) => {
164+
if (item.previousIndex == null) {
165+
this.insertRow(dataRows[currentIndex], currentIndex);
166+
} else if (currentIndex == null) {
167+
this._rowPlaceholder.viewContainer.remove(adjustedPreviousIndex);
168+
} else {
169+
const view = this._rowPlaceholder.viewContainer.get(adjustedPreviousIndex);
170+
this._rowPlaceholder.viewContainer.move(view, currentIndex);
171+
}
172+
});
173+
174+
this._changeDetectorRef.markForCheck();
175+
}
176+
149177
/**
150178
* Create the embedded view for the data row template and place it in the correct index location
151179
* within the data row view container.
152180
*/
153-
insertRow(rowData: any) {
181+
insertRow(rowData: T, index: number) {
154182
// TODO(andrewseguin): Add when predicates to the row definitions
155183
// to find the right template to used based on
156184
// the data rather than choosing the first row definition.
@@ -161,7 +189,7 @@ export class CdkTable implements CollectionViewer {
161189

162190
// TODO(andrewseguin): add some code to enforce that exactly one
163191
// CdkCellOutlet was instantiated as a result of `createEmbeddedView`.
164-
this._rowPlaceholder.viewContainer.createEmbeddedView(row.template, context);
192+
this._rowPlaceholder.viewContainer.createEmbeddedView(row.template, context, index);
165193

166194
// Insert empty cells if there is no data to improve rendering time.
167195
CdkCellOutlet.mostRecentCellOutlet.cells = rowData ? this.getCellTemplatesForRow(row) : [];

0 commit comments

Comments
 (0)