diff --git a/README.md b/README.md index e0881664..39c8d05d 100644 --- a/README.md +++ b/README.md @@ -20,23 +20,8 @@ This package is a thin wrapper around [TinyMCE](https://github.com/tinymce/tinym |<= 8 |3.x | |< 5 | Not supported | -### Not yet Zoneless ( >=Angular v21 ) -* This wrapper still requires `zone.js` to ensure backward compatibility to older Angular versions. Therefore, if your application uses Angular v21 or higher, it needs to include `provideZoneDetection()` in its providers. - -```jsx -import { NgModule, provideZoneChangeDetection } from '@angular/core'; - -@NgModule({ - declarations: [ - // ... - ], - imports: [ - // ... - ], - providers: [ provideZoneChangeDetection() ], - bootstrap: [ AppComponent ] -}) -``` +### Zoneless Support ( >=Angular v19 ) +This wrapper supports Angular's zoneless change detection. No additional configuration is needed — the component works with both zone-based and zoneless applications. ### Issues diff --git a/angular.json b/angular.json index 5bcbb5dc..64492bbe 100644 --- a/angular.json +++ b/angular.json @@ -21,6 +21,7 @@ "configDir": ".storybook", "browserTarget": "angular:build", "compodoc": false, + "experimentalZoneless": true, "port": 9001 } }, @@ -30,7 +31,8 @@ "configDir": ".storybook", "browserTarget": "angular:build", "compodoc": false, - "outputDir": "storybook-static" + "outputDir": "storybook-static", + "experimentalZoneless": true } } } diff --git a/package.json b/package.json index 9f7ad2ee..c10c318f 100644 --- a/package.json +++ b/package.json @@ -71,8 +71,7 @@ "to-string-loader": "^1.1.5", "tslib": "^2.6.2", "typescript": "^5.9.3", - "webpack": "^5.95.0", - "zone.js": "~0.16.0" + "webpack": "^5.95.0" }, "version": "9.1.2-rc", "name": "@tinymce/tinymce-angular", diff --git a/stories/Editor.stories.ts b/stories/Editor.stories.ts index 87db2d26..0429d627 100644 --- a/stories/Editor.stories.ts +++ b/stories/Editor.stories.ts @@ -30,7 +30,7 @@ export const IframeStory: StoryObj = { initialValue: sampleContent, init: { height: 300, - plugins: 'help', + plugins: 'help code', }, } }; diff --git a/tinymce-angular-component/src/main/ts/editor/editor.component.ts b/tinymce-angular-component/src/main/ts/editor/editor.component.ts index 2aff2f9b..6b94a1fb 100644 --- a/tinymce-angular-component/src/main/ts/editor/editor.component.ts +++ b/tinymce-angular-component/src/main/ts/editor/editor.component.ts @@ -2,18 +2,17 @@ import { isPlatformBrowser, CommonModule } from '@angular/common'; import { AfterViewInit, + ChangeDetectionStrategy, + ChangeDetectorRef, Component, ElementRef, forwardRef, Inject, + InjectionToken, Input, - NgZone, OnDestroy, - PLATFORM_ID, - InjectionToken, Optional, - ChangeDetectorRef, - ChangeDetectionStrategy + PLATFORM_ID, } from '@angular/core'; import { FormsModule, ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; import { Subject, takeUntil } from 'rxjs'; @@ -34,7 +33,7 @@ const EDITOR_COMPONENT_VALUE_ACCESSOR = { multi: true }; -export type Version = `${'4' | '5' | '6' | '7' | '8'}${'' | '-dev' | '-testing' | `.${number}` | `.${number}.${number}`}`; +export type Version = `${'5' | '6' | '7' | '8'}${'' | '-dev' | '-testing' | `.${number}` | `.${number}.${number}`}`; @Component({ selector: 'editor', @@ -97,8 +96,6 @@ export class EditorComponent extends Events implements AfterViewInit, ControlVal return this._editor; } - public ngZone: NgZone; - private _elementRef: ElementRef; private _element?: HTMLElement; private _disabled?: boolean; @@ -112,14 +109,12 @@ export class EditorComponent extends Events implements AfterViewInit, ControlVal public constructor( elementRef: ElementRef, - ngZone: NgZone, private cdRef: ChangeDetectorRef, @Inject(PLATFORM_ID) private platformId: object, @Optional() @Inject(TINYMCE_SCRIPT_SRC) private tinymceScriptSrc?: string ) { super(); this._elementRef = elementRef; - this.ngZone = ngZone; } public writeValue(value: string | null): void { @@ -222,9 +217,7 @@ export class EditorComponent extends Events implements AfterViewInit, ControlVal this._element.style.visibility = ''; } - this.ngZone.runOutsideAngular(() => { - getTinymce().init(finalInit); - }); + getTinymce().init(finalInit); }; private getScriptSrc() { @@ -236,24 +229,22 @@ export class EditorComponent extends Events implements AfterViewInit, ControlVal private initEditor(editor: TinyMCEEditor) { listenTinyMCEEvent(editor, 'blur', this.destroy$).subscribe(() => { this.cdRef.markForCheck(); - this.ngZone.run(() => this.onTouchedCallback()); + this.onTouchedCallback(); }); listenTinyMCEEvent(editor, this.modelEvents, this.destroy$).subscribe(() => { this.cdRef.markForCheck(); - this.ngZone.run(() => this.emitOnChange(editor)); + this.emitOnChange(editor); }); if (typeof this.initialValue === 'string') { - this.ngZone.run(() => { - editor.setContent(this.initialValue as string); - if (editor.getContent() !== this.initialValue) { - this.emitOnChange(editor); - } - if (this.onInitNgModel !== undefined) { - this.onInitNgModel.emit(editor as unknown as EventObj); - } - }); + editor.setContent(this.initialValue as string); + if (editor.getContent() !== this.initialValue) { + this.emitOnChange(editor); + } + if (this.onInitNgModel !== undefined) { + this.onInitNgModel.emit(editor as unknown as EventObj); + } } } diff --git a/tinymce-angular-component/src/main/ts/utils/Utils.ts b/tinymce-angular-component/src/main/ts/utils/Utils.ts index 7776e6f6..83a6dd1e 100644 --- a/tinymce-angular-component/src/main/ts/utils/Utils.ts +++ b/tinymce-angular-component/src/main/ts/utils/Utils.ts @@ -28,13 +28,12 @@ const bindHandlers = (ctx: EditorComponent, editor: any, destroy$: Subject const eventEmitter: EventEmitter = ctx[eventName]; listenTinyMCEEvent(editor, eventName.substring(2), destroy$).subscribe((event) => { - // Caretaker note: `ngZone.run()` runs change detection since it notifies the forked Angular zone that it's - // being re-entered. We don't want to run `ApplicationRef.tick()` if anyone listens to the specific event - // within the template. E.g. if the `onSelectionChange` is not listened within the template like: + // Caretaker note: We only emit if the event emitter is observed to avoid scheduling unnecessary change + // detection runs. E.g. if `onSelectionChange` is not bound in the template like: // `` - // then it won't be "observed", and we won't run "dead" change detection. + // then it won't be "observed" and we can skip emmitting the event if (isObserved(eventEmitter)) { - ctx.ngZone.run(() => eventEmitter.emit({ event, editor })); + eventEmitter.emit({ event, editor }); } }); }); diff --git a/tinymce-angular-component/src/test/ts/alien/InitTestEnvironment.ts b/tinymce-angular-component/src/test/ts/alien/InitTestEnvironment.ts index ef30f93d..ff22163b 100644 --- a/tinymce-angular-component/src/test/ts/alien/InitTestEnvironment.ts +++ b/tinymce-angular-component/src/test/ts/alien/InitTestEnvironment.ts @@ -1,13 +1,11 @@ import 'core-js/features/reflect'; -import 'zone.js'; -import 'zone.js/plugins/fake-async-test'; import { TestBed } from '@angular/core/testing'; import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing'; -import { NgModule, provideZoneChangeDetection } from '@angular/core'; +import { NgModule, provideZonelessChangeDetection } from '@angular/core'; @NgModule({ - providers: [ provideZoneChangeDetection() ], + providers: [ provideZonelessChangeDetection() ], }) class AppTestingModule {} diff --git a/tinymce-angular-component/src/test/ts/alien/TestHooks.ts b/tinymce-angular-component/src/test/ts/alien/TestHooks.ts index 262e5e57..785ea916 100644 --- a/tinymce-angular-component/src/test/ts/alien/TestHooks.ts +++ b/tinymce-angular-component/src/test/ts/alien/TestHooks.ts @@ -72,7 +72,7 @@ export const editorHook = (component: Type, moduleDef: TestModul return firstValueFrom( editorComponent.onInit.pipe( - throwTimeout(10000, `Timed out waiting for editor to load`), + throwTimeout(15000, `Timed out waiting for editor to load`), switchMap( ({ editor }) => new Promise((resolve) => { @@ -85,10 +85,14 @@ export const editorHook = (component: Type, moduleDef: TestModul // after global tinymce is removed in a clean up. Specifically, it happens when unloading/loading different versions of TinyMCE if (editor.licenseKeyManager) { editor.licenseKeyManager.validate({}).then(() => { - resolve(editor as Editor); + setTimeout(() => { + resolve(editor as Editor); + }, 500); }).catch((reason) => console.warn(reason)); } else { - resolve(editor as Editor); + setTimeout(() => { + resolve(editor as Editor); + }, 500); } }); }) diff --git a/tinymce-angular-component/src/test/ts/browser/EventBlacklistingTest.ts b/tinymce-angular-component/src/test/ts/browser/EventBlacklistingTest.ts index ffcc2c68..b3351198 100644 --- a/tinymce-angular-component/src/test/ts/browser/EventBlacklistingTest.ts +++ b/tinymce-angular-component/src/test/ts/browser/EventBlacklistingTest.ts @@ -4,19 +4,13 @@ import { describe, it } from '@ephox/bedrock-client'; import { EditorComponent } from '../../../main/ts/public_api'; import { eachVersionContext, editorHook } from '../alien/TestHooks'; -import { map, merge, timer, first, buffer, Observable, tap, firstValueFrom } from 'rxjs'; -import { NgZone } from '@angular/core'; +import { map, merge, timer, first, buffer, firstValueFrom } from 'rxjs'; import { Assertions } from '@ephox/agar'; import { Fun } from '@ephox/katamari'; -import { throwTimeout } from '../alien/TestHelpers'; +import { supportedTinymceVersions, throwTimeout } from '../alien/TestHelpers'; describe('EventBlacklistingTest', () => { - const shouldRunInAngularZone = (source: Observable) => - source.pipe( - tap(() => Assertions.assertEq('Subscribers to events should run within NgZone', true, NgZone.isInAngularZone())) - ); - - eachVersionContext([ '4', '5', '6', '7', '8' ], () => { + eachVersionContext(supportedTinymceVersions(), () => { const createFixture = editorHook(EditorComponent); it('Events should be bound when allowed', async () => { @@ -27,9 +21,9 @@ describe('EventBlacklistingTest', () => { const pEventsCompleted = firstValueFrom( merge( - fixture.editorComponent.onKeyUp.pipe(map(Fun.constant('onKeyUp')), shouldRunInAngularZone), - fixture.editorComponent.onKeyDown.pipe(map(Fun.constant('onKeyDown')), shouldRunInAngularZone), - fixture.editorComponent.onClick.pipe(map(Fun.constant('onClick')), shouldRunInAngularZone) + fixture.editorComponent.onKeyUp.pipe(map(Fun.constant('onKeyUp'))), + fixture.editorComponent.onKeyDown.pipe(map(Fun.constant('onKeyDown'))), + fixture.editorComponent.onClick.pipe(map(Fun.constant('onClick'))) ).pipe(throwTimeout(10000, 'Timed out waiting for some event to fire'), buffer(timer(100)), first()) ); fixture.editor.fire('keydown'); diff --git a/tinymce-angular-component/src/test/ts/browser/FormControlTest.ts b/tinymce-angular-component/src/test/ts/browser/FormControlTest.ts index 720813d8..d20db257 100644 --- a/tinymce-angular-component/src/test/ts/browser/FormControlTest.ts +++ b/tinymce-angular-component/src/test/ts/browser/FormControlTest.ts @@ -10,7 +10,7 @@ import { eachVersionContext, editorHook, fixtureHook } from '../alien/TestHooks' import { By } from '@angular/platform-browser'; import { first, firstValueFrom, switchMap } from 'rxjs'; import type { Editor } from 'tinymce'; -import { fakeTypeInEditor } from '../alien/TestHelpers'; +import { fakeTypeInEditor, supportedTinymceVersions } from '../alien/TestHelpers'; type FormControlProps = Partial>; @@ -21,7 +21,7 @@ describe('FormControlTest', () => { } }; - eachVersionContext([ '4', '5', '6', '7', '8' ], () => { + eachVersionContext(supportedTinymceVersions(), () => { [ ChangeDetectionStrategy.Default, ChangeDetectionStrategy.OnPush ].forEach((changeDetection) => { context(`[formControl] with change detection: ${changeDetection}`, () => { @Component({ diff --git a/tinymce-angular-component/src/test/ts/browser/NgModelTest.ts b/tinymce-angular-component/src/test/ts/browser/NgModelTest.ts index 79323fc5..1d0bb1e1 100644 --- a/tinymce-angular-component/src/test/ts/browser/NgModelTest.ts +++ b/tinymce-angular-component/src/test/ts/browser/NgModelTest.ts @@ -8,14 +8,14 @@ import { describe, it } from '@ephox/bedrock-client'; import { EditorComponent } from '../../../main/ts/editor/editor.component'; import { eachVersionContext, editorHook } from '../alien/TestHooks'; -import { fakeTypeInEditor } from '../alien/TestHelpers'; +import { fakeTypeInEditor, supportedTinymceVersions } from '../alien/TestHelpers'; describe('NgModelTest', () => { const assertNgModelState = (prop: 'valid' | 'pristine' | 'touched', expected: boolean, ngModel: NgModel) => { Assertions.assertEq('assert ngModel ' + prop + ' state', expected, ngModel[prop]); }; - eachVersionContext([ '4', '5', '6', '7', '8' ], () => { + eachVersionContext(supportedTinymceVersions(), () => { @Component({ standalone: true, imports: [ EditorComponent, FormsModule ], diff --git a/tinymce-angular-component/src/test/ts/browser/NgZoneTest.ts b/tinymce-angular-component/src/test/ts/browser/NgZoneTest.ts deleted file mode 100644 index fe41e20b..00000000 --- a/tinymce-angular-component/src/test/ts/browser/NgZoneTest.ts +++ /dev/null @@ -1,42 +0,0 @@ -import '../alien/InitTestEnvironment'; - -import { NgZone } from '@angular/core'; -import { Assertions } from '@ephox/agar'; -import { describe, it } from '@ephox/bedrock-client'; - -import { EditorComponent } from '../../../main/ts/editor/editor.component'; -import { eachVersionContext, fixtureHook } from '../alien/TestHooks'; -import { first } from 'rxjs'; -import { throwTimeout } from '../alien/TestHelpers'; - -describe('NgZoneTest', () => { - eachVersionContext([ '4', '5', '6', '7', '8' ], () => { - const createFixture = fixtureHook(EditorComponent, { imports: [ EditorComponent ] }); - - it('Subscribers to events should run within NgZone', async () => { - const fixture = createFixture(); - const editor = fixture.componentInstance; - fixture.detectChanges(); - await new Promise((resolve) => { - editor.onInit.pipe(first(), throwTimeout(10000, 'Timed out waiting for init event')).subscribe(() => { - Assertions.assertEq('Subscribers to onInit should run within NgZone', true, NgZone.isInAngularZone()); - resolve(); - }); - }); - }); - - // Lets just test one EventEmitter, if one works all should work - it('Subscribers to onKeyUp should run within NgZone', async () => { - const fixture = createFixture(); - const editor = fixture.componentInstance; - fixture.detectChanges(); - await new Promise((resolve) => { - editor.onKeyUp.pipe(first(), throwTimeout(10000, 'Timed out waiting for key up event')).subscribe(() => { - Assertions.assertEq('Subscribers to onKeyUp should run within NgZone', true, NgZone.isInAngularZone()); - resolve(); - }); - editor.editor?.fire('keyup'); - }); - }); - }); -}); diff --git a/tinymce-angular-component/src/test/ts/browser/PropTest.ts b/tinymce-angular-component/src/test/ts/browser/PropTest.ts index 9aaae3c5..4a2a7dc0 100644 --- a/tinymce-angular-component/src/test/ts/browser/PropTest.ts +++ b/tinymce-angular-component/src/test/ts/browser/PropTest.ts @@ -5,7 +5,7 @@ import { context, describe, it } from '@ephox/bedrock-client'; import { EditorComponent } from '../../../main/ts/public_api'; import { eachVersionContext, fixtureHook } from '../alien/TestHooks'; -import { captureLogs, throwTimeout } from '../alien/TestHelpers'; +import { captureLogs, supportedTinymceVersions, throwTimeout } from '../alien/TestHelpers'; import { concatMap, distinct, firstValueFrom, mergeMap, of, toArray } from 'rxjs'; import { ComponentFixture } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; @@ -42,7 +42,7 @@ describe('PropTest', () => { ) ); - eachVersionContext([ '4', '5', '6', '7', '8' ], () => { + eachVersionContext(supportedTinymceVersions(), () => { context('Single editor with ID', () => { @Component({ standalone: true, diff --git a/yarn.lock b/yarn.lock index f2a4697d..a90c6749 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13914,8 +13914,3 @@ zone.js@~0.10.3: version "0.10.3" resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.10.3.tgz#3e5e4da03c607c9dcd92e37dd35687a14a140c16" integrity sha512-LXVLVEq0NNOqK/fLJo3d0kfzd4sxwn2/h67/02pjCjfKDxgx1i9QqpvtHD8CrBnSSwMw5+dy11O7FRX5mkO7Cg== - -zone.js@~0.16.0: - version "0.16.0" - resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.16.0.tgz#955b9e28846d76ca2ef7091c8f9e96f35f79e828" - integrity sha512-LqLPpIQANebrlxY6jKcYKdgN5DTXyyHAKnnWWjE5pPfEQ4n7j5zn7mOEEpwNZVKGqx3kKKmvplEmoBrvpgROTA==