Skip to content

Commit

Permalink
feat(table): add row when predicate
Browse files Browse the repository at this point in the history
  • Loading branch information
andrewseguin committed Sep 1, 2017
1 parent 70bd5fc commit 77ecbe6
Show file tree
Hide file tree
Showing 9 changed files with 297 additions and 45 deletions.
15 changes: 12 additions & 3 deletions src/cdk/table/row.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,13 +75,22 @@ export class CdkHeaderRowDef extends BaseRowDef {

/**
* Data row definition for the CDK table.
* Captures the header row's template and other row properties such as the columns to display.
* Captures the header row's template and other row properties such as the columns to display and
* a when predicate that describes when this row should be used.
*/
@Directive({
selector: '[cdkRowDef]',
inputs: ['columns: cdkRowDefColumns'],
inputs: ['columns: cdkRowDefColumns', 'when: cdkRowDefWhen'],
})
export class CdkRowDef extends BaseRowDef {
export class CdkRowDef<T> extends BaseRowDef {
/**
* Function that should return true if this row template should be used for the provided row data
* and index. If left undefined, this row will be considered the default row template to use when
* no other when functions return true for the data.
* For every row, there must be at least one when function that passes or an undefined to default.
*/
when: (rowData: T, index: number) => boolean;

// TODO(andrewseguin): Add an input for providing a switch function to determine
// if this template should be used.
constructor(template: TemplateRef<any>, _differs: IterableDiffers) {
Expand Down
113 changes: 113 additions & 0 deletions src/cdk/table/table.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ describe('CdkTable', () => {
DuplicateColumnDefNameCdkTableApp,
MissingColumnDefCdkTableApp,
CrazyColumnNameCdkTableApp,
WhenRowCdkTableApp,
WhenRowWithoutDefaultCdkTableApp
],
}).compileComponents();
}));
Expand Down Expand Up @@ -186,6 +188,28 @@ describe('CdkTable', () => {
});
});

describe('using when predicate', () => {
it('should be able to display different row templates based on the row data', () => {
let whenFixture = TestBed.createComponent(WhenRowCdkTableApp);
whenFixture.detectChanges();

let data = whenFixture.componentInstance.dataSource.data;
expectTableToMatchContent(whenFixture.nativeElement.querySelector('cdk-table'), [
['Column A', 'Column B', 'Column C'],
[data[0].a, data[0].b, data[0].c],
['index_1_special_row'],
['c3_special_row'],
[data[3].a, data[3].b, data[3].c],
]);
});

it('should error if there is row data that does not have a matching row template', () => {
let whenFixture = TestBed.createComponent(WhenRowWithoutDefaultCdkTableApp);
expect(() => whenFixture.detectChanges())
.toThrowError('Could not find a matching row definition for the provided row data');
});
});

it('should use differ to add/remove/move rows', () => {
// Each row receives an attribute 'initialIndex' the element's original place
getRows(tableElement).forEach((row: Element, index: number) => {
Expand Down Expand Up @@ -599,6 +623,95 @@ class SimpleCdkTableApp {
@ViewChild(CdkTable) table: CdkTable<TestData>;
}

@Component({
template: `
<cdk-table [dataSource]="dataSource">
<ng-container cdkColumnDef="column_a">
<cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
</ng-container>
<ng-container cdkColumnDef="column_b">
<cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
</ng-container>
<ng-container cdkColumnDef="column_c">
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
</ng-container>
<ng-container cdkColumnDef="index1Column">
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> index_1_special_row </cdk-cell>
</ng-container>
<ng-container cdkColumnDef="c3Column">
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> c3_special_row </cdk-cell>
</ng-container>
<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
<cdk-row *cdkRowDef="let row; columns: columnsToRender"></cdk-row>
<cdk-row *cdkRowDef="let row; columns: ['index1Column']; when: isIndex1"></cdk-row>
<cdk-row *cdkRowDef="let row; columns: ['c3Column']; when: hasC3"></cdk-row>
</cdk-table>
`
})
class WhenRowCdkTableApp {
dataSource: FakeDataSource = new FakeDataSource();
columnsToRender = ['column_a', 'column_b', 'column_c'];
isIndex1 = (_rowData: TestData, index: number) => index == 1;
hasC3 = (rowData: TestData) => rowData.c == 'c_3';

constructor() { this.dataSource.addData(); }

@ViewChild(CdkTable) table: CdkTable<TestData>;
}

@Component({
template: `
<cdk-table [dataSource]="dataSource">
<ng-container cdkColumnDef="column_a">
<cdk-header-cell *cdkHeaderCellDef> Column A</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.a}}</cdk-cell>
</ng-container>
<ng-container cdkColumnDef="column_b">
<cdk-header-cell *cdkHeaderCellDef> Column B</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.b}}</cdk-cell>
</ng-container>
<ng-container cdkColumnDef="column_c">
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> {{row.c}}</cdk-cell>
</ng-container>
<ng-container cdkColumnDef="index1Column">
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> index_1_special_row </cdk-cell>
</ng-container>
<ng-container cdkColumnDef="c3Column">
<cdk-header-cell *cdkHeaderCellDef> Column C</cdk-header-cell>
<cdk-cell *cdkCellDef="let row"> c3_special_row </cdk-cell>
</ng-container>
<cdk-header-row *cdkHeaderRowDef="columnsToRender"></cdk-header-row>
<cdk-row *cdkRowDef="let row; columns: ['index1Column']; when: isIndex1"></cdk-row>
<cdk-row *cdkRowDef="let row; columns: ['c3Column']; when: hasC3"></cdk-row>
</cdk-table>
`
})
class WhenRowWithoutDefaultCdkTableApp {
dataSource: FakeDataSource = new FakeDataSource();
columnsToRender = ['column_a', 'column_b', 'column_c'];
isIndex1 = (_rowData: TestData, index: number) => index == 1;
hasC3 = (rowData: TestData) => rowData.c == 'c_3';

@ViewChild(CdkTable) table: CdkTable<TestData>;
}

@Component({
template: `
<cdk-table [dataSource]="dataSource">
Expand Down
69 changes: 43 additions & 26 deletions src/cdk/table/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,11 +88,14 @@ export class CdkTable<T> implements CollectionViewer {
private _renderChangeSubscription: Subscription | null;

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

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

/** Stores the row definition that does not have a when predicate. */
private _defaultRowDef: CdkRowDef<T> | null;

/**
* Tracking function that will be used to check the differences in data changes. Used similarly
* to `ngFor` `trackBy` function. Optimize row operations by identifying a row based on its data
Expand Down Expand Up @@ -141,13 +144,13 @@ export class CdkTable<T> implements CollectionViewer {
* The column definitions provided by the user that contain what the header and cells should
* render for each column.
*/
@ContentChildren(CdkColumnDef) _columnDefinitions: QueryList<CdkColumnDef>;
@ContentChildren(CdkColumnDef) _columnDefs: QueryList<CdkColumnDef>;

/** Template used as the header container. */
@ContentChild(CdkHeaderRowDef) _headerDefinition: CdkHeaderRowDef;
/** Template definition used as the header container. */
@ContentChild(CdkHeaderRowDef) _headerDef: CdkHeaderRowDef;

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

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

ngAfterContentInit() {
this._cacheColumnDefinitionsByName();
this._columnDefinitions.changes.subscribe(() => this._cacheColumnDefinitionsByName());
this._cacheColumnDefsByName();
this._columnDefs.changes.subscribe(() => this._cacheColumnDefsByName());
this._renderHeaderRow();
}

ngAfterContentChecked() {
this._renderUpdatedColumns();
this._defaultRowDef = this._rowDefs.find(def => !def.when) || null;
if (this.dataSource && !this._renderChangeSubscription) {
this._observeRenderChanges();
}
Expand All @@ -188,15 +192,14 @@ export class CdkTable<T> implements CollectionViewer {
}
}


/** Update the map containing the content's column definitions. */
private _cacheColumnDefinitionsByName() {
this._columnDefinitionsByName.clear();
this._columnDefinitions.forEach(columnDef => {
if (this._columnDefinitionsByName.has(columnDef.name)) {
private _cacheColumnDefsByName() {
this._columnDefsByName.clear();
this._columnDefs.forEach(columnDef => {
if (this._columnDefsByName.has(columnDef.name)) {
throw getTableDuplicateColumnNameError(columnDef.name);
}
this._columnDefinitionsByName.set(columnDef.name, columnDef);
this._columnDefsByName.set(columnDef.name, columnDef);
});
}

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

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

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

// TODO(andrewseguin): add some code to enforce that exactly
// one CdkCellOutlet was instantiated as a result
// of `createEmbeddedView`.
this._headerRowPlaceholder.viewContainer
.createEmbeddedView(this._headerDefinition.template, {cells});
.createEmbeddedView(this._headerDef.template, {cells});

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

/**
* Finds the matching row definition that should be used for this row data. If there is only
* one row definition, it is returned. Otherwise, find the row definition that has a when
* predicate that returns true with the data. If none return true, return the default row
* definition.
*/
_getRowDef(data: T, i: number): CdkRowDef<T> {
if (this._rowDefs.length == 1) { return this._rowDefs.first; }

let rowDef = this._rowDefs.find(def => def.when && def.when(data, i)) || this._defaultRowDef;
if (!rowDef) {
throw Error('Could not find a matching row definition for the provided row data');
}

return rowDef;
}

/**
* Create the embedded view for the data row template and place it in the correct index location
* within the data row view container.
*/
private _insertRow(rowData: T, index: number) {
// TODO(andrewseguin): Add when predicates to the row definitions
// to find the right template to used based on
// the data rather than choosing the first row definition.
const row = this._rowDefinitions.first;
const row = this._getRowDef(rowData, index);

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

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

if (!column) {
throw getTableUnknownColumnError(columnId);
Expand Down
33 changes: 33 additions & 0 deletions src/demo-app/table/person-detail-data-source.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import {DataSource} from '@angular/cdk/collections';
import {Observable} from 'rxjs/Observable';
import {UserData} from './people-database';
import 'rxjs/add/observable/merge';
import 'rxjs/add/operator/map';
import {PersonDataSource} from './person-data-source';

export interface DetailRow {
detailRow: boolean;
data: UserData;
}

export class PersonDetailDataSource extends DataSource<any> {
constructor(private _personDataSource: PersonDataSource) {
super();
}

connect(): Observable<(UserData|DetailRow)[]> {
return this._personDataSource.connect().map(data => {
const rows: (UserData|DetailRow)[] = [];

// Interweave a detail data object for each row data object that will be used for displaying
// row details. Contains the row data.
data.forEach(person => rows.push(person, {detailRow: true, data: person}));

return rows;
});
}

disconnect() {
// No-op
}
}
Loading

0 comments on commit 77ecbe6

Please sign in to comment.