Skip to content

Commit 4eb9921

Browse files
committed
refactor(tree): avoid improper query usage
The way things are set up at the moment we have a `CdkNestedTreeNode` which uses a `ContentChildren` to find a `CdkTreeNodeOutlet` so that it knows where to render its content. On top of this we have a `MatNestedTreeNode` which extends `CdkNestedTreeNode` and defines it's own query at the same property, but looking for a `MatTreeNodeOutlet`. With this setup it means that both `CdkNestedTreeNode` and `MatNestedTreeNode` are trying to write to the same `nodeOutlet` property and it's by pure coincidence that it works. This is very fragile and it isn't guaranteed to work in future versions of the framework. These changes fix the issue by pulling out all of the `CdkNestedTreeNode` functionality into an abstract base class that both `CdkTreeNodeOutlet` and `MatTreeNodeOutlet` extend and which enforces that there's only one query writing to the `nodeOutlet` property.
1 parent f9ebb26 commit 4eb9921

File tree

3 files changed

+96
-69
lines changed

3 files changed

+96
-69
lines changed

src/cdk/tree/nested-node.ts

Lines changed: 53 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -19,57 +19,20 @@ import {Observable} from 'rxjs';
1919
import {takeUntil} from 'rxjs/operators';
2020

2121
import {CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet} from './outlet';
22-
import {CdkTree, CdkTreeNode} from './tree';
22+
import {CdkTree, CdkTreeNode, CdkTreeNodeBase} from './tree';
2323
import {getTreeControlFunctionsMissingError} from './tree-errors';
2424

25-
/**
26-
* Nested node is a child of `<cdk-tree>`. It works with nested tree.
27-
* By using `cdk-nested-tree-node` component in tree node template, children of the parent node will
28-
* be added in the `cdkTreeNodeOutlet` in tree node template.
29-
* For example:
30-
* ```html
31-
* <cdk-nested-tree-node>
32-
* {{node.name}}
33-
* <ng-template cdkTreeNodeOutlet></ng-template>
34-
* </cdk-nested-tree-node>
35-
* ```
36-
* The children of node will be automatically added to `cdkTreeNodeOutlet`, the result dom will be
37-
* like this:
38-
* ```html
39-
* <cdk-nested-tree-node>
40-
* {{node.name}}
41-
* <cdk-nested-tree-node>{{child1.name}}</cdk-nested-tree-node>
42-
* <cdk-nested-tree-node>{{child2.name}}</cdk-nested-tree-node>
43-
* </cdk-nested-tree-node>
44-
* ```
45-
*/
46-
@Directive({
47-
selector: 'cdk-nested-tree-node',
48-
exportAs: 'cdkNestedTreeNode',
49-
host: {
50-
'[attr.aria-expanded]': 'isExpanded',
51-
'[attr.role]': 'role',
52-
'class': 'cdk-tree-node cdk-nested-tree-node',
53-
},
54-
providers: [
55-
{provide: CdkTreeNode, useExisting: CdkNestedTreeNode},
56-
{provide: CDK_TREE_NODE_OUTLET_NODE, useExisting: CdkNestedTreeNode}
57-
]
58-
})
59-
export class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContentInit, OnDestroy {
25+
/** Base class with the functionality of a `CdkNestedTreeNode`. */
26+
export abstract class CdkNestedTreeNodeBase<T> extends CdkTreeNodeBase<T> implements
27+
AfterContentInit, OnDestroy {
6028
/** Differ used to find the changes in the data provided by the data source. */
6129
private _dataDiffer: IterableDiffer<T>;
6230

6331
/** The children data dataNodes of current node. They will be placed in `CdkTreeNodeOutlet`. */
6432
protected _children: T[];
6533

6634
/** The children node placeholder. */
67-
@ContentChildren(CdkTreeNodeOutlet, {
68-
// We need to use `descendants: true`, because Ivy will no longer match
69-
// indirect descendants if it's left as false.
70-
descendants: true
71-
})
72-
nodeOutlet: QueryList<CdkTreeNodeOutlet>;
35+
abstract nodeOutlet: QueryList<CdkTreeNodeOutlet>;
7336

7437
constructor(protected _elementRef: ElementRef<HTMLElement>,
7538
protected _tree: CdkTree<T>,
@@ -133,3 +96,51 @@ export class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContent
13396
}
13497
}
13598
}
99+
100+
/**
101+
* Nested node is a child of `<cdk-tree>`. It works with nested tree.
102+
* By using `cdk-nested-tree-node` component in tree node template, children of the parent node will
103+
* be added in the `cdkTreeNodeOutlet` in tree node template.
104+
* For example:
105+
* ```html
106+
* <cdk-nested-tree-node>
107+
* {{node.name}}
108+
* <ng-template cdkTreeNodeOutlet></ng-template>
109+
* </cdk-nested-tree-node>
110+
* ```
111+
* The children of node will be automatically added to `cdkTreeNodeOutlet`, the result dom will be
112+
* like this:
113+
* ```html
114+
* <cdk-nested-tree-node>
115+
* {{node.name}}
116+
* <cdk-nested-tree-node>{{child1.name}}</cdk-nested-tree-node>
117+
* <cdk-nested-tree-node>{{child2.name}}</cdk-nested-tree-node>
118+
* </cdk-nested-tree-node>
119+
* ```
120+
*/
121+
@Directive({
122+
selector: 'cdk-nested-tree-node',
123+
exportAs: 'cdkNestedTreeNode',
124+
host: {
125+
'[attr.aria-expanded]': 'isExpanded',
126+
'[attr.role]': 'role',
127+
'class': 'cdk-tree-node cdk-nested-tree-node',
128+
},
129+
providers: [
130+
{provide: CdkTreeNode, useExisting: CdkNestedTreeNode},
131+
{provide: CDK_TREE_NODE_OUTLET_NODE, useExisting: CdkNestedTreeNode}
132+
]
133+
})
134+
export class CdkNestedTreeNode<T> extends CdkNestedTreeNodeBase<T> {
135+
/** The children node placeholder. */
136+
@ContentChildren(CdkTreeNodeOutlet, {
137+
// We need to use `descendants: true`, because Ivy will no longer match
138+
// indirect descendants if it's left as false.
139+
descendants: true
140+
})
141+
nodeOutlet: QueryList<CdkTreeNodeOutlet>;
142+
143+
constructor(elementRef: ElementRef<HTMLElement>, tree: CdkTree<T>, differs: IterableDiffers) {
144+
super(elementRef, tree, differs);
145+
}
146+
}

src/cdk/tree/tree.ts

Lines changed: 25 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -279,26 +279,13 @@ export class CdkTree<T> implements AfterContentChecked, CollectionViewer, OnDest
279279
}
280280
}
281281

282-
283-
/**
284-
* Tree node for CdkTree. It contains the data in the tree node.
285-
*/
286-
@Directive({
287-
selector: 'cdk-tree-node',
288-
exportAs: 'cdkTreeNode',
289-
host: {
290-
'[attr.aria-expanded]': 'isExpanded',
291-
'[attr.aria-level]': 'role === "treeitem" ? level : null',
292-
'[attr.role]': 'role',
293-
'class': 'cdk-tree-node',
294-
},
295-
})
296-
export class CdkTreeNode<T> implements FocusableOption, OnDestroy {
282+
/** Base class with the functionality of a `CdkTreeNode`. */
283+
export class CdkTreeNodeBase<T> implements FocusableOption, OnDestroy {
297284
/**
298285
* The most recently created `CdkTreeNode`. We save it in static variable so we can retrieve it
299286
* in `CdkTree` and set the data to it.
300287
*/
301-
static mostRecentTreeNode: CdkTreeNode<any> | null = null;
288+
static mostRecentTreeNode: CdkTreeNodeBase<any> | null = null;
302289

303290
/** Subject that emits when the component has been destroyed. */
304291
protected _destroyed = new Subject<void>();
@@ -333,14 +320,14 @@ export class CdkTreeNode<T> implements FocusableOption, OnDestroy {
333320

334321
constructor(protected _elementRef: ElementRef<HTMLElement>,
335322
protected _tree: CdkTree<T>) {
336-
CdkTreeNode.mostRecentTreeNode = this as CdkTreeNode<T>;
323+
CdkTreeNodeBase.mostRecentTreeNode = this as CdkTreeNodeBase<T>;
337324
}
338325

339326
ngOnDestroy() {
340327
// If this is the last tree node being destroyed,
341328
// clear out the reference to avoid leaking memory.
342-
if (CdkTreeNode.mostRecentTreeNode === this) {
343-
CdkTreeNode.mostRecentTreeNode = null;
329+
if (CdkTreeNodeBase.mostRecentTreeNode === this) {
330+
CdkTreeNodeBase.mostRecentTreeNode = null;
344331
}
345332

346333
this._dataChanges.complete();
@@ -374,3 +361,22 @@ export class CdkTreeNode<T> implements FocusableOption, OnDestroy {
374361
this.role = children && children.length ? 'group' : 'treeitem';
375362
}
376363
}
364+
365+
/**
366+
* Tree node for CdkTree. It contains the data in the tree node.
367+
*/
368+
@Directive({
369+
selector: 'cdk-tree-node',
370+
exportAs: 'cdkTreeNode',
371+
host: {
372+
'[attr.aria-expanded]': 'isExpanded',
373+
'[attr.aria-level]': 'role === "treeitem" ? level : null',
374+
'[attr.role]': 'role',
375+
'class': 'cdk-tree-node',
376+
},
377+
})
378+
export class CdkTreeNode<T> extends CdkTreeNodeBase<T> {
379+
constructor(elementRef: ElementRef<HTMLElement>, tree: CdkTree<T>) {
380+
super(elementRef, tree);
381+
}
382+
}

src/material/tree/node.ts

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
CdkTree,
1313
CdkTreeNode,
1414
CdkTreeNodeDef,
15+
CdkNestedTreeNodeBase,
1516
} from '@angular/cdk/tree';
1617
import {
1718
AfterContentInit,
@@ -32,16 +33,13 @@ import {
3233
mixinDisabled,
3334
mixinTabIndex,
3435
} from '@angular/material/core';
36+
import {coerceBooleanProperty} from '@angular/cdk/coercion';
3537

3638
import {MatTreeNodeOutlet} from './outlet';
3739

3840
const _MatTreeNodeMixinBase: HasTabIndexCtor & CanDisableCtor & typeof CdkTreeNode =
3941
mixinTabIndex(mixinDisabled(CdkTreeNode));
4042

41-
const _MatNestedTreeNodeMixinBase:
42-
HasTabIndexCtor & CanDisableCtor & typeof CdkNestedTreeNode =
43-
mixinTabIndex(mixinDisabled(CdkNestedTreeNode));
44-
4543
/**
4644
* Wrapper for the CdkTree node with Material design styles.
4745
*/
@@ -95,15 +93,14 @@ export class MatTreeNodeDef<T> extends CdkTreeNodeDef<T> {
9593
'[attr.role]': 'role',
9694
'class': 'mat-nested-tree-node',
9795
},
98-
inputs: ['disabled', 'tabIndex'],
9996
providers: [
10097
{provide: CdkNestedTreeNode, useExisting: MatNestedTreeNode},
10198
{provide: CdkTreeNode, useExisting: MatNestedTreeNode},
10299
{provide: CDK_TREE_NODE_OUTLET_NODE, useExisting: MatNestedTreeNode}
103100
]
104101
})
105-
export class MatNestedTreeNode<T> extends _MatNestedTreeNodeMixinBase<T> implements
106-
AfterContentInit, CanDisable, HasTabIndex, OnDestroy {
102+
export class MatNestedTreeNode<T> extends CdkNestedTreeNodeBase<T> implements AfterContentInit,
103+
OnDestroy {
107104
@Input('matNestedTreeNode') node: T;
108105

109106
/** The children node placeholder. */
@@ -114,12 +111,25 @@ export class MatNestedTreeNode<T> extends _MatNestedTreeNodeMixinBase<T> impleme
114111
})
115112
nodeOutlet: QueryList<MatTreeNodeOutlet>;
116113

114+
/** Whether the tree node is disabled. */
115+
@Input()
116+
get disabled() { return this._disabled; }
117+
set disabled(value: any) { this._disabled = coerceBooleanProperty(value); }
118+
private _disabled = false;
119+
120+
/** Tabindex for the tree node. */
121+
@Input()
122+
get tabIndex(): number { return this.disabled ? -1 : this._tabIndex; }
123+
set tabIndex(value: number) {
124+
this._tabIndex = value != null ? value : 0;
125+
}
126+
private _tabIndex: number;
127+
117128
constructor(protected _elementRef: ElementRef<HTMLElement>,
118129
protected _tree: CdkTree<T>,
119130
protected _differs: IterableDiffers,
120131
@Attribute('tabindex') tabIndex: string) {
121132
super(_elementRef, _tree, _differs);
122-
123133
this.tabIndex = Number(tabIndex) || 0;
124134
}
125135

0 commit comments

Comments
 (0)