Skip to content

Commit 77ecbe6

Browse files
committed
feat(table): add row when predicate
1 parent 70bd5fc commit 77ecbe6

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
@@ -30,6 +30,8 @@ describe('CdkTable', () => {
3030
DuplicateColumnDefNameCdkTableApp,
3131
MissingColumnDefCdkTableApp,
3232
CrazyColumnNameCdkTableApp,
33+
WhenRowCdkTableApp,
34+
WhenRowWithoutDefaultCdkTableApp
3335
],
3436
}).compileComponents();
3537
}));
@@ -186,6 +188,28 @@ describe('CdkTable', () => {
186188
});
187189
});
188190

191+
describe('using when predicate', () => {
192+
it('should be able to display different row templates based on the row data', () => {
193+
let whenFixture = TestBed.createComponent(WhenRowCdkTableApp);
194+
whenFixture.detectChanges();
195+
196+
let data = whenFixture.componentInstance.dataSource.data;
197+
expectTableToMatchContent(whenFixture.nativeElement.querySelector('cdk-table'), [
198+
['Column A', 'Column B', 'Column C'],
199+
[data[0].a, data[0].b, data[0].c],
200+
['index_1_special_row'],
201+
['c3_special_row'],
202+
[data[3].a, data[3].b, data[3].c],
203+
]);
204+
});
205+
206+
it('should error if there is row data that does not have a matching row template', () => {
207+
let whenFixture = TestBed.createComponent(WhenRowWithoutDefaultCdkTableApp);
208+
expect(() => whenFixture.detectChanges())
209+
.toThrowError('Could not find a matching row definition for the provided row data');
210+
});
211+
});
212+
189213
it('should use differ to add/remove/move rows', () => {
190214
// Each row receives an attribute 'initialIndex' the element's original place
191215
getRows(tableElement).forEach((row: Element, index: number) => {
@@ -599,6 +623,95 @@ class SimpleCdkTableApp {
599623
@ViewChild(CdkTable) table: CdkTable<TestData>;
600624
}
601625

626+
@Component({
627+
template: `
628+
<cdk-table [dataSource]="dataSource">
629+
<ng-container cdkColumnDef="column_a">
630+
<cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
631+
<cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
632+
</ng-container>
633+
634+
<ng-container cdkColumnDef="column_b">
635+
<cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
636+
<cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
637+
</ng-container>
638+
639+
<ng-container cdkColumnDef="column_c">
640+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
641+
<cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
642+
</ng-container>
643+
644+
<ng-container cdkColumnDef="index1Column">
645+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
646+
<cdk-cell *cdkCellDef="let row"> index_1_special_row </cdk-cell>
647+
</ng-container>
648+
649+
<ng-container cdkColumnDef="c3Column">
650+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
651+
<cdk-cell *cdkCellDef="let row"> c3_special_row </cdk-cell>
652+
</ng-container>
653+
654+
<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
655+
<cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
656+
<cdk-row *cdkRowDef="let row; columns: ['index1Column']; when: isIndex1"></cdk-row>
657+
<cdk-row *cdkRowDef="let row; columns: ['c3Column']; when: hasC3"></cdk-row>
658+
</cdk-table>
659+
`
660+
})
661+
class WhenRowCdkTableApp {
662+
dataSource: FakeDataSource = new FakeDataSource();
663+
columnsToRender = ['column_a', 'column_b', 'column_c'];
664+
isIndex1 = (_rowData: TestData, index: number) => index == 1;
665+
hasC3 = (rowData: TestData) => rowData.c == 'c_3';
666+
667+
constructor() { this.dataSource.addData(); }
668+
669+
@ViewChild(CdkTable) table: CdkTable<TestData>;
670+
}
671+
672+
@Component({
673+
template: `
674+
<cdk-table [dataSource]="dataSource">
675+
<ng-container cdkColumnDef="column_a">
676+
<cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
677+
<cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
678+
</ng-container>
679+
680+
<ng-container cdkColumnDef="column_b">
681+
<cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
682+
<cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
683+
</ng-container>
684+
685+
<ng-container cdkColumnDef="column_c">
686+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
687+
<cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
688+
</ng-container>
689+
690+
<ng-container cdkColumnDef="index1Column">
691+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
692+
<cdk-cell *cdkCellDef="let row"> index_1_special_row </cdk-cell>
693+
</ng-container>
694+
695+
<ng-container cdkColumnDef="c3Column">
696+
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
697+
<cdk-cell *cdkCellDef="let row"> c3_special_row </cdk-cell>
698+
</ng-container>
699+
700+
<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
701+
<cdk-row *cdkRowDef="let row; columns: ['index1Column']; when: isIndex1"></cdk-row>
702+
<cdk-row *cdkRowDef="let row; columns: ['c3Column']; when: hasC3"></cdk-row>
703+
</cdk-table>
704+
`
705+
})
706+
class WhenRowWithoutDefaultCdkTableApp {
707+
dataSource: FakeDataSource = new FakeDataSource();
708+
columnsToRender = ['column_a', 'column_b', 'column_c'];
709+
isIndex1 = (_rowData: TestData, index: number) => index == 1;
710+
hasC3 = (rowData: TestData) => rowData.c == 'c_3';
711+
712+
@ViewChild(CdkTable) table: CdkTable<TestData>;
713+
}
714+
602715
@Component({
603716
template: `
604717
<cdk-table [dataSource]="dataSource">

src/cdk/table/table.ts

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

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

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

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

146-
/** Template used as the header container. */
147-
@ContentChild(CdkHeaderRowDef) _headerDefinition: CdkHeaderRowDef;
149+
/** Template definition used as the header container. */
150+
@ContentChild(CdkHeaderRowDef) _headerDef: CdkHeaderRowDef;
148151

149-
/** Set of templates that used as the data row containers. */
150-
@ContentChildren(CdkRowDef) _rowDefinitions: QueryList<CdkRowDef>;
152+
/** Set of template definitions that used as the data row containers. */
153+
@ContentChildren(CdkRowDef) _rowDefs: QueryList<CdkRowDef<T>>;
151154

152155
constructor(private readonly _differs: IterableDiffers,
153156
private readonly _changeDetectorRef: ChangeDetectorRef,
@@ -165,13 +168,14 @@ export class CdkTable<T> implements CollectionViewer {
165168
}
166169

167170
ngAfterContentInit() {
168-
this._cacheColumnDefinitionsByName();
169-
this._columnDefinitions.changes.subscribe(() => this._cacheColumnDefinitionsByName());
171+
this._cacheColumnDefsByName();
172+
this._columnDefs.changes.subscribe(() => this._cacheColumnDefsByName());
170173
this._renderHeaderRow();
171174
}
172175

173176
ngAfterContentChecked() {
174177
this._renderUpdatedColumns();
178+
this._defaultRowDef = this._rowDefs.find(def => !def.when) || null;
175179
if (this.dataSource && !this._renderChangeSubscription) {
176180
this._observeRenderChanges();
177181
}
@@ -188,15 +192,14 @@ export class CdkTable<T> implements CollectionViewer {
188192
}
189193
}
190194

191-
192195
/** Update the map containing the content's column definitions. */
193-
private _cacheColumnDefinitionsByName() {
194-
this._columnDefinitionsByName.clear();
195-
this._columnDefinitions.forEach(columnDef => {
196-
if (this._columnDefinitionsByName.has(columnDef.name)) {
196+
private _cacheColumnDefsByName() {
197+
this._columnDefsByName.clear();
198+
this._columnDefs.forEach(columnDef => {
199+
if (this._columnDefsByName.has(columnDef.name)) {
197200
throw getTableDuplicateColumnNameError(columnDef.name);
198201
}
199-
this._columnDefinitionsByName.set(columnDef.name, columnDef);
202+
this._columnDefsByName.set(columnDef.name, columnDef);
200203
});
201204
}
202205

@@ -206,8 +209,8 @@ export class CdkTable<T> implements CollectionViewer {
206209
*/
207210
private _renderUpdatedColumns() {
208211
// Re-render the rows when the row definition columns change.
209-
this._rowDefinitions.forEach(rowDefinition => {
210-
if (!!rowDefinition.getColumnsDiff()) {
212+
this._rowDefs.forEach(def => {
213+
if (!!def.getColumnsDiff()) {
211214
// Reset the data to an empty array so that renderRowChanges will re-render all new rows.
212215
this._dataDiffer.diff([]);
213216

@@ -217,7 +220,7 @@ export class CdkTable<T> implements CollectionViewer {
217220
});
218221

219222
// Re-render the header row if there is a difference in its columns.
220-
if (this._headerDefinition.getColumnsDiff()) {
223+
if (this._headerDef.getColumnsDiff()) {
221224
this._headerRowPlaceholder.viewContainer.clear();
222225
this._renderHeaderRow();
223226
}
@@ -262,14 +265,14 @@ export class CdkTable<T> implements CollectionViewer {
262265
* Create the embedded view for the header template and place it in the header row view container.
263266
*/
264267
private _renderHeaderRow() {
265-
const cells = this._getHeaderCellTemplatesForRow(this._headerDefinition);
268+
const cells = this._getHeaderCellTemplatesForRow(this._headerDef);
266269
if (!cells.length) { return; }
267270

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

274277
cells.forEach(cell => {
275278
CdkCellOutlet.mostRecentCellOutlet._viewContainer.createEmbeddedView(cell.template, {});
@@ -299,15 +302,29 @@ export class CdkTable<T> implements CollectionViewer {
299302
this._updateRowContext();
300303
}
301304

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

312329
// Row context that will be provided to both the created embedded row view and its cells.
313330
const context: CdkCellOutletRowContext<T> = {$implicit: rowData};
@@ -351,7 +368,7 @@ export class CdkTable<T> implements CollectionViewer {
351368
private _getHeaderCellTemplatesForRow(headerDef: CdkHeaderRowDef): CdkHeaderCellDef[] {
352369
if (!headerDef.columns) { return []; }
353370
return headerDef.columns.map(columnId => {
354-
const column = this._columnDefinitionsByName.get(columnId);
371+
const column = this._columnDefsByName.get(columnId);
355372

356373
if (!column) {
357374
throw getTableUnknownColumnError(columnId);
@@ -365,10 +382,10 @@ export class CdkTable<T> implements CollectionViewer {
365382
* Returns the cell template definitions to insert in the provided row
366383
* as defined by its list of columns to display.
367384
*/
368-
private _getCellTemplatesForRow(rowDef: CdkRowDef): CdkCellDef[] {
385+
private _getCellTemplatesForRow(rowDef: CdkRowDef<T>): CdkCellDef[] {
369386
if (!rowDef.columns) { return []; }
370387
return rowDef.columns.map(columnId => {
371-
const column = this._columnDefinitionsByName.get(columnId);
388+
const column = this._columnDefsByName.get(columnId);
372389

373390
if (!column) {
374391
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)