feat(table): allow multiple header/footer rows (#11245)

This commit is contained in:
Andrew Seguin
2018-05-11 10:38:13 -07:00
committed by Jeremy Elbourn
parent d26735ccad
commit 641edc3d51
7 changed files with 404 additions and 108 deletions
+1 -1
View File
@@ -45,7 +45,7 @@ export function getTableMissingMatchingRowDefError(data: any) {
* @docs-private
*/
export function getTableMissingRowDefsError() {
return Error('Missing definitions for header and row, ' +
return Error('Missing definitions for header, footer, and row; ' +
'cannot determine which columns should be rendered.');
}
+137 -22
View File
@@ -69,11 +69,11 @@ describe('CdkTable', () => {
});
it('with a rendered header with the right number of header cells', () => {
const header = getHeaderRow(tableElement);
const header = getHeaderRows(tableElement)[0];
expect(header).toBeTruthy();
expect(header.classList).toContain('customHeaderRowClass');
expect(getHeaderCells(tableElement).length).toBe(component.columnsToRender.length);
expect(getHeaderCells(header).length).toBe(component.columnsToRender.length);
});
it('with rendered rows with right number of row cells', () => {
@@ -87,7 +87,8 @@ describe('CdkTable', () => {
});
it('with column class names provided to header and data row cells', () => {
getHeaderCells(tableElement).forEach((headerCell, index) => {
const header = getHeaderRows(tableElement)[0];
getHeaderCells(header).forEach((headerCell, index) => {
expect(headerCell.classList).toContain(`cdk-column-${component.columnsToRender[index]}`);
});
@@ -101,8 +102,9 @@ describe('CdkTable', () => {
it('with the right accessibility roles', () => {
expect(tableElement.getAttribute('role')).toBe('grid');
expect(getHeaderRow(tableElement).getAttribute('role')).toBe('row');
getHeaderCells(tableElement).forEach(cell => {
expect(getHeaderRows(tableElement)[0].getAttribute('role')).toBe('row');
const header = getHeaderRows(tableElement)[0];
getHeaderCells(header).forEach(cell => {
expect(cell.getAttribute('role')).toBe('columnheader');
});
@@ -279,6 +281,40 @@ describe('CdkTable', () => {
expect(getRows(tableElement).length).toBe(0);
}));
it('should be able to render multiple header and footer rows', () => {
setupTableTestApp(MultipleHeaderFooterRowsCdkTableApp);
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['first-header'],
['second-header'],
['first-footer'],
['second-footer'],
]);
});
it('should be able to render and change multiple header and footer rows', () => {
setupTableTestApp(MultipleHeaderFooterRowsCdkTableApp);
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['first-header'],
['second-header'],
['first-footer'],
['second-footer'],
]);
component.showAlternativeHeadersAndFooters = true;
fixture.detectChanges();
expectTableToMatchContent(tableElement, [
['first-header'],
['second-header'],
['first-footer'],
['second-footer'],
]);
});
describe('with different data inputs other than data source', () => {
let baseData: TestData[] = [
{a: 'a_1', b: 'b_1', c: 'c_1'},
@@ -460,7 +496,8 @@ describe('CdkTable', () => {
it('should be able to apply class-friendly css class names for the column cells', () => {
setupTableTestApp(CrazyColumnNameCdkTableApp);
// Column was named 'crazy-column-NAME-1!@#$%^-_&*()2'
expect(getHeaderCells(tableElement)[0].classList)
const header = getHeaderRows(tableElement)[0];
expect(getHeaderCells(header)[0].classList)
.toContain('cdk-column-crazy-column-NAME-1-------_----2');
});
@@ -488,7 +525,7 @@ describe('CdkTable', () => {
setupTableTestApp(UndefinedColumnsCdkTableApp);
// Header should be empty since there are no columns to display.
const headerRow = getHeaderRow(tableElement);
const headerRow = getHeaderRows(tableElement)[0];
expect(headerRow.textContent).toBe('');
// Rows should be empty since there are no columns to display.
@@ -657,6 +694,8 @@ describe('CdkTable', () => {
});
});
describe('with trackBy', () => {
function createTestComponentWithTrackyByTable(trackByStrategy) {
fixture = createComponent(TrackByCdkTableApp);
@@ -1053,6 +1092,62 @@ class NullDataCdkTableApp {
dataSource = observableOf(null);
}
@Component({
template: `
<cdk-table [dataSource]="[]">
<ng-container cdkColumnDef="first-header">
<th cdk-header-cell *cdkHeaderCellDef> first-header </th>
</ng-container>
<ng-container cdkColumnDef="second-header">
<th cdk-header-cell *cdkHeaderCellDef> second-header </th>
</ng-container>
<ng-container cdkColumnDef="first-footer">
<td cdk-footer-cell *cdkFooterCellDef> first-footer </td>
</ng-container>
<ng-container cdkColumnDef="second-footer">
<td cdk-footer-cell *cdkFooterCellDef> second-footer </td>
</ng-container>
<ng-container *ngIf="!showAlternativeHeadersAndFooters">
<tr cdk-header-row *cdkHeaderRowDef="['first-header']"></tr>
<tr cdk-header-row *cdkHeaderRowDef="['second-header']"></tr>
<tr cdk-footer-row *cdkFooterRowDef="['first-footer']"></tr>
<tr cdk-footer-row *cdkFooterRowDef="['second-footer']"></tr>
</ng-container>
<ng-container cdkColumnDef="alt-first-header">
<th cdk-header-cell *cdkHeaderCellDef> alt-first-header </th>
</ng-container>
<ng-container cdkColumnDef="alt-second-header">
<th cdk-header-cell *cdkHeaderCellDef> alt-second-header </th>
</ng-container>
<ng-container cdkColumnDef="alt-first-footer">
<td cdk-footer-cell *cdkFooterCellDef> alt-first-footer </td>
</ng-container>
<ng-container cdkColumnDef="alt-second-footer">
<td cdk-footer-cell *cdkFooterCellDef> alt-second-footer </td>
</ng-container>
<ng-container *ngIf="showAlternativeHeadersAndFooters">
<tr cdk-header-row *cdkHeaderRowDef="['alt-first-header']"></tr>
<tr cdk-header-row *cdkHeaderRowDef="['alt-second-header']"></tr>
<tr cdk-footer-row *cdkFooterRowDef="['alt-first-footer']"></tr>
<tr cdk-footer-row *cdkFooterRowDef="['alt-second-footer']"></tr>
</ng-container>
</cdk-table>
`
})
class MultipleHeaderFooterRowsCdkTableApp {
showAlternativeHeadersAndFooters = false;
}
@Component({
template: `
<cdk-table [dataSource]="dataSource" [multiTemplateDataRows]="multiTemplateDataRows">
@@ -1586,42 +1681,62 @@ function getElements(element: Element, query: string): Element[] {
return [].slice.call(element.querySelectorAll(query));
}
function getHeaderRow(tableElement: Element): Element {
return tableElement.querySelector('.cdk-header-row')!;
function getHeaderRows(tableElement: Element): Element[] {
return [].slice.call(tableElement.querySelectorAll('.cdk-header-row'))!;
}
function getFooterRow(tableElement: Element): Element {
return tableElement.querySelector('.cdk-footer-row')!;
function getFooterRows(tableElement: Element): Element[] {
return [].slice.call(tableElement.querySelectorAll('.cdk-footer-row'))!;
}
function getRows(tableElement: Element): Element[] {
return getElements(tableElement, '.cdk-row');
}
function getCells(row: Element): Element[] {
return row ? getElements(row, '.cdk-cell') : [];
if (!row) {
return [];
}
let cells = getElements(row, 'cdk-cell');
if (!cells.length) {
cells = getElements(row, 'td');
}
return cells;
}
function getHeaderCells(tableElement: Element): Element[] {
return getElements(getHeaderRow(tableElement), '.cdk-header-cell');
function getHeaderCells(headerRow: Element): Element[] {
let cells = getElements(headerRow, 'cdk-header-cell');
if (!cells.length) {
cells = getElements(headerRow, 'th');
}
return cells;
}
function getFooterCells(tableElement: Element): Element[] {
return getElements(getFooterRow(tableElement), '.cdk-footer-cell');
function getFooterCells(footerRow: Element): Element[] {
let cells = getElements(footerRow, 'cdk-footer-cell');
if (!cells.length) {
cells = getElements(footerRow, 'td');
}
return cells;
}
function getActualTableContent(tableElement: Element): string[][] {
let actualTableContent: Element[][] = [];
if (getHeaderRow(tableElement)) {
actualTableContent.push(getHeaderCells(tableElement));
}
getHeaderRows(tableElement).forEach(row => {
actualTableContent.push(getHeaderCells(row));
});
// Check data row cells
const rows = getRows(tableElement).map(row => getCells(row));
actualTableContent = actualTableContent.concat(rows);
if (getFooterRow(tableElement)) {
actualTableContent.push(getFooterCells(tableElement));
}
getFooterRows(tableElement).forEach(row => {
actualTableContent.push(getFooterCells(row));
});
// Convert the nodes into their text content;
return actualTableContent.map(row => row.map(cell => cell.textContent!.trim()));
+119 -64
View File
@@ -13,7 +13,6 @@ import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ContentChild,
ContentChildren,
Directive,
ElementRef,
@@ -153,7 +152,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
private _data: T[];
/** List of the rendered rows as identified by their `RenderRow` object. */
private _renderRows: RenderRow<T>[];
private _renderRows: RenderRow<T>[];
/** Subscription that listens for the data provided by the data source. */
private _renderChangeSubscription: Subscription | null;
@@ -166,34 +165,70 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
private _columnDefsByName = new Map<string, CdkColumnDef>();
/**
* Set of all row defitions that can be used by this table. Populated by the rows gathered by
* Set of all row definitions that can be used by this table. Populated by the rows gathered by
* using `ContentChildren` as well as any custom row definitions added to `_customRowDefs`.
*/
private _rowDefs: CdkRowDef<T>[];
/**
* Set of all header row definitions that can be used by this table. Populated by the rows
* gathered by using `ContentChildren` as well as any custom row definitions added to
* `_customHeaderRowDefs`.
*/
private _headerRowDefs: CdkHeaderRowDef[];
/**
* Set of all row definitions that can be used by this table. Populated by the rows gathered by
* using `ContentChildren` as well as any custom row definitions added to
* `_customFooterRowDefs`.
*/
private _footerRowDefs: CdkFooterRowDef[];
/** Differ used to find the changes in the data provided by the data source. */
private _dataDiffer: IterableDiffer<RenderRow<T>>;
/** Stores the row definition that does not have a when predicate. */
private _defaultRowDef: CdkRowDef<T> | null;
/** Column definitions that were defined outside of the direct content children of the table. */
/**
* Column definitions that were defined outside of the direct content children of the table.
* These will be defined when, e.g., creating a wrapper around the cdkTable that has
* column definitions as *it's* content child.
*/
private _customColumnDefs = new Set<CdkColumnDef>();
/** Row definitions that were defined outside of the direct content children of the table. */
/**
* Data row definitions that were defined outside of the direct content children of the table.
* These will be defined when, e.g., creating a wrapper around the cdkTable that has
* built-in data rows as *it's* content child.
*/
private _customRowDefs = new Set<CdkRowDef<T>>();
/**
* Whether the header row definition has been changed. Triggers an update to the header row after
* content is checked.
* Header row definitions that were defined outside of the direct content children of the table.
* These will be defined when, e.g., creating a wrapper around the cdkTable that has
* built-in header rows as *it's* content child.
*/
private _headerRowDefChanged = false;
private _customHeaderRowDefs = new Set<CdkHeaderRowDef>();
/**
* Footer row definitions that were defined outside of the direct content children of the table.
* These will be defined when, e.g., creating a wrapper around the cdkTable that has a
* built-in footer row as *it's* content child.
*/
private _customFooterRowDefs = new Set<CdkFooterRowDef>();
/**
* Whether the header row definition has been changed. Triggers an update to the header row after
* content is checked. Initialized as true so that the table renders the initial set of rows.
*/
private _headerRowDefChanged = true;
/**
* Whether the footer row definition has been changed. Triggers an update to the footer row after
* content is checked.
* content is checked. Initialized as true so that the table renders the initial set of rows.
*/
private _footerRowDefChanged = false;
private _footerRowDefChanged = true;
/**
* Cache of the latest rendered `RenderRow` objects as a map for easy retrieval when constructing
@@ -268,7 +303,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
set multiTemplateDataRows(v: boolean) {
this._multiTemplateDataRows = coerceBooleanProperty(v);
if (this._rowOutlet.viewContainer.length) {
this._forceRenderRows();
this._forceRenderDataRows();
}
}
_multiTemplateDataRows: boolean = false;
@@ -293,24 +328,14 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
*/
@ContentChildren(CdkColumnDef) _contentColumnDefs: QueryList<CdkColumnDef>;
/** Set of template definitions that used as the data row containers. */
/** Set of data row definitions that were provided to the table as content children. */
@ContentChildren(CdkRowDef) _contentRowDefs: QueryList<CdkRowDef<T>>;
/**
* Template definition used as the header container. By default it stores the header row
* definition found as a direct content child. Override this value through `setHeaderRowDef` if
* the header row definition should be changed or was not defined as a part of the table's
* content.
*/
@ContentChild(CdkHeaderRowDef) _headerRowDef: CdkHeaderRowDef;
/** Set of header row definitions that were provided to the table as content children. */
@ContentChildren(CdkHeaderRowDef) _contentHeaderRowDefs: QueryList<CdkHeaderRowDef>;
/**
* Template definition used as the footer container. By default it stores the footer row
* definition found as a direct content child. Override this value through `setFooterRowDef` if
* the footer row definition should be changed or was not defined as a part of the table's
* content.
*/
@ContentChild(CdkFooterRowDef) _footerRowDef: CdkFooterRowDef;
/** Set of footer row definitions that were provided to the table as content children. */
@ContentChildren(CdkFooterRowDef) _contentFooterRowDefs: QueryList<CdkFooterRowDef>;
constructor(protected readonly _differs: IterableDiffers,
protected readonly _changeDetectorRef: ChangeDetectorRef,
@@ -332,11 +357,6 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
this._dataDiffer = this._differs.find([]).create((_i: number, dataRow: RenderRow<T>) => {
return this.trackBy ? this.trackBy(dataRow.dataIndex, dataRow.data) : dataRow;
});
// If the table has header or footer row definitions defined as part of its content, mark that
// there is a change so that the content check will render the row.
this._headerRowDefChanged = !!this._headerRowDef;
this._footerRowDefChanged = !!this._footerRowDef;
}
ngAfterContentChecked() {
@@ -345,7 +365,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
this._cacheColumnDefs();
// Make sure that the user has at least added header, footer, or data row def.
if (!this._headerRowDef && !this._footerRowDef && !this._rowDefs.length) {
if (!this._headerRowDefs.length && !this._footerRowDefs.length && !this._rowDefs.length) {
throw getTableMissingRowDefsError();
}
@@ -354,13 +374,13 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
// If the header row definition has been changed, trigger a render to the header row.
if (this._headerRowDefChanged) {
this._renderHeaderRow();
this._forceRenderHeaderRows();
this._headerRowDefChanged = false;
}
// If the footer row definition has been changed, trigger a render to the footer row.
if (this._footerRowDefChanged) {
this._renderFooterRow();
this._forceRenderFooterRows();
this._footerRowDefChanged = false;
}
@@ -429,9 +449,12 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
* Sets the header row definition to be used. Overrides the header row definition gathered by
* using `ContentChild`, if one exists. Sets a flag that will re-render the header row after the
* table's content is checked.
* @docs-private
* @deprecated Use `addHeaderRowDef` and `removeHeaderRowDef` instead
* @deletion-target 8.0.0
*/
setHeaderRowDef(headerRowDef: CdkHeaderRowDef) {
this._headerRowDef = headerRowDef;
this._customHeaderRowDefs = new Set([headerRowDef]);
this._headerRowDefChanged = true;
}
@@ -439,32 +462,59 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
* Sets the footer row definition to be used. Overrides the footer row definition gathered by
* using `ContentChild`, if one exists. Sets a flag that will re-render the footer row after the
* table's content is checked.
* @docs-private
* @deprecated Use `addFooterRowDef` and `removeFooterRowDef` instead
* @deletion-target 8.0.0
*/
setFooterRowDef(footerRowDef: CdkFooterRowDef) {
this._footerRowDef = footerRowDef;
this._customFooterRowDefs = new Set([footerRowDef]);
this._footerRowDefChanged = true;
}
/** Adds a column definition that was not included as part of the direct content children. */
/** Adds a column definition that was not included as part of the content children. */
addColumnDef(columnDef: CdkColumnDef) {
this._customColumnDefs.add(columnDef);
}
/** Removes a column definition that was not included as part of the direct content children. */
/** Removes a column definition that was not included as part of the content children. */
removeColumnDef(columnDef: CdkColumnDef) {
this._customColumnDefs.delete(columnDef);
}
/** Adds a row definition that was not included as part of the direct content children. */
/** Adds a row definition that was not included as part of the content children. */
addRowDef(rowDef: CdkRowDef<T>) {
this._customRowDefs.add(rowDef);
}
/** Removes a row definition that was not included as part of the direct content children. */
/** Removes a row definition that was not included as part of the content children. */
removeRowDef(rowDef: CdkRowDef<T>) {
this._customRowDefs.delete(rowDef);
}
/** Adds a header row definition that was not included as part of the content children. */
addHeaderRowDef(headerRowDef: CdkHeaderRowDef) {
this._customHeaderRowDefs.add(headerRowDef);
this._headerRowDefChanged = true;
}
/** Removes a header row definition that was not included as part of the content children. */
removeHeaderRowDef(headerRowDef: CdkHeaderRowDef) {
this._customHeaderRowDefs.delete(headerRowDef);
this._headerRowDefChanged = true;
}
/** Adds a footer row definition that was not included as part of the content children. */
addFooterRowDef(footerRowDef: CdkFooterRowDef) {
this._customFooterRowDefs.add(footerRowDef);
this._footerRowDefChanged = true;
}
/** Removes a footer row definition that was not included as part of the content children. */
removeFooterRowDef(footerRowDef: CdkFooterRowDef) {
this._customFooterRowDefs.delete(footerRowDef);
this._footerRowDefChanged = true;
}
/**
* Get the list of RenderRow objects to render according to the current list of data and defined
* row definitions. If the previous list already contained a particular pair, it should be reused
@@ -529,9 +579,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
private _cacheColumnDefs() {
this._columnDefsByName.clear();
const columnDefs = this._contentColumnDefs ? this._contentColumnDefs.toArray() : [];
this._customColumnDefs.forEach(columnDef => columnDefs.push(columnDef));
const columnDefs = mergeQueryListAndSet(this._contentColumnDefs, this._customColumnDefs);
columnDefs.forEach(columnDef => {
if (this._columnDefsByName.has(columnDef.name)) {
throw getTableDuplicateColumnNameError(columnDef.name);
@@ -542,9 +590,14 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
/** Update the list of all available row definitions that can be used. */
private _cacheRowDefs() {
this._rowDefs = this._contentRowDefs ? this._contentRowDefs.toArray() : [];
this._customRowDefs.forEach(rowDef => this._rowDefs.push(rowDef));
this._headerRowDefs =
mergeQueryListAndSet(this._contentHeaderRowDefs, this._customHeaderRowDefs);
this._footerRowDefs =
mergeQueryListAndSet(this._contentFooterRowDefs, this._customFooterRowDefs);
this._rowDefs =
mergeQueryListAndSet(this._contentRowDefs, this._customRowDefs);
// After all row definitions are determined, find the row definition to be considered default.
const defaultRowDefs = this._rowDefs.filter(def => !def.when);
if (!this.multiTemplateDataRows && defaultRowDefs.length > 1) {
throw getTableMultipleDefaultRowDefsError();
@@ -557,21 +610,18 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
* If there is a diff, then re-render that section.
*/
private _renderUpdatedColumns() {
// Re-render the rows when the row definition columns change.
this._rowDefs.forEach(def => {
if (def.getColumnsDiff()) {
this._forceRenderRows();
}
});
const defColumnsDiffReducer = (accumulator, def) => accumulator || !!def.getColumnsDiff();
// Re-render the header row if there is a difference in its columns.
if (this._headerRowDef && this._headerRowDef.getColumnsDiff()) {
this._renderHeaderRow();
if (this._rowDefs.reduce(defColumnsDiffReducer, false)) {
this._forceRenderDataRows();
}
// Re-render the footer row if there is a difference in its columns.
if (this._footerRowDef && this._footerRowDef.getColumnsDiff()) {
this._renderFooterRow();
if (this._headerRowDefs.reduce(defColumnsDiffReducer, false)) {
this._forceRenderHeaderRows();
}
if (this._footerRowDefs.reduce(defColumnsDiffReducer, false)) {
this._forceRenderFooterRows();
}
}
@@ -638,26 +688,26 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
* Clears any existing content in the header row outlet and creates a new embedded view
* in the outlet using the header row definition.
*/
private _renderHeaderRow() {
private _forceRenderHeaderRows() {
// Clear the footer row outlet if any content exists.
if (this._headerRowOutlet.viewContainer.length > 0) {
this._headerRowOutlet.viewContainer.clear();
}
this._renderRow(this._headerRowOutlet, this._headerRowDef);
this._headerRowDefs.forEach((def, i) => this._renderRow(this._headerRowOutlet, def, i));
}
/**
* Clears any existing content in the footer row outlet and creates a new embedded view
* in the outlet using the footer row definition.
*/
private _renderFooterRow() {
private _forceRenderFooterRows() {
// Clear the footer row outlet if any content exists.
if (this._footerRowOutlet.viewContainer.length > 0) {
this._footerRowOutlet.viewContainer.clear();
}
this._renderRow(this._footerRowOutlet, this._footerRowDef);
this._footerRowDefs.forEach((def, i) => this._renderRow(this._footerRowOutlet, def, i));
}
/**
@@ -694,7 +744,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
private _insertRow(renderRow: RenderRow<T>, renderIndex: number) {
const rowDef = renderRow.rowDef;
const context: RowContext<T> = {$implicit: renderRow.data};
this._renderRow(this._rowOutlet, rowDef, context, renderIndex);
this._renderRow(this._rowOutlet, rowDef, renderIndex, context);
}
/**
@@ -703,7 +753,7 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
* of where to place the new row template in the outlet.
*/
private _renderRow(
outlet: RowOutlet, rowDef: BaseRowDef, context: RowContext<T> = {}, index = 0) {
outlet: RowOutlet, rowDef: BaseRowDef, index: number, context: RowContext<T> = {}) {
// TODO(andrewseguin): enforce that one outlet was instantiated from createEmbeddedView
outlet.viewContainer.createEmbeddedView(rowDef.template, context, index);
@@ -774,9 +824,14 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
* change that affects the evaluation of which rows should be rendered, e.g. toggling
* `multiTemplateDataRows` or adding/removing row definitions.
*/
private _forceRenderRows() {
private _forceRenderDataRows() {
this._dataDiffer.diff([]);
this._rowOutlet.viewContainer.clear();
this.renderRows();
}
}
/** Utility function that gets a merged list of the entries in a QueryList and values of a Set. */
function mergeQueryListAndSet<T>(queryList: QueryList<T>, set: Set<T>): T[] {
return queryList.toArray().concat(Array.from(set));
}
+51 -21
View File
@@ -48,12 +48,12 @@ describe('MatTable', () => {
const tableElement = fixture.nativeElement.querySelector('.mat-table');
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['Column A'],
['a_1'],
['a_2'],
['a_3'],
['fourth_row'],
['Footer A', 'Footer B', 'Footer C'],
['Footer A'],
]);
});
@@ -64,13 +64,13 @@ describe('MatTable', () => {
const tableElement = fixture.nativeElement.querySelector('.mat-table');
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['Column A'],
['a_1'],
['a_2'],
['a_3'],
['a_4'], // With multiple rows, this row shows up along with the special 'when' fourth_row
['fourth_row'],
['Footer A', 'Footer B', 'Footer C'],
['Footer A'],
]);
});
});
@@ -693,42 +693,62 @@ function getElements(element: Element, query: string): Element[] {
return [].slice.call(element.querySelectorAll(query));
}
function getHeaderRow(tableElement: Element): Element {
return tableElement.querySelector('.mat-header-row')!;
function getHeaderRows(tableElement: Element): Element[] {
return [].slice.call(tableElement.querySelectorAll('.mat-header-row'))!;
}
function getFooterRow(tableElement: Element): Element {
return tableElement.querySelector('.mat-footer-row')!;
function getFooterRows(tableElement: Element): Element[] {
return [].slice.call(tableElement.querySelectorAll('.mat-footer-row'))!;
}
function getRows(tableElement: Element): Element[] {
return getElements(tableElement, '.mat-row');
}
function getCells(row: Element): Element[] {
return row ? getElements(row, '.mat-cell') : [];
if (!row) {
return [];
}
let cells = getElements(row, 'mat-cell');
if (!cells.length) {
cells = getElements(row, 'td');
}
return cells;
}
function getHeaderCells(tableElement: Element): Element[] {
return getElements(getHeaderRow(tableElement), '.mat-header-cell');
function getHeaderCells(headerRow: Element): Element[] {
let cells = getElements(headerRow, 'mat-header-cell');
if (!cells.length) {
cells = getElements(headerRow, 'th');
}
return cells;
}
function getFooterCells(tableElement: Element): Element[] {
return getElements(getFooterRow(tableElement), '.mat-footer-cell');
function getFooterCells(footerRow: Element): Element[] {
let cells = getElements(footerRow, 'mat-footer-cell');
if (!cells.length) {
cells = getElements(footerRow, 'td');
}
return cells;
}
function getActualTableContent(tableElement: Element): string[][] {
let actualTableContent: Element[][] = [];
if (getHeaderRow(tableElement)) {
actualTableContent.push(getHeaderCells(tableElement));
}
getHeaderRows(tableElement).forEach(row => {
actualTableContent.push(getHeaderCells(row));
});
// Check data row cells
const rows = getRows(tableElement).map(row => getCells(row));
actualTableContent = actualTableContent.concat(rows);
if (getFooterRow(tableElement)) {
actualTableContent.push(getFooterCells(tableElement));
}
getFooterRows(tableElement).forEach(row => {
actualTableContent.push(getFooterCells(row));
});
// Convert the nodes into their text content;
return actualTableContent.map(row => row.map(cell => cell.textContent!.trim()));
@@ -738,19 +758,29 @@ function expectTableToMatchContent(tableElement: Element, expected: any[]) {
const missedExpectations: string[] = [];
function checkCellContent(actualCell: string, expectedCell: string) {
if (actualCell !== expectedCell) {
missedExpectations.push(`Expected cell contents to be ${actualCell} but was ${expectedCell}`);
missedExpectations.push(`Expected cell contents to be ${expectedCell} but was ${actualCell}`);
}
}
const actual = getActualTableContent(tableElement);
// Make sure the number of rows match
if (actual.length !== expected.length) {
missedExpectations.push(`Expected ${expected.length} total rows but got ${actual.length}`);
fail(missedExpectations.join('\n'));
}
actual.forEach((row, rowIndex) => {
const expectedRow = expected[rowIndex];
// Make sure the number of cells match
if (row.length !== expectedRow.length) {
missedExpectations.push(`Expected ${expectedRow.length} cells in row but got ${row.length}`);
fail(missedExpectations.join('\n'));
}
row.forEach((actualCell, cellIndex) => {
const expectedCell = expected[rowIndex] ? expected[rowIndex][cellIndex] : null;
const expectedCell = expectedRow ? expectedRow[cellIndex] : null;
checkCellContent(actualCell, expectedCell);
});
});
@@ -0,0 +1,19 @@
table {
width: 100%;
}
.example-first-header-row th {
border-bottom: none;
}
.example-second-header-row {
font-style: italic;
}
.example-first-footer-row {
font-weight: bold;
}
.example-second-footer-row td {
color: #900000;
}
@@ -0,0 +1,45 @@
<table mat-table [dataSource]="transactions" class="mat-elevation-z8">
<!-- Item Column -->
<ng-container matColumnDef="item">
<th mat-header-cell *matHeaderCellDef> Item </th>
<td mat-cell *matCellDef="let transaction"> {{transaction.item}} </td>
<td mat-footer-cell *matFooterCellDef> Total </td>
</ng-container>
<!-- Cost Column -->
<ng-container matColumnDef="cost">
<th mat-header-cell *matHeaderCellDef> Cost </th>
<td mat-cell *matCellDef="let transaction"> {{transaction.cost | currency}} </td>
<td mat-footer-cell *matFooterCellDef> {{getTotalCost() | currency}} </td>
</ng-container>
<!-- Item Description Column -->
<ng-container matColumnDef="item-description">
<th mat-header-cell *matHeaderCellDef> Name of the item purchased </th>
</ng-container>
<!-- Cost Description Column -->
<ng-container matColumnDef="cost-description">
<th mat-header-cell *matHeaderCellDef> Cost of the item in USD </th>
</ng-container>
<!-- Disclaimer column -->
<ng-container matColumnDef="disclaimer">
<td mat-footer-cell *matFooterCellDef colspan="2">
Please note that the cost of items displayed are completely and totally made up.
</td>
</ng-container>
<!-- The table will render two header rows, one data row per data object, and two footer rows. -->
<tr mat-header-row *matHeaderRowDef="displayedColumns"
class="example-first-header-row">
</tr>
<tr mat-header-row *matHeaderRowDef="['item-description', 'cost-description']"
class="example-second-header-row">
</tr>
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
<tr mat-footer-row *matFooterRowDef="displayedColumns" class="example-first-footer-row"></tr>
<tr mat-footer-row *matFooterRowDef="['disclaimer']" class="example-second-footer-row"></tr>
</table>
@@ -0,0 +1,32 @@
import {Component} from '@angular/core';
import {Transaction} from '../table-footer-row/table-footer-row-example';
export interface Transaction {
item: string;
cost: number;
}
/**
* @title Table with multiple header and footer rows
*/
@Component({
selector: 'table-multiple-header-footer-example',
styleUrls: ['table-multiple-header-footer-example.css'],
templateUrl: 'table-multiple-header-footer-example.html',
})
export class TableMultipleHeaderFooterExample {
displayedColumns = ['item', 'cost'];
transactions: Transaction[] = [
{item: 'Beach ball', cost: 4},
{item: 'Towel', cost: 5},
{item: 'Frisbee', cost: 2},
{item: 'Sunscreen', cost: 4},
{item: 'Cooler', cost: 25},
{item: 'Swim suit', cost: 15},
];
/** Gets the total cost of all transactions. */
getTotalCost() {
return this.transactions.map(t => t.cost).reduce((acc, value) => acc + value, 0);
}
}