From f0077a86658a7666025ec87e8fae96dd0709c739 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Fri, 12 Jun 2026 08:55:14 +0300 Subject: [PATCH 1/3] fix(for-of): call recalcUpdateSizes via afterNextRender in zoneless mode --- .../for-of/for_of.directive.spec.ts | 32 +++++++++++++++-- .../src/directives/for-of/for_of.directive.ts | 34 +++++++++++++++---- .../grids/grid/src/grid-base.directive.ts | 9 +++-- 3 files changed, 65 insertions(+), 10 deletions(-) diff --git a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts index 5b0e44db0cd..921c021b94e 100644 --- a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts +++ b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts @@ -1,6 +1,6 @@ import { AsyncPipe, NgClass, NgForOfContext } from '@angular/common'; -import { AfterViewInit, ChangeDetectorRef, Component, Directive, Injectable, IterableDiffers, NgZone, OnInit, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, DebugElement, Pipe, PipeTransform, inject } from '@angular/core'; -import { TestBed, ComponentFixture, waitForAsync } from '@angular/core/testing'; +import { AfterViewInit, ChangeDetectorRef, Component, Directive, Injectable, IterableDiffers, NgZone, OnInit, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, DebugElement, Pipe, PipeTransform, inject, provideZonelessChangeDetection } from '@angular/core'; +import { TestBed, ComponentFixture, waitForAsync, fakeAsync, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { BehaviorSubject, Observable } from 'rxjs'; import { IForOfState, IgxForOfDirective } from './for_of.directive'; @@ -1230,6 +1230,34 @@ describe('IgxForOf directive -', () => { }); }); + describe('zoneless', () => { + let fix: ComponentFixture; + + beforeEach(async () => { + TestBed.resetTestingModule(); + await TestBed.configureTestingModule({ + imports: [VerticalVirtualComponent], + providers: [provideZonelessChangeDetection()] + }).compileComponents(); + + fix = TestBed.createComponent(VerticalVirtualComponent); + dg.generateData(300, 5, fix.componentInstance); + fix.detectChanges(); + }); + + it('should call recalcUpdateSizes after vertical scroll', fakeAsync(() => { + const virtDir = fix.componentInstance.parentVirtDir; + const spy = spyOn(virtDir, 'recalcUpdateSizes').and.callThrough(); + + fix.componentInstance.scrollTop(300); + tick(100); + fix.detectChanges(); + + expect(spy).toHaveBeenCalled(); + })); + + }); + describe('on create new instance', () => { let fix: ComponentFixture; diff --git a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts index c90cb47014e..fd0715a571e 100644 --- a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts @@ -960,16 +960,21 @@ export class IgxForOfDirective extends IgxForOfToken { afterNextRender({ write: () => { this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`; - } + }, + read: isZoneless ? () => { + this.recalcUpdateSizes(); + } : undefined }); }); - - this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this)); + if (!isZoneless) { + this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this)); + } this.dc.changeDetectorRef.detectChanges(); if (prevStartIndex !== this.state.startIndex) { @@ -1175,7 +1180,18 @@ export class IgxForOfDirective extends IgxForOfToken { + afterNextRender({ + read: () => { + this.recalcUpdateSizes(); + } + }); + }); + } else { + this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this)); + } this.dc.changeDetectorRef.detectChanges(); if (prevStartIndex !== this.state.startIndex) { @@ -1777,12 +1793,18 @@ export class IgxGridForOfDirective extends IgxForOfDirec } const prevState = Object.assign({}, this.state); const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition); + const isZoneless = this._zone.constructor.name === 'NoopNgZone'; runInInjectionContext(this._injector, () => { afterNextRender({ write: () => { this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`; - this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this, prevState)); - } + if (!isZoneless) { + this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this, prevState)); + } + }, + read: isZoneless ? () => { + this.recalcUpdateSizes(prevState); + } : undefined }); }); diff --git a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts index 5562b556958..53bd7832fb0 100644 --- a/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts +++ b/projects/igniteui-angular/grids/grid/src/grid-base.directive.ts @@ -3733,7 +3733,7 @@ export abstract class IgxGridBaseDirective implements GridType, destructor ) .subscribe(() => { - this.zone.run(() => { + const work = () => { // do not trigger reflow if element is detached. if (this.nativeElement.isConnected) { if (this.shouldResize) { @@ -3748,7 +3748,12 @@ export abstract class IgxGridBaseDirective implements GridType, } this.notifyChanges(true); } - }); + }; + if (this.isZonelessChangeDetection()) { + work(); + } else { + this.zone.run(work); + } }); this.pipeTriggerNotifier.pipe(takeUntil(this.destroy$)).subscribe(() => this.pipeTrigger++); From 8eb12c7fd2b74ff117099a9127faa640755d4ebc Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Fri, 12 Jun 2026 09:33:26 +0300 Subject: [PATCH 2/3] fix(for-of): improve zoneless change detection handling in IgxForOfDirective --- .../src/directives/for-of/for_of.directive.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts index fd0715a571e..cf52d57a1be 100644 --- a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts +++ b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.ts @@ -857,6 +857,10 @@ export class IgxForOfDirective extends IgxForOfToken extends IgxForOfToken { afterNextRender({ write: () => { this.dc.instance._viewContainer.element.nativeElement.style.transform = `translateY(${-scrollOffset}px)`; }, - read: isZoneless ? () => { + mixedReadWrite: isZoneless ? () => { this.recalcUpdateSizes(); } : undefined }); @@ -1180,11 +1184,11 @@ export class IgxForOfDirective extends IgxForOfToken { afterNextRender({ - read: () => { + mixedReadWrite: () => { this.recalcUpdateSizes(); } }); @@ -1793,7 +1797,7 @@ export class IgxGridForOfDirective extends IgxForOfDirec } const prevState = Object.assign({}, this.state); const scrollOffset = this.fixedUpdateAllElements(this._virtScrollPosition); - const isZoneless = this._zone.constructor.name === 'NoopNgZone'; + const isZoneless = this.isZonelessChangeDetection(); runInInjectionContext(this._injector, () => { afterNextRender({ write: () => { @@ -1802,7 +1806,7 @@ export class IgxGridForOfDirective extends IgxForOfDirec this._zone.onStable.pipe(first()).subscribe(this.recalcUpdateSizes.bind(this, prevState)); } }, - read: isZoneless ? () => { + mixedReadWrite: isZoneless ? () => { this.recalcUpdateSizes(prevState); } : undefined }); From 438ff8559834b832b9a3616e21e12c7fca5ee9b8 Mon Sep 17 00:00:00 2001 From: Viktor Kombov Date: Fri, 12 Jun 2026 13:17:22 +0300 Subject: [PATCH 3/3] fix(for-of): improve scroll size recalculation tests --- .../for-of/for_of.directive.spec.ts | 25 +++++++++++++------ 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts index 921c021b94e..beaa8480d19 100644 --- a/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts +++ b/projects/igniteui-angular/directives/src/directives/for-of/for_of.directive.spec.ts @@ -1,6 +1,6 @@ import { AsyncPipe, NgClass, NgForOfContext } from '@angular/common'; import { AfterViewInit, ChangeDetectorRef, Component, Directive, Injectable, IterableDiffers, NgZone, OnInit, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef, DebugElement, Pipe, PipeTransform, inject, provideZonelessChangeDetection } from '@angular/core'; -import { TestBed, ComponentFixture, waitForAsync, fakeAsync, tick } from '@angular/core/testing'; +import { TestBed, ComponentFixture, waitForAsync } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { BehaviorSubject, Observable } from 'rxjs'; import { IForOfState, IgxForOfDirective } from './for_of.directive'; @@ -1231,30 +1231,41 @@ describe('IgxForOf directive -', () => { }); describe('zoneless', () => { - let fix: ComponentFixture; + let fix: ComponentFixture; beforeEach(async () => { TestBed.resetTestingModule(); await TestBed.configureTestingModule({ - imports: [VerticalVirtualComponent], + imports: [VirtualComponent], providers: [provideZonelessChangeDetection()] }).compileComponents(); - fix = TestBed.createComponent(VerticalVirtualComponent); + fix = TestBed.createComponent(VirtualComponent); dg.generateData(300, 5, fix.componentInstance); fix.detectChanges(); }); - it('should call recalcUpdateSizes after vertical scroll', fakeAsync(() => { + it('should call recalcUpdateSizes after vertical scroll', async () => { const virtDir = fix.componentInstance.parentVirtDir; const spy = spyOn(virtDir, 'recalcUpdateSizes').and.callThrough(); fix.componentInstance.scrollTop(300); - tick(100); + await wait(100); fix.detectChanges(); expect(spy).toHaveBeenCalled(); - })); + }); + + it('should call recalcUpdateSizes after horizontal scroll', async () => { + const childDirs = fix.componentInstance.childVirtDirs.toArray(); + const spy = spyOn(childDirs[0], 'recalcUpdateSizes').and.callThrough(); + + fix.componentInstance.scrollLeft(500); + await wait(100); + fix.detectChanges(); + + expect(spy).toHaveBeenCalled(); + }); });