mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
feat(table): allow multiple header/footer rows (#11245)
This commit is contained in:
committed by
Jeremy Elbourn
parent
d26735ccad
commit
641edc3d51
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
+19
@@ -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;
|
||||
}
|
||||
+45
@@ -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>
|
||||
+32
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user