Skip to content

Commit 9b9b0a1

Browse files
committed
feat(table): add row when predicate
1 parent 3571f68 commit 9b9b0a1

File tree

9 files changed

+297
-45
lines changed

9 files changed

+297
-45
lines changed

src/cdk/table/row.ts

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -75,13 +75,22 @@ export class CdkHeaderRowDef extends BaseRowDef {
7575

7676
/**
7777
* Data row definition for the CDK table.
78-
* Captures the header row's template and other row properties such as the columns to display.
78+
* Captures the header row's template and other row properties such as the columns to display and
79+
* a when predicate that describes when this row should be used.
7980
*/
8081
@Directive({
8182
selector: '[cdkRowDef]',
82-
inputs: ['columns: cdkRowDefColumns'],
83+
inputs: ['columns: cdkRowDefColumns', 'when: cdkRowDefWhen'],
8384
})
84-
export class CdkRowDef extends BaseRowDef {
85+
export class CdkRowDef<T> extends BaseRowDef {
86+
/**
87+
* Function that should return true if this row template should be used for the provided row data
88+
* and index. If left undefined, this row will be considered the default row template to use when
89+
* no other when functions return true for the data.
90+
* For every row, there must be at least one when function that passes or an undefined to default.
91+
*/
92+
when: (rowData: T, index: number) => boolean;
93+
8594
// TODO(andrewseguin): Add an input for providing a switch function to determine
8695
// if this template should be used.
8796
constructor(template: TemplateRef<any>, _differs: IterableDiffers) {

src/cdk/table/table.spec.ts

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ describe('CdkTable', () => {
3131
MissingColumnDefCdkTableApp,
3232
CrazyColumnNameCdkTableApp,
3333
UndefinedColumnsCdkTableApp,
34+
WhenRowCdkTableApp,
35+
WhenRowWithoutDefaultCdkTableApp
3436
],
3537
}).compileComponents();
3638
}));
@@ -202,6 +204,28 @@ describe('CdkTable', () => {
202204
});
203205
});
204206

207+
describe('using when predicate', () => {
208+
it('should be able to display different row templates based on the row data', () => {
209+
let whenFixture = TestBed.createComponent(WhenRowCdkTableApp);
210+
whenFixture.detectChanges();
211+
212+
let data = whenFixture.componentInstance.dataSource.data;
213+
expectTableToMatchContent(whenFixture.nativeElement.querySelector('cdk-table'), [
214+
['Column A', 'Column B', 'Column C'],
215+
[data[0].a, data[0].b, data[0].c],
216+
['index_1_special_row'],
217+
['c3_special_row'],
218+
[data[3].a, data[3].b, data[3].c],
219+
]);
220+
});
221+
222+
it('should error if there is row data that does not have a matching row template', () => {
223+
let whenFixture = TestBed.createComponent(WhenRowWithoutDefaultCdkTableApp);
224+
expect(() => whenFixture.detectChanges())
225+
.toThrowError('Could not find a matching row definition for the provided row data');
226+
});
227+
});
228+
205229
it('should use differ to add/remove/move rows', () => {
206230
// Each row receives an attribute 'initialIndex' the element's original place
207231
getRows(tableElement).forEach((row: Element, index: number) => {
@@ -615,6 +639,95 @@ class SimpleCdkTableApp {
615639
@ViewChild(CdkTable) table: CdkTable<TestData>;
616640
}
617641

642+
@Component({
643+
template: `
644+
<cdk-table [dataSource]="dataSource">
645+
<ng-container cdkColumnDef="column_a">
646+
<cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
647+
<cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
648+
</ng-container>
649+
650+
<ng-container cdkColumnDef="column_b">
651+
<cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
652+
<cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
653+
</ng-container>
654+
655+
<ng-container cdkColumnDef="column_c">
656+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
657+
<cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
658+
</ng-container>
659+
660+
<ng-container cdkColumnDef="index1Column">
661+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
662+
<cdk-cell *cdkCellDef="let row"> index_1_special_row </cdk-cell>
663+
</ng-container>
664+
665+
<ng-container cdkColumnDef="c3Column">
666+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
667+
<cdk-cell *cdkCellDef="let row"> c3_special_row </cdk-cell>
668+
</ng-container>
669+
670+
<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
671+
<cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
672+
<cdk-row *cdkRowDef="let row; columns: ['index1Column']; when: isIndex1"></cdk-row>
673+
<cdk-row *cdkRowDef="let row; columns: ['c3Column']; when: hasC3"></cdk-row>
674+
</cdk-table>
675+
`
676+
})
677+
class WhenRowCdkTableApp {
678+
dataSource: FakeDataSource = new FakeDataSource();
679+
columnsToRender = ['column_a', 'column_b', 'column_c'];
680+
isIndex1 = (_rowData: TestData, index: number) => index == 1;
681+
hasC3 = (rowData: TestData) => rowData.c == 'c_3';
682+
683+
constructor() { this.dataSource.addData(); }
684+
685+
@ViewChild(CdkTable) table: CdkTable<TestData>;
686+
}
687+
688+
@Component({
689+
template: `
690+
<cdk-table [dataSource]="dataSource">
691+
<ng-container cdkColumnDef="column_a">
692+
<cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
693+
<cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
694+
</ng-container>
695+
696+
<ng-container cdkColumnDef="column_b">
697+
<cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
698+
<cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
699+
</ng-container>
700+
701+
<ng-container cdkColumnDef="column_c">
702+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
703+
<cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
704+
</ng-container>
705+
706+
<ng-container cdkColumnDef="index1Column">
707+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
708+
<cdk-cell *cdkCellDef="let row"> index_1_special_row </cdk-cell>
709+
</ng-container>
710+
711+
<ng-container cdkColumnDef="c3Column">
712+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
713+
<cdk-cell *cdkCellDef="let row"> c3_special_row </cdk-cell>
714+
</ng-container>
715+
716+
<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
717+
<cdk-row *cdkRowDef="let row; columns: ['index1Column']; when: isIndex1"></cdk-row>
718+
<cdk-row *cdkRowDef="let row; columns: ['c3Column']; when: hasC3"></cdk-row>
719+
</cdk-table>
720+
`
721+
})
722+
class WhenRowWithoutDefaultCdkTableApp {
723+
dataSource: FakeDataSource = new FakeDataSource();
724+
columnsToRender = ['column_a', 'column_b', 'column_c'];
725+
isIndex1 = (_rowData: TestData, index: number) => index == 1;
726+
hasC3 = (rowData: TestData) => rowData.c == 'c_3';
727+
728+
@ViewChild(CdkTable) table: CdkTable<TestData>;
729+
}
730+
618731
@Component({
619732
template: `
620733
<cdk-table [dataSource]="dataSource">

src/cdk/table/table.ts

Lines changed: 43 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -90,11 +90,14 @@ export class CdkTable<T> implements CollectionViewer {
9090
private _renderChangeSubscription: Subscription | null;
9191

9292
/** Map of all the user's defined columns (header and data cell template) identified by name. */
93-
private _columnDefinitionsByName = new Map<string, CdkColumnDef>();
93+
private _columnDefsByName = new Map<string, CdkColumnDef>();
9494

9595
/** Differ used to find the changes in the data provided by the data source. */
9696
private _dataDiffer: IterableDiffer<T>;
9797

98+
/** Stores the row definition that does not have a when predicate. */
99+
private _defaultRowDef: CdkRowDef<T> | null;
100+
98101
/**
99102
* Tracking function that will be used to check the differences in data changes. Used similarly
100103
* to `ngFor` `trackBy` function. Optimize row operations by identifying a row based on its data
@@ -143,13 +146,13 @@ export class CdkTable<T> implements CollectionViewer {
143146
* The column definitions provided by the user that contain what the header and cells should
144147
* render for each column.
145148
*/
146-
@ContentChildren(CdkColumnDef) _columnDefinitions: QueryList<CdkColumnDef>;
149+
@ContentChildren(CdkColumnDef) _columnDefs: QueryList<CdkColumnDef>;
147150

148-
/** Template used as the header container. */
149-
@ContentChild(CdkHeaderRowDef) _headerDefinition: CdkHeaderRowDef;
151+
/** Template definition used as the header container. */
152+
@ContentChild(CdkHeaderRowDef) _headerDef: CdkHeaderRowDef;
150153

151-
/** Set of templates that used as the data row containers. */
152-
@ContentChildren(CdkRowDef) _rowDefinitions: QueryList<CdkRowDef>;
154+
/** Set of template definitions that used as the data row containers. */
155+
@ContentChildren(CdkRowDef) _rowDefs: QueryList<CdkRowDef<T>>;
153156

154157
constructor(private readonly _differs: IterableDiffers,
155158
private readonly _changeDetectorRef: ChangeDetectorRef,
@@ -167,13 +170,14 @@ export class CdkTable<T> implements CollectionViewer {
167170
}
168171

169172
ngAfterContentInit() {
170-
this._cacheColumnDefinitionsByName();
171-
this._columnDefinitions.changes.subscribe(() => this._cacheColumnDefinitionsByName());
173+
this._cacheColumnDefsByName();
174+
this._columnDefs.changes.subscribe(() => this._cacheColumnDefsByName());
172175
this._renderHeaderRow();
173176
}
174177

175178
ngAfterContentChecked() {
176179
this._renderUpdatedColumns();
180+
this._defaultRowDef = this._rowDefs.find(def => !def.when) || null;
177181
if (this.dataSource && !this._renderChangeSubscription) {
178182
this._observeRenderChanges();
179183
}
@@ -190,15 +194,14 @@ export class CdkTable<T> implements CollectionViewer {
190194
}
191195
}
192196

193-
194197
/** Update the map containing the content's column definitions. */
195-
private _cacheColumnDefinitionsByName() {
196-
this._columnDefinitionsByName.clear();
197-
this._columnDefinitions.forEach(columnDef => {
198-
if (this._columnDefinitionsByName.has(columnDef.name)) {
198+
private _cacheColumnDefsByName() {
199+
this._columnDefsByName.clear();
200+
this._columnDefs.forEach(columnDef => {
201+
if (this._columnDefsByName.has(columnDef.name)) {
199202
throw getTableDuplicateColumnNameError(columnDef.name);
200203
}
201-
this._columnDefinitionsByName.set(columnDef.name, columnDef);
204+
this._columnDefsByName.set(columnDef.name, columnDef);
202205
});
203206
}
204207

@@ -208,8 +211,8 @@ export class CdkTable<T> implements CollectionViewer {
208211
*/
209212
private _renderUpdatedColumns() {
210213
// Re-render the rows when the row definition columns change.
211-
this._rowDefinitions.forEach(rowDefinition => {
212-
if (!!rowDefinition.getColumnsDiff()) {
214+
this._rowDefs.forEach(def => {
215+
if (!!def.getColumnsDiff()) {
213216
// Reset the data to an empty array so that renderRowChanges will re-render all new rows.
214217
this._dataDiffer.diff([]);
215218

@@ -219,7 +222,7 @@ export class CdkTable<T> implements CollectionViewer {
219222
});
220223

221224
// Re-render the header row if there is a difference in its columns.
222-
if (this._headerDefinition.getColumnsDiff()) {
225+
if (this._headerDef.getColumnsDiff()) {
223226
this._headerRowPlaceholder.viewContainer.clear();
224227
this._renderHeaderRow();
225228
}
@@ -264,14 +267,14 @@ export class CdkTable<T> implements CollectionViewer {
264267
* Create the embedded view for the header template and place it in the header row view container.
265268
*/
266269
private _renderHeaderRow() {
267-
const cells = this._getHeaderCellTemplatesForRow(this._headerDefinition);
270+
const cells = this._getHeaderCellTemplatesForRow(this._headerDef);
268271
if (!cells.length) { return; }
269272

270273
// TODO(andrewseguin): add some code to enforce that exactly
271274
// one CdkCellOutlet was instantiated as a result
272275
// of `createEmbeddedView`.
273276
this._headerRowPlaceholder.viewContainer
274-
.createEmbeddedView(this._headerDefinition.template, {cells});
277+
.createEmbeddedView(this._headerDef.template, {cells});
275278

276279
cells.forEach(cell => {
277280
CdkCellOutlet.mostRecentCellOutlet._viewContainer.createEmbeddedView(cell.template, {});
@@ -301,15 +304,29 @@ export class CdkTable<T> implements CollectionViewer {
301304
this._updateRowContext();
302305
}
303306

307+
/**
308+
* Finds the matching row definition that should be used for this row data. If there is only
309+
* one row definition, it is returned. Otherwise, find the row definition that has a when
310+
* predicate that returns true with the data. If none return true, return the default row
311+
* definition.
312+
*/
313+
_getRowDef(data: T, i: number): CdkRowDef<T> {
314+
if (this._rowDefs.length == 1) { return this._rowDefs.first; }
315+
316+
let rowDef = this._rowDefs.find(def => def.when && def.when(data, i)) || this._defaultRowDef;
317+
if (!rowDef) {
318+
throw Error('Could not find a matching row definition for the provided row data');
319+
}
320+
321+
return rowDef;
322+
}
323+
304324
/**
305325
* Create the embedded view for the data row template and place it in the correct index location
306326
* within the data row view container.
307327
*/
308328
private _insertRow(rowData: T, index: number) {
309-
// TODO(andrewseguin): Add when predicates to the row definitions
310-
// to find the right template to used based on
311-
// the data rather than choosing the first row definition.
312-
const row = this._rowDefinitions.first;
329+
const row = this._getRowDef(rowData, index);
313330

314331
// Row context that will be provided to both the created embedded row view and its cells.
315332
const context: CdkCellOutletRowContext<T> = {$implicit: rowData};
@@ -353,7 +370,7 @@ export class CdkTable<T> implements CollectionViewer {
353370
private _getHeaderCellTemplatesForRow(headerDef: CdkHeaderRowDef): CdkHeaderCellDef[] {
354371
if (!headerDef.columns) { return []; }
355372
return headerDef.columns.map(columnId => {
356-
const column = this._columnDefinitionsByName.get(columnId);
373+
const column = this._columnDefsByName.get(columnId);
357374

358375
if (!column) {
359376
throw getTableUnknownColumnError(columnId);
@@ -367,10 +384,10 @@ export class CdkTable<T> implements CollectionViewer {
367384
* Returns the cell template definitions to insert in the provided row
368385
* as defined by its list of columns to display.
369386
*/
370-
private _getCellTemplatesForRow(rowDef: CdkRowDef): CdkCellDef[] {
387+
private _getCellTemplatesForRow(rowDef: CdkRowDef<T>): CdkCellDef[] {
371388
if (!rowDef.columns) { return []; }
372389
return rowDef.columns.map(columnId => {
373-
const column = this._columnDefinitionsByName.get(columnId);
390+
const column = this._columnDefsByName.get(columnId);
374391

375392
if (!column) {
376393
throw getTableUnknownColumnError(columnId);
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import {DataSource} from '@angular/cdk/collections';
2+
import {Observable} from 'rxjs/Observable';
3+
import {UserData} from './people-database';
4+
import 'rxjs/add/observable/merge';
5+
import 'rxjs/add/operator/map';
6+
import {PersonDataSource} from './person-data-source';
7+
8+
export interface DetailRow {
9+
detailRow: boolean;
10+
data: UserData;
11+
}
12+
13+
export class PersonDetailDataSource extends DataSource<any> {
14+
constructor(private _personDataSource: PersonDataSource) {
15+
super();
16+
}
17+
18+
connect(): Observable<(UserData|DetailRow)[]> {
19+
return this._personDataSource.connect().map(data => {
20+
const rows: (UserData|DetailRow)[] = [];
21+
22+
// Interweave a detail data object for each row data object that will be used for displaying
23+
// row details. Contains the row data.
24+
data.forEach(person => rows.push(person, {detailRow: true, data: person}));
25+
26+
return rows;
27+
});
28+
}
29+
30+
disconnect() {
31+
// No-op
32+
}
33+
}

0 commit comments

Comments
 (0)