components
components copied to clipboard
bug(cdk-tree): CDK Tree can't be opened when trackBy function is used after update of datasource
Is this a regression?
- [x] Yes, this behavior used to work in the previous version
The previous version in which this bug was not present was
Angular 17 (here is the same example: https://stackblitz.com/edit/zmfrj6ks?file=src%2Fexample%2Fcdk-tree-nested-example.ts,src%2Fexample%2Fcdk-tree-nested-example.html with Angular 17)
Description
Hi, there is a bug in the Angular CDK Tree. When you update the data of the tree and a trackBy function is given to the tree, you can't interact with the tree anymore.
Reproduction
StackBlitz link: https://stackblitz.com/edit/fesfbplt?file=src%2Fexample%2Fcdk-tree-nested-children-accessor-example.html Steps to reproduce:
- interact with the tree (not mandatory, but you see that it works)
- click on "changeTree" button
- try to open the tree again => this doesn't work
Expected Behavior
Tree should be updateable and interactive after an update. I would also expect that the open state is kept when only part of the tree is changed (in the example one subtree is removed, the other persists) and a trackby function is given.
Actual Behavior
Tree is closed and not interactive anymore.
Environment
- Angular: 18/19
- CDK/Material: 18/19
- Browser(s): all
- Operating System (e.g. Windows, macOS, Ubuntu): windows
Potential duplicate of https://github.com/angular/components/issues/11381
Edit: After investigating, it seems this is unrelated to https://github.com/angular/components/issues/11381 and instead seems to be a weird interaction between trackBy and expansion
it works when you use
[expansionKey]="expansionKeyFn"
and add an Id to each node
expansionKeyFn = (node: NestedFoodNode) => node.id;
(if no id is provided and you for example use node.name it would expand all nodes with the same name, f.e. "green")
You might wanna have a look at my other comment in which I did some debugging with mat flat and nested tree. One of the (working) solutions should also work with CDK tree
Thanks for the infos. Here is an adapted stackblitz with expansionKey set: stackblitz. The tree stays open and interactive, but the data is not updated.
@MeMeMax as stated in my other comment:
//doesnt work with nested tree -> no update //works with flat tree // trackByFn = (index: number, node: FoodNode) => this.expansionKeyFn(node); // expansionKeyFn = (node: FoodNode) => node.id;
node.id doesnt work for trackByFn.
update your code to:
trackBy(_: number, node: any) {
return node;
}
@passee sorry, I missed that. Thanks for clarification. You are right. It does work with that setup.
No worries, youre welcome. But this doesnt close the issue I would say, as imo. returning the whole object for trackBy invalidates its purpose, this is just a workaround for being able to work with it. Any dev should have a look at this and decide if its meant to work like this or if trackBy cant work with the id, in these circumstances, because of a bug.
Yes for me this is still a bug. It is not documented and as you say it invalidates the purpose of trackBy. Also it is a regression so it is likely that the bug was introduced through other changes by accident.
Yes for me this is still a bug. It is not documented and as you say it invalidates the purpose of trackBy. Also it is a regression so it is likely that the bug was introduced through other changes by accident.
Regarding trackBy, Here is an example that has 0-n project nodes and 0-n scene nodes per project node. Assume this code:
interface BaseNode {
id: string;
name: string;
kind: NodeType;
children?: BaseNode[];
level: number;
}
interface SceneNode extends BaseNode {
kind: 'scene';
}
interface ProjectNode extends BaseNode {
kind: 'project';
}
protected nodes = computed<BaseNode[]>(() =>
this.projects().map<ProjectNode>(p => ({
id: p.id,
name: p.name,
kind: 'project' as const,
children: (p.scenes ?? []).map<SceneNode>(s => ({
id: s.id,
name: s.name,
kind: 'scene' as const,
})),
}))
);
childrenAccessor = (n: BaseNode) => {
return n.children ?? [];
};
trackBy = (_: number, n: BaseNode) => {
return n.id;
}
expansionKey = (n: BaseNode) => {
return n.id;
};
When using a mat-tree with mat-nested-tree-node + childrenAccessor + trackBy + expansionKey when adding a new scene the visual tree is not updated when trackBy just returns the projects id. The underlying data gets a new entry and the childrenAccessor is even called with the new scene as argument but the newly added nodes are not rendered. This is because trackBy is just called for the parent nodes (ProjectNode) after the change, the project ids did not change and the tree assumes no changes happened to the subtree.
As already mentioned in the previous comments you can fix it by just returning the node object itself if the underlying data also creates a new ProjectNode object when a Scene was added. OR trackBy must return an identifier that identifies the complete subtree or any changes made to it (if you also want to capture that). When you use a trackBy that identifies the complete subtree then you will notice that at least that subtree remains interactive but other sibling nodes still do not.
In my example when adding a new scene to the 1. project then its no longer possible to expand the 2. project node but the 1. project (and its nested nodes) remain interactive. Similarly when you remove the second project from the data then you can no longer expand the first project node.
Alternatively use a flat data source with levelAccessor instead of childrenAccessor (mat-nested-tree-nodes can still be used in the template). Like this
protected nodes = computed<BaseNode[]>(() =>
this.projects().flatMap(p => {
const pNode: ProjectNode = {
id: p.id,
name: p.name,
kind: 'project',
level: 0
};
const sNodes = (p.scenes?? []).map<SceneNode>(s => ({
id: s.id,
name: s.name,
kind: 'scene' as const,
level: 1,
}));
return [pNode, ...sNodes];
})
);
levelAccessor(n: BaseNode): number {
return n.level;
}
Here you can just return the id with trackBy but you still have to use expansionKey otherwise the tree reimains unresponsive.
However these fixes are unrelated to the actual problem - trackBy makes the nodes no longer interactive when the data changes and expansionKey is NOT used.
One additional note when using childrenAccessor: Adding a scene does not update the visual tree when expansionKey is used and a trackBy function is NOT used. But using levelAccessor instead does not lead to this problem.
So as of right now in my experience when using
- childrenAccessor: you should provide both expansionKey and a working trackBy that identifies the complete subtree
- levelAccessor: only provide expansionKey or both working trackBy and expansionKey