Skip to content

Commit e81619b

Browse files
authored
feat(data-table): re-render when columns change (#4830)
* feat(data-table): re-render when columns change * minor changes * comment stuff * add column type * explicit testing: * fix observable type * sync * remove fdescribe * fix test
1 parent cac7610 commit e81619b

12 files changed

+205
-43
lines changed

src/demo-app/data-table/data-table-demo.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
<div class="demo-table-container mat-elevation-z4">
22

3+
<table-header-demo (shiftColumns)="propertiesToDisplay.push(propertiesToDisplay.shift())"
4+
(toggleColorColumn)="toggleColorColumn()">
5+
</table-header-demo>
6+
37
<cdk-table #table [dataSource]="dataSource">
48

59
<!-- Column Definition: ID -->

src/demo-app/data-table/data-table-demo.scss

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
display: flex;
44
flex-direction: column;
55
max-height: 800px;
6+
background: white;
67

78
// Table fills in the remaining area with a scroll
89
.cdk-table {
@@ -17,7 +18,6 @@
1718
*/
1819
.cdk-table {
1920
display: block;
20-
background: white;
2121
}
2222

2323
.cdk-row, .cdk-header-row {

src/demo-app/data-table/data-table-demo.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import {Component} from '@angular/core';
22
import {PeopleDatabase} from './people-database';
33
import {PersonDataSource} from './person-data-source';
44

5+
type UserProperties = 'userId' | 'userName' | 'progress' | 'color';
6+
57
@Component({
68
moduleId: module.id,
79
selector: 'data-table-demo',
@@ -10,7 +12,7 @@ import {PersonDataSource} from './person-data-source';
1012
})
1113
export class DataTableDemo {
1214
dataSource: PersonDataSource;
13-
propertiesToDisplay = ['userId', 'userName', 'progress', 'color'];
15+
propertiesToDisplay: UserProperties[] = ['userId', 'userName', 'progress', 'color'];
1416

1517
constructor(private _peopleDatabase: PeopleDatabase) { }
1618

@@ -22,4 +24,13 @@ export class DataTableDemo {
2224
let distanceFromMiddle = Math.abs(50 - progress);
2325
return distanceFromMiddle / 50 + .3;
2426
}
27+
28+
toggleColorColumn() {
29+
let colorColumnIndex = this.propertiesToDisplay.indexOf('color');
30+
if (colorColumnIndex == -1) {
31+
this.propertiesToDisplay.push('color');
32+
} else {
33+
this.propertiesToDisplay.splice(colorColumnIndex, 1);
34+
}
35+
}
2536
}

src/demo-app/data-table/person-data-source.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export class PersonDataSource extends DataSource<any> {
1010
}
1111

1212
connect(collectionViewer: CollectionViewer): Observable<UserData[]> {
13-
return collectionViewer.viewChanged.map((view: {start: number, end: number}) => {
13+
return collectionViewer.viewChange.map((view: {start: number, end: number}) => {
1414
// Set the rendered rows length to the virtual page size. Fill in the data provided
1515
// from the index start until the end index or pagination size, whichever is smaller.
1616
this._renderedData.length = this._peopleDatabase.data.length;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
<div class="title">
2+
Users
3+
</div>
4+
5+
<div class="actions">
6+
<button md-icon-button [mdMenuTriggerFor]="menu">
7+
<md-icon>more_vert</md-icon>
8+
</button>
9+
<md-menu #menu="mdMenu">
10+
<button md-menu-item (click)="shiftColumns.next()">
11+
<md-icon>subdirectory_arrow_left</md-icon>
12+
Shift Columns Left
13+
</button>
14+
<button md-menu-item (click)="toggleColorColumn.next()">
15+
<md-icon>color_lens</md-icon>
16+
Toggle Color Column
17+
</button>
18+
</md-menu>
19+
</div>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
:host {
2+
display: flex;
3+
align-items: center;
4+
justify-content: space-between;
5+
min-height: 64px;
6+
padding: 0 16px;
7+
}
8+
9+
.title {
10+
font-size: 20px;
11+
}
12+
13+
.actions {
14+
color: rgba(0, 0, 0, 0.54);
15+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import {Component, EventEmitter, Output} from '@angular/core';
2+
3+
@Component({
4+
moduleId: module.id,
5+
selector: 'table-header-demo',
6+
templateUrl: 'table-header-demo.html',
7+
styleUrls: ['table-header-demo.css'],
8+
})
9+
export class TableHeaderDemo {
10+
@Output() shiftColumns = new EventEmitter<void>();
11+
@Output() toggleColorColumn = new EventEmitter<void>();
12+
}

src/demo-app/demo-app-module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import {
7575
MdTooltipModule,
7676
OverlayContainer
7777
} from '@angular/material';
78+
import {TableHeaderDemo} from './data-table/table-header-demo';
7879

7980
/**
8081
* NgModule that includes all Material modules that are required to serve the demo-app.
@@ -165,6 +166,7 @@ export class DemoMaterialModule {}
165166
SlideToggleDemo,
166167
SpagettiPanel,
167168
StyleDemo,
169+
TableHeaderDemo,
168170
ToolbarDemo,
169171
TooltipDemo,
170172
TabsDemo,

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import {Observable} from 'rxjs/Observable';
22

33
export interface CollectionViewer {
4-
viewChanged: Observable<{start: number, end: number}>;
4+
viewChange: Observable<{start: number, end: number}>;
55
}
66

77
export abstract class DataSource<T> {

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

Lines changed: 45 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,8 @@ describe('CdkTable', () => {
9696
});
9797
});
9898

99+
// TODO(andrewseguin): Add test for dynamic classes on header/rows
100+
99101
it('should use differ to add/remove/move rows', () => {
100102
// Each row receives an attribute 'initialIndex' the element's original place
101103
getRows(tableElement).forEach((row: Element, index: number) => {
@@ -129,26 +131,57 @@ describe('CdkTable', () => {
129131
expect(changedRows[2].getAttribute('initialIndex')).toBe(null);
130132
});
131133

132-
// TODO(andrewseguin): Add test for dynamic classes on header/rows
133-
134134
it('should match the right table content with dynamic data', () => {
135135
const initialDataLength = dataSource.data.length;
136136
expect(dataSource.data.length).toBe(3);
137-
const headerContent = ['Column A', 'Column B', 'Column C'];
138137

139-
const initialTableContent = [headerContent];
140-
dataSource.data.forEach(rowData => initialTableContent.push([rowData.a, rowData.b, rowData.c]));
141-
expect(tableElement).toMatchTableContent(initialTableContent);
138+
let data = dataSource.data;
139+
expect(tableElement).toMatchTableContent([
140+
['Column A', 'Column B', 'Column C'],
141+
[data[0].a, data[0].b, data[0].c],
142+
[data[1].a, data[1].b, data[1].c],
143+
[data[2].a, data[2].b, data[2].c],
144+
]);
142145

143146
// Add data to the table and recreate what the rendered output should be.
144147
dataSource.addData();
145148
expect(dataSource.data.length).toBe(initialDataLength + 1); // Make sure data was added
149+
150+
data = dataSource.data;
151+
expect(tableElement).toMatchTableContent([
152+
['Column A', 'Column B', 'Column C'],
153+
[data[0].a, data[0].b, data[0].c],
154+
[data[1].a, data[1].b, data[1].c],
155+
[data[2].a, data[2].b, data[2].c],
156+
[data[3].a, data[3].b, data[3].c],
157+
]);
158+
});
159+
160+
it('should be able to dynamically change the columns for header and rows', () => {
161+
expect(dataSource.data.length).toBe(3);
162+
163+
let data = dataSource.data;
164+
expect(tableElement).toMatchTableContent([
165+
['Column A', 'Column B', 'Column C'],
166+
[data[0].a, data[0].b, data[0].c],
167+
[data[1].a, data[1].b, data[1].c],
168+
[data[2].a, data[2].b, data[2].c],
169+
]);
170+
171+
// Remove column_a and swap column_b/column_c.
172+
component.columnsToRender = ['column_c', 'column_b'];
146173
fixture.detectChanges();
147-
fixture.detectChanges();
148174

149-
const changedTableContent = [headerContent];
150-
dataSource.data.forEach(rowData => changedTableContent.push([rowData.a, rowData.b, rowData.c]));
151-
expect(tableElement).toMatchTableContent(changedTableContent);
175+
let changedTableContent = [['Column C', 'Column B']];
176+
dataSource.data.forEach(rowData => changedTableContent.push([rowData.c, rowData.b]));
177+
178+
data = dataSource.data;
179+
expect(tableElement).toMatchTableContent([
180+
['Column C', 'Column B'],
181+
[data[0].c, data[0].b],
182+
[data[1].c, data[1].b],
183+
[data[2].c, data[2].b],
184+
]);
152185
});
153186
});
154187

@@ -172,11 +205,8 @@ class FakeDataSource extends DataSource<TestData> {
172205

173206
connect(collectionViewer: CollectionViewer): Observable<TestData[]> {
174207
this.isConnected = true;
175-
const streams = [collectionViewer.viewChanged, this._dataChange];
176-
return Observable.combineLatest(streams).map((results: any[]) => {
177-
const [view, data] = results;
178-
return data;
179-
});
208+
const streams = [this._dataChange, collectionViewer.viewChange];
209+
return Observable.combineLatest(streams).map(([data]) => data);
180210
}
181211

182212
addData() {

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

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,14 @@ import {
1515
ViewContainerRef,
1616
ViewEncapsulation
1717
} from '@angular/core';
18+
import {CollectionViewer, DataSource} from './data-source';
19+
import {BaseRowDef, CdkCellOutlet, CdkHeaderRowDef, CdkRowDef} from './row';
20+
import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef} from './cell';
21+
import {Observable} from 'rxjs/Observable';
1822
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
1923
import 'rxjs/add/operator/let';
2024
import 'rxjs/add/operator/debounceTime';
2125
import 'rxjs/add/observable/combineLatest';
22-
import {CollectionViewer, DataSource} from './data-source';
23-
import {CdkCellOutlet, CdkHeaderRowDef, CdkRowDef} from './row';
24-
import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef} from './cell';
2526

2627
/**
2728
* 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<T> implements CollectionViewer {
6667
@Input() dataSource: DataSource<T>;
6768

6869
// TODO(andrewseguin): Remove max value as the end index
69-
// and instead calculate the view on init and scroll.
70+
// and instead calculate the view on init and scroll.
7071
/**
7172
* Stream containing the latest information on what rows are being displayed on screen.
7273
* Can be used by the data source to as a heuristic of what data should be provided.
7374
*/
74-
viewChanged =
75+
viewChange =
7576
new BehaviorSubject<{start: number, end: number}>({start: 0, end: Number.MAX_VALUE});
7677

78+
/** Stream that emits when a row def has a change to its array of columns to render. */
79+
_columnsChange = new Observable<void>();
80+
7781
/**
7882
* Map of all the user's defined columns identified by name.
7983
* Contains the header and data-cell templates.
@@ -115,30 +119,45 @@ export class CdkTable<T> implements CollectionViewer {
115119

116120
ngOnDestroy() {
117121
// TODO(andrewseguin): Disconnect from the data source so
118-
// that it can unsubscribe from its streams.
122+
// that it can unsubscribe from its streams.
119123
}
120124

121125
ngOnInit() {
122126
// TODO(andrewseguin): Setup a listener for scroll events
123-
// and emit the calculated view to this.viewChanged
127+
// and emit the calculated view to this.viewChange
124128
}
125129

126130
ngAfterContentInit() {
127131
// TODO(andrewseguin): Throw an error if two columns share the same name
128132
this._columnDefinitions.forEach(columnDef => {
129133
this._columnDefinitionsByName.set(columnDef.name, columnDef);
130134
});
135+
136+
// Get and merge the streams for column changes made to the row defs
137+
const rowDefs = [...this._rowDefinitions.toArray(), this._headerDefinition];
138+
const columnChangeStreams =
139+
rowDefs.map((rowDef: BaseRowDef) => rowDef.columnsChange);
140+
this._columnsChange = Observable.merge(...columnChangeStreams);
131141
}
132142

133143
ngAfterViewInit() {
134-
// TODO(andrewseguin): Re-render the header when the header's columns change.
135144
this.renderHeaderRow();
136145

137-
// TODO(andrewseguin): Re-render rows when their list of columns change.
146+
// Re-render the header row if the columns changed.
147+
this._columnsChange.subscribe(() => {
148+
this._headerRowPlaceholder.viewContainer.clear();
149+
this.renderHeaderRow();
150+
151+
// Reset the data to an empty array so that renderRowChanges will re-render all new rows.
152+
this._rowPlaceholder.viewContainer.clear();
153+
this._dataDiffer.diff([]);
154+
});
155+
138156
// TODO(andrewseguin): If the data source is not
139157
// present after view init, connect it when it is defined.
140158
// TODO(andrewseguin): Unsubscribe from this on destroy.
141-
this.dataSource.connect(this).subscribe((rowsData: NgIterable<T>) => {
159+
const streams = [this.dataSource.connect(this), this._columnsChange];
160+
Observable.combineLatest(streams).subscribe(([rowsData]) => {
142161
this.renderRowChanges(rowsData);
143162
});
144163
}
@@ -150,8 +169,8 @@ export class CdkTable<T> implements CollectionViewer {
150169
const cells = this.getHeaderCellTemplatesForRow(this._headerDefinition);
151170

152171
// TODO(andrewseguin): add some code to enforce that exactly
153-
// one CdkCellOutlet was instantiated as a result
154-
// of `createEmbeddedView`.
172+
// one CdkCellOutlet was instantiated as a result
173+
// of `createEmbeddedView`.
155174
this._headerRowPlaceholder.viewContainer
156175
.createEmbeddedView(this._headerDefinition.template, {cells});
157176
CdkCellOutlet.mostRecentCellOutlet.cells = cells;
@@ -206,6 +225,7 @@ export class CdkTable<T> implements CollectionViewer {
206225
*/
207226
getHeaderCellTemplatesForRow(headerDef: CdkHeaderRowDef): CdkHeaderCellDef[] {
208227
return headerDef.columns.map(columnId => {
228+
// TODO(andrewseguin): Throw an error if there is no column with this columnId
209229
return this._columnDefinitionsByName.get(columnId).headerCell;
210230
});
211231
}
@@ -216,6 +236,7 @@ export class CdkTable<T> implements CollectionViewer {
216236
*/
217237
getCellTemplatesForRow(rowDef: CdkRowDef): CdkCellDef[] {
218238
return rowDef.columns.map(columnId => {
239+
// TODO(andrewseguin): Throw an error if there is no column with this columnId
219240
return this._columnDefinitionsByName.get(columnId).cell;
220241
});
221242
}

0 commit comments

Comments
 (0)