Skip to content

Commit 32aec93

Browse files
authored
fix(table): error when nesting tables (#18832)
Previously we used to support nesting tables, but in v9 we had to make some changes in order to handle all cases in Ivy. As a result, nesting was broken due to parent tables picking up the cell definitions of their children. These changes add some logic to account for tables being nested. Fixes #18768.
1 parent 7c75d6e commit 32aec93

File tree

12 files changed

+296
-42
lines changed

12 files changed

+296
-42
lines changed

src/cdk/table/cell.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,17 @@
77
*/
88

99
import {BooleanInput, coerceBooleanProperty} from '@angular/cdk/coercion';
10-
import {ContentChild, Directive, ElementRef, Input, TemplateRef} from '@angular/core';
10+
import {
11+
ContentChild,
12+
Directive,
13+
ElementRef,
14+
Input,
15+
TemplateRef,
16+
Inject,
17+
Optional,
18+
} from '@angular/core';
1119
import {CanStick, CanStickCtor, mixinHasStickyInput} from './can-stick';
20+
import {CDK_TABLE} from './tokens';
1221

1322

1423
/** Base interface for a cell definition. Captures a column's cell template definition. */
@@ -67,12 +76,10 @@ export class CdkColumnDef extends _CdkColumnDefBase implements CanStick {
6776
set name(name: string) {
6877
// If the directive is set without a name (updated programatically), then this setter will
6978
// trigger with an empty string and should not overwrite the programatically set value.
70-
if (!name) {
71-
return;
79+
if (name) {
80+
this._name = name;
81+
this.cssClassFriendlyName = name.replace(/[^a-z0-9_-]/ig, '-');
7282
}
73-
74-
this._name = name;
75-
this.cssClassFriendlyName = name.replace(/[^a-z0-9_-]/ig, '-');
7683
}
7784
_name: string;
7885

@@ -108,6 +115,10 @@ export class CdkColumnDef extends _CdkColumnDefBase implements CanStick {
108115
*/
109116
cssClassFriendlyName: string;
110117

118+
constructor(@Inject(CDK_TABLE) @Optional() public _table?: any) {
119+
super();
120+
}
121+
111122
static ngAcceptInputType_sticky: BooleanInput;
112123
static ngAcceptInputType_stickyEnd: BooleanInput;
113124
}

src/cdk/table/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ export * from './table-module';
1313
export * from './sticky-styler';
1414
export * from './can-stick';
1515
export * from './text-column';
16+
export * from './tokens';
1617

1718
/** Re-export DataSource for a more intuitive experience for users of just the table. */
1819
export {DataSource} from '@angular/cdk/collections';

src/cdk/table/row.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,13 @@ import {
1919
SimpleChanges,
2020
TemplateRef,
2121
ViewContainerRef,
22-
ViewEncapsulation
22+
ViewEncapsulation,
23+
Inject,
24+
Optional
2325
} from '@angular/core';
2426
import {CanStick, CanStickCtor, mixinHasStickyInput} from './can-stick';
2527
import {CdkCellDef, CdkColumnDef} from './cell';
28+
import {CDK_TABLE} from './tokens';
2629

2730
/**
2831
* The row template that can be used by the mat-table. Should not be used outside of the
@@ -91,7 +94,10 @@ const _CdkHeaderRowDefBase: CanStickCtor&typeof CdkHeaderRowDefBase =
9194
inputs: ['columns: cdkHeaderRowDef', 'sticky: cdkHeaderRowDefSticky'],
9295
})
9396
export class CdkHeaderRowDef extends _CdkHeaderRowDefBase implements CanStick, OnChanges {
94-
constructor(template: TemplateRef<any>, _differs: IterableDiffers) {
97+
constructor(
98+
template: TemplateRef<any>,
99+
_differs: IterableDiffers,
100+
@Inject(CDK_TABLE) @Optional() public _table?: any) {
95101
super(template, _differs);
96102
}
97103

@@ -119,7 +125,10 @@ const _CdkFooterRowDefBase: CanStickCtor&typeof CdkFooterRowDefBase =
119125
inputs: ['columns: cdkFooterRowDef', 'sticky: cdkFooterRowDefSticky'],
120126
})
121127
export class CdkFooterRowDef extends _CdkFooterRowDefBase implements CanStick, OnChanges {
122-
constructor(template: TemplateRef<any>, _differs: IterableDiffers) {
128+
constructor(
129+
template: TemplateRef<any>,
130+
_differs: IterableDiffers,
131+
@Inject(CDK_TABLE) @Optional() public _table?: any) {
123132
super(template, _differs);
124133
}
125134

@@ -152,7 +161,10 @@ export class CdkRowDef<T> extends BaseRowDef {
152161

153162
// TODO(andrewseguin): Add an input for providing a switch function to determine
154163
// if this template should be used.
155-
constructor(template: TemplateRef<any>, _differs: IterableDiffers) {
164+
constructor(
165+
template: TemplateRef<any>,
166+
_differs: IterableDiffers,
167+
@Inject(CDK_TABLE) @Optional() public _table?: any) {
156168
super(template, _differs);
157169
}
158170
}

src/cdk/table/table.spec.ts

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,21 @@ describe('CdkTable', () => {
482482
]);
483483
});
484484

485+
it('should be able to nest tables', () => {
486+
const thisFixture = createComponent(NestedHtmlTableApp);
487+
thisFixture.detectChanges();
488+
const outerTable = thisFixture.nativeElement.querySelector('table');
489+
const innerTable = outerTable.querySelector('table');
490+
const outerRows = Array.from<HTMLTableRowElement>(outerTable.querySelector('tbody').rows);
491+
const innerRows = Array.from<HTMLTableRowElement>(innerTable.querySelector('tbody').rows);
492+
493+
expect(outerTable).toBeTruthy();
494+
expect(outerRows.map(row => row.cells.length)).toEqual([3, 3, 3]);
495+
496+
expect(innerTable).toBeTruthy();
497+
expect(innerRows.map(row => row.cells.length)).toEqual([3, 3, 3]);
498+
});
499+
485500
it('should apply correct roles for native table elements', () => {
486501
const thisFixture = createComponent(NativeHtmlTableApp);
487502
const thisTableElement: HTMLTableElement = thisFixture.nativeElement.querySelector('table');
@@ -2276,6 +2291,55 @@ class NativeHtmlTableApp {
22762291
@ViewChild(CdkTable) table: CdkTable<TestData>;
22772292
}
22782293

2294+
2295+
@Component({
2296+
template: `
2297+
<table cdk-table [dataSource]="dataSource">
2298+
<ng-container cdkColumnDef="column_a">
2299+
<th cdk-header-cell *cdkHeaderCellDef> Column A</th>
2300+
<td cdk-cell *cdkCellDef="let row">{{row.a}}</td>
2301+
</ng-container>
2302+
2303+
<ng-container cdkColumnDef="column_b">
2304+
<th cdk-header-cell *cdkHeaderCellDef> Column B</th>
2305+
<td cdk-cell *cdkCellDef="let row">
2306+
<table cdk-table [dataSource]="dataSource">
2307+
<ng-container cdkColumnDef="column_a">
2308+
<th cdk-header-cell *cdkHeaderCellDef> Column A</th>
2309+
<td cdk-cell *cdkCellDef="let row"> {{row.a}}</td>
2310+
</ng-container>
2311+
2312+
<ng-container cdkColumnDef="column_b">
2313+
<th cdk-header-cell *cdkHeaderCellDef> Column B</th>
2314+
<td cdk-cell *cdkCellDef="let row"> {{row.b}}</td>
2315+
</ng-container>
2316+
2317+
<ng-container cdkColumnDef="column_c">
2318+
<th cdk-header-cell *cdkHeaderCellDef> Column C</th>
2319+
<td cdk-cell *cdkCellDef="let row"> {{row.c}}</td>
2320+
</ng-container>
2321+
2322+
<tr cdk-header-row *cdkHeaderRowDef="columnsToRender"></tr>
2323+
<tr cdk-row *cdkRowDef="let row; columns: columnsToRender" class="customRowClass"></tr>
2324+
</table>
2325+
</td>
2326+
</ng-container>
2327+
2328+
<ng-container cdkColumnDef="column_c">
2329+
<th cdk-header-cell *cdkHeaderCellDef> Column C</th>
2330+
<td cdk-cell *cdkCellDef="let row">{{row.c}}</td>
2331+
</ng-container>
2332+
2333+
<tr cdk-header-row *cdkHeaderRowDef="columnsToRender"></tr>
2334+
<tr cdk-row *cdkRowDef="let row; columns: columnsToRender" class="customRowClass"></tr>
2335+
</table>
2336+
`
2337+
})
2338+
class NestedHtmlTableApp {
2339+
dataSource: FakeDataSource | undefined = new FakeDataSource();
2340+
columnsToRender = ['column_a', 'column_b', 'column_c'];
2341+
}
2342+
22792343
@Component({
22802344
template: `
22812345
<table cdk-table [dataSource]="dataSource">

src/cdk/table/table.ts

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ import {
6565
getTableUnknownColumnError,
6666
getTableUnknownDataSourceError
6767
} from './table-errors';
68+
import {CDK_TABLE} from './tokens';
6869

6970
/** Interface used to provide an outlet for rows to be inserted into. */
7071
export interface RowOutlet {
@@ -171,6 +172,7 @@ export interface RenderRow<T> {
171172
// declared elsewhere, they are checked when their declaration points are checked.
172173
// tslint:disable-next-line:validate-decorators
173174
changeDetection: ChangeDetectionStrategy.Default,
175+
providers: [{provide: CDK_TABLE, useExisting: CdkTable}]
174176
})
175177
export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit {
176178
private _document: Document;
@@ -752,7 +754,8 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
752754
private _cacheColumnDefs() {
753755
this._columnDefsByName.clear();
754756

755-
const columnDefs = mergeQueryListAndSet(this._contentColumnDefs, this._customColumnDefs);
757+
const columnDefs = mergeArrayAndSet(
758+
this._getOwnDefs(this._contentColumnDefs), this._customColumnDefs);
756759
columnDefs.forEach(columnDef => {
757760
if (this._columnDefsByName.has(columnDef.name)) {
758761
throw getTableDuplicateColumnNameError(columnDef.name);
@@ -763,11 +766,12 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
763766

764767
/** Update the list of all available row definitions that can be used. */
765768
private _cacheRowDefs() {
766-
this._headerRowDefs =
767-
mergeQueryListAndSet(this._contentHeaderRowDefs, this._customHeaderRowDefs);
768-
this._footerRowDefs =
769-
mergeQueryListAndSet(this._contentFooterRowDefs, this._customFooterRowDefs);
770-
this._rowDefs = mergeQueryListAndSet(this._contentRowDefs, this._customRowDefs);
769+
this._headerRowDefs = mergeArrayAndSet(
770+
this._getOwnDefs(this._contentHeaderRowDefs), this._customHeaderRowDefs);
771+
this._footerRowDefs = mergeArrayAndSet(
772+
this._getOwnDefs(this._contentFooterRowDefs), this._customFooterRowDefs);
773+
this._rowDefs = mergeArrayAndSet(
774+
this._getOwnDefs(this._contentRowDefs), this._customRowDefs);
771775

772776
// After all row definitions are determined, find the row definition to be considered default.
773777
const defaultRowDefs = this._rowDefs.filter(def => !def.when);
@@ -1084,10 +1088,15 @@ export class CdkTable<T> implements AfterContentChecked, CollectionViewer, OnDes
10841088
});
10851089
}
10861090

1091+
/** Filters definitions that belong to this table from a QueryList. */
1092+
private _getOwnDefs<I extends {_table?: any}>(items: QueryList<I>): I[] {
1093+
return items.filter(item => !item._table || item._table === this);
1094+
}
1095+
10871096
static ngAcceptInputType_multiTemplateDataRows: BooleanInput;
10881097
}
10891098

1090-
/** Utility function that gets a merged list of the entries in a QueryList and values of a Set. */
1091-
function mergeQueryListAndSet<T>(queryList: QueryList<T>, set: Set<T>): T[] {
1092-
return queryList.toArray().concat(Array.from(set));
1099+
/** Utility function that gets a merged list of the entries in an array and values of a Set. */
1100+
function mergeArrayAndSet<T>(array: T[], set: Set<T>): T[] {
1101+
return array.concat(Array.from(set));
10931102
}

src/cdk/table/text-column.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
} from './table-errors';
88
import {CdkTableModule} from './table-module';
99
import {expectTableToMatchContent} from './table.spec';
10-
import {TEXT_COLUMN_OPTIONS, TextColumnOptions} from './text-column';
10+
import {TEXT_COLUMN_OPTIONS, TextColumnOptions} from './tokens';
1111

1212

1313
describe('CdkTextColumn', () => {

src/cdk/table/text-column.ts

Lines changed: 1 addition & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import {
1010
ChangeDetectionStrategy,
1111
Component,
1212
Inject,
13-
InjectionToken,
1413
Input,
1514
OnDestroy,
1615
OnInit,
@@ -25,24 +24,9 @@ import {
2524
getTableTextColumnMissingParentTableError,
2625
getTableTextColumnMissingNameError,
2726
} from './table-errors';
27+
import {TEXT_COLUMN_OPTIONS, TextColumnOptions} from './tokens';
2828

2929

30-
/** Configurable options for `CdkTextColumn`. */
31-
export interface TextColumnOptions<T> {
32-
/**
33-
* Default function that provides the header text based on the column name if a header
34-
* text is not provided.
35-
*/
36-
defaultHeaderTextTransform?: (name: string) => string;
37-
38-
/** Default data accessor to use if one is not provided. */
39-
defaultDataAccessor?: (data: T, name: string) => string;
40-
}
41-
42-
/** Injection token that can be used to specify the text column options. */
43-
export const TEXT_COLUMN_OPTIONS =
44-
new InjectionToken<TextColumnOptions<any>>('text-column-options');
45-
4630
/**
4731
* Column that simply shows text content for the header and row cells. Assumes that the table
4832
* is using the native table implementation (`<table>`).

src/cdk/table/tokens.ts

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.io/license
7+
*/
8+
9+
import {InjectionToken} from '@angular/core';
10+
11+
/**
12+
* Used to provide a table to some of the sub-components without causing a circular dependency.
13+
* @docs-private
14+
*/
15+
export const CDK_TABLE = new InjectionToken<any>('CDK_TABLE');
16+
17+
/** Configurable options for `CdkTextColumn`. */
18+
export interface TextColumnOptions<T> {
19+
/**
20+
* Default function that provides the header text based on the column name if a header
21+
* text is not provided.
22+
*/
23+
defaultHeaderTextTransform?: (name: string) => string;
24+
25+
/** Default data accessor to use if one is not provided. */
26+
defaultDataAccessor?: (data: T, name: string) => string;
27+
}
28+
29+
/** Injection token that can be used to specify the text column options. */
30+
export const TEXT_COLUMN_OPTIONS =
31+
new InjectionToken<TextColumnOptions<any>>('text-column-options');

0 commit comments

Comments
 (0)