From cf12f51462143895e0dc733a8d9f462aa16cdc26 Mon Sep 17 00:00:00 2001 From: Martin Dragnev Date: Tue, 8 Sep 2026 12:28:49 +0300 Subject: [PATCH 1/4] test(elements): Add a test --- .../src/app/custom-strategy.spec.ts | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts index af2ee8404e2..4b4f3c9c7d9 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts @@ -333,5 +333,28 @@ describe('Elements: ', () => { expect(actionStrip.hidden).toBeTrue(); expect(actionStrip.isConnected).toBeTrue(); }); + + it('should trigger detectChanges after a method has been invoked', async () => { + const gridEl = document.createElement("igc-grid"); + const columnID = document.createElement("igc-column"); + columnID.setAttribute("field", "ProductID"); + gridEl.appendChild(columnID); + const columnName = document.createElement("igc-column"); + columnName.setAttribute("field", "ProductName"); + gridEl.appendChild(columnName); + + gridEl.data = SampleTestData.foodProductData(); + testContainer.appendChild(gridEl); + + await firstValueFrom(fromEvent(gridEl, "childrenResolved")); + await firstValueFrom(fromEvent(gridEl, "dataChanged")); + + gridEl.findNext("Ch", false ,false); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + gridEl.clearSearch(); + await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + const rows = gridEl.rowList.toArray(); + expect((rows[0].cells as any)!.first.nativeElement.children.length).toBe(1); + }); }); }); From f359f3060cc7377d9cecaff1f0a078445d31ed73 Mon Sep 17 00:00:00 2001 From: Martin Dragnev Date: Tue, 8 Sep 2026 12:47:59 +0300 Subject: [PATCH 2/4] fix(elements): manually trigger change detection in custom element methods --- .../src/app/create-custom-element.ts | 8 +++++++- .../src/app/custom-strategy.ts | 11 +++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular-elements/src/app/create-custom-element.ts b/projects/igniteui-angular-elements/src/app/create-custom-element.ts index 641b3e73e70..c23aa8c870c 100644 --- a/projects/igniteui-angular-elements/src/app/create-custom-element.ts +++ b/projects/igniteui-angular-elements/src/app/create-custom-element.ts @@ -23,7 +23,13 @@ export function createIgxCustomElement(component: Type, config: IgxNgEleme for (const method of componentConfig?.methods!) { elementCtor.prototype[method] = function() { const instance = this.ngElementStrategy.componentRef.instance; - return this.ngElementStrategy.runInZone(() => instance[method].apply(instance, arguments)); + return this.ngElementStrategy.runInZone(() => { + // Now when we removed the zone.js dependency, we must manually trigger change detection + // because of the nature of the browser event listeners without zone.js. + const result = instance[method].apply(instance, arguments); + this.ngElementStrategy.notifyChanges(); + return result; + }); } } diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.ts index d4754a47300..dc8e1b0845e 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.ts @@ -1,4 +1,4 @@ -import { ComponentRef, createComponent, DestroyRef, EventEmitter, Injector, QueryList, Type, ViewContainerRef, reflectComponentType } from '@angular/core'; +import { ComponentRef, createComponent, DestroyRef, EventEmitter, Injector, QueryList, Type, ViewContainerRef, reflectComponentType, ɵNotificationSource as NotificationSource, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { NgElement, NgElementStrategyEvent } from '@angular/elements'; import { fromEvent, Observable } from 'rxjs'; @@ -70,6 +70,13 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { super(_component, _injector, _inputMap); } + /** + * Expose a mechanism to manually schedule change detection for the component. + */ + public notifyChanges() { + (this as any).cdScheduler.notify(NotificationSource.CustomElement); + } + protected override async initializeComponent(element: HTMLElement) { if (!element.isConnected) { // D.P. 2022-09-20 do not initialize on connectedCallback that is not actually connected @@ -112,7 +119,7 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { } } // select closest of all possible config parents - let parent = parents[0]?.deref(); + const parent = parents[0]?.deref(); // Collected parents may include direct Angular HGrids, so only wait for configured parent elements: const configParent = configParents.find(x => x!.selector === parent?.tagName.toLocaleLowerCase()); From 876837e9c0e8df95b908b0baea9c574ae387815c Mon Sep 17 00:00:00 2001 From: Martin Dragnev Date: Tue, 8 Sep 2026 15:53:08 +0300 Subject: [PATCH 3/4] chore(*): update comments to accurately describe the behavior --- .../src/app/create-custom-element.ts | 6 ++++-- .../igniteui-angular-elements/src/app/custom-strategy.ts | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/projects/igniteui-angular-elements/src/app/create-custom-element.ts b/projects/igniteui-angular-elements/src/app/create-custom-element.ts index c23aa8c870c..9e7c53d009d 100644 --- a/projects/igniteui-angular-elements/src/app/create-custom-element.ts +++ b/projects/igniteui-angular-elements/src/app/create-custom-element.ts @@ -24,8 +24,10 @@ export function createIgxCustomElement(component: Type, config: IgxNgEleme elementCtor.prototype[method] = function() { const instance = this.ngElementStrategy.componentRef.instance; return this.ngElementStrategy.runInZone(() => { - // Now when we removed the zone.js dependency, we must manually trigger change detection - // because of the nature of the browser event listeners without zone.js. + // Angular normally wraps listeners and schedules change detection to preserve Zone.js behavior. + // Like Angular Elements' setInputValue, we notify the scheduler explicitly because custom-element methods bypass that listener path. + // This behavior may change in a future Angular version. + // https://github.com/angular/angular/blob/9a58353b1b680f162a55969965ae6a90ae20316d/packages/core/src/change_detection/scheduling/zoneless_scheduling_impl.ts#L140 const result = instance[method].apply(instance, arguments); this.ngElementStrategy.notifyChanges(); return result; diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.ts index dc8e1b0845e..4f06b415140 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.ts @@ -71,6 +71,7 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { } /** + * @hidden @internal * Expose a mechanism to manually schedule change detection for the component. */ public notifyChanges() { From ec28d2c1bc8a06fed3295bdcad4b01928c9c373c Mon Sep 17 00:00:00 2001 From: Martin Dragnev Date: Tue, 8 Sep 2026 15:53:20 +0300 Subject: [PATCH 4/4] test(*): update test --- .../src/app/custom-strategy.spec.ts | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts index 4b4f3c9c7d9..d056d5c67af 100644 --- a/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts +++ b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts @@ -334,7 +334,8 @@ describe('Elements: ', () => { expect(actionStrip.isConnected).toBeTrue(); }); - it('should trigger detectChanges after a method has been invoked', async () => { + it('should update the UI correctly after invoking a method', async () => { + // Regression coverage for UI updates after removing the zone.js dependency. const gridEl = document.createElement("igc-grid"); const columnID = document.createElement("igc-column"); columnID.setAttribute("field", "ProductID"); @@ -349,12 +350,20 @@ describe('Elements: ', () => { await firstValueFrom(fromEvent(gridEl, "childrenResolved")); await firstValueFrom(fromEvent(gridEl, "dataChanged")); + const HIGHLIGHT_ACTIVE_CSS_CLASS = '.igx-highlight__active'; gridEl.findNext("Ch", false ,false); await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); + + // verify that a cell is highlighted + let highlightedCell = gridEl.querySelector(HIGHLIGHT_ACTIVE_CSS_CLASS); + expect(highlightedCell).not.toBeNull(); + gridEl.clearSearch(); await firstValueFrom(timer(10 /* SCHEDULE_DELAY */ * 2)); - const rows = gridEl.rowList.toArray(); - expect((rows[0].cells as any)!.first.nativeElement.children.length).toBe(1); + + // verify that no cell is highlighted after clearing the search + highlightedCell = gridEl.querySelector(HIGHLIGHT_ACTIVE_CSS_CLASS); + expect(highlightedCell).toBeNull(); }); }); });