Skip to content

Commit e651262

Browse files
committed
refactor(tree): rework to account for ivy changes
Reworks the `MatTree` and `CdkTree` components to account for some of the changes in Ivy. This includes: * Changing the padding directive to account for a different time at which static inputs are initialized. * Changing the toggle directive to account for inheritance of host listeners working differently. * Switching to `Default` change detection, because the way embedded views are checked is slightly different. * Using `descendants: true` to find the nested node outlet in order to handle a slight difference in how indirect content children are matched.
1 parent 3b0f7fc commit e651262

File tree

9 files changed

+115
-44
lines changed

9 files changed

+115
-44
lines changed

src/cdk/tree/nested-node.ts

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,16 @@ import {
1010
ContentChildren,
1111
Directive,
1212
ElementRef,
13-
IterableDiffers,
1413
IterableDiffer,
14+
IterableDiffers,
1515
OnDestroy,
1616
QueryList,
1717
} from '@angular/core';
1818
import {Observable} from 'rxjs';
1919
import {takeUntil} from 'rxjs/operators';
2020

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

2525
/**
@@ -51,7 +51,10 @@ import {getTreeControlFunctionsMissingError} from './tree-errors';
5151
'[attr.role]': 'role',
5252
'class': 'cdk-tree-node cdk-nested-tree-node',
5353
},
54-
providers: [{provide: CdkTreeNode, useExisting: CdkNestedTreeNode}]
54+
providers: [
55+
{provide: CdkTreeNode, useExisting: CdkNestedTreeNode},
56+
{provide: CDK_TREE_NODE_OUTLET_NODE, useExisting: CdkNestedTreeNode}
57+
]
5558
})
5659
export class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContentInit, OnDestroy {
5760
/** Differ used to find the changes in the data provided by the data source. */
@@ -61,7 +64,12 @@ export class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContent
6164
protected _children: T[];
6265

6366
/** The children node placeholder. */
64-
@ContentChildren(CdkTreeNodeOutlet) nodeOutlet: QueryList<CdkTreeNodeOutlet>;
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>;
6573

6674
constructor(protected _elementRef: ElementRef<HTMLElement>,
6775
protected _tree: CdkTree<T>,
@@ -92,11 +100,12 @@ export class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContent
92100

93101
/** Add children dataNodes to the NodeOutlet */
94102
protected updateChildrenNodes(children?: T[]): void {
103+
const outlet = this._getNodeOutlet();
95104
if (children) {
96105
this._children = children;
97106
}
98-
if (this.nodeOutlet.length && this._children) {
99-
const viewContainer = this.nodeOutlet.first.viewContainer;
107+
if (outlet && this._children) {
108+
const viewContainer = outlet.viewContainer;
100109
this._tree.renderNodeChanges(this._children, this._dataDiffer, viewContainer, this._data);
101110
} else {
102111
// Reset the data differ if there's no children nodes displayed
@@ -106,9 +115,21 @@ export class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContent
106115

107116
/** Clear the children dataNodes. */
108117
protected _clear(): void {
109-
if (this.nodeOutlet && this.nodeOutlet.first) {
110-
this.nodeOutlet.first.viewContainer.clear();
118+
const outlet = this._getNodeOutlet();
119+
if (outlet) {
120+
outlet.viewContainer.clear();
111121
this._dataDiffer.diff([]);
112122
}
113123
}
124+
125+
/** Gets the outlet for the current node. */
126+
private _getNodeOutlet() {
127+
const outlets = this.nodeOutlet;
128+
129+
if (outlets) {
130+
// Note that since we use `descendants: true` on the query, we have to ensure
131+
// that we don't pick up the outlet of a child node by accident.
132+
return outlets.find(outlet => !outlet._node || outlet._node === this);
133+
}
134+
}
114135
}

src/cdk/tree/outlet.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,19 @@
77
*/
88
import {
99
Directive,
10+
Inject,
11+
InjectionToken,
12+
Optional,
1013
ViewContainerRef,
1114
} from '@angular/core';
1215

16+
/**
17+
* Injection token used to provide a `CdkTreeNode` to its outlet.
18+
* Used primarily to avoid circular imports.
19+
* @docs-private
20+
*/
21+
export const CDK_TREE_NODE_OUTLET_NODE = new InjectionToken<{}>('CDK_TREE_NODE_OUTLET_NODE');
22+
1323
/**
1424
* Outlet for nested CdkNode. Put `[cdkTreeNodeOutlet]` on a tag to place children dataNodes
1525
* inside the outlet.
@@ -18,5 +28,7 @@ import {
1828
selector: '[cdkTreeNodeOutlet]'
1929
})
2030
export class CdkTreeNodeOutlet {
21-
constructor(public viewContainer: ViewContainerRef) {}
31+
constructor(
32+
public viewContainer: ViewContainerRef,
33+
@Inject(CDK_TREE_NODE_OUTLET_NODE) @Optional() public _node?: any) {}
2234
}

src/cdk/tree/padding.ts

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ const cssUnitPattern = /([A-Za-z%]+)$/;
2424
selector: '[cdkTreeNodePadding]',
2525
})
2626
export class CdkTreeNodePadding<T> implements OnDestroy {
27+
/** Current padding value applied to the element. Used to avoid unnecessarily hitting the DOM. */
28+
private _currentPadding: string|null;
29+
2730
/** Subject that emits when the component has been destroyed. */
2831
private _destroyed = new Subject<void>();
2932

@@ -68,8 +71,13 @@ export class CdkTreeNodePadding<T> implements OnDestroy {
6871
@Optional() private _dir: Directionality) {
6972
this._setPadding();
7073
if (_dir) {
71-
_dir.change.pipe(takeUntil(this._destroyed)).subscribe(() => this._setPadding());
74+
_dir.change.pipe(takeUntil(this._destroyed)).subscribe(() => this._setPadding(true));
7275
}
76+
77+
// In Ivy the indentation binding might be set before the tree node's data has been added,
78+
// which means that we'll miss the first render. We have to subscribe to changes in the
79+
// data to ensure that everything is up to date.
80+
_treeNode._dataChanges.subscribe(() => this._setPadding());
7381
}
7482

7583
ngOnDestroy() {
@@ -86,13 +94,16 @@ export class CdkTreeNodePadding<T> implements OnDestroy {
8694
return level ? `${level * this._indent}${this.indentUnits}` : null;
8795
}
8896

89-
_setPadding() {
90-
const element = this._element.nativeElement;
97+
_setPadding(forceChange = false) {
9198
const padding = this._paddingIndent();
92-
const paddingProp = this._dir && this._dir.value === 'rtl' ? 'paddingRight' : 'paddingLeft';
93-
const resetProp = paddingProp === 'paddingLeft' ? 'paddingRight' : 'paddingLeft';
9499

95-
this._renderer.setStyle(element, paddingProp, padding);
96-
this._renderer.setStyle(element, resetProp, '');
100+
if (padding !== this._currentPadding || forceChange) {
101+
const element = this._element.nativeElement;
102+
const paddingProp = this._dir && this._dir.value === 'rtl' ? 'paddingRight' : 'paddingLeft';
103+
const resetProp = paddingProp === 'paddingLeft' ? 'paddingRight' : 'paddingLeft';
104+
this._renderer.setStyle(element, paddingProp, padding);
105+
this._renderer.setStyle(element, resetProp, null);
106+
this._currentPadding = padding;
107+
}
97108
}
98109
}

src/cdk/tree/toggle.ts

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,21 +7,14 @@
77
*/
88

99
import {coerceBooleanProperty} from '@angular/cdk/coercion';
10-
import {
11-
Directive,
12-
Input,
13-
} from '@angular/core';
10+
import {Directive, HostListener, Input} from '@angular/core';
11+
1412
import {CdkTree, CdkTreeNode} from './tree';
1513

1614
/**
1715
* Node toggle to expand/collapse the node.
1816
*/
19-
@Directive({
20-
selector: '[cdkTreeNodeToggle]',
21-
host: {
22-
'(click)': '_toggle($event)',
23-
}
24-
})
17+
@Directive({selector: '[cdkTreeNodeToggle]'})
2518
export class CdkTreeNodeToggle<T> {
2619
/** Whether expand/collapse the node recursively. */
2720
@Input('cdkTreeNodeToggleRecursive')
@@ -32,6 +25,12 @@ export class CdkTreeNodeToggle<T> {
3225
constructor(protected _tree: CdkTree<T>,
3326
protected _treeNode: CdkTreeNode<T>) {}
3427

28+
// We have to use a `HostListener` here in order to support both Ivy and ViewEngine.
29+
// In Ivy the `host` bindings will be merged when this class is extended, whereas in
30+
// ViewEngine they're overwritten.
31+
// TODO(crisbeto): we move this back into `host` once Ivy is turned on by default.
32+
// tslint:disable-next-line:no-host-decorator-in-concrete
33+
@HostListener('click', ['$event'])
3534
_toggle(event: Event): void {
3635
this.recursive
3736
? this._tree.treeControl.toggleDescendants(this._treeNode.data)

src/cdk/tree/tree.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,10 +54,14 @@ import {
5454
'role': 'tree',
5555
},
5656
encapsulation: ViewEncapsulation.None,
57-
changeDetection: ChangeDetectionStrategy.OnPush
57+
58+
// The "OnPush" status for the `CdkTree` component is effectively a noop, so we are removing it.
59+
// The view for `CdkTree` consists entirely of templates declared in other views. As they are
60+
// declared elsewhere, they are checked when their declaration points are checked.
61+
// tslint:disable-next-line:validate-decorators
62+
changeDetection: ChangeDetectionStrategy.Default
5863
})
59-
export class CdkTree<T>
60-
implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit {
64+
export class CdkTree<T> implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit {
6165
/** Subject that emits when the component has been destroyed. */
6266
private _onDestroy = new Subject<void>();
6367

@@ -299,11 +303,17 @@ export class CdkTreeNode<T> implements FocusableOption, OnDestroy {
299303
/** Subject that emits when the component has been destroyed. */
300304
protected _destroyed = new Subject<void>();
301305

306+
/** Emits when the node's data has changed. */
307+
_dataChanges = new Subject<void>();
308+
302309
/** The tree node's data. */
303310
get data(): T { return this._data; }
304311
set data(value: T) {
305-
this._data = value;
306-
this._setRoleFromData();
312+
if (value !== this._data) {
313+
this._data = value;
314+
this._setRoleFromData();
315+
this._dataChanges.next();
316+
}
307317
}
308318
protected _data: T;
309319

@@ -333,6 +343,7 @@ export class CdkTreeNode<T> implements FocusableOption, OnDestroy {
333343
CdkTreeNode.mostRecentTreeNode = null;
334344
}
335345

346+
this._dataChanges.complete();
336347
this._destroyed.next();
337348
this._destroyed.complete();
338349
}

src/lib/tree/node.ts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,13 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {CdkNestedTreeNode, CdkTree, CdkTreeNode, CdkTreeNodeDef} from '@angular/cdk/tree';
9+
import {
10+
CDK_TREE_NODE_OUTLET_NODE,
11+
CdkNestedTreeNode,
12+
CdkTree,
13+
CdkTreeNode,
14+
CdkTreeNodeDef,
15+
} from '@angular/cdk/tree';
1016
import {
1117
AfterContentInit,
1218
Attribute,
@@ -19,12 +25,14 @@ import {
1925
QueryList,
2026
} from '@angular/core';
2127
import {
22-
CanDisable, CanDisableCtor,
28+
CanDisable,
29+
CanDisableCtor,
2330
HasTabIndex,
2431
HasTabIndexCtor,
2532
mixinDisabled,
2633
mixinTabIndex,
2734
} from '@angular/material/core';
35+
2836
import {MatTreeNodeOutlet} from './outlet';
2937

3038
export const _MatTreeNodeMixinBase: HasTabIndexCtor & CanDisableCtor & typeof CdkTreeNode =
@@ -90,15 +98,21 @@ export class MatTreeNodeDef<T> extends CdkTreeNodeDef<T> {
9098
inputs: ['disabled', 'tabIndex'],
9199
providers: [
92100
{provide: CdkNestedTreeNode, useExisting: MatNestedTreeNode},
93-
{provide: CdkTreeNode, useExisting: MatNestedTreeNode}
101+
{provide: CdkTreeNode, useExisting: MatNestedTreeNode},
102+
{provide: CDK_TREE_NODE_OUTLET_NODE, useExisting: MatNestedTreeNode}
94103
]
95104
})
96-
export class MatNestedTreeNode<T> extends _MatNestedTreeNodeMixinBase<T>
97-
implements AfterContentInit, CanDisable, HasTabIndex, OnDestroy {
98-
105+
export class MatNestedTreeNode<T> extends _MatNestedTreeNodeMixinBase<T> implements
106+
AfterContentInit, CanDisable, HasTabIndex, OnDestroy {
99107
@Input('matNestedTreeNode') node: T;
100108

101-
@ContentChildren(MatTreeNodeOutlet) nodeOutlet: QueryList<MatTreeNodeOutlet>;
109+
/** The children node placeholder. */
110+
@ContentChildren(MatTreeNodeOutlet, {
111+
// We need to use `descendants: true`, because Ivy will no longer match
112+
// indirect descendants if it's left as false.
113+
descendants: true
114+
})
115+
nodeOutlet: QueryList<MatTreeNodeOutlet>;
102116

103117
constructor(protected _elementRef: ElementRef<HTMLElement>,
104118
protected _tree: CdkTree<T>,

src/lib/tree/outlet.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
* Use of this source code is governed by an MIT-style license that can be
66
* found in the LICENSE file at https://angular.io/license
77
*/
8-
import {CdkTreeNodeOutlet} from '@angular/cdk/tree';
8+
import {CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet} from '@angular/cdk/tree';
99
import {
1010
Directive,
11+
Inject,
12+
Optional,
1113
ViewContainerRef,
1214
} from '@angular/core';
1315

@@ -19,5 +21,7 @@ import {
1921
selector: '[matTreeNodeOutlet]'
2022
})
2123
export class MatTreeNodeOutlet implements CdkTreeNodeOutlet {
22-
constructor(public viewContainer: ViewContainerRef) {}
24+
constructor(
25+
public viewContainer: ViewContainerRef,
26+
@Inject(CDK_TREE_NODE_OUTLET_NODE) @Optional() public _node?: any) {}
2327
}

src/lib/tree/toggle.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,6 @@ import {Directive, Input} from '@angular/core';
1414
*/
1515
@Directive({
1616
selector: '[matTreeNodeToggle]',
17-
host: {
18-
'(click)': '_toggle($event)',
19-
},
2017
providers: [{provide: CdkTreeNodeToggle, useExisting: MatTreeNodeToggle}]
2118
})
2219
export class MatTreeNodeToggle<T> extends CdkTreeNodeToggle<T> {

src/lib/tree/tree.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,9 @@ import {MatTreeNodeOutlet} from './outlet';
2424
},
2525
styleUrls: ['tree.css'],
2626
encapsulation: ViewEncapsulation.None,
27-
changeDetection: ChangeDetectionStrategy.OnPush,
27+
// See note on CdkTree for explanation on why this uses the default change detection strategy.
28+
// tslint:disable-next-line:validate-decorators
29+
changeDetection: ChangeDetectionStrategy.Default,
2830
providers: [{provide: CdkTree, useExisting: MatTree}]
2931
})
3032
export class MatTree<T> extends CdkTree<T> {

0 commit comments

Comments
 (0)