diff --git a/src/app/app.component.html b/src/app/app.component.html index 60b25a8..ab4868b 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -7,6 +7,7 @@

CKEditor 5 integration with Angular

  • Integration with reactive forms (formControlName)
  • Integration with CKEditor Watchdog
  • Integration with CKEditor Context
  • +
  • Catching error when editor crashes during initialization
  • diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 29bb669..20c19e6 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -11,6 +11,7 @@ import { DemoFormComponent } from './demo-form/demo-form.component'; import { DemoReactiveFormComponent } from './demo-reactive-form/demo-reactive-form.component'; import { ContextDemoComponent } from './context-demo/context-demo'; import { WatchdogDemoComponent } from './watchdog-demo/watchdog-demo'; +import { InitializationCrashComponent } from './initialization-crash/initialization-crash.component'; const appRoutes: Routes = [ { path: '', redirectTo: '/simple-usage', pathMatch: 'full' }, @@ -18,7 +19,8 @@ const appRoutes: Routes = [ { path: 'forms', component: DemoFormComponent }, { path: 'reactive-forms', component: DemoReactiveFormComponent }, { path: 'watchdog', component: WatchdogDemoComponent }, - { path: 'simple-usage', component: SimpleUsageComponent } + { path: 'simple-usage', component: SimpleUsageComponent }, + { path: 'init-crash', component: InitializationCrashComponent } ]; @NgModule( { @@ -35,7 +37,8 @@ const appRoutes: Routes = [ DemoFormComponent, DemoReactiveFormComponent, SimpleUsageComponent, - WatchdogDemoComponent + WatchdogDemoComponent, + InitializationCrashComponent ], providers: [], bootstrap: [ AppComponent ] diff --git a/src/app/initialization-crash/initialization-crash.component.css b/src/app/initialization-crash/initialization-crash.component.css new file mode 100644 index 0000000..e69de29 diff --git a/src/app/initialization-crash/initialization-crash.component.html b/src/app/initialization-crash/initialization-crash.component.html new file mode 100644 index 0000000..b30ad53 --- /dev/null +++ b/src/app/initialization-crash/initialization-crash.component.html @@ -0,0 +1,30 @@ +

    Initialization crash demo

    +

    Without watchdog

    + + + + + + + + + + +

    An error has occurred, please contact us at: support@company.com to resolve the situation.

    +
    + +

    With watchdog

    + + + + + + + + + + +

    An error has occurred, please contact us at: support@company.com to resolve the situation.

    +
    diff --git a/src/app/initialization-crash/initialization-crash.component.ts b/src/app/initialization-crash/initialization-crash.component.ts new file mode 100644 index 0000000..a63be5e --- /dev/null +++ b/src/app/initialization-crash/initialization-crash.component.ts @@ -0,0 +1,64 @@ +import { Component, ViewChild } from '@angular/core'; +import { CKEditorComponent } from 'src/ckeditor'; +import AngularEditor from 'ckeditor/build/ckeditor'; +import type { ContextWatchdog } from '@ckeditor/ckeditor5-watchdog'; + +@Component( { + selector: 'app-initialization-crash', + templateUrl: './initialization-crash.component.html', + styleUrls: [ './initialization-crash.component.css' ] +} ) +export class InitializationCrashComponent { + public Editor = AngularEditor; + public EditorWatchdog = AngularEditor; + + @ViewChild( CKEditorComponent ) public ckeditor?: CKEditorComponent; + + public config: any; + public ready = false; + + public errorOccurred = false; + public errorOccurredWatchdog = false; + + public watchdog?: ContextWatchdog; + + public ngOnInit(): void { + const contextConfig: any = { + foo: 'bar' + }; + + this.config = { + extraPlugins: [ + function( editor: any ) { + editor.data.on( 'init', () => { + // Simulate an error. + // Create a non-existing position, then try to get its parent. + const position = editor.model.createPositionFromPath( editor.model.document.getRoot(), [ 1, 2, 3 ] ); + + return position.parent; + } ); + } + ], + collaboration: { + channelId: 'foobar-baz' + } + }; + + this.watchdog = new AngularEditor.ContextWatchdog( AngularEditor.Context ); + + this.watchdog.create( contextConfig ) + .then( () => { + this.ready = true; + } ); + } + + public onError( error: any ): void { + console.error( 'Editor without watchdog threw an error which was caught', error ); + this.errorOccurred = true; + } + + public onErrorWatchdog( error: any ): void { + console.error( 'Editor with watchdog threw an error which was caught', error ); + this.errorOccurredWatchdog = true; + } +} diff --git a/src/app/watchdog-demo/watchdog-demo.html b/src/app/watchdog-demo/watchdog-demo.html index 755c716..94d7970 100644 --- a/src/app/watchdog-demo/watchdog-demo.html +++ b/src/app/watchdog-demo/watchdog-demo.html @@ -1,7 +1,18 @@

    Watchdog demo

    - + - - + + + +

    Type '1' or '2' to crash the editor.

    + + + + +
    + + +

    An error has occurred, please contast us at: support@company.com to resolve the situation.

    +
    diff --git a/src/app/watchdog-demo/watchdog-demo.ts b/src/app/watchdog-demo/watchdog-demo.ts index 71a207f..0952d93 100644 --- a/src/app/watchdog-demo/watchdog-demo.ts +++ b/src/app/watchdog-demo/watchdog-demo.ts @@ -1,5 +1,4 @@ -import { Component, ElementRef, ViewChild } from '@angular/core'; -import { CKEditorComponent } from '../../ckeditor/ckeditor.component'; +import { Component } from '@angular/core'; import AngularEditor from '../../../ckeditor/build/ckeditor'; import type { ContextWatchdog } from '@ckeditor/ckeditor5-watchdog'; @@ -11,16 +10,31 @@ import type { ContextWatchdog } from '@ckeditor/ckeditor5-watchdog'; export class WatchdogDemoComponent { public Editor = AngularEditor; - @ViewChild( CKEditorComponent ) public ckeditor?: ElementRef; - public config: any; public watchdog?: ContextWatchdog; public ready = false; public isDisabled = false; + public errorOccurred = false; public onReady( editor: AngularEditor ): void { console.log( editor ); + + const inputCommand = editor.commands.get( 'input' )!; + + inputCommand.on( 'execute', ( evt, data ) => { + const commandArgs = data[ 0 ]; + + if ( commandArgs.text === '1' ) { + // Simulate an error. + throw new Error( 'a-custom-editor-error' ); + } + + if ( commandArgs.text === '2' ) { + // Simulate an error. + throw 'foobar'; + } + } ); } public ngOnInit(): void { @@ -45,4 +59,8 @@ export class WatchdogDemoComponent { public toggle(): void { this.isDisabled = !this.isDisabled; } + + public onError(): void { + this.errorOccurred = true; + } } diff --git a/src/ckeditor/ckeditor.component.spec.ts b/src/ckeditor/ckeditor.component.spec.ts index a4bbd22..ce5b8b3 100644 --- a/src/ckeditor/ckeditor.component.spec.ts +++ b/src/ckeditor/ckeditor.component.spec.ts @@ -491,6 +491,66 @@ describe( 'CKEditorComponent', () => { } ); } ); } ); + + describe( 'initialization errors are catched', () => { + let config: any; + + beforeEach( () => { + config = { + extraPlugins: [ + function( editor: any ) { + editor.data.on( 'init', () => { + // Simulate an error. + // Create a non-existing position, then try to get its parent. + const position = editor.model.createPositionFromPath( editor.model.document.getRoot(), [ 1, 2, 3 ] ); + + return position.parent; + } ); + } + ], + collaboration: { + channelId: 'foobar-baz' + } + }; + } ); + + it( 'when internal watchdog is created', async () => { + fixture = TestBed.createComponent( CKEditorComponent ); + const component = fixture.componentInstance; + const errorSpy = jasmine.createSpy( 'errorSpy' ); + component.error.subscribe( errorSpy ); + component.editor = AngularEditor; + component.config = config; + + fixture.detectChanges(); + await waitCycle(); + + expect( errorSpy ).toHaveBeenCalledTimes( 1 ); + + fixture.destroy(); + } ); + + it( 'when external watchdog is provided', async () => { + fixture = TestBed.createComponent( CKEditorComponent ); + const component = fixture.componentInstance; + const errorSpy = jasmine.createSpy( 'errorSpy' ); + component.error.subscribe( errorSpy ); + const contextWatchdog = new AngularEditor.ContextWatchdog( AngularEditor.Context ); + + await contextWatchdog.create(); + + component.watchdog = contextWatchdog; + component.editor = AngularEditor; + component.config = config; + + fixture.detectChanges(); + await waitCycle(); + + expect( errorSpy ).toHaveBeenCalledTimes( 1 ); + + fixture.destroy(); + } ); + } ); } ); describe( 'change detection', () => { diff --git a/src/ckeditor/ckeditor.component.ts b/src/ckeditor/ckeditor.component.ts index 9b8808b..4d80804 100644 --- a/src/ckeditor/ckeditor.component.ts +++ b/src/ckeditor/ckeditor.component.ts @@ -164,7 +164,7 @@ export class CKEditorComponent implements After /** * Fires when the editor component crashes. */ - @Output() public error = new EventEmitter(); + @Output() public error = new EventEmitter(); /** * The instance of the editor created by this component. @@ -367,16 +367,15 @@ export class CKEditorComponent implements After this.elementRef.nativeElement.removeChild( this.editorElement! ); }; - const emitError = () => { + const emitError = ( e?: unknown ) => { // Do not run change detection by re-entering the Angular zone if the `error` // emitter doesn't have any subscribers. // Subscribers are pushed onto the list whenever `error` is listened inside the template: // ``. if ( hasObservers( this.error ) ) { - this.ngZone.run( () => this.error.emit() ); + this.ngZone.run( () => this.error.emit( e ) ); } }; - const element = document.createElement( this.tagName ); const config = this.getConfig(); @@ -392,6 +391,8 @@ export class CKEditorComponent implements After destructor, sourceElementOrData: element, config + } ).catch( e => { + emitError( e ); } ); this.watchdog.on( 'itemError', ( _, { itemId } ) => { @@ -410,11 +411,12 @@ export class CKEditorComponent implements After editorWatchdog.on( 'error', emitError ); this.editorWatchdog = editorWatchdog; - this.ngZone.runOutsideAngular( () => { // Note: must be called outside of the Angular zone too because `create` is calling // `_startErrorHandling` within a microtask which sets up `error` listener on the window. - editorWatchdog.create( element, config ); + editorWatchdog.create( element, config ).catch( e => { + emitError( e ); + } ); } ); } }