From ee7e35f80dc51ad88b6d13c7ba42ead2d5b3dd27 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Tue, 21 Jul 2026 16:52:54 +0300 Subject: [PATCH 1/5] fix(tree): render deferred selection in zoneless apps --- .../tree/src/tree/tree-selection.spec.ts | 91 ++++++++++++++++++- .../tree/src/tree/tree.component.ts | 4 +- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/tree/src/tree/tree-selection.spec.ts b/projects/igniteui-angular/tree/src/tree/tree-selection.spec.ts index a7438f0c994..7ce9114d3ea 100644 --- a/projects/igniteui-angular/tree/src/tree/tree-selection.spec.ts +++ b/projects/igniteui-angular/tree/src/tree/tree-selection.spec.ts @@ -1,6 +1,7 @@ import { TestBed, fakeAsync, waitForAsync } from '@angular/core/testing'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; -import { ChangeDetectorRef, ElementRef, EventEmitter, QueryList } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { ChangeDetectionStrategy, ChangeDetectorRef, Component, ElementRef, EventEmitter, QueryList, provideZonelessChangeDetection, signal } from '@angular/core'; import { IgxTreeComponent } from './tree.component'; import { UIInteractions } from '../../../test-utils/ui-interactions.spec'; import { TreeTestFunctions, TREE_NODE_DIV_SELECTION_CHECKBOX_CSS_CLASS } from './tree-functions.spec'; @@ -11,6 +12,48 @@ import { IgxTreeNodeComponent } from './tree-node/tree-node.component'; import { IgxTreeNavigationService } from './tree-navigation.service'; import { IgxTreeSelectionSampleComponent, IgxTreeSimpleComponent } from './tree-samples.spec'; +@Component({ + template: ` + + + Parent + First child + @if (showSecondChild()) { + Second child + } + + + `, + changeDetection: ChangeDetectionStrategy.Eager, + imports: [IgxTreeComponent, IgxTreeNodeComponent] +}) +class IgxTreeZonelessNodeDeletionComponent { + protected selection = IgxTreeSelectionType.Cascading; + protected firstChildSelected = signal(false); + protected showSecondChild = signal(true); + + public selectFirstChild(): void { + this.firstChildSelected.set(true); + } + + public removeSecondChild(): void { + this.showSecondChild.set(false); + } +} + +@Component({ + template: ` + + Selected node + + `, + changeDetection: ChangeDetectionStrategy.Eager, + imports: [IgxTreeComponent, IgxTreeNodeComponent] +}) +class IgxTreeZonelessInitialSelectionComponent { + protected selection = IgxTreeSelectionType.BiState; +} + describe('IgxTree - Selection #treeView', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ @@ -688,3 +731,49 @@ describe('IgxTree - Selection #treeView', () => { }); }); +describe('IgxTree selection in zoneless change detection #treeView', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [ + NoopAnimationsModule, + IgxTreeZonelessNodeDeletionComponent, + IgxTreeZonelessInitialSelectionComponent + ], + providers: [provideZonelessChangeDetection()] + }); + }); + + it('should render an initially selected node', async () => { + const fixture = TestBed.createComponent(IgxTreeZonelessInitialSelectionComponent); + fixture.detectChanges(); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + await fixture.whenStable(); + + const tree = fixture.debugElement.query(By.directive(IgxTreeComponent)).componentInstance as IgxTreeComponent; + const node = tree.nodes.first as IgxTreeNodeComponent; + + expect(node.selected).toBeTrue(); + expect(TreeTestFunctions.getNodeCheckboxInput(node.nativeElement).checked).toBeTrue(); + }); + + it('should render cascading selection changes after a child is removed', async () => { + const fixture = TestBed.createComponent(IgxTreeZonelessNodeDeletionComponent); + fixture.detectChanges(); + await fixture.whenStable(); + + const tree = fixture.debugElement.query(By.directive(IgxTreeComponent)).componentInstance as IgxTreeComponent; + fixture.componentInstance.selectFirstChild(); + await fixture.whenStable(); + + const parent = tree.nodes.first as IgxTreeNodeComponent; + expect(parent.indeterminate).toBeTrue(); + + fixture.componentInstance.removeSecondChild(); + await fixture.whenStable(); + await new Promise(resolve => requestAnimationFrame(() => resolve())); + await fixture.whenStable(); + + expect(parent.selected).toBeTrue(); + expect(TreeTestFunctions.getNodeCheckboxInput(parent.nativeElement).checked).toBeTrue(); + }); +}); diff --git a/projects/igniteui-angular/tree/src/tree/tree.component.ts b/projects/igniteui-angular/tree/src/tree/tree.component.ts index df9d16a34e5..a4dcc417ffb 100644 --- a/projects/igniteui-angular/tree/src/tree/tree.component.ts +++ b/projects/igniteui-angular/tree/src/tree/tree.component.ts @@ -1,4 +1,4 @@ -import { Component, QueryList, Input, Output, EventEmitter, ContentChild, Directive, TemplateRef, OnInit, AfterViewInit, ContentChildren, OnDestroy, HostBinding, ElementRef, booleanAttribute, inject, ChangeDetectionStrategy } from '@angular/core'; +import { Component, QueryList, Input, Output, EventEmitter, ContentChild, Directive, TemplateRef, OnInit, AfterViewInit, ContentChildren, OnDestroy, HostBinding, ElementRef, booleanAttribute, inject, ChangeDetectionStrategy, ChangeDetectorRef } from '@angular/core'; import { Subject } from 'rxjs'; import { takeUntil, throttleTime } from 'rxjs/operators'; @@ -84,6 +84,7 @@ export class IgxTreeComponent implements IgxTree, OnInit, AfterViewInit, OnDestr private treeService = inject(IgxTreeService); private element = inject>(ElementRef); private platform = inject(PlatformUtil); + private cdr = inject(ChangeDetectorRef, { optional: true }); @HostBinding('class.igx-tree') @@ -490,6 +491,7 @@ export class IgxTreeComponent implements IgxTree, OnInit, AfterViewInit, OnDestr if(this.platform.isBrowser) { requestAnimationFrame(() => { this.selectionService.selectNodesWithNoEvent(toBeSelected); + this.cdr?.markForCheck(); }); } this.forceSelect = []; From 352238a3f2ad8356028b9745e639dac49dabefe6 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Thu, 23 Jul 2026 12:24:47 +0300 Subject: [PATCH 2/5] perf(tree): avoid redundant selection change detection --- projects/igniteui-angular/tree/src/tree/tree.component.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/tree/src/tree/tree.component.ts b/projects/igniteui-angular/tree/src/tree/tree.component.ts index a4dcc417ffb..d2a154141e4 100644 --- a/projects/igniteui-angular/tree/src/tree/tree.component.ts +++ b/projects/igniteui-angular/tree/src/tree/tree.component.ts @@ -488,13 +488,13 @@ export class IgxTreeComponent implements IgxTree, OnInit, AfterViewInit, OnDestr private subToChanges() { this.unsubChildren$.next(); const toBeSelected = [...this.forceSelect]; - if(this.platform.isBrowser) { + this.forceSelect = []; + if (this.platform.isBrowser && toBeSelected.length) { requestAnimationFrame(() => { this.selectionService.selectNodesWithNoEvent(toBeSelected); this.cdr?.markForCheck(); }); } - this.forceSelect = []; this.nodes.forEach(node => { node.expandedChange.pipe(takeUntil(this.unsubChildren$)).subscribe(nodeState => { this.navService.update_visible_cache(node, nodeState); From faa2e4389e54ae1e2728a9bbb5b426a6b01ee4e0 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Thu, 23 Jul 2026 12:39:51 +0300 Subject: [PATCH 3/5] fix(tree): notify zoneless selection changes --- .../tree/src/tree/tree-selection.service.ts | 6 +++++- projects/igniteui-angular/tree/src/tree/tree.component.ts | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular/tree/src/tree/tree-selection.service.ts b/projects/igniteui-angular/tree/src/tree/tree-selection.service.ts index 88b3a4aaf52..d574f6fc0a3 100644 --- a/projects/igniteui-angular/tree/src/tree/tree-selection.service.ts +++ b/projects/igniteui-angular/tree/src/tree/tree-selection.service.ts @@ -11,14 +11,16 @@ interface CascadeSelectionNodeCollection { @Injectable() export class IgxTreeSelectionService { private tree: IgxTree; + private markForCheck: () => void = () => {}; private nodeSelection: Set> = new Set>(); private indeterminateNodes: Set> = new Set>(); private nodesToBeSelected: Set>; private nodesToBeIndeterminate: Set>; - public register(tree: IgxTree) { + public register(tree: IgxTree, markForCheck?: () => void) { this.tree = tree; + this.markForCheck = markForCheck ?? (() => {}); } /** Select range from last selected node to the current specified node. */ @@ -110,6 +112,7 @@ export class IgxTreeSelectionService { if (this.isNodeSelected(node)) { // node is destroyed, do not emit event this.deselectNodesWithNoEvent([node], false); + this.markForCheck(); } else { if (!node.parentNode) { return; @@ -119,6 +122,7 @@ export class IgxTreeSelectionService { return; } this.retriggerNodeState(assitantLeafNode); + this.markForCheck(); } }); } diff --git a/projects/igniteui-angular/tree/src/tree/tree.component.ts b/projects/igniteui-angular/tree/src/tree/tree.component.ts index d2a154141e4..e1e5326b8df 100644 --- a/projects/igniteui-angular/tree/src/tree/tree.component.ts +++ b/projects/igniteui-angular/tree/src/tree/tree.component.ts @@ -315,7 +315,7 @@ export class IgxTreeComponent implements IgxTree, OnInit, AfterViewInit, OnDestr private unsubChildren$ = new Subject(); constructor() { - this.selectionService.register(this); + this.selectionService.register(this, () => this.cdr?.markForCheck()); this.treeService.register(this); this.navService.register(this); } From c3fe49399912c00f391a284c618420680f93bcc0 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Thu, 23 Jul 2026 14:17:31 +0300 Subject: [PATCH 4/5] chore(*): revert not needed changes --- .../tree/src/tree/tree-selection.service.ts | 6 +----- projects/igniteui-angular/tree/src/tree/tree.component.ts | 4 ++-- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/projects/igniteui-angular/tree/src/tree/tree-selection.service.ts b/projects/igniteui-angular/tree/src/tree/tree-selection.service.ts index d574f6fc0a3..88b3a4aaf52 100644 --- a/projects/igniteui-angular/tree/src/tree/tree-selection.service.ts +++ b/projects/igniteui-angular/tree/src/tree/tree-selection.service.ts @@ -11,16 +11,14 @@ interface CascadeSelectionNodeCollection { @Injectable() export class IgxTreeSelectionService { private tree: IgxTree; - private markForCheck: () => void = () => {}; private nodeSelection: Set> = new Set>(); private indeterminateNodes: Set> = new Set>(); private nodesToBeSelected: Set>; private nodesToBeIndeterminate: Set>; - public register(tree: IgxTree, markForCheck?: () => void) { + public register(tree: IgxTree) { this.tree = tree; - this.markForCheck = markForCheck ?? (() => {}); } /** Select range from last selected node to the current specified node. */ @@ -112,7 +110,6 @@ export class IgxTreeSelectionService { if (this.isNodeSelected(node)) { // node is destroyed, do not emit event this.deselectNodesWithNoEvent([node], false); - this.markForCheck(); } else { if (!node.parentNode) { return; @@ -122,7 +119,6 @@ export class IgxTreeSelectionService { return; } this.retriggerNodeState(assitantLeafNode); - this.markForCheck(); } }); } diff --git a/projects/igniteui-angular/tree/src/tree/tree.component.ts b/projects/igniteui-angular/tree/src/tree/tree.component.ts index e1e5326b8df..116da1bd3ef 100644 --- a/projects/igniteui-angular/tree/src/tree/tree.component.ts +++ b/projects/igniteui-angular/tree/src/tree/tree.component.ts @@ -315,7 +315,7 @@ export class IgxTreeComponent implements IgxTree, OnInit, AfterViewInit, OnDestr private unsubChildren$ = new Subject(); constructor() { - this.selectionService.register(this, () => this.cdr?.markForCheck()); + this.selectionService.register(this); this.treeService.register(this); this.navService.register(this); } @@ -489,7 +489,7 @@ export class IgxTreeComponent implements IgxTree, OnInit, AfterViewInit, OnDestr this.unsubChildren$.next(); const toBeSelected = [...this.forceSelect]; this.forceSelect = []; - if (this.platform.isBrowser && toBeSelected.length) { + if (this.platform.isBrowser) { requestAnimationFrame(() => { this.selectionService.selectNodesWithNoEvent(toBeSelected); this.cdr?.markForCheck(); From 283f0b2b6ed50fc66424f8eb87720ede698e9faf Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Thu, 23 Jul 2026 14:21:07 +0300 Subject: [PATCH 5/5] fix(tree): preserve selection refresh on node changes --- projects/igniteui-angular/tree/src/tree/tree.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/igniteui-angular/tree/src/tree/tree.component.ts b/projects/igniteui-angular/tree/src/tree/tree.component.ts index 116da1bd3ef..cde10620186 100644 --- a/projects/igniteui-angular/tree/src/tree/tree.component.ts +++ b/projects/igniteui-angular/tree/src/tree/tree.component.ts @@ -488,13 +488,13 @@ export class IgxTreeComponent implements IgxTree, OnInit, AfterViewInit, OnDestr private subToChanges() { this.unsubChildren$.next(); const toBeSelected = [...this.forceSelect]; - this.forceSelect = []; if (this.platform.isBrowser) { requestAnimationFrame(() => { this.selectionService.selectNodesWithNoEvent(toBeSelected); this.cdr?.markForCheck(); }); } + this.forceSelect = []; this.nodes.forEach(node => { node.expandedChange.pipe(takeUntil(this.unsubChildren$)).subscribe(nodeState => { this.navService.update_visible_cache(node, nodeState);