refactor(tree): rework to account for ivy changes (#15503)

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.
This commit is contained in:
Kristiyan Kostadinov
2019-03-18 17:52:27 +01:00
committed by mmalerba
parent c8ddaa80e7
commit 1421184ed7
11 changed files with 123 additions and 47 deletions
+29 -8
View File
@@ -10,16 +10,16 @@ import {
ContentChildren,
Directive,
ElementRef,
IterableDiffers,
IterableDiffer,
IterableDiffers,
OnDestroy,
QueryList,
} from '@angular/core';
import {Observable} from 'rxjs';
import {takeUntil} from 'rxjs/operators';
import {CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet} from './outlet';
import {CdkTree, CdkTreeNode} from './tree';
import {CdkTreeNodeOutlet} from './outlet';
import {getTreeControlFunctionsMissingError} from './tree-errors';
/**
@@ -51,7 +51,10 @@ import {getTreeControlFunctionsMissingError} from './tree-errors';
'[attr.role]': 'role',
'class': 'cdk-tree-node cdk-nested-tree-node',
},
providers: [{provide: CdkTreeNode, useExisting: CdkNestedTreeNode}]
providers: [
{provide: CdkTreeNode, useExisting: CdkNestedTreeNode},
{provide: CDK_TREE_NODE_OUTLET_NODE, useExisting: CdkNestedTreeNode}
]
})
export class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContentInit, OnDestroy {
/** 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
protected _children: T[];
/** The children node placeholder. */
@ContentChildren(CdkTreeNodeOutlet) nodeOutlet: QueryList<CdkTreeNodeOutlet>;
@ContentChildren(CdkTreeNodeOutlet, {
// We need to use `descendants: true`, because Ivy will no longer match
// indirect descendants if it's left as false.
descendants: true
})
nodeOutlet: QueryList<CdkTreeNodeOutlet>;
constructor(protected _elementRef: ElementRef<HTMLElement>,
protected _tree: CdkTree<T>,
@@ -92,11 +100,12 @@ export class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContent
/** Add children dataNodes to the NodeOutlet */
protected updateChildrenNodes(children?: T[]): void {
const outlet = this._getNodeOutlet();
if (children) {
this._children = children;
}
if (this.nodeOutlet.length && this._children) {
const viewContainer = this.nodeOutlet.first.viewContainer;
if (outlet && this._children) {
const viewContainer = outlet.viewContainer;
this._tree.renderNodeChanges(this._children, this._dataDiffer, viewContainer, this._data);
} else {
// Reset the data differ if there's no children nodes displayed
@@ -106,9 +115,21 @@ export class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContent
/** Clear the children dataNodes. */
protected _clear(): void {
if (this.nodeOutlet && this.nodeOutlet.first) {
this.nodeOutlet.first.viewContainer.clear();
const outlet = this._getNodeOutlet();
if (outlet) {
outlet.viewContainer.clear();
this._dataDiffer.diff([]);
}
}
/** Gets the outlet for the current node. */
private _getNodeOutlet() {
const outlets = this.nodeOutlet;
if (outlets) {
// Note that since we use `descendants: true` on the query, we have to ensure
// that we don't pick up the outlet of a child node by accident.
return outlets.find(outlet => !outlet._node || outlet._node === this);
}
}
}
+13 -1
View File
@@ -7,9 +7,19 @@
*/
import {
Directive,
Inject,
InjectionToken,
Optional,
ViewContainerRef,
} from '@angular/core';
/**
* Injection token used to provide a `CdkTreeNode` to its outlet.
* Used primarily to avoid circular imports.
* @docs-private
*/
export const CDK_TREE_NODE_OUTLET_NODE = new InjectionToken<{}>('CDK_TREE_NODE_OUTLET_NODE');
/**
* Outlet for nested CdkNode. Put `[cdkTreeNodeOutlet]` on a tag to place children dataNodes
* inside the outlet.
@@ -18,5 +28,7 @@ import {
selector: '[cdkTreeNodeOutlet]'
})
export class CdkTreeNodeOutlet {
constructor(public viewContainer: ViewContainerRef) {}
constructor(
public viewContainer: ViewContainerRef,
@Inject(CDK_TREE_NODE_OUTLET_NODE) @Optional() public _node?: any) {}
}
+18 -7
View File
@@ -24,6 +24,9 @@ const cssUnitPattern = /([A-Za-z%]+)$/;
selector: '[cdkTreeNodePadding]',
})
export class CdkTreeNodePadding<T> implements OnDestroy {
/** Current padding value applied to the element. Used to avoid unnecessarily hitting the DOM. */
private _currentPadding: string|null;
/** Subject that emits when the component has been destroyed. */
private _destroyed = new Subject<void>();
@@ -68,8 +71,13 @@ export class CdkTreeNodePadding<T> implements OnDestroy {
@Optional() private _dir: Directionality) {
this._setPadding();
if (_dir) {
_dir.change.pipe(takeUntil(this._destroyed)).subscribe(() => this._setPadding());
_dir.change.pipe(takeUntil(this._destroyed)).subscribe(() => this._setPadding(true));
}
// In Ivy the indentation binding might be set before the tree node's data has been added,
// which means that we'll miss the first render. We have to subscribe to changes in the
// data to ensure that everything is up to date.
_treeNode._dataChanges.subscribe(() => this._setPadding());
}
ngOnDestroy() {
@@ -86,13 +94,16 @@ export class CdkTreeNodePadding<T> implements OnDestroy {
return level ? `${level * this._indent}${this.indentUnits}` : null;
}
_setPadding() {
const element = this._element.nativeElement;
_setPadding(forceChange = false) {
const padding = this._paddingIndent();
const paddingProp = this._dir && this._dir.value === 'rtl' ? 'paddingRight' : 'paddingLeft';
const resetProp = paddingProp === 'paddingLeft' ? 'paddingRight' : 'paddingLeft';
this._renderer.setStyle(element, paddingProp, padding);
this._renderer.setStyle(element, resetProp, '');
if (padding !== this._currentPadding || forceChange) {
const element = this._element.nativeElement;
const paddingProp = this._dir && this._dir.value === 'rtl' ? 'paddingRight' : 'paddingLeft';
const resetProp = paddingProp === 'paddingLeft' ? 'paddingRight' : 'paddingLeft';
this._renderer.setStyle(element, paddingProp, padding);
this._renderer.setStyle(element, resetProp, null);
this._currentPadding = padding;
}
}
}
+9 -10
View File
@@ -7,21 +7,14 @@
*/
import {coerceBooleanProperty} from '@angular/cdk/coercion';
import {
Directive,
Input,
} from '@angular/core';
import {Directive, HostListener, Input} from '@angular/core';
import {CdkTree, CdkTreeNode} from './tree';
/**
* Node toggle to expand/collapse the node.
*/
@Directive({
selector: '[cdkTreeNodeToggle]',
host: {
'(click)': '_toggle($event)',
}
})
@Directive({selector: '[cdkTreeNodeToggle]'})
export class CdkTreeNodeToggle<T> {
/** Whether expand/collapse the node recursively. */
@Input('cdkTreeNodeToggleRecursive')
@@ -32,6 +25,12 @@ export class CdkTreeNodeToggle<T> {
constructor(protected _tree: CdkTree<T>,
protected _treeNode: CdkTreeNode<T>) {}
// We have to use a `HostListener` here in order to support both Ivy and ViewEngine.
// In Ivy the `host` bindings will be merged when this class is extended, whereas in
// ViewEngine they're overwritten.
// TODO(crisbeto): we move this back into `host` once Ivy is turned on by default.
// tslint:disable-next-line:no-host-decorator-in-concrete
@HostListener('click', ['$event'])
_toggle(event: Event): void {
this.recursive
? this._tree.treeControl.toggleDescendants(this._treeNode.data)
+16 -5
View File
@@ -54,10 +54,14 @@ import {
'role': 'tree',
},
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
// The "OnPush" status for the `CdkTree` component is effectively a noop, so we are removing it.
// The view for `CdkTree` consists entirely of templates declared in other views. As they are
// declared elsewhere, they are checked when their declaration points are checked.
// tslint:disable-next-line:validate-decorators
changeDetection: ChangeDetectionStrategy.Default
})
export class CdkTree<T>
implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit {
export class CdkTree<T> implements AfterContentChecked, CollectionViewer, OnDestroy, OnInit {
/** Subject that emits when the component has been destroyed. */
private _onDestroy = new Subject<void>();
@@ -299,11 +303,17 @@ export class CdkTreeNode<T> implements FocusableOption, OnDestroy {
/** Subject that emits when the component has been destroyed. */
protected _destroyed = new Subject<void>();
/** Emits when the node's data has changed. */
_dataChanges = new Subject<void>();
/** The tree node's data. */
get data(): T { return this._data; }
set data(value: T) {
this._data = value;
this._setRoleFromData();
if (value !== this._data) {
this._data = value;
this._setRoleFromData();
this._dataChanges.next();
}
}
protected _data: T;
@@ -333,6 +343,7 @@ export class CdkTreeNode<T> implements FocusableOption, OnDestroy {
CdkTreeNode.mostRecentTreeNode = null;
}
this._dataChanges.complete();
this._destroyed.next();
this._destroyed.complete();
}
+21 -7
View File
@@ -6,7 +6,13 @@
* found in the LICENSE file at https://angular.io/license
*/
import {CdkNestedTreeNode, CdkTree, CdkTreeNode, CdkTreeNodeDef} from '@angular/cdk/tree';
import {
CDK_TREE_NODE_OUTLET_NODE,
CdkNestedTreeNode,
CdkTree,
CdkTreeNode,
CdkTreeNodeDef,
} from '@angular/cdk/tree';
import {
AfterContentInit,
Attribute,
@@ -19,12 +25,14 @@ import {
QueryList,
} from '@angular/core';
import {
CanDisable, CanDisableCtor,
CanDisable,
CanDisableCtor,
HasTabIndex,
HasTabIndexCtor,
mixinDisabled,
mixinTabIndex,
} from '@angular/material/core';
import {MatTreeNodeOutlet} from './outlet';
export const _MatTreeNodeMixinBase: HasTabIndexCtor & CanDisableCtor & typeof CdkTreeNode =
@@ -90,15 +98,21 @@ export class MatTreeNodeDef<T> extends CdkTreeNodeDef<T> {
inputs: ['disabled', 'tabIndex'],
providers: [
{provide: CdkNestedTreeNode, useExisting: MatNestedTreeNode},
{provide: CdkTreeNode, useExisting: MatNestedTreeNode}
{provide: CdkTreeNode, useExisting: MatNestedTreeNode},
{provide: CDK_TREE_NODE_OUTLET_NODE, useExisting: MatNestedTreeNode}
]
})
export class MatNestedTreeNode<T> extends _MatNestedTreeNodeMixinBase<T>
implements AfterContentInit, CanDisable, HasTabIndex, OnDestroy {
export class MatNestedTreeNode<T> extends _MatNestedTreeNodeMixinBase<T> implements
AfterContentInit, CanDisable, HasTabIndex, OnDestroy {
@Input('matNestedTreeNode') node: T;
@ContentChildren(MatTreeNodeOutlet) nodeOutlet: QueryList<MatTreeNodeOutlet>;
/** The children node placeholder. */
@ContentChildren(MatTreeNodeOutlet, {
// We need to use `descendants: true`, because Ivy will no longer match
// indirect descendants if it's left as false.
descendants: true
})
nodeOutlet: QueryList<MatTreeNodeOutlet>;
constructor(protected _elementRef: ElementRef<HTMLElement>,
protected _tree: CdkTree<T>,
+6 -2
View File
@@ -5,9 +5,11 @@
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.io/license
*/
import {CdkTreeNodeOutlet} from '@angular/cdk/tree';
import {CDK_TREE_NODE_OUTLET_NODE, CdkTreeNodeOutlet} from '@angular/cdk/tree';
import {
Directive,
Inject,
Optional,
ViewContainerRef,
} from '@angular/core';
@@ -19,5 +21,7 @@ import {
selector: '[matTreeNodeOutlet]'
})
export class MatTreeNodeOutlet implements CdkTreeNodeOutlet {
constructor(public viewContainer: ViewContainerRef) {}
constructor(
public viewContainer: ViewContainerRef,
@Inject(CDK_TREE_NODE_OUTLET_NODE) @Optional() public _node?: any) {}
}
-3
View File
@@ -14,9 +14,6 @@ import {Directive, Input} from '@angular/core';
*/
@Directive({
selector: '[matTreeNodeToggle]',
host: {
'(click)': '_toggle($event)',
},
providers: [{provide: CdkTreeNodeToggle, useExisting: MatTreeNodeToggle}]
})
export class MatTreeNodeToggle<T> extends CdkTreeNodeToggle<T> {
+3 -1
View File
@@ -24,7 +24,9 @@ import {MatTreeNodeOutlet} from './outlet';
},
styleUrls: ['tree.css'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush,
// See note on CdkTree for explanation on why this uses the default change detection strategy.
// tslint:disable-next-line:validate-decorators
changeDetection: ChangeDetectionStrategy.Default,
providers: [{provide: CdkTree, useExisting: MatTree}]
})
export class MatTree<T> extends CdkTree<T> {
+6 -2
View File
@@ -16,6 +16,8 @@ export declare abstract class BaseTreeControl<T> implements TreeControl<T> {
toggleDescendants(dataNode: T): void;
}
export declare const CDK_TREE_NODE_OUTLET_NODE: InjectionToken<{}>;
export declare class CdkNestedTreeNode<T> extends CdkTreeNode<T> implements AfterContentInit, OnDestroy {
protected _children: T[];
protected _differs: IterableDiffers;
@@ -53,6 +55,7 @@ export declare class CdkTreeModule {
export declare class CdkTreeNode<T> implements FocusableOption, OnDestroy {
protected _data: T;
_dataChanges: Subject<void>;
protected _destroyed: Subject<void>;
protected _elementRef: ElementRef<HTMLElement>;
protected _tree: CdkTree<T>;
@@ -75,8 +78,9 @@ export declare class CdkTreeNodeDef<T> {
}
export declare class CdkTreeNodeOutlet {
_node?: any;
viewContainer: ViewContainerRef;
constructor(viewContainer: ViewContainerRef);
constructor(viewContainer: ViewContainerRef, _node?: any);
}
export declare class CdkTreeNodeOutletContext<T> {
@@ -95,7 +99,7 @@ export declare class CdkTreeNodePadding<T> implements OnDestroy {
level: number;
constructor(_treeNode: CdkTreeNode<T>, _tree: CdkTree<T>, _renderer: Renderer2, _element: ElementRef<HTMLElement>, _dir: Directionality);
_paddingIndent(): string | null;
_setPadding(): void;
_setPadding(forceChange?: boolean): void;
ngOnDestroy(): void;
}
+2 -1
View File
@@ -61,8 +61,9 @@ export declare class MatTreeNodeDef<T> extends CdkTreeNodeDef<T> {
}
export declare class MatTreeNodeOutlet implements CdkTreeNodeOutlet {
_node?: any;
viewContainer: ViewContainerRef;
constructor(viewContainer: ViewContainerRef);
constructor(viewContainer: ViewContainerRef, _node?: any);
}
export declare class MatTreeNodePadding<T> extends CdkTreeNodePadding<T> {