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..9e7c53d009d 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,15 @@ 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(() => { + // 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.spec.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.spec.ts index af2ee8404e2..d056d5c67af 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,37 @@ describe('Elements: ', () => { expect(actionStrip.hidden).toBeTrue(); expect(actionStrip.isConnected).toBeTrue(); }); + + 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"); + 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")); + + 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)); + + // verify that no cell is highlighted after clearing the search + highlightedCell = gridEl.querySelector(HIGHLIGHT_ACTIVE_CSS_CLASS); + expect(highlightedCell).toBeNull(); + }); }); }); diff --git a/projects/igniteui-angular-elements/src/app/custom-strategy.ts b/projects/igniteui-angular-elements/src/app/custom-strategy.ts index d4754a47300..4f06b415140 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,14 @@ class IgxCustomNgElementStrategy extends ComponentNgElementStrategy { super(_component, _injector, _inputMap); } + /** + * @hidden @internal + * 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 +120,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());