Skip to content

Commit 51ecc18

Browse files
andrewseguinjelbourn
authored andcommitted
feat(data-table): add row context (#5219)
1 parent 37efb54 commit 51ecc18

File tree

6 files changed

+233
-27
lines changed

6 files changed

+233
-27
lines changed

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

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,16 @@
11
<div class="demo-data-source-actions">
2-
<button md-raised-button (click)="connect()">Connect New Data Source</button>
3-
<button md-raised-button (click)="disconnect()" [disabled]="!dataSource">Disconnect Data Source</button>
2+
<div>
3+
<button md-raised-button (click)="connect()">Connect New Data Source</button>
4+
<button md-raised-button (click)="disconnect()" [disabled]="!dataSource">Disconnect Data Source</button>
5+
</div>
6+
7+
<div class="demo-highlighter">
8+
Highlight:
9+
<md-checkbox (change)="toggleHighlight('first', $event.checked)">First Row</md-checkbox>
10+
<md-checkbox (change)="toggleHighlight('last', $event.checked)">Last Row</md-checkbox>
11+
<md-checkbox (change)="toggleHighlight('even', $event.checked)">Even Rows</md-checkbox>
12+
<md-checkbox (change)="toggleHighlight('odd', $event.checked)">Odd Rows</md-checkbox>
13+
</div>
414
</div>
515

616
<div class="demo-table-container mat-elevation-z4">
@@ -44,6 +54,14 @@
4454
</ng-container>
4555

4656
<cdk-header-row *cdkHeaderRowDef="propertiesToDisplay"></cdk-header-row>
47-
<cdk-row *cdkRowDef="let row; columns: propertiesToDisplay"></cdk-row>
57+
<cdk-row *cdkRowDef="let row; columns: propertiesToDisplay;
58+
let first = first; let last = last; let even = even; let odd = odd"
59+
[ngClass]="{
60+
'demo-row-highlight-first': highlights.has('first') && first,
61+
'demo-row-highlight-last': highlights.has('last') && last,
62+
'demo-row-highlight-even': highlights.has('even') && even,
63+
'demo-row-highlight-odd': highlights.has('odd') && odd
64+
}">
65+
</cdk-row>
4866
</cdk-table>
4967
</div>

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

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,20 @@
1313
}
1414

1515
.demo-data-source-actions {
16-
margin: 16px 0;
16+
& > div {
17+
margin: 16px 0;
18+
}
19+
20+
.demo-highlighter .mat-checkbox {
21+
margin: 0 8px;
22+
}
1723
}
1824

25+
.demo-row-highlight-first { background: #f3f315; }
26+
.demo-row-highlight-last { background: #0dd5fc; }
27+
.demo-row-highlight-even { background: #ff0099; }
28+
.demo-row-highlight-odd { background: #83f52c; }
29+
1930
/*
2031
* Styles to make the demo's cdk-table match the material design spec
2132
* https://material.io/guidelines/components/data-tables.html

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export type UserProperties = 'userId' | 'userName' | 'progress' | 'color' | unde
1313
export class DataTableDemo {
1414
dataSource: PersonDataSource | null;
1515
propertiesToDisplay: UserProperties[] = [];
16+
highlights = new Set<string>();
1617

1718
constructor(private _peopleDatabase: PeopleDatabase) {
1819
this.connect();
@@ -42,4 +43,8 @@ export class DataTableDemo {
4243
this.propertiesToDisplay.splice(colorColumnIndex, 1);
4344
}
4445
}
46+
47+
toggleHighlight(property: string, enable: boolean) {
48+
enable ? this.highlights.add(property) : this.highlights.delete(property);
49+
}
4550
}

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

Lines changed: 128 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,16 @@ describe('CdkTable', () => {
2121

2222
TestBed.configureTestingModule({
2323
imports: [CdkDataTableModule],
24-
declarations: [SimpleCdkTableApp, DynamicDataSourceCdkTableApp, CustomRoleCdkTableApp],
24+
declarations: [
25+
SimpleCdkTableApp,
26+
DynamicDataSourceCdkTableApp,
27+
CustomRoleCdkTableApp,
28+
RowContextCdkTableApp
29+
],
2530
}).compileComponents();
31+
}));
2632

33+
beforeEach(() => {
2734
fixture = TestBed.createComponent(SimpleCdkTableApp);
2835

2936
component = fixture.componentInstance;
@@ -33,7 +40,7 @@ describe('CdkTable', () => {
3340

3441
fixture.detectChanges(); // Let the component and table create embedded views
3542
fixture.detectChanges(); // Let the cells render
36-
}));
43+
});
3744

3845
describe('should initialize', () => {
3946
it('with a connected data source', () => {
@@ -153,6 +160,7 @@ describe('CdkTable', () => {
153160
// Add data to the table and recreate what the rendered output should be.
154161
dataSource.addData();
155162
expect(dataSource.data.length).toBe(initialDataLength + 1); // Make sure data was added
163+
fixture.detectChanges();
156164

157165
data = dataSource.data;
158166
expect(tableElement).toMatchTableContent([
@@ -198,6 +206,88 @@ describe('CdkTable', () => {
198206
]);
199207
});
200208

209+
it('should be able to apply classes to rows based on their context', () => {
210+
const contextFixture = TestBed.createComponent(RowContextCdkTableApp);
211+
const contextComponent = contextFixture.componentInstance;
212+
tableElement = contextFixture.nativeElement.querySelector('cdk-table');
213+
214+
contextFixture.detectChanges(); // Let the table initialize its view
215+
contextFixture.detectChanges(); // Let the table render the rows and cells
216+
217+
const rowElements = contextFixture.nativeElement.querySelectorAll('cdk-row');
218+
219+
// Rows should not have any context classes
220+
for (let i = 0; i < rowElements.length; i++) {
221+
expect(rowElements[i].classList.contains('custom-row-class-first')).toBe(false);
222+
expect(rowElements[i].classList.contains('custom-row-class-last')).toBe(false);
223+
expect(rowElements[i].classList.contains('custom-row-class-even')).toBe(false);
224+
expect(rowElements[i].classList.contains('custom-row-class-odd')).toBe(false);
225+
}
226+
227+
// Enable all the context classes
228+
contextComponent.enableRowContextClasses = true;
229+
contextFixture.detectChanges();
230+
231+
expect(rowElements[0].classList.contains('custom-row-class-first')).toBe(true);
232+
expect(rowElements[0].classList.contains('custom-row-class-last')).toBe(false);
233+
expect(rowElements[0].classList.contains('custom-row-class-even')).toBe(true);
234+
expect(rowElements[0].classList.contains('custom-row-class-odd')).toBe(false);
235+
236+
expect(rowElements[1].classList.contains('custom-row-class-first')).toBe(false);
237+
expect(rowElements[1].classList.contains('custom-row-class-last')).toBe(false);
238+
expect(rowElements[1].classList.contains('custom-row-class-even')).toBe(false);
239+
expect(rowElements[1].classList.contains('custom-row-class-odd')).toBe(true);
240+
241+
expect(rowElements[2].classList.contains('custom-row-class-first')).toBe(false);
242+
expect(rowElements[2].classList.contains('custom-row-class-last')).toBe(true);
243+
expect(rowElements[2].classList.contains('custom-row-class-even')).toBe(true);
244+
expect(rowElements[2].classList.contains('custom-row-class-odd')).toBe(false);
245+
});
246+
247+
it('should be able to apply classes to cells based on their row context', () => {
248+
const contextFixture = TestBed.createComponent(RowContextCdkTableApp);
249+
const contextComponent = contextFixture.componentInstance;
250+
tableElement = contextFixture.nativeElement.querySelector('cdk-table');
251+
252+
contextFixture.detectChanges(); // Let the table initialize its view
253+
contextFixture.detectChanges(); // Let the table render the rows and cells
254+
255+
const rowElements = contextFixture.nativeElement.querySelectorAll('cdk-row');
256+
257+
for (let i = 0; i < rowElements.length; i++) {
258+
// Cells should not have any context classes
259+
const cellElements = rowElements[i].querySelectorAll('cdk-cell');
260+
for (let j = 0; j < cellElements.length; j++) {
261+
expect(cellElements[j].classList.contains('custom-cell-class-first')).toBe(false);
262+
expect(cellElements[j].classList.contains('custom-cell-class-last')).toBe(false);
263+
expect(cellElements[j].classList.contains('custom-cell-class-even')).toBe(false);
264+
expect(cellElements[j].classList.contains('custom-cell-class-odd')).toBe(false);
265+
}
266+
}
267+
268+
// Enable the context classes
269+
contextComponent.enableCellContextClasses = true;
270+
contextFixture.detectChanges();
271+
272+
let cellElement = rowElements[0].querySelectorAll('cdk-cell')[0];
273+
expect(cellElement.classList.contains('custom-cell-class-first')).toBe(true);
274+
expect(cellElement.classList.contains('custom-cell-class-last')).toBe(false);
275+
expect(cellElement.classList.contains('custom-cell-class-even')).toBe(true);
276+
expect(cellElement.classList.contains('custom-cell-class-odd')).toBe(false);
277+
278+
cellElement = rowElements[1].querySelectorAll('cdk-cell')[0];
279+
expect(cellElement.classList.contains('custom-cell-class-first')).toBe(false);
280+
expect(cellElement.classList.contains('custom-cell-class-last')).toBe(false);
281+
expect(cellElement.classList.contains('custom-cell-class-even')).toBe(false);
282+
expect(cellElement.classList.contains('custom-cell-class-odd')).toBe(true);
283+
284+
cellElement = rowElements[2].querySelectorAll('cdk-cell')[0];
285+
expect(cellElement.classList.contains('custom-cell-class-first')).toBe(false);
286+
expect(cellElement.classList.contains('custom-cell-class-last')).toBe(true);
287+
expect(cellElement.classList.contains('custom-cell-class-even')).toBe(true);
288+
expect(cellElement.classList.contains('custom-cell-class-odd')).toBe(false);
289+
});
290+
201291
it('should be able to dynamically change the columns for header and rows', () => {
202292
expect(dataSource.data.length).toBe(3);
203293

@@ -336,6 +426,42 @@ class CustomRoleCdkTableApp {
336426
@ViewChild(CdkTable) table: CdkTable<TestData>;
337427
}
338428

429+
@Component({
430+
template: `
431+
<cdk-table [dataSource]="dataSource">
432+
<ng-container cdkColumnDef="column_a">
433+
<cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
434+
<cdk-cell *cdkCellDef="let row; let first = first;
435+
let last = last; let even = even; let odd = odd"
436+
[ngClass]="{
437+
'custom-cell-class-first': enableCellContextClasses && first,
438+
'custom-cell-class-last': enableCellContextClasses && last,
439+
'custom-cell-class-even': enableCellContextClasses && even,
440+
'custom-cell-class-odd': enableCellContextClasses && odd
441+
}">
442+
{{row.a}}
443+
</cdk-cell>
444+
</ng-container>
445+
<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
446+
<cdk-row *cdkRowDef="let row; columns: columnsToRender;
447+
let first = first; let last = last; let even = even; let odd = odd"
448+
[ngClass]="{
449+
'custom-row-class-first': enableRowContextClasses && first,
450+
'custom-row-class-last': enableRowContextClasses && last,
451+
'custom-row-class-even': enableRowContextClasses && even,
452+
'custom-row-class-odd': enableRowContextClasses && odd
453+
}">
454+
</cdk-row>
455+
</cdk-table>
456+
`
457+
})
458+
class RowContextCdkTableApp {
459+
dataSource: FakeDataSource = new FakeDataSource();
460+
columnsToRender = ['column_a'];
461+
enableRowContextClasses = false;
462+
enableCellContextClasses = false;
463+
}
464+
339465
function getElements(element: Element, query: string): Element[] {
340466
return [].slice.call(element.querySelectorAll(query));
341467
}

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

Lines changed: 42 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
ContentChildren,
1616
Directive,
1717
ElementRef,
18+
EmbeddedViewRef,
1819
Input,
1920
IterableChangeRecord,
2021
IterableDiffer,
@@ -27,15 +28,15 @@ import {
2728
ViewEncapsulation
2829
} from '@angular/core';
2930
import {CollectionViewer, DataSource} from './data-source';
30-
import {BaseRowDef, CdkCellOutlet, CdkHeaderRowDef, CdkRowDef} from './row';
31-
import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef} from './cell';
31+
import {CdkCellOutlet, CdkCellOutletRowContext, CdkHeaderRowDef, CdkRowDef} from './row';
3232
import {Observable} from 'rxjs/Observable';
3333
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
3434
import 'rxjs/add/operator/let';
3535
import 'rxjs/add/operator/debounceTime';
3636
import 'rxjs/add/observable/combineLatest';
3737
import {Subscription} from 'rxjs/Subscription';
3838
import {Subject} from 'rxjs/Subject';
39+
import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef} from './cell';
3940

4041
/**
4142
* Returns an error to be thrown when attempting to find an unexisting column.
@@ -198,13 +199,14 @@ export class CdkTable<T> implements CollectionViewer {
198199
}
199200

200201
ngAfterViewInit() {
202+
this._isViewInitialized = true;
201203
this._renderHeaderRow();
204+
}
202205

203-
if (this.dataSource) {
206+
ngDoCheck() {
207+
if (this._isViewInitialized && this.dataSource && !this._renderChangeSubscription) {
204208
this._observeRenderChanges();
205209
}
206-
207-
this._isViewInitialized = true;
208210
}
209211

210212
/**
@@ -251,8 +253,10 @@ export class CdkTable<T> implements CollectionViewer {
251253
// of `createEmbeddedView`.
252254
this._headerRowPlaceholder.viewContainer
253255
.createEmbeddedView(this._headerDefinition.template, {cells});
254-
CdkCellOutlet.mostRecentCellOutlet.cells = cells;
255-
CdkCellOutlet.mostRecentCellOutlet.context = {};
256+
257+
cells.forEach(cell => {
258+
CdkCellOutlet.mostRecentCellOutlet._viewContainer.createEmbeddedView(cell.template, {});
259+
});
256260

257261
this._changeDetectorRef.markForCheck();
258262
}
@@ -262,17 +266,20 @@ export class CdkTable<T> implements CollectionViewer {
262266
const changes = this._dataDiffer.diff(this._data);
263267
if (!changes) { return; }
264268

269+
const viewContainer = this._rowPlaceholder.viewContainer;
265270
changes.forEachOperation(
266271
(item: IterableChangeRecord<any>, adjustedPreviousIndex: number, currentIndex: number) => {
267272
if (item.previousIndex == null) {
268273
this._insertRow(this._data[currentIndex], currentIndex);
269274
} else if (currentIndex == null) {
270-
this._rowPlaceholder.viewContainer.remove(adjustedPreviousIndex);
275+
viewContainer.remove(adjustedPreviousIndex);
271276
} else {
272-
const view = this._rowPlaceholder.viewContainer.get(adjustedPreviousIndex);
273-
this._rowPlaceholder.viewContainer.move(view!, currentIndex);
277+
const view = viewContainer.get(adjustedPreviousIndex);
278+
viewContainer.move(view!, currentIndex);
274279
}
275280
});
281+
282+
this._updateRowContext();
276283
}
277284

278285
/**
@@ -285,20 +292,41 @@ export class CdkTable<T> implements CollectionViewer {
285292
// the data rather than choosing the first row definition.
286293
const row = this._rowDefinitions.first;
287294

288-
// TODO(andrewseguin): Add more context, such as first/last/isEven/etc
289-
const context = {$implicit: rowData};
295+
// Row context that will be provided to both the created embedded row view and its cells.
296+
const context: CdkCellOutletRowContext<T> = {$implicit: rowData};
290297

291298
// TODO(andrewseguin): add some code to enforce that exactly one
292299
// CdkCellOutlet was instantiated as a result of `createEmbeddedView`.
293300
this._rowPlaceholder.viewContainer.createEmbeddedView(row.template, context, index);
294301

295302
// Insert empty cells if there is no data to improve rendering time.
296-
CdkCellOutlet.mostRecentCellOutlet.cells = rowData ? this._getCellTemplatesForRow(row) : [];
297-
CdkCellOutlet.mostRecentCellOutlet.context = context;
303+
const cells = rowData ? this._getCellTemplatesForRow(row) : [];
304+
305+
cells.forEach(cell => {
306+
CdkCellOutlet.mostRecentCellOutlet._viewContainer.createEmbeddedView(cell.template, context);
307+
});
298308

299309
this._changeDetectorRef.markForCheck();
300310
}
301311

312+
/**
313+
* Updates the context for each row to reflect any data changes that may have caused
314+
* rows to be added, removed, or moved. The view container contains the same context
315+
* that was provided to each of its cells.
316+
*/
317+
private _updateRowContext() {
318+
const viewContainer = this._rowPlaceholder.viewContainer;
319+
for (let index = 0, count = viewContainer.length; index < count; index++) {
320+
const viewRef = viewContainer.get(index) as EmbeddedViewRef<CdkCellOutletRowContext<T>>;
321+
viewRef.context.index = index;
322+
viewRef.context.count = count;
323+
viewRef.context.first = index === 0;
324+
viewRef.context.last = index === count - 1;
325+
viewRef.context.even = index % 2 === 0;
326+
viewRef.context.odd = index % 2 !== 0;
327+
}
328+
}
329+
302330
/**
303331
* Returns the cell template definitions to insert into the header
304332
* as defined by its list of columns to display.

0 commit comments

Comments
 (0)