Skip to content

Commit 641edc3

Browse files
andrewseguinjelbourn
authored andcommitted
feat(table): allow multiple header/footer rows (#11245)
1 parent d26735c commit 641edc3

File tree

7 files changed

+404
-108
lines changed

7 files changed

+404
-108
lines changed

src/cdk/table/table-errors.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ export function getTableMissingMatchingRowDefError(data: any) {
4545
* @docs-private
4646
*/
4747
export function getTableMissingRowDefsError() {
48-
return Error('Missing definitions for header and row, ' +
48+
return Error('Missing definitions for header, footer, and row; ' +
4949
'cannot determine which columns should be rendered.');
5050
}
5151

src/cdk/table/table.spec.ts

Lines changed: 137 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -69,11 +69,11 @@ describe('CdkTable', () => {
6969
});
7070

7171
it('with a rendered header with the right number of header cells', () => {
72-
const header = getHeaderRow(tableElement);
72+
const header = getHeaderRows(tableElement)[0];
7373

7474
expect(header).toBeTruthy();
7575
expect(header.classList).toContain('customHeaderRowClass');
76-
expect(getHeaderCells(tableElement).length).toBe(component.columnsToRender.length);
76+
expect(getHeaderCells(header).length).toBe(component.columnsToRender.length);
7777
});
7878

7979
it('with rendered rows with right number of row cells', () => {
@@ -87,7 +87,8 @@ describe('CdkTable', () => {
8787
});
8888

8989
it('with column class names provided to header and data row cells', () => {
90-
getHeaderCells(tableElement).forEach((headerCell, index) => {
90+
const header = getHeaderRows(tableElement)[0];
91+
getHeaderCells(header).forEach((headerCell, index) => {
9192
expect(headerCell.classList).toContain(`cdk-column-${component.columnsToRender[index]}`);
9293
});
9394

@@ -101,8 +102,9 @@ describe('CdkTable', () => {
101102
it('with the right accessibility roles', () => {
102103
expect(tableElement.getAttribute('role')).toBe('grid');
103104

104-
expect(getHeaderRow(tableElement).getAttribute('role')).toBe('row');
105-
getHeaderCells(tableElement).forEach(cell => {
105+
expect(getHeaderRows(tableElement)[0].getAttribute('role')).toBe('row');
106+
const header = getHeaderRows(tableElement)[0];
107+
getHeaderCells(header).forEach(cell => {
106108
expect(cell.getAttribute('role')).toBe('columnheader');
107109
});
108110

@@ -279,6 +281,40 @@ describe('CdkTable', () => {
279281
expect(getRows(tableElement).length).toBe(0);
280282
}));
281283

284+
it('should be able to render multiple header and footer rows', () => {
285+
setupTableTestApp(MultipleHeaderFooterRowsCdkTableApp);
286+
fixture.detectChanges();
287+
288+
expectTableToMatchContent(tableElement, [
289+
['first-header'],
290+
['second-header'],
291+
['first-footer'],
292+
['second-footer'],
293+
]);
294+
});
295+
296+
it('should be able to render and change multiple header and footer rows', () => {
297+
setupTableTestApp(MultipleHeaderFooterRowsCdkTableApp);
298+
fixture.detectChanges();
299+
300+
expectTableToMatchContent(tableElement, [
301+
['first-header'],
302+
['second-header'],
303+
['first-footer'],
304+
['second-footer'],
305+
]);
306+
307+
component.showAlternativeHeadersAndFooters = true;
308+
fixture.detectChanges();
309+
310+
expectTableToMatchContent(tableElement, [
311+
['first-header'],
312+
['second-header'],
313+
['first-footer'],
314+
['second-footer'],
315+
]);
316+
});
317+
282318
describe('with different data inputs other than data source', () => {
283319
let baseData: TestData[] = [
284320
{a: 'a_1', b: 'b_1', c: 'c_1'},
@@ -460,7 +496,8 @@ describe('CdkTable', () => {
460496
it('should be able to apply class-friendly css class names for the column cells', () => {
461497
setupTableTestApp(CrazyColumnNameCdkTableApp);
462498
// Column was named 'crazy-column-NAME-1!@#$%^-_&*()2'
463-
expect(getHeaderCells(tableElement)[0].classList)
499+
const header = getHeaderRows(tableElement)[0];
500+
expect(getHeaderCells(header)[0].classList)
464501
.toContain('cdk-column-crazy-column-NAME-1-------_----2');
465502
});
466503

@@ -488,7 +525,7 @@ describe('CdkTable', () => {
488525
setupTableTestApp(UndefinedColumnsCdkTableApp);
489526

490527
// Header should be empty since there are no columns to display.
491-
const headerRow = getHeaderRow(tableElement);
528+
const headerRow = getHeaderRows(tableElement)[0];
492529
expect(headerRow.textContent).toBe('');
493530

494531
// Rows should be empty since there are no columns to display.
@@ -657,6 +694,8 @@ describe('CdkTable', () => {
657694
});
658695
});
659696

697+
698+
660699
describe('with trackBy', () => {
661700
function createTestComponentWithTrackyByTable(trackByStrategy) {
662701
fixture = createComponent(TrackByCdkTableApp);
@@ -1053,6 +1092,62 @@ class NullDataCdkTableApp {
10531092
dataSource = observableOf(null);
10541093
}
10551094

1095+
1096+
@Component({
1097+
template: `
1098+
<cdk-table [dataSource]="[]">
1099+
<ng-container cdkColumnDef="first-header">
1100+
<th cdk-header-cell *cdkHeaderCellDef> first-header </th>
1101+
</ng-container>
1102+
1103+
<ng-container cdkColumnDef="second-header">
1104+
<th cdk-header-cell *cdkHeaderCellDef> second-header </th>
1105+
</ng-container>
1106+
1107+
<ng-container cdkColumnDef="first-footer">
1108+
<td cdk-footer-cell *cdkFooterCellDef> first-footer </td>
1109+
</ng-container>
1110+
1111+
<ng-container cdkColumnDef="second-footer">
1112+
<td cdk-footer-cell *cdkFooterCellDef> second-footer </td>
1113+
</ng-container>
1114+
1115+
<ng-container *ngIf="!showAlternativeHeadersAndFooters">
1116+
<tr cdk-header-row *cdkHeaderRowDef="['first-header']"></tr>
1117+
<tr cdk-header-row *cdkHeaderRowDef="['second-header']"></tr>
1118+
<tr cdk-footer-row *cdkFooterRowDef="['first-footer']"></tr>
1119+
<tr cdk-footer-row *cdkFooterRowDef="['second-footer']"></tr>
1120+
</ng-container>
1121+
1122+
<ng-container cdkColumnDef="alt-first-header">
1123+
<th cdk-header-cell *cdkHeaderCellDef> alt-first-header </th>
1124+
</ng-container>
1125+
1126+
<ng-container cdkColumnDef="alt-second-header">
1127+
<th cdk-header-cell *cdkHeaderCellDef> alt-second-header </th>
1128+
</ng-container>
1129+
1130+
<ng-container cdkColumnDef="alt-first-footer">
1131+
<td cdk-footer-cell *cdkFooterCellDef> alt-first-footer </td>
1132+
</ng-container>
1133+
1134+
<ng-container cdkColumnDef="alt-second-footer">
1135+
<td cdk-footer-cell *cdkFooterCellDef> alt-second-footer </td>
1136+
</ng-container>
1137+
1138+
<ng-container *ngIf="showAlternativeHeadersAndFooters">
1139+
<tr cdk-header-row *cdkHeaderRowDef="['alt-first-header']"></tr>
1140+
<tr cdk-header-row *cdkHeaderRowDef="['alt-second-header']"></tr>
1141+
<tr cdk-footer-row *cdkFooterRowDef="['alt-first-footer']"></tr>
1142+
<tr cdk-footer-row *cdkFooterRowDef="['alt-second-footer']"></tr>
1143+
</ng-container>
1144+
</cdk-table>
1145+
`
1146+
})
1147+
class MultipleHeaderFooterRowsCdkTableApp {
1148+
showAlternativeHeadersAndFooters = false;
1149+
}
1150+
10561151
@Component({
10571152
template: `
10581153
<cdk-table [dataSource]="dataSource" [multiTemplateDataRows]="multiTemplateDataRows">
@@ -1586,42 +1681,62 @@ function getElements(element: Element, query: string): Element[] {
15861681
return [].slice.call(element.querySelectorAll(query));
15871682
}
15881683

1589-
function getHeaderRow(tableElement: Element): Element {
1590-
return tableElement.querySelector('.cdk-header-row')!;
1684+
function getHeaderRows(tableElement: Element): Element[] {
1685+
return [].slice.call(tableElement.querySelectorAll('.cdk-header-row'))!;
15911686
}
15921687

1593-
function getFooterRow(tableElement: Element): Element {
1594-
return tableElement.querySelector('.cdk-footer-row')!;
1688+
function getFooterRows(tableElement: Element): Element[] {
1689+
return [].slice.call(tableElement.querySelectorAll('.cdk-footer-row'))!;
15951690
}
15961691

15971692
function getRows(tableElement: Element): Element[] {
15981693
return getElements(tableElement, '.cdk-row');
15991694
}
1695+
16001696
function getCells(row: Element): Element[] {
1601-
return row ? getElements(row, '.cdk-cell') : [];
1697+
if (!row) {
1698+
return [];
1699+
}
1700+
1701+
let cells = getElements(row, 'cdk-cell');
1702+
if (!cells.length) {
1703+
cells = getElements(row, 'td');
1704+
}
1705+
1706+
return cells;
16021707
}
16031708

1604-
function getHeaderCells(tableElement: Element): Element[] {
1605-
return getElements(getHeaderRow(tableElement), '.cdk-header-cell');
1709+
function getHeaderCells(headerRow: Element): Element[] {
1710+
let cells = getElements(headerRow, 'cdk-header-cell');
1711+
if (!cells.length) {
1712+
cells = getElements(headerRow, 'th');
1713+
}
1714+
1715+
return cells;
16061716
}
16071717

1608-
function getFooterCells(tableElement: Element): Element[] {
1609-
return getElements(getFooterRow(tableElement), '.cdk-footer-cell');
1718+
function getFooterCells(footerRow: Element): Element[] {
1719+
let cells = getElements(footerRow, 'cdk-footer-cell');
1720+
if (!cells.length) {
1721+
cells = getElements(footerRow, 'td');
1722+
}
1723+
1724+
return cells;
16101725
}
16111726

16121727
function getActualTableContent(tableElement: Element): string[][] {
16131728
let actualTableContent: Element[][] = [];
1614-
if (getHeaderRow(tableElement)) {
1615-
actualTableContent.push(getHeaderCells(tableElement));
1616-
}
1729+
getHeaderRows(tableElement).forEach(row => {
1730+
actualTableContent.push(getHeaderCells(row));
1731+
});
16171732

16181733
// Check data row cells
16191734
const rows = getRows(tableElement).map(row => getCells(row));
16201735
actualTableContent = actualTableContent.concat(rows);
16211736

1622-
if (getFooterRow(tableElement)) {
1623-
actualTableContent.push(getFooterCells(tableElement));
1624-
}
1737+
getFooterRows(tableElement).forEach(row => {
1738+
actualTableContent.push(getFooterCells(row));
1739+
});
16251740

16261741
// Convert the nodes into their text content;
16271742
return actualTableContent.map(row => row.map(cell => cell.textContent!.trim()));

0 commit comments

Comments
 (0)