Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
b15b1f7
Angular: zoneless tests and core components fixes
vorobey Oct 19, 2026
ada4f12
Angular: added changes for changeDetectorRef
vorobey Oct 19, 2026
4abc728
Angular: don't run change detection on hover
vorobey Oct 19, 2026
33e4d7e
Merge branch 'feature/angular-zoneless-support' into feature/angular-…
vorobey Oct 19, 2026
5a6d36a
Angular: remove ngZone from constructor
vorobey Oct 19, 2026
3dd73f0
Revert "Angular: remove ngZone from constructor"
vorobey Oct 20, 2026
a06a0b1
Angular: fixes comments from PR review
vorobey Oct 20, 2026
69266d8
Angular: added test for optionChanged case
vorobey Oct 20, 2026
eb6ef1b
Angular: trigger mark for change by calling public options
vorobey Oct 22, 2026
33985bd
Disable no-underscore-dangle rule in ESLint config
vorobey Oct 22, 2026
abdcdc7
Merge branch 'feature/angular-zoneless-support' into feature/angular-…
vorobey Oct 22, 2026
cfcf7c7
Angular: dont pass changeDetectorRef to nested event strategy
vorobey Oct 26, 2026
700adf6
Angular: fix for form changes
vorobey Oct 26, 2026
ff8ea42
Angular: fix outside angular excessive renders
vorobey Oct 26, 2026
8a2dd5a
Angular: add listen readable argument list
vorobey Oct 26, 2026
d3222d8
Angular: remove getPublic options gate, add change detec to forms
vorobey Oct 27, 2026
ead21b8
Merge branch 'feature/angular-zoneless-support' into feature/angular-…
vorobey Oct 27, 2026
c9f5ebe
Angular: add new public event check, rename integration unclear vars
vorobey Oct 27, 2026
6a4ef32
Angular: replace counter with boolean in integration outside angular …
vorobey Oct 27, 2026
8ed1786
Angular: fix regression bug with excessive fireEvent ticks
vorobey Oct 27, 2026
54078a0
Angular: fix extra mark for check for form update in zoneless
vorobey Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/devextreme-angular/.gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
metadata
Chrome_Headless**
2 changes: 1 addition & 1 deletion packages/devextreme-angular/eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export default [
rules: {
'import/no-cycle': 'warn',
'no-param-reassign': 'warn',
'no-underscore-dangle': 'warn',
'no-underscore-dangle': 'off',
'spellcheck/spell-checker': [1, {
lang: 'en_US',
comments: false,
Expand Down
3 changes: 0 additions & 3 deletions packages/devextreme-angular/karma.common.test.shim.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,4 @@ require('core-js/es6');
require('core-js/client/shim.min');
require('reflect-metadata');

require('zone.js');
require('zone.js/testing');

__karma__.loaded = function () {};
2 changes: 1 addition & 1 deletion packages/devextreme-angular/karma.hydration.test.shim.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require('./karma.common.test.shim');
require('./karma.zone-polyfills.test.shim');

const testing = require('@angular/core/testing');
const server = require('@angular/platform-server/testing');
Expand Down
2 changes: 1 addition & 1 deletion packages/devextreme-angular/karma.server.test.shim.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require('./karma.common.test.shim');
require('./karma.zone-polyfills.test.shim');

const testing = require('@angular/core/testing');
const server = require('@angular/platform-server/testing');
Expand Down
4 changes: 2 additions & 2 deletions packages/devextreme-angular/karma.test.shim.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
require('./karma.common.test.shim');
require('./karma.zone-polyfills.test.shim');

const testing = require('@angular/core/testing');
const browser = require('@angular/platform-browser-dynamic/testing');
Expand All @@ -9,6 +9,6 @@ testing.TestBed.initTestEnvironment(
browser.platformBrowserDynamicTesting(),
);

const context = require.context('./tests/dist', true, /^.\/(?!.*\/(ssr-components|hydration)\.spec\.js$).*\.spec\.js$/);
const context = require.context('./tests/dist', true, /^.\/(?!zone-modes\/)(?!.*\/(ssr-components|hydration)\.spec\.js$).*\.spec\.js$/);
context.keys().map(context);
__karma__.start();
4 changes: 4 additions & 0 deletions packages/devextreme-angular/karma.zone-polyfills.test.shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
require('./karma.common.test.shim');

require('zone.js');
require('zone.js/testing');
14 changes: 14 additions & 0 deletions packages/devextreme-angular/karma.zone.test.shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
require('./karma.zone-polyfills.test.shim');

const testing = require('@angular/core/testing');
const browser = require('@angular/platform-browser-dynamic/testing');
const { ZoneTestingModule } = require('./karma.zone-testing.module');

testing.TestBed.initTestEnvironment(
[browser.BrowserDynamicTestingModule, ZoneTestingModule],
browser.platformBrowserDynamicTesting(),
);

const context = require.context('./tests/dist/zone-modes', true, /\.spec\.js$/);
context.keys().map(context);
__karma__.start();
7 changes: 7 additions & 0 deletions packages/devextreme-angular/karma.zoneless-testing.module.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
const { NgModule, provideZonelessChangeDetection } = require('@angular/core');

const ZonelessTestingModule = NgModule({
providers: [provideZonelessChangeDetection()],
})(class ZonelessTestingModule {});

module.exports = { ZonelessTestingModule };
18 changes: 18 additions & 0 deletions packages/devextreme-angular/karma.zoneless.test.shim.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require('./karma.common.test.shim');

if (typeof Zone !== 'undefined') {
throw new Error('zone.js is loaded in the zoneless test environment');
}

const testing = require('@angular/core/testing');
const browser = require('@angular/platform-browser-dynamic/testing');
const { ZonelessTestingModule } = require('./karma.zoneless-testing.module');

testing.TestBed.initTestEnvironment(
[browser.BrowserDynamicTestingModule, ZonelessTestingModule],
browser.platformBrowserDynamicTesting(),
);

const context = require.context('./tests/dist/zone-modes', true, /\.spec\.js$/);
context.keys().map(context);
__karma__.start();
57 changes: 55 additions & 2 deletions packages/devextreme-angular/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -350,7 +350,9 @@
"environments": [
"client",
"server",
"hydration"
"hydration",
"zone",
"zoneless"
]
},
"dependsOn": [
Expand Down Expand Up @@ -428,6 +430,50 @@
"{projectRoot}/tests/dist/**/*"
]
},
"test:components-zone-modes": {
"executor": "devextreme-nx-infra-plugin:karma-multi-env",
"options": {
"karmaConfig": "karma.conf.js",
"environments": [
"zone",
"zoneless"
]
},
"dependsOn": [
"build:tests"
],
"inputs": [
"default",
"test",
"{projectRoot}/tests/dist/**/*"
]
},
"test:components-zone-debug": {
"executor": "devextreme-nx-infra-plugin:karma-multi-env",
"options": {
"karmaConfig": "karma.conf.js",
"environments": [
"zone"
],
"debug": true
},
"dependsOn": [
"build:tests"
]
},
"test:components-zoneless-debug": {
"executor": "devextreme-nx-infra-plugin:karma-multi-env",
"options": {
"karmaConfig": "karma.conf.js",
"environments": [
"zoneless"
],
"debug": true
},
"dependsOn": [
"build:tests"
]
},
"watch:test": {
"executor": "devextreme-nx-infra-plugin:karma-multi-env",
"options": {
Expand All @@ -453,7 +499,9 @@
"environments": [
"client",
"server",
"hydration"
"hydration",
"zone",
"zoneless"
]
},
"dependsOn": [
Expand Down Expand Up @@ -498,7 +546,12 @@
"{projectRoot}/karma.test.shim.js",
"{projectRoot}/karma.server.test.shim.js",
"{projectRoot}/karma.hydration.test.shim.js",
"{projectRoot}/karma.common.test.shim.js",
"{projectRoot}/karma.zone-polyfills.test.shim.js",
"{projectRoot}/karma.zone.test.shim.js",
"{projectRoot}/karma.zoneless.test.shim.js",
"{projectRoot}/karma.zone-testing.module.js",
"{projectRoot}/karma.zoneless-testing.module.js",
"{projectRoot}/webpack.test.js",
"{projectRoot}/tsconfig.tests.json",
"{projectRoot}/src/**/*.spec.ts",
Expand Down
21 changes: 18 additions & 3 deletions packages/devextreme-angular/src/core/component.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/* eslint-disable max-classes-per-file */
import {
TransferState,
makeStateKey,
ChangeDetectorRef,
Component,
ElementRef,
NgZone,
Expand Down Expand Up @@ -39,7 +41,7 @@ import {
CollectionNestedOptionContainerImpl,
} from './nested-option';

import { DxIntegrationModule } from './integration';
import { DxIntegrationModule, canMarkForCheck, isInOutsideAngularHandler } from './integration';

config({
buyNowLink: 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeAngular.aspx',
Expand Down Expand Up @@ -86,6 +88,8 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo

templateUpdateRequired = false;

private readonly changeDetectorRef = inject(ChangeDetectorRef);

private _updateTemplates() {
if (this.templates.length && this.templateUpdateRequired) {
const updatedTemplates = {};
Expand All @@ -105,6 +109,15 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo
const value = e.name === e.fullName ? e.value : e.component.option(e.name);
this.eventHelper.fireNgEvent(`${e.name}Change`, [value]);
this.optionChangedHandlers.emit(e);

if (
!this.widgetUpdateLocked
&& !isInOutsideAngularHandler()
&& this[`${e.name}Change`]
&& canMarkForCheck(this.ngZone)
) {
this.changeDetectorRef.markForCheck();
}
});
}

Expand All @@ -122,10 +135,11 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo

protected _createEventEmitters(events) {
const zone = this.ngZone;
const { changeDetectorRef } = this;
this.eventHelper.createEmitters(events);

this._initialOptions.eventsStrategy = (instance) => {
const strategy = new NgEventsStrategy(instance, zone);
const strategy = new NgEventsStrategy(instance, zone, changeDetectorRef);

events.filter((event) => event.subscribe).forEach((event) => {
strategy.addEmitter(event.subscribe, this[event.emit]);
Expand Down Expand Up @@ -166,6 +180,7 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo
if (!this.widgetUpdateLocked && this.instance) {
this.instance.beginUpdate();
this.widgetUpdateLocked = true;
this.changeDetectorRef.markForCheck();
}
}

Expand Down Expand Up @@ -237,7 +252,7 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo
this.templates = [];
templateHost.setHost(this);
this._collectionContainerImpl = new CollectionNestedOptionContainerImpl(this._setOption.bind(this));
this.eventHelper = new EmitterHelper(ngZone, this);
this.eventHelper = new EmitterHelper(ngZone, this, this.changeDetectorRef);
}

ngOnChanges(changes: SimpleChanges) {
Expand Down
25 changes: 21 additions & 4 deletions packages/devextreme-angular/src/core/events-strategy.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { EventEmitter, NgZone } from '@angular/core';
/* eslint-disable max-classes-per-file */
import { ChangeDetectorRef, EventEmitter, NgZone } from '@angular/core';
import { DxComponent } from './component';
import { canMarkForCheck } from './integration';

interface IEventSubscription {
handler: any;
Expand All @@ -10,7 +12,11 @@ export class NgEventsStrategy {

private events: { [key: string]: EventEmitter<any> } = {};

constructor(private readonly instance: any, private readonly zone: NgZone) { }
constructor(
private readonly instance: any,
private readonly zone: NgZone,
private readonly changeDetectorRef?: ChangeDetectorRef,
) { }

hasEvent(name: string) {
return this.getEmitter(name).observers.length !== 0;
Expand All @@ -22,8 +28,14 @@ export class NgEventsStrategy {
const internalSubs = this.subscriptions[name] || [];
if (internalSubs.length === emitter.observers.length) {
emitter.next(args?.[0]);
if (name !== 'optionChanged' && canMarkForCheck(this.zone)) {
this.changeDetectorRef?.markForCheck();
}
} else {
this.zone.run(() => emitter.next(args?.[0]));
this.zone.run(() => {
emitter.next(args?.[0]);
this.changeDetectorRef?.markForCheck();
});
}
}
}
Expand Down Expand Up @@ -80,7 +92,11 @@ export class NgEventsStrategy {
export class EmitterHelper {
lockedValueChangeEvent = false;

constructor(private readonly zone: NgZone, private component: DxComponent) { }
constructor(
private readonly zone: NgZone,
private component: DxComponent,
private readonly changeDetectorRef?: ChangeDetectorRef,
) { }

fireNgEvent(eventName: string, eventArgs: any) {
if (this.lockedValueChangeEvent && eventName === 'valueChange') {
Expand All @@ -90,6 +106,7 @@ export class EmitterHelper {
if (emitter?.observers.length) {
this.zone.run(() => {
emitter.next(eventArgs?.[0]);
this.changeDetectorRef?.markForCheck();
});
}
}
Expand Down
39 changes: 27 additions & 12 deletions packages/devextreme-angular/src/core/integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,15 @@ import domAdapter from 'devextreme/core/dom_adapter';
import readyCallbacks from 'devextreme/core/utils/ready_callbacks';
import eventsEngine from 'devextreme/common/core/events/core/events_engine';

const outsideZoneEvents = ['mousemove', 'mouseover', 'mouseout'];
const insideZoneEvents = ['mouseup', 'click', 'mousedown', 'transitionend', 'wheel'];
const outsideAngularEvents = ['mousemove', 'mouseover', 'mouseout'];
const insideAngularEvents = ['mouseup', 'click', 'mousedown', 'transitionend', 'wheel'];

let inOutsideAngularHandler = false;

export const isInOutsideAngularHandler = () => inOutsideAngularHandler;

export const isZoneless = (ngZone: NgZone) => !(ngZone instanceof NgZone);
export const canMarkForCheck = (ngZone: NgZone) => isZoneless(ngZone) || NgZone.isInAngularZone();

type ReadyCallback = () => void;
type ReadyCallbackAdd = (callback: ReadyCallback) => void;
Expand Down Expand Up @@ -42,29 +49,37 @@ let doInjections = (document: any, ngZone: NgZone, xhrFactory: XhrFactory) => {
domAdapter.inject({
_document: document,

listen(this: { callBase: (...args: any[]) => any }, ...args) {
const eventName = args[1];
if (outsideZoneEvents.includes(eventName)) {
return ngZone.runOutsideAngular(() => this.callBase.apply(this, args));
listen(this: { callBase: (...args: any[]) => any }, element, event: string, callback, options) {
if (outsideAngularEvents.includes(event)) {
const trackedCallback = function (this: unknown, e: Event) {
const prevInOutsideAngularHandler = inOutsideAngularHandler;
inOutsideAngularHandler = true;
try {
return callback.call(this, e);
} finally {
inOutsideAngularHandler = prevInOutsideAngularHandler;
}
};
return ngZone.runOutsideAngular(() => this.callBase(element, event, trackedCallback, options));
}

if (ngZone.isStable && insideZoneEvents.includes(eventName)) {
return ngZone.run(() => this.callBase.apply(this, args));
if (ngZone.isStable && insideAngularEvents.includes(event)) {
return ngZone.run(() => this.callBase(element, event, callback, options));
}

return this.callBase.apply(this, args);
return this.callBase(element, event, callback, options);
},

isElementNode(element) {
return element && element.nodeType === 1;
return element?.nodeType === 1;
},

isTextNode(element) {
return element && element.nodeType === 3;
return element?.nodeType === 3;
},

isDocument(element) {
return element && element.nodeType === 9;
return element?.nodeType === 9;
},
});

Expand Down
1 change: 1 addition & 0 deletions packages/devextreme-angular/src/core/nested-option.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
/* eslint-disable max-classes-per-file */
import {
Component, QueryList, ElementRef, Renderer2, EventEmitter,
} from '@angular/core';
Expand Down
Loading
Loading