Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 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
e77adea
Merge branch 'feature/angular-zoneless-support' into feature/angular-…
vorobey Oct 29, 2026
2e570c5
Angular: cleanup and small refactor for core changes
vorobey Oct 29, 2026
2bb167d
Angular: format method for readability
vorobey Oct 29, 2026
2e92906
Angular: small fix for condition order in component
vorobey Oct 29, 2026
dd1b79c
Angular: move zone helpers to a separate file
vorobey Oct 29, 2026
c495b37
Angular: remove unnecessary mark for checks
vorobey Oct 29, 2026
c93da72
Angular: move zone/zoneless logic to strategy
vorobey Oct 29, 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 @@ -59,7 +59,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
30 changes: 26 additions & 4 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 @@ -41,6 +43,8 @@ import {

import { DxIntegrationModule } from './integration';

import { createZoneMode, type ZoneMode } from './zone-mode';

config({
buyNowLink: 'https://go.devexpress.com/Licensing_Installer_Watermark_DevExtremeAngular.aspx',
licensingDocLink: 'https://www.devexpress.com/go/Licensing_Documentation_DevExpress_Angular.aspx',
Expand All @@ -66,6 +70,8 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo

private readonly _collectionContainerImpl: ICollectionNestedOptionContainer;

private readonly zoneMode: ZoneMode;

eventHelper: EmitterHelper;

optionChangedHandlers: EventEmitter<any> = new EventEmitter();
Expand All @@ -86,6 +92,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 +113,13 @@ 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
&& this[`${e.name}Change`]
) {
this.zoneMode.markForOptionChange();
}
});
}

Expand All @@ -121,11 +136,12 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo
}

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

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

events.filter((event) => event.subscribe).forEach((event) => {
strategy.addEmitter(event.subscribe, this[event.emit]);
Expand All @@ -136,12 +152,16 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo

this._initialOptions.nestedComponentOptions = function (component) {
return {
eventsStrategy: (instance) => new NgEventsStrategy(instance, zone),
eventsStrategy: (instance) => new NgEventsStrategy(instance, nestedZoneMode),
nestedComponentOptions: component.option('nestedComponentOptions'),
};
};
}

protected _markForCheck() {
this.zoneMode.markForCheck();
}

_shouldOptionChange(name: string, value: any) {
if (this.changedOptions.hasOwnProperty(name)) {
const prevValue = this.changedOptions[name];
Expand All @@ -166,6 +186,7 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo
if (!this.widgetUpdateLocked && this.instance) {
this.instance.beginUpdate();
this.widgetUpdateLocked = true;
this.zoneMode.markForCheck();
}
}

Expand Down Expand Up @@ -235,9 +256,10 @@ export abstract class DxComponent implements OnChanges, OnInit, DoCheck, AfterCo
}

this.templates = [];
this.zoneMode = createZoneMode(this.ngZone, this.changeDetectorRef);
templateHost.setHost(this);
this._collectionContainerImpl = new CollectionNestedOptionContainerImpl(this._setOption.bind(this));
this.eventHelper = new EmitterHelper(ngZone, this);
this.eventHelper = new EmitterHelper(this.zoneMode, this);
}

ngOnChanges(changes: SimpleChanges) {
Expand Down
35 changes: 22 additions & 13 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 { EventEmitter } from '@angular/core';
import { DxComponent } from './component';
import type { ZoneMode } from './zone-mode';

interface IEventSubscription {
handler: any;
Expand All @@ -10,21 +12,27 @@ 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 zoneMode: ZoneMode,
) { }

hasEvent(name: string) {
return this.getEmitter(name).observers.length !== 0;
}

fireEvent(name, args) {
const emitter = this.getEmitter(name);
if (emitter.observers.length) {
const internalSubs = this.subscriptions[name] || [];
if (internalSubs.length === emitter.observers.length) {
emitter.next(args?.[0]);
} else {
this.zone.run(() => emitter.next(args?.[0]));
}
if (!emitter.observers.length) {
return;
}
const internalSubs = this.subscriptions[name] || [];
const hasAngularSubscribers = emitter.observers.length > internalSubs.length;

if (hasAngularSubscribers) {
this.zoneMode.emit(emitter, args?.[0]);
} else {
this.zoneMode.emitInternal(emitter, args?.[0], name !== 'optionChanged');
}
}

Expand Down Expand Up @@ -80,17 +88,18 @@ export class NgEventsStrategy {
export class EmitterHelper {
lockedValueChangeEvent = false;

constructor(private readonly zone: NgZone, private component: DxComponent) { }
constructor(
private readonly zoneMode: ZoneMode,
private component: DxComponent,
) { }

fireNgEvent(eventName: string, eventArgs: any) {
if (this.lockedValueChangeEvent && eventName === 'valueChange') {
return;
}
const emitter = this.component[eventName];
if (emitter?.observers.length) {
this.zone.run(() => {
emitter.next(eventArgs?.[0]);
});
this.zoneMode.emit(emitter, eventArgs?.[0]);
}
}

Expand Down
Loading
Loading