Skip to content

Commit 7441ba7

Browse files
committed
feat(data-table): re-render when columns change
1 parent 1fce545 commit 7441ba7

File tree

10 files changed

+169
-26
lines changed

10 files changed

+169
-26
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: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,13 @@ export class DataTableDemo {
2222
let distanceFromMiddle = Math.abs(50 - progress);
2323
return distanceFromMiddle / 50 + .3;
2424
}
25+
26+
toggleColorColumn() {
27+
let colorColumnIndex = this.propertiesToDisplay.findIndex((col: string) => col === 'color');
28+
if (colorColumnIndex == -1) {
29+
this.propertiesToDisplay.push('color');
30+
} else {
31+
this.propertiesToDisplay.splice(colorColumnIndex, 1);
32+
}
33+
}
2534
}
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
@@ -72,6 +72,7 @@ import {
7272
MdTooltipModule,
7373
OverlayContainer
7474
} from '@angular/material';
75+
import {TableHeaderDemo} from './data-table/table-header-demo';
7576

7677
/**
7778
* NgModule that includes all Material modules that are required to serve the demo-app.
@@ -161,6 +162,7 @@ export class DemoMaterialModule {}
161162
SlideToggleDemo,
162163
SpagettiPanel,
163164
StyleDemo,
165+
TableHeaderDemo,
164166
ToolbarDemo,
165167
TooltipDemo,
166168
TabsDemo,

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

Lines changed: 17 additions & 1 deletion
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';
@@ -118,6 +117,23 @@ describe('CdkTable', () => {
118117
dataSource.data.forEach(rowData => changedTableContent.push([rowData.a, rowData.b, rowData.c]));
119118
expect(tableElement).toMatchTableContent(changedTableContent);
120119
});
120+
121+
it('should be able to dynamically change the columns for header and rows', () => {
122+
expect(dataSource.data.length).toBe(3);
123+
124+
let initialTableContent = [['Column A', 'Column B', 'Column C']];
125+
dataSource.data.forEach(rowData => initialTableContent.push([rowData.a, rowData.b, rowData.c]));
126+
expect(tableElement).toMatchTableContent(initialTableContent);
127+
128+
// Remove column_a and swap column_b/column_c.
129+
component.columnsToRender = ['column_c', 'column_b'];
130+
fixture.detectChanges();
131+
fixture.detectChanges();
132+
133+
let changedTableContent = [['Column C', 'Column B']];
134+
dataSource.data.forEach(rowData => changedTableContent.push([rowData.c, rowData.b]));
135+
expect(tableElement).toMatchTableContent(changedTableContent);
136+
});
121137
});
122138

123139
interface TestData {

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

Lines changed: 30 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@ import {
55
ContentChild,
66
ContentChildren,
77
Directive,
8-
Input,
8+
Input, IterableChanges,
99
QueryList,
1010
ViewChild,
1111
ViewContainerRef,
1212
ViewEncapsulation
1313
} from '@angular/core';
14+
import {CollectionViewer, DataSource} from './data-source';
15+
import {BaseRowDef, CdkCellOutlet, CdkHeaderRowDef, CdkRowDef} from './row';
16+
import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef} from './cell';
17+
import {Subject} from 'rxjs/Subject';
18+
import {Observable} from 'rxjs/Observable';
1419
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
1520
import 'rxjs/add/operator/let';
1621
import 'rxjs/add/operator/debounceTime';
1722
import 'rxjs/add/observable/combineLatest';
18-
import {CollectionViewer, DataSource} from './data-source';
19-
import {CdkCellOutlet, CdkHeaderRowDef, CdkRowDef} from './row';
20-
import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef} from './cell';
2123

2224
/**
2325
* Provides a handle for the table to grab the view container's ng-container to insert data rows.
@@ -62,14 +64,17 @@ export class CdkTable implements CollectionViewer {
6264
@Input() dataSource: DataSource<any>;
6365

6466
// TODO(andrewseguin): Remove max value as the end index
65-
// and instead calculate the view on init and scroll.
67+
// and instead calculate the view on init and scroll.
6668
/**
6769
* Stream containing the latest information on what rows are being displayed on screen.
6870
* Can be used by the data source to as a heuristic of what data should be provided.
6971
*/
7072
viewChanged =
7173
new BehaviorSubject<{start: number, end: number}>({start: 0, end: Number.MAX_VALUE});
7274

75+
/** Stream that emits when a row def has a change to its array of columns to render. */
76+
columnsChange = new Observable<void[]>();
77+
7378
/**
7479
* Map of all the user's defined columns identified by name.
7580
* Contains the header and data-cell templates.
@@ -99,7 +104,7 @@ export class CdkTable implements CollectionViewer {
99104

100105
ngOnDestroy() {
101106
// TODO(andrewseguin): Disconnect from the data source so
102-
// that it can unsubscribe from its streams.
107+
// that it can unsubscribe from its streams.
103108
}
104109

105110
ngOnInit() {
@@ -112,21 +117,32 @@ export class CdkTable implements CollectionViewer {
112117
this._columnDefinitions.forEach(columnDef => {
113118
this._columnDefinitionsByName.set(columnDef.name, columnDef);
114119
});
120+
121+
// Get and merge the streams for column changes made to the row defs
122+
const rowDefs = this._rowDefinitions.toArray().concat(this._headerDefinition);
123+
const columnChangeStreams = rowDefs.map((rowDef: BaseRowDef) => rowDef.columnsChange);
124+
this.columnsChange = Observable.combineLatest(columnChangeStreams);
115125
}
116126

117127
ngAfterViewInit() {
118-
// TODO(andrewseguin): Re-render the header when the header's columns change.
119128
this.renderHeaderRow();
120129

121-
// TODO(andrewseguin): Re-render rows when their list of columns change.
130+
// Re-render the header row if the columns changed.
131+
this.columnsChange.subscribe(() => {
132+
this._headerRowPlaceholder.viewContainer.clear();
133+
this.renderHeaderRow();
134+
});
135+
122136
// TODO(andrewseguin): If the data source is not
123137
// present after view init, connect it when it is defined.
124138
// TODO(andrewseguin): Unsubscribe from this on destroy.
125-
this.dataSource.connect(this).subscribe((rowsData: any[]) => {
139+
const streams = [this.dataSource.connect(this), this.columnsChange];
140+
Observable.combineLatest(streams).subscribe((result: any[]) => {
141+
console.log('Rendering all rows');
126142
// TODO(andrewseguin): Add a differ that will check if the data has changed,
127143
// rather than re-rendering all rows
128144
this._rowPlaceholder.viewContainer.clear();
129-
rowsData.forEach(rowData => this.insertRow(rowData));
145+
result[0].forEach(rowData => this.insertRow(rowData));
130146
this._changeDetectorRef.markForCheck();
131147
});
132148
}
@@ -138,8 +154,8 @@ export class CdkTable implements CollectionViewer {
138154
const cells = this.getHeaderCellTemplatesForRow(this._headerDefinition);
139155

140156
// TODO(andrewseguin): add some code to enforce that exactly
141-
// one CdkCellOutlet was instantiated as a result
142-
// of `createEmbeddedView`.
157+
// one CdkCellOutlet was instantiated as a result
158+
// of `createEmbeddedView`.
143159
this._headerRowPlaceholder.viewContainer
144160
.createEmbeddedView(this._headerDefinition.template, {cells});
145161
CdkCellOutlet.mostRecentCellOutlet.cells = cells;
@@ -174,6 +190,7 @@ export class CdkTable implements CollectionViewer {
174190
*/
175191
getHeaderCellTemplatesForRow(headerDef: CdkHeaderRowDef): CdkHeaderCellDef[] {
176192
return headerDef.columns.map(columnId => {
193+
// TODO(andrewseguin): Throw an error if there is no column with this columnId
177194
return this._columnDefinitionsByName.get(columnId).headerCell;
178195
});
179196
}
@@ -184,6 +201,7 @@ export class CdkTable implements CollectionViewer {
184201
*/
185202
getCellTemplatesForRow(rowDef: CdkRowDef): CdkCellDef[] {
186203
return rowDef.columns.map(columnId => {
204+
// TODO(andrewseguin): Throw an error if there is no column with this columnId
187205
return this._columnDefinitionsByName.get(columnId).cell;
188206
});
189207
}

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

Lines changed: 60 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,35 +2,83 @@ import {
22
ChangeDetectionStrategy,
33
Component,
44
Directive,
5-
Input,
5+
IterableDiffer,
6+
IterableDiffers,
7+
SimpleChanges,
68
TemplateRef,
79
ViewContainerRef
810
} from '@angular/core';
911
import {CdkCellDef} from './cell';
12+
import {Subject} from 'rxjs/Subject';
13+
14+
/**
15+
* Base class for the CdkHeaderRowDef and CdkRowDef that handles checking their columns inputs
16+
* for changes and notifying the table.
17+
*/
18+
export abstract class BaseRowDef {
19+
/** The columns to be displayed on this row. */
20+
columns: string[];
21+
22+
/** Event stream that emits when changes are made to the columns. */
23+
columnsChange: Subject<void> = new Subject<void>();
24+
25+
/** Differ used to check if any changes were made to the columns. */
26+
protected _columnsDiffer: IterableDiffer<any>;
27+
28+
private viewInitialized = false;
29+
30+
constructor(public template: TemplateRef<any>,
31+
protected _differs: IterableDiffers) { }
32+
33+
ngAfterViewInit() {
34+
this.viewInitialized = true;
35+
}
36+
37+
ngOnChanges(changes: SimpleChanges): void {
38+
// Create a new columns differ if one does not yet exist. Initialize it based on initial value
39+
// of the columns property.
40+
if (!this._columnsDiffer) {
41+
this._columnsDiffer = this._differs.find(changes['columns'].currentValue).create();
42+
}
43+
}
44+
45+
ngDoCheck(): void {
46+
if (!this.viewInitialized || !this._columnsDiffer || !this.columns) { return; }
47+
48+
// Notify the table if there are any changes to the columns.
49+
const changes = this._columnsDiffer.diff(this.columns);
50+
if (changes) { this.columnsChange.next(); }
51+
}
52+
}
1053

1154
/**
1255
* Header row definition for the CDK data-table.
1356
* Captures the header row's template and other header properties such as the columns to display.
1457
*/
15-
@Directive({selector: '[cdkHeaderRowDef]'})
16-
export class CdkHeaderRowDef {
17-
@Input('cdkHeaderRowDef') columns: string[];
18-
19-
constructor(public template: TemplateRef<any>) { }
58+
@Directive({
59+
selector: '[cdkHeaderRowDef]',
60+
inputs: ['columns: cdkHeaderRowDef'],
61+
})
62+
export class CdkHeaderRowDef extends BaseRowDef {
63+
constructor(template: TemplateRef<any>, _differs: IterableDiffers) {
64+
super(template, _differs);
65+
}
2066
}
2167

2268
/**
2369
* Data row definition for the CDK data-table.
2470
* Captures the header row's template and other row properties such as the columns to display.
2571
*/
26-
@Directive({selector: '[cdkRowDef]'})
27-
export class CdkRowDef {
28-
@Input('cdkRowDefColumns') columns: string[];
29-
72+
@Directive({
73+
selector: '[cdkRowDef]',
74+
inputs: ['columns: cdkRowDefColumns'],
75+
})
76+
export class CdkRowDef extends BaseRowDef {
3077
// TODO(andrewseguin): Add an input for providing a switch function to determine
3178
// if this template should be used.
32-
33-
constructor(public template: TemplateRef<any>) { }
79+
constructor(template: TemplateRef<any>, _differs: IterableDiffers) {
80+
super(template, _differs);
81+
}
3482
}
3583

3684
/**

0 commit comments

Comments
 (0)