components
components copied to clipboard
MatTree does not update items when using trackBy
When using trackBy, CdkTree / MatTree does not update items which share the same identity (as returned by trackBy), but are actually different obejcts (with property changes). This behavior differs from the behavior shown by ngFor in combination with trackBy.
Reproduction
StackBlitz: https://stackblitz.com/edit/angular-cfph4s
Steps to reproduce:
- Click the button
Expected Behavior
The tree gets updated as the list created by ngFor does.
Actual Behavior
The tree does not change, only the list generated by ngFor.
Environment
- Angular: 9.0.1
- CDK/Material: 9.1.0
- Browser(s): Chrome
- Operating System (e.g. Windows, macOS, Ubuntu): macOS
Thanks for this issue. This seems like a valid bug to me. The NgForOf
directive also respects the identity, correct. See:
https://cs.opensource.google/angular/angular/+/master:packages/common/src/directives/ng_for_of.ts;l=260-264?q=NgFor&ss=angular
We should probably add the same to the tree here, but not sure if it would be considered breaking.
Any news on this one?
I ran into the same issue and quick and dirty fixed it by using a custom tree component.
I think, we have to get the instance of MatTreeNode
/CdkTreeNode
and set the data
property, but I don't know how to get it. So I just ended up overriding the outlet context.
@Component({
selector: 'app-fixed-tree',
template: '<ng-container matTreeNodeOutlet></ng-container>',
host: {
'class': 'mat-tree cdk-tree app-fixed-tree',
'role': 'tree',
},
// you might want to include the entire styles from tree.scss, but it is not needed in my case
styles: ['.app-fixed-tree {display:block;}'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.Default,
providers: [{
provide: CdkTree,
useExisting: FixedTreeComponent,
}],
})
export class FixedTreeComponent<T, K = T> extends MatTree<T, K> {
renderNodeChanges(data: T[] | ReadonlyArray<T>, dataDiffer?: IterableDiffer<T>, viewContainer: ViewContainerRef = this._nodeOutlet.viewContainer, parentData?: T): void {
super.renderNodeChanges(data, dataDiffer, viewContainer, parentData);
// apply new data objects
if (this.trackBy != null) {
data.forEach((d, i) => {
const view: any = viewContainer.get(i);
if (view && view.context) {
view.context.$implicit = d;
}
});
}
}
}
Any news on this one?
I ran into the same issue and quick and dirty fixed it by using a custom tree component.
I think, we have to get the instance of
MatTreeNode
/CdkTreeNode
and set thedata
property, but I don't know how to get it. So I just ended up overriding the outlet context.@Component({ selector: 'app-fixed-tree', template: '<ng-container matTreeNodeOutlet></ng-container>', host: { 'class': 'mat-tree cdk-tree app-fixed-tree', 'role': 'tree', }, // you might want to include the entire styles from tree.scss, but it is not needed in my case styles: ['.app-fixed-tree {display:block;}'], encapsulation: ViewEncapsulation.None, changeDetection: ChangeDetectionStrategy.Default, providers: [{ provide: CdkTree, useExisting: FixedTreeComponent, }], }) export class FixedTreeComponent<T, K = T> extends MatTree<T, K> { renderNodeChanges(data: T[] | ReadonlyArray<T>, dataDiffer?: IterableDiffer<T>, viewContainer: ViewContainerRef = this._nodeOutlet.viewContainer, parentData?: T): void { super.renderNodeChanges(data, dataDiffer, viewContainer, parentData); // apply new data objects if (this.trackBy != null) { data.forEach((d, i) => { const view: any = viewContainer.get(i); if (view && view.context) { view.context.$implicit = d; } }); } } }
This still leads to the component not updating, plus type of FixedTreeComponent should take one argument
FixedTreeComponent<T>
This still leads to the component not updating
Ok, really? Maybe it's because I update the expansion model right after updating the source which leads to a new change detection. Then it's just a works-for-me-fix, not worth to mention.
plus type of FixedTreeComponent should take one argument
FixedTreeComponent<T>
That's not true. K
is the type of the trackBy
function result and we have to pass it to the MatTree
.
I think I might be affected by this too. Once I add a trackBy function to the tree when refreshing the data, the toggling breaks
same issue for me...
Same issue, seems to be an issue with CDK Tree. Can't believe this hasn't been fixed over 2 years.
I tried to implement a solution, but so far, it seems that the current implementation has some clear flaws and shortcomings that prevent an easy fix.
This is my analysis so far:
-
CdkTree
does not apply any updates on "identity changes" in theIterableDiffer
. I tried fixing this in theCdkTree.renderNodeChanges()
:
changes.forEachIdentityChange((record) => {
if (record.currentIndex !== null) {
console.log('processing', data[record.currentIndex]);
const viewRef = <EmbeddedViewRef<CdkTreeNodeOutletContext<T>>>viewContainer.get(record.currentIndex);
viewRef.context.$implicit = record.item;
}
});
That fixes one issue.
- The
BaseTreeControl
(whichNestedTreeControl
extends) has atrackBy
property that is not set. Manually setting it in its constructor fixes that issue:treeControl = new NestedTreeControl<FoodNode, string>(node => node.children, { trackBy: (node) => node.id });
. In my opinion, this should be handled byCdkTree
automatically or documented more clearly. - At this point, it works as expected, but only on the first level. Child nodes are still not updated. However, it seems that the current implementation doesn't allow existing child nodes to get data updates.
CdkTree
relies on some weird static variableCdkTreeNode.mostRecentTreeNode
that keeps track of the latest rendered node in order to set the data on it, but that doesn't work with data updates.
I shared my fixes in this pull request: https://github.com/angular/components/pull/26508.
Those fixes are only part of the solution, I still rely on a workaround (see PR). Any contributions, ideas or feedback is welcome. I am also considering a custom implementation.
The issue seems to be still present in 2024
@devversion Do you have any plans on fixing this in an upcoming version?
This prevents users from optimizing their trees for performance and a side effect of re-rendering the entire tree is that you cannot use css transitions for e.g. the expansion icon as all icons will be re-rendered and all icons will be animated / rotated again.
Sorry @JonasDev17, I'm not actively working on Angular Components, but maybe a PR can be created that re-uses some of the logic from NgForOf
.