feat(table): add optional footer row (#10330)

This commit is contained in:
Andrew Seguin
2018-04-23 14:25:45 -07:00
committed by GitHub
parent a0c77e20d4
commit 6df37099c3
17 changed files with 942 additions and 532 deletions
+45 -6
View File
@@ -8,12 +8,17 @@
import {ContentChild, Directive, ElementRef, Input, TemplateRef} from '@angular/core';
/** Base interface for a cell definition. Captures a column's cell template definition. */
export interface CellDef {
template: TemplateRef<any>;
}
/**
* Cell definition for a CDK table.
* Captures the template of a column's data row cell as well as cell-specific properties.
*/
@Directive({selector: '[cdkCellDef]'})
export class CdkCellDef {
export class CdkCellDef implements CellDef {
constructor(/** @docs-private */ public template: TemplateRef<any>) { }
}
@@ -22,7 +27,16 @@ export class CdkCellDef {
* Captures the template of a column's header cell and as well as cell-specific properties.
*/
@Directive({selector: '[cdkHeaderCellDef]'})
export class CdkHeaderCellDef {
export class CdkHeaderCellDef implements CellDef {
constructor(/** @docs-private */ public template: TemplateRef<any>) { }
}
/**
* Footer cell definition for a CDK table.
* Captures the template of a column's footer cell and as well as cell-specific properties.
*/
@Directive({selector: '[cdkFooterCellDef]'})
export class CdkFooterCellDef implements CellDef {
constructor(/** @docs-private */ public template: TemplateRef<any>) { }
}
@@ -51,6 +65,9 @@ export class CdkColumnDef {
/** @docs-private */
@ContentChild(CdkHeaderCellDef) headerCell: CdkHeaderCellDef;
/** @docs-private */
@ContentChild(CdkFooterCellDef) footerCell: CdkFooterCellDef;
/**
* Transformed version of the column name that can be used as part of a CSS classname. Excludes
* all non-alphanumeric characters and the special characters '-' and '_'. Any characters that
@@ -59,6 +76,14 @@ export class CdkColumnDef {
cssClassFriendlyName: string;
}
/** Base class for the cells. Adds a CSS classname that identifies the column it renders in. */
export class BaseCdkCell {
constructor(columnDef: CdkColumnDef, elementRef: ElementRef) {
const columnClassName = `cdk-column-${columnDef.cssClassFriendlyName}`;
elementRef.nativeElement.classList.add(columnClassName);
}
}
/** Header cell template container that adds the right classes and role. */
@Directive({
selector: 'cdk-header-cell, th[cdk-header-cell]',
@@ -67,9 +92,23 @@ export class CdkColumnDef {
'role': 'columnheader',
},
})
export class CdkHeaderCell {
export class CdkHeaderCell extends BaseCdkCell {
constructor(columnDef: CdkColumnDef, elementRef: ElementRef) {
elementRef.nativeElement.classList.add(`cdk-column-${columnDef.cssClassFriendlyName}`);
super(columnDef, elementRef);
}
}
/** Footer cell template container that adds the right classes and role. */
@Directive({
selector: 'cdk-footer-cell, td[cdk-footer-cell]',
host: {
'class': 'cdk-footer-cell',
'role': 'gridcell',
},
})
export class CdkFooterCell extends BaseCdkCell {
constructor(columnDef: CdkColumnDef, elementRef: ElementRef) {
super(columnDef, elementRef);
}
}
@@ -81,8 +120,8 @@ export class CdkHeaderCell {
'role': 'gridcell',
},
})
export class CdkCell {
export class CdkCell extends BaseCdkCell {
constructor(columnDef: CdkColumnDef, elementRef: ElementRef) {
elementRef.nativeElement.classList.add(`cdk-column-${columnDef.cssClassFriendlyName}`);
super(columnDef, elementRef);
}
}
+49 -2
View File
@@ -18,7 +18,7 @@ import {
ViewContainerRef,
ViewEncapsulation,
} from '@angular/core';
import {CdkCellDef} from './cell';
import {CdkCellDef, CdkColumnDef} from './cell';
/**
* The row template that can be used by the mat-table. Should not be used outside of the
@@ -57,6 +57,9 @@ export abstract class BaseRowDef {
getColumnsDiff(): IterableChanges<any> | null {
return this._columnsDiffer.diff(this.columns);
}
/** Gets this row def's relevant cell template from the provided column def. */
abstract extractCellTemplate(column: CdkColumnDef): TemplateRef<any>;
}
/**
@@ -71,6 +74,30 @@ export class CdkHeaderRowDef extends BaseRowDef {
constructor(template: TemplateRef<any>, _differs: IterableDiffers) {
super(template, _differs);
}
/** Gets this row def's relevant cell template from the provided column def. */
extractCellTemplate(column: CdkColumnDef): TemplateRef<any> {
return column.headerCell.template;
}
}
/**
* Footer row definition for the CDK table.
* Captures the footer row's template and other footer properties such as the columns to display.
*/
@Directive({
selector: '[cdkFooterRowDef]',
inputs: ['columns: cdkFooterRowDef'],
})
export class CdkFooterRowDef extends BaseRowDef {
constructor(template: TemplateRef<any>, _differs: IterableDiffers) {
super(template, _differs);
}
/** Gets this row def's relevant cell template from the provided column def. */
extractCellTemplate(column: CdkColumnDef): TemplateRef<any> {
return column.footerCell.template;
}
}
/**
@@ -96,12 +123,17 @@ export class CdkRowDef<T> extends BaseRowDef {
constructor(template: TemplateRef<any>, _differs: IterableDiffers) {
super(template, _differs);
}
/** Gets this row def's relevant cell template from the provided column def. */
extractCellTemplate(column: CdkColumnDef): TemplateRef<any> {
return column.cell.template;
}
}
/** Context provided to the row cells */
export interface CdkCellOutletRowContext<T> {
/** Data for the row that this cell is located within. */
$implicit: T;
$implicit?: T;
/** Index location of the row that this cell is located within. */
index?: number;
@@ -162,6 +194,21 @@ export class CdkCellOutlet {
})
export class CdkHeaderRow { }
/** Footer template container that contains the cell outlet. Adds the right class and role. */
@Component({
moduleId: module.id,
selector: 'cdk-footer-row, tr[cdk-footer-row]',
template: CDK_ROW_TEMPLATE,
host: {
'class': 'cdk-footer-row',
'role': 'row',
},
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
})
export class CdkFooterRow { }
/** Data row template container that contains the cell outlet. Adds the right class and role. */
@Component({
moduleId: module.id,
+16 -5
View File
@@ -8,9 +8,15 @@
import {CommonModule} from '@angular/common';
import {NgModule} from '@angular/core';
import {CdkTable, HeaderRowPlaceholder, RowPlaceholder} from './table';
import {CdkCellOutlet, CdkHeaderRow, CdkHeaderRowDef, CdkRow, CdkRowDef} from './row';
import {CdkCell, CdkCellDef, CdkColumnDef, CdkHeaderCell, CdkHeaderCellDef} from './cell';
import {HeaderRowOutlet, DataRowOutlet, CdkTable, FooterRowOutlet} from './table';
import {
CdkCellOutlet, CdkFooterRow, CdkFooterRowDef, CdkHeaderRow, CdkHeaderRowDef, CdkRow,
CdkRowDef
} from './row';
import {
CdkColumnDef, CdkHeaderCellDef, CdkHeaderCell, CdkCell, CdkCellDef,
CdkFooterCellDef, CdkFooterCell
} from './cell';
const EXPORTED_DECLARATIONS = [
CdkTable,
@@ -18,14 +24,19 @@ const EXPORTED_DECLARATIONS = [
CdkCellDef,
CdkCellOutlet,
CdkHeaderCellDef,
CdkFooterCellDef,
CdkColumnDef,
CdkCell,
CdkRow,
CdkHeaderCell,
CdkFooterCell,
CdkHeaderRow,
CdkHeaderRowDef,
RowPlaceholder,
HeaderRowPlaceholder,
CdkFooterRow,
CdkFooterRowDef,
DataRowOutlet,
HeaderRowOutlet,
FooterRowOutlet,
];
@NgModule({
+9 -3
View File
@@ -18,13 +18,15 @@ top of the CDK data-table.
The first step to writing the data-table template is to define the columns.
A column definition is specified via an `<ng-container>` with the `cdkColumnDef` directive, giving
the column a name. Each column definition then further defines both a header-cell template
(`cdkHeaderCellDef`) and a data-cell template (`cdkCellDef`).
the column a name. Each column definition can contain a header-cell template
(`cdkHeaderCellDef`), data-cell template (`cdkCellDef`), and footer-cell
template (`cdkFooterCellDef`).
```html
<ng-container cdkColumnDef="username">
<th cdk-header-cell *cdkHeaderCellDef> User name </th>
<td cdk-cell *cdkCellDef="let row"> {{row.a}} </td>
<td cdk-footer-cell *cdkFooterCellDef> User name </td>
</ng-container>
```
@@ -35,11 +37,15 @@ Note that `cdkCellDef` exports the row context such that the row data can be ref
template. The directive also exports the same properties as `ngFor` (index, even, odd, first,
last).
The next step is to define the table's header-row (`cdkHeaderRowDef`) and data-row (`cdkRowDef`).
The next step is to define the table's header-row (`cdkHeaderRowDef`), data-row (`cdkRowDef`),
and fitler-row (`cdkFooterRowDef`). Note that each of these are optional to include, depending on
what type of rows you want rendered (e.g. if you do not need a footer row, simply do not add
its definition).
```html
<tr cdk-header-row *cdkHeaderRowDef="['username', 'age', 'title']"></tr>
<tr cdk-row *cdkRowDef="let row; columns: ['username', 'age', 'title']"></tr>
<tr cdk-footer-row *cdkFooterRowDef="['username', 'age', 'title']"></tr>
```
These row templates accept the specific columns to be rendered via the name given to the
File diff suppressed because it is too large Load Diff
+145 -101
View File
@@ -23,17 +23,24 @@ import {
IterableDiffer,
IterableDiffers,
OnInit,
QueryList,
QueryList, TemplateRef,
TrackByFunction,
ViewChild,
ViewContainerRef,
ViewEncapsulation,
} from '@angular/core';
import {CollectionViewer, DataSource} from '@angular/cdk/collections';
import {CdkCellOutlet, CdkCellOutletRowContext, CdkHeaderRowDef, CdkRowDef} from './row';
import {
BaseRowDef,
CdkCellOutlet,
CdkCellOutletRowContext,
CdkFooterRowDef,
CdkHeaderRowDef,
CdkRowDef
} from './row';
import {takeUntil} from 'rxjs/operators';
import {of as observableOf, BehaviorSubject, Observable, Subscription, Subject} from 'rxjs';
import {CdkCellDef, CdkColumnDef, CdkHeaderCellDef} from './cell';
import {CdkColumnDef} from './cell';
import {
getTableDuplicateColumnNameError,
getTableMissingMatchingRowDefError,
@@ -43,12 +50,17 @@ import {
getTableUnknownDataSourceError
} from './table-errors';
/** Interface used to provide an outlet for rows to be inserted into. */
export interface RowOutlet {
viewContainer: ViewContainerRef;
}
/**
* Provides a handle for the table to grab the view container's ng-container to insert data rows.
* @docs-private
*/
@Directive({selector: '[rowPlaceholder]'})
export class RowPlaceholder {
@Directive({selector: '[rowOutlet]'})
export class DataRowOutlet implements RowOutlet {
constructor(public viewContainer: ViewContainerRef, public elementRef: ElementRef) { }
}
@@ -56,18 +68,29 @@ export class RowPlaceholder {
* Provides a handle for the table to grab the view container's ng-container to insert the header.
* @docs-private
*/
@Directive({selector: '[headerRowPlaceholder]'})
export class HeaderRowPlaceholder {
@Directive({selector: '[headerRowOutlet]'})
export class HeaderRowOutlet implements RowOutlet {
constructor(public viewContainer: ViewContainerRef, public elementRef: ElementRef) { }
}
/**
* Provides a handle for the table to grab the view container's ng-container to insert the footer.
* @docs-private
*/
@Directive({selector: '[footerRowOutlet]'})
export class FooterRowOutlet implements RowOutlet {
constructor(public viewContainer: ViewContainerRef, public elementRef: ElementRef) { }
}
/**
* The table template that can be used by the mat-table. Should not be used outside of the
* material library.
* @docs-private
*/
export const CDK_TABLE_TEMPLATE = `
<ng-container headerRowPlaceholder></ng-container>
<ng-container rowPlaceholder></ng-container>`;
<ng-container headerRowOutlet></ng-container>
<ng-container rowOutlet></ng-container>
<ng-container footerRowOutlet></ng-container>`;
/**
* Class used to conveniently type the embedded view ref for rows with a context.
@@ -76,10 +99,10 @@ export const CDK_TABLE_TEMPLATE = `
abstract class RowViewRef<T> extends EmbeddedViewRef<CdkCellOutletRowContext<T>> { }
/**
* A data table that renders a header row and data rows. Uses the dataSource input to determine
* the data to be rendered. The data can be provided either as a data array, an Observable stream
* that emits the data array to render, or a DataSource with a connect function that will
* return an Observable stream that emits the data array to render.
* A data table that can render a header row, data rows, and a footer row.
* Uses the dataSource input to determine the data to be rendered. The data can be provided either
* as a data array, an Observable stream that emits the data array to render, or a DataSource with a
* connect function that will return an Observable stream that emits the data array to render.
*/
@Component({
moduleId: module.id,
@@ -103,9 +126,9 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
private _renderChangeSubscription: Subscription | null;
/**
* Map of all the user's defined columns (header and data cell template) identified by name.
* Collection populated by the column definitions gathered by `ContentChildren` as well as any
* custom column definitions added to `_customColumnDefs`.
* Map of all the user's defined columns (header, data, and footer cell template) identified by
* name. Collection populated by the column definitions gathered by `ContentChildren` as well as
* any custom column definitions added to `_customColumnDefs`.
*/
private _columnDefsByName = new Map<string, CdkColumnDef>();
@@ -133,6 +156,12 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
*/
private _headerRowDefChanged = false;
/**
* Whether the footer row definition has been changed. Triggers an update to the footer row after
* content is checked.
*/
private _footerRowDefChanged = false;
/**
* 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
@@ -178,7 +207,7 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
this._switchDataSource(dataSource);
}
}
private _dataSource: DataSource<T> | Observable<T[]> | T[];
private _dataSource: DataSource<T> | Observable<T[]> | T[] | T[];
// TODO(andrewseguin): Remove max value as the end index
// and instead calculate the view on init and scroll.
@@ -189,13 +218,14 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
viewChange: BehaviorSubject<{start: number, end: number}> =
new BehaviorSubject<{start: number, end: number}>({start: 0, end: Number.MAX_VALUE});
// Placeholders within the table's template where the header and data rows will be inserted.
@ViewChild(RowPlaceholder) _rowPlaceholder: RowPlaceholder;
@ViewChild(HeaderRowPlaceholder) _headerRowPlaceholder: HeaderRowPlaceholder;
// Outlets in the table's template where the header, data rows, and footer will be inserted.
@ViewChild(DataRowOutlet) _rowOutlet: DataRowOutlet;
@ViewChild(HeaderRowOutlet) _headerRowOutlet: HeaderRowOutlet;
@ViewChild(FooterRowOutlet) _footerRowOutlet: FooterRowOutlet;
/**
* The column definitions provided by the user that contain what the header and cells should
* render for each column.
* The column definitions provided by the user that contain what the header, data, and footer
* cells should render for each column.
*/
@ContentChildren(CdkColumnDef) _contentColumnDefs: QueryList<CdkColumnDef>;
@@ -210,6 +240,14 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
*/
@ContentChild(CdkHeaderRowDef) _headerRowDef: 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;
constructor(private readonly _differs: IterableDiffers,
private readonly _changeDetectorRef: ChangeDetectorRef,
private readonly _elementRef: ElementRef,
@@ -227,11 +265,10 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
// TODO(andrewseguin): Setup a listener for scrolling, emit the calculated view to viewChange
this._dataDiffer = this._differs.find([]).create(this._trackByFn);
// If the table has a header row definition defined as part of its content, flag this as a
// header row def change so that the content check will render the header row.
if (this._headerRowDef) {
this._headerRowDefChanged = true;
}
// 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() {
@@ -239,12 +276,12 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
this._cacheRowDefs();
this._cacheColumnDefs();
// Make sure that the user has at least added a header row or row def.
if (!this._headerRowDef && !this._rowDefs.length) {
// Make sure that the user has at least added header, footer, or data row def.
if (!this._headerRowDef && !this._footerRowDef && !this._rowDefs.length) {
throw getTableMissingRowDefsError();
}
// Render updates if the list of columns have been changed for the header or row definitions.
// Render updates if the list of columns have been changed for the header, row, or footer defs.
this._renderUpdatedColumns();
// If the header row definition has been changed, trigger a render to the header row.
@@ -253,6 +290,12 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
this._headerRowDefChanged = false;
}
// If the footer row definition has been changed, trigger a render to the footer row.
if (this._footerRowDefChanged) {
this._renderFooterRow();
this._footerRowDefChanged = false;
}
// If there is a data source and row definitions, connect to the data source unless a
// connection has already been made.
if (this.dataSource && this._rowDefs.length > 0 && !this._renderChangeSubscription) {
@@ -261,8 +304,9 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
}
ngOnDestroy() {
this._rowPlaceholder.viewContainer.clear();
this._headerRowPlaceholder.viewContainer.clear();
this._rowOutlet.viewContainer.clear();
this._headerRowOutlet.viewContainer.clear();
this._footerRowOutlet.viewContainer.clear();
this._onDestroy.next();
this._onDestroy.complete();
@@ -285,7 +329,7 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
const changes = this._dataDiffer.diff(this._data);
if (!changes) { return; }
const viewContainer = this._rowPlaceholder.viewContainer;
const viewContainer = this._rowOutlet.viewContainer;
changes.forEachOperation(
(record: IterableChangeRecord<T>, adjustedPreviousIndex: number, currentIndex: number) => {
if (record.previousIndex == null) {
@@ -319,6 +363,16 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
this._headerRowDefChanged = true;
}
/**
* 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.
*/
setFooterRowDef(footerRowDef: CdkFooterRowDef) {
this._footerRowDef = footerRowDef;
this._footerRowDefChanged = true;
}
/** Adds a column definition that was not included as part of the direct content children. */
addColumnDef(columnDef: CdkColumnDef) {
this._customColumnDefs.add(columnDef);
@@ -365,8 +419,8 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
}
/**
* Check if the header or rows have changed what columns they want to display. If there is a diff,
* then re-render that section.
* Check if the header, data, or footer rows have changed what columns they want to display.
* If there is a diff, then re-render that section.
*/
private _renderUpdatedColumns() {
// Re-render the rows when the row definition columns change.
@@ -375,7 +429,7 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
// Reset the data to an empty array so that renderRowChanges will re-render all new rows.
this._dataDiffer.diff([]);
this._rowPlaceholder.viewContainer.clear();
this._rowOutlet.viewContainer.clear();
this.renderRows();
}
});
@@ -384,12 +438,17 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
if (this._headerRowDef && this._headerRowDef.getColumnsDiff()) {
this._renderHeaderRow();
}
// Re-render the footer row if there is a difference in its columns.
if (this._footerRowDef && this._footerRowDef.getColumnsDiff()) {
this._renderFooterRow();
}
}
/**
* Switch to the provided data source by resetting the data and unsubscribing from the current
* render change subscription if one exists. If the data source is null, interpret this by
* clearing the row placeholder. Otherwise start listening for new data.
* clearing the row outlet. Otherwise start listening for new data.
*/
private _switchDataSource(dataSource: DataSource<T> | Observable<T[]> | T[]) {
this._data = [];
@@ -408,7 +467,7 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
if (this._dataDiffer) {
this._dataDiffer.diff([]);
}
this._rowPlaceholder.viewContainer.clear();
this._rowOutlet.viewContainer.clear();
}
this._dataSource = dataSource;
@@ -446,31 +505,29 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
}
/**
* Clears any existing content in the header row placeholder and creates a new embedded view
* in the placeholder using the header row definition.
* 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() {
// Clear the header row placeholder if any content exists.
if (this._headerRowPlaceholder.viewContainer.length > 0) {
this._headerRowPlaceholder.viewContainer.clear();
// Clear the footer row outlet if any content exists.
if (this._headerRowOutlet.viewContainer.length > 0) {
this._headerRowOutlet.viewContainer.clear();
}
const cells = this._getHeaderCellTemplatesForRow(this._headerRowDef);
if (!cells.length) { return; }
this._renderRow(this._headerRowOutlet, this._headerRowDef);
}
// TODO(andrewseguin): add some code to enforce that exactly
// one CdkCellOutlet was instantiated as a result
// of `createEmbeddedView`.
this._headerRowPlaceholder.viewContainer
.createEmbeddedView(this._headerRowDef.template, {cells});
/**
* 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() {
// Clear the footer row outlet if any content exists.
if (this._footerRowOutlet.viewContainer.length > 0) {
this._footerRowOutlet.viewContainer.clear();
}
cells.forEach(cell => {
if (CdkCellOutlet.mostRecentCellOutlet) {
CdkCellOutlet.mostRecentCellOutlet._viewContainer.createEmbeddedView(cell.template, {});
}
});
this._changeDetectorRef.markForCheck();
this._renderRow(this._footerRowOutlet, this._footerRowDef);
}
/**
@@ -493,21 +550,26 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
* within the data row view container.
*/
private _insertRow(rowData: T, index: number) {
const row = this._getRowDef(rowData, index);
// Row context that will be provided to both the created embedded row view and its cells.
const rowDef = this._getRowDef(rowData, index);
const context: CdkCellOutletRowContext<T> = {$implicit: rowData};
this._renderRow(this._rowOutlet, rowDef, context, index);
}
// TODO(andrewseguin): add some code to enforce that exactly one
// CdkCellOutlet was instantiated as a result of `createEmbeddedView`.
this._rowPlaceholder.viewContainer.createEmbeddedView(row.template, context, index);
/**
* Creates a new row template in the outlet and fills it with the set of cell templates.
* Optionally takes a context to provide to the row and cells, as well as an optional index
* of where to place the new row template in the outlet.
*/
private _renderRow(
outlet: RowOutlet, rowDef: BaseRowDef, context: CdkCellOutletRowContext<T> = {}, index = 0) {
// TODO(andrewseguin): enforce that one outlet was instantiated from createEmbeddedView
outlet.viewContainer.createEmbeddedView(rowDef.template, context, index);
this._getCellTemplatesForRow(row).forEach(cell => {
for (let cellTemplate of this._getCellTemplates(rowDef)) {
if (CdkCellOutlet.mostRecentCellOutlet) {
CdkCellOutlet.mostRecentCellOutlet._viewContainer
.createEmbeddedView(cell.template, context);
CdkCellOutlet.mostRecentCellOutlet._viewContainer.createEmbeddedView(cellTemplate, context);
}
});
}
this._changeDetectorRef.markForCheck();
}
@@ -517,7 +579,7 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
* e.g. first/last/even/odd.
*/
private _updateRowIndexContext() {
const viewContainer = this._rowPlaceholder.viewContainer;
const viewContainer = this._rowOutlet.viewContainer;
for (let index = 0, count = viewContainer.length; index < count; index++) {
const viewRef = viewContainer.get(index) as RowViewRef<T>;
viewRef.context.index = index;
@@ -529,30 +591,9 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
}
}
/**
* Returns the cell template definitions to insert into the header
* as defined by its list of columns to display.
*/
private _getHeaderCellTemplatesForRow(headerDef: CdkHeaderRowDef): CdkHeaderCellDef[] {
if (!headerDef || !headerDef.columns) { return []; }
return Array.from(headerDef.columns, columnId => {
const column = this._columnDefsByName.get(columnId);
if (!column) {
throw getTableUnknownColumnError(columnId);
}
return column.headerCell;
});
}
/**
* Returns the cell template definitions to insert in the provided row
* as defined by its list of columns to display.
*/
private _getCellTemplatesForRow(rowDef: CdkRowDef<T>): CdkCellDef[] {
if (!rowDef.columns) { return []; }
/** Gets the column definitions for the provided row def. */
private _getCellTemplates(rowDef: BaseRowDef): TemplateRef<any>[] {
if (!rowDef || !rowDef.columns) { return []; }
return Array.from(rowDef.columns, columnId => {
const column = this._columnDefsByName.get(columnId);
@@ -560,19 +601,22 @@ export class CdkTable<T> implements CollectionViewer, OnInit, AfterContentChecke
throw getTableUnknownColumnError(columnId);
}
return column.cell;
return rowDef.extractCellTemplate(column);
});
}
/** Adds native table sections (e.g. tbody) and moves the row placeholders into them. */
/** Adds native table sections (e.g. tbody) and moves the row outlets into them. */
private _applyNativeTableSections() {
const thead = document.createElement('thead');
const tbody = document.createElement('tbody');
const sections = [
{tag: 'thead', outlet: this._headerRowOutlet},
{tag: 'tbody', outlet: this._rowOutlet},
{tag: 'tfoot', outlet: this._footerRowOutlet},
];
this._elementRef.nativeElement.appendChild(thead);
this._elementRef.nativeElement.appendChild(tbody);
thead.appendChild(this._headerRowPlaceholder.elementRef.nativeElement);
tbody.appendChild(this._rowPlaceholder.elementRef.nativeElement);
for (const section of sections) {
const element = document.createElement(section.tag);
element.appendChild(section.outlet.elementRef.nativeElement);
this._elementRef.nativeElement.appendChild(element);
}
}
}
+8
View File
@@ -4,10 +4,12 @@
<ng-container matColumnDef="{{column}}" *ngFor="let column of columns">
<th mat-header-cell *matHeaderCellDef> {{column}} </th>
<td mat-cell *matCellDef="let element"> {{element[column]}} </td>
<td mat-footer-cell *matFooterCellDef> Footer {{column}}</td>
</ng-container>
<tr mat-header-row *matHeaderRowDef="columns"></tr>
<tr mat-row *matRowDef="let data; columns: columns;"></tr>
<tr mat-footer-row *matFooterRowDef="columns;"></tr>
</table>
</mat-card>
@@ -17,10 +19,12 @@
<ng-container matColumnDef="{{column}}" *ngFor="let column of columns">
<mat-header-cell *matHeaderCellDef> {{column}} </mat-header-cell>
<mat-cell *matCellDef="let element"> {{element[column]}} </mat-cell>
<mat-footer-cell *matFooterCellDef> Footer {{column}}</mat-footer-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columns"></mat-header-row>
<mat-row *matRowDef="let data; columns: columns;"></mat-row>
<mat-footer-row *matFooterRowDef="columns;"></mat-footer-row>>
</mat-table>
</mat-card>
@@ -30,10 +34,12 @@
<ng-container cdkColumnDef="{{column}}" *ngFor="let column of columns">
<th cdk-header-cell *cdkHeaderCellDef> {{column}} </th>
<td cdk-cell *cdkCellDef="let element"> {{element[column]}} </td>
<td cdk-footer-cell *cdkFooterCellDef> Footer {{column}}</td>
</ng-container>
<tr cdk-header-row *cdkHeaderRowDef="columns"></tr>
<tr cdk-row *cdkRowDef="let data; columns: columns;"></tr>
<tr cdk-footer-row *cdkFooterRowDef="columns;"></tr>
</table>
</mat-card>
@@ -43,9 +49,11 @@
<ng-container cdkColumnDef="{{column}}" *ngFor="let column of columns">
<cdk-header-cell *cdkHeaderCellDef> {{column}} </cdk-header-cell>
<cdk-cell *cdkCellDef="let element"> {{element[column]}} </cdk-cell>
<cdk-footer-cell *cdkFooterCellDef> Footer {{column}}</cdk-footer-cell>
</ng-container>
<cdk-header-row *cdkHeaderRowDef="columns"></cdk-header-row>
<cdk-row *cdkRowDef="let data; columns: columns;"></cdk-row>
<cdk-footer-row *cdkFooterRowDef="columns;"></cdk-footer-row>>
</cdk-table>
</mat-card>
+4 -4
View File
@@ -10,8 +10,8 @@
background: mat-color($background, 'card');
}
mat-row, mat-header-row, th.mat-header-cell, td.mat-cell {
mat-row, mat-header-row, mat-footer-row,
th.mat-header-cell, td.mat-cell, td.mat-footer-cell {
border-bottom-color: mat-color($foreground, divider);
}
@@ -19,7 +19,7 @@
color: mat-color($foreground, secondary-text);
}
.mat-cell {
.mat-cell, .mat-footer-cell {
color: mat-color($foreground, text);
}
}
@@ -34,7 +34,7 @@
font-weight: mat-font-weight($config, body-2);
}
.mat-cell {
.mat-cell, .mat-footer-cell {
font-size: mat-font-size($config, body-1);
}
}
+27 -1
View File
@@ -10,7 +10,7 @@ import {Directive, ElementRef, Input} from '@angular/core';
import {
CdkCell,
CdkCellDef,
CdkColumnDef,
CdkColumnDef, CdkFooterCell, CdkFooterCellDef,
CdkHeaderCell,
CdkHeaderCellDef,
} from '@angular/cdk/table';
@@ -35,6 +35,16 @@ export class MatCellDef extends CdkCellDef { }
})
export class MatHeaderCellDef extends CdkHeaderCellDef { }
/**
* Footer cell definition for the mat-table.
* Captures the template of a column's footer cell and as well as cell-specific properties.
*/
@Directive({
selector: '[matFooterCellDef]',
providers: [{provide: CdkFooterCellDef, useExisting: MatFooterCellDef}]
})
export class MatFooterCellDef extends CdkFooterCellDef { }
/**
* Column definition for the mat-table.
* Defines a set of cells available for a table column.
@@ -64,6 +74,22 @@ export class MatHeaderCell extends CdkHeaderCell {
}
}
/** Footer cell template container that adds the right classes and role. */
@Directive({
selector: 'mat-footer-cell, td[mat-footer-cell]',
host: {
'class': 'mat-footer-cell',
'role': 'gridcell',
},
})
export class MatFooterCell extends CdkFooterCell {
constructor(columnDef: CdkColumnDef,
elementRef: ElementRef) {
super(columnDef, elementRef);
elementRef.nativeElement.classList.add(`mat-column-${columnDef.cssClassFriendlyName}`);
}
}
/** Cell template container that adds the right classes and role. */
@Directive({
selector: 'mat-cell, td[mat-cell]',
+29 -3
View File
@@ -8,7 +8,7 @@
import {ChangeDetectionStrategy, Component, Directive, ViewEncapsulation} from '@angular/core';
import {
CDK_ROW_TEMPLATE,
CDK_ROW_TEMPLATE, CdkFooterRow, CdkFooterRowDef,
CdkHeaderRow,
CdkHeaderRowDef,
CdkRow,
@@ -26,9 +26,20 @@ import {
})
export class MatHeaderRowDef extends CdkHeaderRowDef { }
/**
* Footer row definition for the mat-table.
* Captures the footer row's template and other footer properties such as the columns to display.
*/
@Directive({
selector: '[matFooterRowDef]',
providers: [{provide: CdkFooterRowDef, useExisting: MatFooterRowDef}],
inputs: ['columns: matFooterRowDef'],
})
export class MatFooterRowDef extends CdkFooterRowDef { }
/**
* Data row definition for the mat-table.
* Captures the header row's template and other row properties such as the columns to display and
* Captures the footer row's template and other footer properties such as the columns to display and
* a when predicate that describes when this row should be used.
*/
@Directive({
@@ -39,7 +50,7 @@ export class MatHeaderRowDef extends CdkHeaderRowDef { }
export class MatRowDef<T> extends CdkRowDef<T> {
}
/** Header template container that contains the cell outlet. Adds the right class and role. */
/** Footer template container that contains the cell outlet. Adds the right class and role. */
@Component({
moduleId: module.id,
selector: 'mat-header-row, tr[mat-header-row]',
@@ -54,6 +65,21 @@ export class MatRowDef<T> extends CdkRowDef<T> {
})
export class MatHeaderRow extends CdkHeaderRow { }
/** Footer template container that contains the cell outlet. Adds the right class and role. */
@Component({
moduleId: module.id,
selector: 'mat-footer-row, tr[mat-footer-row]',
template: CDK_ROW_TEMPLATE,
host: {
'class': 'mat-footer-row',
'role': 'row',
},
changeDetection: ChangeDetectionStrategy.OnPush,
encapsulation: ViewEncapsulation.None,
exportAs: 'matFooterRow',
})
export class MatFooterRow extends CdkFooterRow { }
/** Data row template container that contains the cell outlet. Adds the right class and role. */
@Component({
moduleId: module.id,
+23 -4
View File
@@ -9,8 +9,23 @@
import {NgModule} from '@angular/core';
import {MatTable} from './table';
import {CdkTableModule} from '@angular/cdk/table';
import {MatCell, MatCellDef, MatColumnDef, MatHeaderCell, MatHeaderCellDef} from './cell';
import {MatHeaderRow, MatHeaderRowDef, MatRow, MatRowDef} from './row';
import {
MatCell,
MatCellDef,
MatColumnDef,
MatFooterCell,
MatFooterCellDef,
MatHeaderCell,
MatHeaderCellDef
} from './cell';
import {
MatFooterRow,
MatFooterRowDef,
MatHeaderRow,
MatHeaderRowDef,
MatRow,
MatRowDef
} from './row';
import {CommonModule} from '@angular/common';
import {MatCommonModule} from '@angular/material/core';
@@ -19,19 +34,23 @@ const EXPORTED_DECLARATIONS = [
MatTable,
// Template defs
MatCellDef,
MatHeaderCellDef,
MatColumnDef,
MatHeaderRowDef,
MatColumnDef,
MatCellDef,
MatRowDef,
MatFooterCellDef,
MatFooterRowDef,
// Cell directives
MatHeaderCell,
MatCell,
MatFooterCell,
// Row directions
MatHeaderRow,
MatRow,
MatFooterRow,
];
@NgModule({
+22
View File
@@ -257,6 +257,28 @@ masterToggle() {
}
```
#### Footer row
A footer row can be added to the table by adding a footer row definition to the table and adding
footer cell templates to column definitions. The footer row will be rendered after the rendered
data rows.
```html
<ng-container matColumnDef="cost">
<th mat-header-cell *matHeaderCellDef> Cost </th>
<td mat-cell *matCellDef="let data"> {{data.cost}} </td>
<td mat-footer-cell *matFooterCellDef> {{totalCost}} </td>
</ng-container>
...
<tr mat-header-row *matHeaderRowDef="columnsToDisplay"></tr>
<tr mat-row *matRowDef="let myRowData; columns: columnsToDisplay"></tr>
<tr mat-footer-row *matFooterRowDef="columnsToDisplay"></tr
```
<!--- example(table-footer-row) -->
##### 4. Include overflow styling
Finally, adjust the styling for the select column so that its overflow is not hidden. This allows
+13 -10
View File
@@ -13,14 +13,17 @@ mat-header-row {
min-height: $mat-header-row-height;
}
mat-row {
mat-row, mat-footer-row {
min-height: $mat-row-height;
}
mat-row, mat-header-row {
mat-row, mat-header-row, mat-footer-row {
display: flex;
// Define a border style, but then widths default to 3px. Reset them to 0px except the bottom
// which should be 1px;
border-width: 0;
border-bottom-width: 1px;
border-bottom-style: solid;
border-style: solid;
align-items: center;
box-sizing: border-box;
@@ -34,7 +37,7 @@ mat-row, mat-header-row {
}
}
mat-cell:first-child, mat-header-cell:first-child {
mat-cell:first-child, mat-header-cell:first-child, mat-footer-cell:first-child {
padding-left: $mat-row-horizontal-padding;
[dir='rtl'] & {
@@ -43,7 +46,7 @@ mat-cell:first-child, mat-header-cell:first-child {
}
}
mat-cell:last-child, mat-header-cell:last-child {
mat-cell:last-child, mat-header-cell:last-child, mat-footer-cell:last-child {
padding-right: $mat-row-horizontal-padding;
[dir='rtl'] & {
@@ -52,7 +55,7 @@ mat-cell:last-child, mat-header-cell:last-child {
}
}
mat-cell, mat-header-cell {
mat-cell, mat-header-cell, mat-footer-cell {
flex: 1;
display: flex;
align-items: center;
@@ -72,7 +75,7 @@ tr.mat-header-row {
height: $mat-header-row-height;
}
tr.mat-row {
tr.mat-row, tr.mat-footer-row {
height: $mat-row-height;
}
@@ -80,16 +83,16 @@ th.mat-header-cell {
text-align: left;
}
th.mat-header-cell, td.mat-cell {
th.mat-header-cell, td.mat-cell, td.mat-footer-cell {
padding: 0;
border-bottom-width: 1px;
border-bottom-style: solid;
}
th.mat-header-cell:first-child, td.mat-cell:first-child {
th.mat-header-cell:first-child, td.mat-cell:first-child, td.mat-footer-cell:first-child {
padding-left: $mat-row-horizontal-padding;
}
th.mat-header-cell:last-child, td.mat-cell:last-child {
th.mat-header-cell:last-child, td.mat-cell:last-child, td.mat-footer-cell:last-child {
padding-right: $mat-row-horizontal-padding;
}
+71 -27
View File
@@ -37,7 +37,8 @@ describe('MatTable', () => {
[data[0].a, data[0].b, data[0].c],
[data[1].a, data[1].b, data[1].c],
[data[2].a, data[2].b, data[2].c],
['fourth_row']
['fourth_row'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
@@ -51,7 +52,8 @@ describe('MatTable', () => {
['a_1'],
['a_2'],
['a_3'],
['fourth_row']
['fourth_row'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
});
@@ -120,6 +122,7 @@ describe('MatTable', () => {
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
@@ -133,6 +136,7 @@ describe('MatTable', () => {
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['a_4', 'b_4', 'c_4'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Remove data
@@ -145,6 +149,7 @@ describe('MatTable', () => {
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['a_4', 'b_4', 'c_4'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
@@ -157,6 +162,7 @@ describe('MatTable', () => {
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_1', 'b_1', 'c_1'],
['Footer A', 'Footer B', 'Footer C'],
]);
flushMicrotasks(); // Resolve promise that updates paginator's length
@@ -170,6 +176,7 @@ describe('MatTable', () => {
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_2', 'b_2', 'c_2'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Change filter to empty string, should match all rows
@@ -184,6 +191,7 @@ describe('MatTable', () => {
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Change filter function and filter, should match to rows with zebra.
@@ -203,6 +211,7 @@ describe('MatTable', () => {
expectTableToMatchContent(tableElement, [
['Column A', 'Column B', 'Column C'],
['a_2', 'b_2', 'c_2'],
['Footer A', 'Footer B', 'Footer C'],
]);
}));
@@ -215,6 +224,7 @@ describe('MatTable', () => {
['a_1', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Activate column A sort again (reverse direction)
@@ -225,6 +235,7 @@ describe('MatTable', () => {
['a_3', 'b_3', 'c_3'],
['a_2', 'b_2', 'c_2'],
['a_1', 'b_1', 'c_1'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Change sort function to customize how it sorts - first column 1, then 3, then 2
@@ -243,6 +254,7 @@ describe('MatTable', () => {
['a_1', 'b_1', 'c_1'],
['a_3', 'b_3', 'c_3'],
['a_2', 'b_2', 'c_2'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
@@ -258,6 +270,7 @@ describe('MatTable', () => {
['', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Expect that empty string row comes before the other values
@@ -268,6 +281,7 @@ describe('MatTable', () => {
['a_3', 'b_3', 'c_3'],
['a_2', 'b_2', 'c_2'],
['', 'b_1', 'c_1'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
@@ -283,6 +297,7 @@ describe('MatTable', () => {
['', 'b_1', 'c_1'],
['a_2', 'b_2', 'c_2'],
['a_3', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
@@ -294,6 +309,7 @@ describe('MatTable', () => {
['a_3', 'b_3', 'c_3'],
['a_2', 'b_2', 'c_2'],
['', 'b_1', 'c_1'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
@@ -311,6 +327,7 @@ describe('MatTable', () => {
['-1', 'b_3', 'c_3'],
['0', 'b_2', 'c_2'],
['1', 'b_1', 'c_1'],
['Footer A', 'Footer B', 'Footer C'],
]);
@@ -323,6 +340,7 @@ describe('MatTable', () => {
['1', 'b_1', 'c_1'],
['0', 'b_2', 'c_2'],
['-1', 'b_3', 'c_3'],
['Footer A', 'Footer B', 'Footer C'],
]);
});
@@ -340,6 +358,7 @@ describe('MatTable', () => {
['a_3', 'b_3', 'c_3'],
['a_4', 'b_4', 'c_4'],
['a_5', 'b_5', 'c_5'],
['Footer A', 'Footer B', 'Footer C'],
]);
// Navigate to the next page
@@ -352,6 +371,7 @@ describe('MatTable', () => {
['a_8', 'b_8', 'c_8'],
['a_9', 'b_9', 'c_9'],
['a_10', 'b_10', 'c_10'],
['Footer A', 'Footer B', 'Footer C'],
]);
}));
});
@@ -399,16 +419,19 @@ class FakeDataSource extends DataSource<TestData> {
<ng-container matColumnDef="column_a">
<mat-header-cell *matHeaderCellDef> Column A</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.a}}</mat-cell>
<mat-footer-cell *matFooterCellDef> Footer A</mat-footer-cell>
</ng-container>
<ng-container matColumnDef="column_b">
<mat-header-cell *matHeaderCellDef> Column B</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.b}}</mat-cell>
<mat-footer-cell *matFooterCellDef> Footer B</mat-footer-cell>
</ng-container>
<ng-container matColumnDef="column_c">
<mat-header-cell *matHeaderCellDef> Column C</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.c}}</mat-cell>
<mat-footer-cell *matFooterCellDef> Footer C</mat-footer-cell>
</ng-container>
<ng-container matColumnDef="special_column">
@@ -418,6 +441,7 @@ class FakeDataSource extends DataSource<TestData> {
<mat-header-row *matHeaderRowDef="columnsToRender"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnsToRender"></mat-row>
<mat-row *matRowDef="let row; columns: ['special_column']; when: isFourthRow"></mat-row>
<mat-footer-row *matFooterRowDef="columnsToRender"></mat-footer-row>
</mat-table>
`
})
@@ -466,6 +490,7 @@ class NativeHtmlTableApp {
<ng-container matColumnDef="column_a">
<mat-header-cell *matHeaderCellDef> Column A</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.a}}</mat-cell>
<mat-footer-cell *matFooterCellDef> Footer A</mat-footer-cell>
</ng-container>
<ng-container matColumnDef="special_column">
@@ -475,6 +500,7 @@ class NativeHtmlTableApp {
<mat-header-row *matHeaderRowDef="['column_a']"></mat-header-row>
<mat-row *matRowDef="let row; columns: ['column_a']"></mat-row>
<mat-row *matRowDef="let row; columns: ['special_column']; when: isFourthRow"></mat-row>
<mat-footer-row *matFooterRowDef="['column_a']"></mat-footer-row>
</mat-table>
`
})
@@ -492,20 +518,24 @@ class MatTableWithWhenRowApp {
<ng-container matColumnDef="column_a">
<mat-header-cell *matHeaderCellDef mat-sort-header="a"> Column A</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.a}}</mat-cell>
<mat-footer-cell *matFooterCellDef> Footer A</mat-footer-cell>
</ng-container>
<ng-container matColumnDef="column_b">
<mat-header-cell *matHeaderCellDef> Column B</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.b}}</mat-cell>
<mat-footer-cell *matFooterCellDef> Footer B</mat-footer-cell>
</ng-container>
<ng-container matColumnDef="column_c">
<mat-header-cell *matHeaderCellDef> Column C</mat-header-cell>
<mat-cell *matCellDef="let row"> {{row.c}}</mat-cell>
<mat-footer-cell *matFooterCellDef> Footer C</mat-footer-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="columnsToRender"></mat-header-row>
<mat-row *matRowDef="let row; columns: columnsToRender"></mat-row>
<mat-footer-row *matFooterRowDef="columnsToRender"></mat-footer-row>
</mat-table>
<mat-paginator [pageSize]="5"></mat-paginator>
@@ -641,7 +671,6 @@ class MatTableWithPaginatorApp {
}
}
// Utilities copied from CDKTable's spec
function getElements(element: Element, query: string): Element[] {
return [].slice.call(element.querySelectorAll(query));
}
@@ -650,6 +679,10 @@ function getHeaderRow(tableElement: Element): Element {
return tableElement.querySelector('.mat-header-row')!;
}
function getFooterRow(tableElement: Element): Element {
return tableElement.querySelector('.mat-footer-row')!;
}
function getRows(tableElement: Element): Element[] {
return getElements(tableElement, '.mat-row');
}
@@ -661,35 +694,46 @@ function getHeaderCells(tableElement: Element): Element[] {
return getElements(getHeaderRow(tableElement), '.mat-header-cell');
}
function expectTableToMatchContent(tableElement: Element, expectedTableContent: any[]) {
function getFooterCells(tableElement: Element): Element[] {
return getElements(getFooterRow(tableElement), '.mat-footer-cell');
}
function getActualTableContent(tableElement: Element): string[][] {
let actualTableContent: Element[][] = [];
if (getHeaderRow(tableElement)) {
actualTableContent.push(getHeaderCells(tableElement));
}
// Check data row cells
const rows = getRows(tableElement).map(row => getCells(row));
actualTableContent = actualTableContent.concat(rows);
if (getFooterRow(tableElement)) {
actualTableContent.push(getFooterCells(tableElement));
}
// Convert the nodes into their text content;
return actualTableContent.map(row => row.map(cell => cell.textContent!.trim()));
}
function expectTableToMatchContent(tableElement: Element, expected: any[]) {
const missedExpectations: string[] = [];
function checkCellContent(cell: Element, expectedTextContent: string) {
const actualTextContent = cell.textContent!.trim();
if (actualTextContent !== expectedTextContent) {
missedExpectations.push(
`Expected cell contents to be ${expectedTextContent} but was ${actualTextContent}`);
function checkCellContent(actualCell: string, expectedCell: string) {
if (actualCell !== expectedCell) {
missedExpectations.push(`Expected cell contents to be ${actualCell} but was ${expectedCell}`);
}
}
// Check header cells
const expectedHeaderContent = expectedTableContent.shift();
getHeaderCells(tableElement).forEach((cell, index) => {
const expected = expectedHeaderContent ?
expectedHeaderContent[index] :
null;
checkCellContent(cell, expected);
});
const actual = getActualTableContent(tableElement);
// Check data row cells
const rows = getRows(tableElement);
expect(rows.length).toBe(expectedTableContent.length,
`Found ${rows.length} rows but expected ${expectedTableContent.length}`);
rows.forEach((row, rowIndex) => {
getCells(row).forEach((cell, cellIndex) => {
const expected = expectedTableContent.length ?
expectedTableContent[rowIndex][cellIndex] :
null;
checkCellContent(cell, expected);
if (actual.length !== expected.length) {
missedExpectations.push(`Expected ${expected.length} total rows but got ${actual.length}`);
fail(missedExpectations.join('\n'));
}
actual.forEach((row, rowIndex) => {
row.forEach((actualCell, cellIndex) => {
const expectedCell = expected[rowIndex] ? expected[rowIndex][cellIndex] : null;
checkCellContent(actualCell, expectedCell);
});
});
@@ -0,0 +1,11 @@
.example-container {
display: flex;
flex-direction: column;
max-height: 500px;
min-width: 300px;
}
.mat-table {
overflow: auto;
max-height: 500px;
}
@@ -0,0 +1,22 @@
<div class="example-container mat-elevation-z8">
<mat-table [dataSource]="transactions">
<!-- Item Column -->
<ng-container matColumnDef="item">
<mat-header-cell *matHeaderCellDef> Item </mat-header-cell>
<mat-cell *matCellDef="let transaction"> {{transaction.item}} </mat-cell>
<mat-footer-cell *matFooterCellDef></mat-footer-cell>
</ng-container>
<!-- Cost Column -->
<ng-container matColumnDef="cost">
<mat-header-cell *matHeaderCellDef> Cost </mat-header-cell>
<mat-cell *matCellDef="let transaction"> {{transaction.cost | currency}} </mat-cell>
<mat-footer-cell *matFooterCellDef> getTotalCost() </mat-footer-cell>
</ng-container>
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
<mat-footer-row *matFooterRowDef="displayedColumns"></mat-footer-row>
</mat-table>
</div>
@@ -0,0 +1,31 @@
import {Component} from '@angular/core';
export interface Transaction {
item: string;
cost: number;
}
/**
* @title Footer row table
*/
@Component({
selector: 'table-footer-row-example',
styleUrls: ['table-footer-row-example.css'],
templateUrl: 'table-footer-row-example.html',
})
export class TableFooterRowExample {
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);
}
}