Skip to content

Commit d60ee5e

Browse files
MeAkibkirjs
authored andcommitted
refactor(devtools): replace deprecated tree APIs in the component tree
`FlatTreeControl` from `@angular/cdk/tree` and `MatTreeFlattener` from `@angular/material/tree` are deprecated in favor of the tree's `levelAccessor`/`childrenAccessor` APIs. The component tree (directive forest) isn't rendered with a CDK tree but as a flat list in a virtual scroll viewport. It only used `FlatTreeControl` to track which nodes are expanded, and `MatTreeFlattener` to flatten the forest and compute the visible nodes. Replace them with: - An `ExpansionModel` that tracks expanded nodes with a `SelectionModel`, like `FlatTreeControl` did internally, and exposes the `expand`, `collapse`, `toggle`, `isExpanded` and `changed` members used by devtools. - Local helpers in `ComponentDataSource` that flatten the forest in depth-first order and return the nodes whose ancestors are all expanded, matching the previous behavior. Also add a spec covering which nodes are visible as nodes are expanded and collapsed.
1 parent a1bffc2 commit d60ee5e

12 files changed

Lines changed: 176 additions & 89 deletions

File tree

‎devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/BUILD.bazel‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ ng_project(
1818
],
1919
deps = [
2020
":directive_forest_utils",
21+
":expansion_model",
2122
"//:node_modules/@angular/cdk",
2223
"//:node_modules/@angular/core",
2324
"//:node_modules/@angular/material",
@@ -34,11 +35,20 @@ ng_project(
3435
],
3536
)
3637

38+
ts_project(
39+
name = "expansion_model",
40+
srcs = ["expansion-model.ts"],
41+
deps = [
42+
"//:node_modules/@angular/cdk",
43+
"//:node_modules/rxjs",
44+
],
45+
)
46+
3747
ts_project(
3848
name = "directive_forest_utils",
3949
srcs = ["directive-forest-utils.ts"],
4050
deps = [
41-
"//:node_modules/@angular/cdk",
51+
":expansion_model",
4252
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source",
4353
],
4454
)

‎devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source/BUILD.bazel‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,10 @@ ts_project(
88
deps = [
99
"//:node_modules/@angular/cdk",
1010
"//:node_modules/@angular/core",
11-
"//:node_modules/@angular/material",
1211
"//:node_modules/@types",
1312
"//:node_modules/rxjs",
1413
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/diffing",
14+
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest:expansion_model",
1515
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/index-forest",
1616
"//devtools/projects/protocol",
1717
],
@@ -22,7 +22,8 @@ ts_test_library(
2222
srcs = glob(["**/*.spec.ts"]),
2323
deps = [
2424
":component-data-source",
25-
"//:node_modules/@angular/cdk",
25+
"//:node_modules/rxjs",
26+
"//devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest:expansion_model",
2627
"//devtools/projects/protocol",
2728
],
2829
)

‎devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source/component-data-source.spec.ts‎

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {FlatTreeControl} from '@angular/cdk/tree';
9+
import {of} from 'rxjs';
10+
1011
import {DevToolsNode} from '../../../../../../../protocol';
1112

1213
import {ComponentDataSource, FlatNode} from '.';
14+
import {ExpansionModel} from '../expansion-model';
1315

1416
const tree1: DevToolsNode = {
1517
tagName: 'app',
@@ -213,12 +215,9 @@ const tree4: DevToolsNode = {
213215

214216
describe('ComponentDataSource', () => {
215217
let dataSource: ComponentDataSource;
216-
const treeControl = new FlatTreeControl<FlatNode>(
217-
(node) => node.level,
218-
(node) => node.expandable,
219-
);
218+
const expansionModel = new ExpansionModel<FlatNode>();
220219

221-
beforeEach(() => (dataSource = new ComponentDataSource(treeControl)));
220+
beforeEach(() => (dataSource = new ComponentDataSource(expansionModel)));
222221

223222
it('should return new and old items', () => {
224223
const result = dataSource.update([tree1], true);
@@ -250,4 +249,27 @@ describe('ComponentDataSource', () => {
250249

251250
expect(result.newItems[1].position).toEqual([0, 0, 0, 0, 0, 0]);
252251
});
252+
253+
it('should only show the children of expanded nodes', () => {
254+
const expansionModel = new ExpansionModel<FlatNode>();
255+
const source = new ComponentDataSource(expansionModel);
256+
source.update([tree1], true);
257+
const [app, bar] = source.data;
258+
expect([app.level, bar.level]).toEqual([0, 1]);
259+
260+
let visibleNodes: string[] = [];
261+
const subscription = source
262+
.connect({viewChange: of({start: 0, end: Number.MAX_VALUE})})
263+
.subscribe((nodes) => (visibleNodes = nodes.map((node) => node.name)));
264+
265+
expect(visibleNodes).toEqual(['app']);
266+
267+
expansionModel.expand(app);
268+
expect(visibleNodes).toEqual(['app', 'bar']);
269+
270+
expansionModel.collapse(app);
271+
expect(visibleNodes).toEqual(['app']);
272+
273+
subscription.unsubscribe();
274+
});
253275
});

‎devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/component-data-source/index.ts‎

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

99
import {CollectionViewer, DataSource} from '@angular/cdk/collections';
10-
import {FlatTreeControl} from '@angular/cdk/tree';
1110
import {DefaultIterableDiffer, TrackByFunction} from '@angular/core';
12-
import {MatTreeFlattener} from '@angular/material/tree';
1311
import {
1412
DevToolsNode,
1513
ControlFlowBlock,
@@ -19,6 +17,7 @@ import {
1917
import {BehaviorSubject, merge, Observable, map} from 'rxjs';
2018

2119
import {diff} from '../../diffing';
20+
import {ExpansionModel} from '../expansion-model';
2221
import {IndexedNode, indexForest} from '../index-forest';
2322

2423
/** Flat node with expandable and level information */
@@ -87,47 +86,77 @@ const filterCommentNodes = (nodes: IndexedNode[]) => {
8786
return nodes;
8887
};
8988

89+
/**
90+
* Returns the nodes of a flattened tree that are visible, i.e. whose ancestors are all expanded.
91+
*
92+
* @param nodes flat nodes in depth-first order.
93+
* @param isExpanded whether the given node is expanded.
94+
*/
95+
const getVisibleNodes = (nodes: FlatNode[], isExpanded: (node: FlatNode) => boolean) => {
96+
const visibleNodes: FlatNode[] = [];
97+
// Whether all ancestors are expanded, indexed by level.
98+
const expandedAtLevel: boolean[] = [true];
99+
for (const node of nodes) {
100+
let visible = true;
101+
for (let i = 0; i <= node.level; i++) {
102+
visible = visible && !!expandedAtLevel[i];
103+
}
104+
if (visible) {
105+
visibleNodes.push(node);
106+
}
107+
if (node.expandable) {
108+
expandedAtLevel[node.level + 1] = isExpanded(node);
109+
}
110+
}
111+
return visibleNodes;
112+
};
113+
90114
export class ComponentDataSource extends DataSource<FlatNode> {
91115
private _differ = new DefaultIterableDiffer<FlatNode>(trackBy);
92116
private _expandedData = new BehaviorSubject<FlatNode[]>([]);
93117
private _flattenedData = new BehaviorSubject<FlatNode[]>([]);
94118
private _nodeToFlat = new WeakMap<IndexedNode, FlatNode>();
95119

96-
private _treeFlattener = new MatTreeFlattener(
97-
(node: IndexedNode, level: number) => {
98-
if (this._nodeToFlat.has(node)) {
99-
return this._nodeToFlat.get(node);
100-
}
101-
const flatNode: FlatNode = {
102-
expandable: expandable(node),
103-
id: getId(node),
104-
// We can compare the nodes in the navigation functions above
105-
// based on this identifier directly, since it's a reference type
106-
// and the reference is preserved after transformation.
107-
position: node.position,
108-
name: node.component ? node.component.name : (node.tagName ?? ''),
109-
directives: node.directives?.map((d) => d.name) ?? [],
110-
original: node,
111-
level,
112-
hydration: node.hydration,
113-
controlFlowBlock: node.controlFlowBlock,
114-
static: node.static,
115-
changeDetection: node.changeDetection,
116-
hasNativeElement: node.hasNativeElement,
117-
collapsedByDefault: node.children.every((n) => n.static),
118-
};
119-
this._nodeToFlat.set(node, flatNode);
120-
return flatNode;
121-
},
122-
(node) => (node ? node.level : -1),
123-
(node) => (node ? node.expandable : false),
124-
(node) => (node ? node.children : []),
125-
);
126-
127-
constructor(private _treeControl: FlatTreeControl<FlatNode>) {
120+
constructor(private _expansionModel: ExpansionModel<FlatNode>) {
128121
super();
129122
}
130123

124+
/** Flattens the forest into a list of nodes in depth-first order. */
125+
private _flattenNodes(nodes: IndexedNode[], level = 0, result: FlatNode[] = []): FlatNode[] {
126+
for (const node of nodes) {
127+
const flatNode = this._toFlatNode(node, level);
128+
result.push(flatNode);
129+
if (flatNode.expandable) {
130+
this._flattenNodes(node.children, level + 1, result);
131+
}
132+
}
133+
return result;
134+
}
135+
136+
private _toFlatNode(node: IndexedNode, level: number): FlatNode {
137+
const existingNode = this._nodeToFlat.get(node);
138+
if (existingNode) {
139+
return existingNode;
140+
}
141+
const flatNode: FlatNode = {
142+
expandable: expandable(node),
143+
id: getId(node),
144+
position: node.position,
145+
name: node.component ? node.component.name : (node.tagName ?? ''),
146+
directives: node.directives?.map((d) => d.name) ?? [],
147+
original: node,
148+
level,
149+
hydration: node.hydration,
150+
controlFlowBlock: node.controlFlowBlock,
151+
static: node.static,
152+
changeDetection: node.changeDetection,
153+
hasNativeElement: node.hasNativeElement,
154+
collapsedByDefault: node.children.every((n) => n.static),
155+
};
156+
this._nodeToFlat.set(node, flatNode);
157+
return flatNode;
158+
}
159+
131160
get data(): FlatNode[] {
132161
return this._flattenedData.value;
133162
}
@@ -174,27 +203,26 @@ export class ComponentDataSource extends DataSource<FlatNode> {
174203
indexedForest = filterCommentNodes(indexedForest);
175204
}
176205

177-
const flattenedCollection = this._treeFlattener.flattenNodes(indexedForest) as FlatNode[];
206+
const flattenedCollection = this._flattenNodes(indexedForest);
178207

179208
this.data.forEach((i) => (i.newItem = false));
180209

181210
const expandedNodes: Record<string, boolean> = {};
182211
this.data.forEach((item) => {
183-
expandedNodes[item.id] = this._treeControl.isExpanded(item);
212+
expandedNodes[item.id] = this._expansionModel.isExpanded(item);
184213
});
185214

186215
const {newItems, movedItems, removedItems} = diff<FlatNode>(
187216
this._differ,
188217
this.data,
189218
flattenedCollection,
190219
);
191-
this._treeControl.dataNodes = this.data;
192220
this._flattenedData.next(this.data);
193221

194222
movedItems.forEach((i) => {
195223
this._nodeToFlat.set(i.original, i);
196224
if (expandedNodes[i.id]) {
197-
this._treeControl.expand(i);
225+
this._expansionModel.expand(i);
198226
}
199227
});
200228
newItems.forEach((i) => (i.newItem = true));
@@ -206,16 +234,13 @@ export class ComponentDataSource extends DataSource<FlatNode> {
206234
override connect(collectionViewer: CollectionViewer): Observable<FlatNode[]> {
207235
const changes = [
208236
collectionViewer.viewChange,
209-
this._treeControl.expansionModel.changed,
237+
this._expansionModel.changed,
210238
this._flattenedData,
211239
];
212240
return merge<unknown[]>(...changes).pipe(
213241
map(() => {
214242
this._expandedData.next(
215-
this._treeFlattener.expandFlattenedNodes(
216-
this.data,
217-
this._treeControl as FlatTreeControl<FlatNode | undefined>,
218-
) as FlatNode[],
243+
getVisibleNodes(this.data, (node) => this._expansionModel.isExpanded(node)),
219244
);
220245
return this._expandedData.value;
221246
}),

‎devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest-utils.ts‎

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,8 @@
66
* found in the LICENSE file at https://angular.dev/license
77
*/
88

9-
import {FlatTreeControl} from '@angular/cdk/tree';
10-
119
import {FlatNode} from './component-data-source/index';
10+
import {ExpansionModel} from './expansion-model';
1211

1312
export const isChildOf = (childPosition: number[], parentPosition: number[]) => {
1413
if (childPosition.length <= parentPosition.length) {
@@ -25,11 +24,11 @@ export const isChildOf = (childPosition: number[], parentPosition: number[]) =>
2524
export const parentCollapsed = (
2625
nodeIdx: number,
2726
all: FlatNode[],
28-
treeControl: FlatTreeControl<FlatNode>,
27+
expansionModel: ExpansionModel<FlatNode>,
2928
) => {
3029
const node = all[nodeIdx];
3130
for (let i = nodeIdx - 1; i >= 0; i--) {
32-
if (isChildOf(node.position, all[i].position) && !treeControl.isExpanded(all[i])) {
31+
if (isChildOf(node.position, all[i].position) && !expansionModel.isExpanded(all[i])) {
3332
return true;
3433
}
3534
}

‎devtools/projects/ng-devtools/src/lib/devtools-tabs/directive-explorer/directive-forest/directive-forest.component.html‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
[node]="node"
1414
[selectedNode]="selectedNode()"
1515
[highlightedId]="highlightIdInTreeFromElement()"
16-
[treeControl]="treeControl"
16+
[expansionModel]="expansionModel"
1717
[textMatches]="matchedNodes().get(idx) ?? []"
1818
[nodeCdData]="mappedCdData().get(node.original.original)"
1919
(selectNode)="selectAndEnsureVisible($event)"

0 commit comments

Comments
 (0)