Skip to content

Commit

Permalink
Merge pull request #390 from ckeditor/watchdog-demo-update
Browse files Browse the repository at this point in the history
Other: Added catching and emitting errors that happen during editor initialization. Closes #392
  • Loading branch information
scofalik authored Oct 11, 2023
2 parents 0d3935f + ef1a387 commit eb8c355
Show file tree
Hide file tree
Showing 9 changed files with 205 additions and 16 deletions.
1 change: 1 addition & 0 deletions src/app/app.component.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ <h1>CKEditor 5 integration with Angular</h1>
<li><a routerLink="/reactive-forms" routerLinkActive="active">Integration with reactive forms (<code>formControlName</code>)</a></li>
<li><a routerLink="/watchdog" routerLinkActive="active">Integration with CKEditor Watchdog</a></li>
<li><a routerLink="/context" routerLinkActive="active">Integration with CKEditor Context</a></li>
<li><a routerLink="/init-crash" routerLinkActive="active">Catching error when editor crashes during initialization</a></li>
</ul>
</nav>

Expand Down
7 changes: 5 additions & 2 deletions src/app/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ 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' },
{ path: 'context', component: ContextDemoComponent },
{ 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( {
Expand All @@ -35,7 +37,8 @@ const appRoutes: Routes = [
DemoFormComponent,
DemoReactiveFormComponent,
SimpleUsageComponent,
WatchdogDemoComponent
WatchdogDemoComponent,
InitializationCrashComponent
],
providers: [],
bootstrap: [ AppComponent ]
Expand Down
Empty file.
30 changes: 30 additions & 0 deletions src/app/initialization-crash/initialization-crash.component.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<h2>Initialization crash demo</h2>
<h3>Without watchdog</h3>
<ng-container *ngIf="errorOccurred; then errorMessage; else editor">

</ng-container>

<ng-template #editor>
<ckeditor data="Crash demo without watchdog" [config]="config" [editor]="Editor"
(error)="onError($event)">
</ckeditor>
</ng-template>

<ng-template #errorMessage>
<p>An error has occurred, please contact us at: [email protected] to resolve the situation.</p>
</ng-template>

<h3>With watchdog</h3>
<ng-container *ngIf="errorOccurredWatchdog; then errorMessageWatchdog; else editorWatchdog">

</ng-container>

<ng-template #editorWatchdog>
<ckeditor data="Crash demo with watchdog" [config]="config" [editor]="EditorWatchdog"
[watchdog]="watchdog" (error)="onErrorWatchdog($event)">
</ckeditor>
</ng-template>

<ng-template #errorMessageWatchdog>
<p>An error has occurred, please contact us at: [email protected] to resolve the situation.</p>
</ng-template>
64 changes: 64 additions & 0 deletions src/app/initialization-crash/initialization-crash.component.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
19 changes: 15 additions & 4 deletions src/app/watchdog-demo/watchdog-demo.html
Original file line number Diff line number Diff line change
@@ -1,7 +1,18 @@
<h2>Watchdog demo</h2>

<button (click)="toggle()">Toggle disabled mode</button>
<ng-container *ngIf="errorOccurred; then errorMessage; else editor">

<ckeditor data="Watchdog demo" [config]="config" [editor]="Editor" (ready)="onReady($event)" [watchdog]="watchdog"
[disabled]="isDisabled">
</ckeditor>
</ng-container>

<ng-template #editor>
<p>Type '1' or '2' to crash the editor.</p>
<button (click)="toggle()">Toggle disabled mode</button>

<ckeditor data="Watchdog demo" [config]="config" [editor]="Editor" (ready)="onReady($event)" [watchdog]="watchdog"
[disabled]="isDisabled" (error)="onError()">
</ckeditor>
</ng-template>

<ng-template #errorMessage>
<p>An error has occurred, please contast us at: [email protected] to resolve the situation.</p>
</ng-template>
26 changes: 22 additions & 4 deletions src/app/watchdog-demo/watchdog-demo.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand All @@ -11,16 +10,31 @@ import type { ContextWatchdog } from '@ckeditor/ckeditor5-watchdog';
export class WatchdogDemoComponent {
public Editor = AngularEditor;

@ViewChild( CKEditorComponent ) public ckeditor?: ElementRef<CKEditorComponent>;

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 {
Expand All @@ -45,4 +59,8 @@ export class WatchdogDemoComponent {
public toggle(): void {
this.isDisabled = !this.isDisabled;
}

public onError(): void {
this.errorOccurred = true;
}
}
60 changes: 60 additions & 0 deletions src/ckeditor/ckeditor.component.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
14 changes: 8 additions & 6 deletions src/ckeditor/ckeditor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,7 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
/**
* Fires when the editor component crashes.
*/
@Output() public error = new EventEmitter<void>();
@Output() public error = new EventEmitter<unknown>();

/**
* The instance of the editor created by this component.
Expand Down Expand Up @@ -367,16 +367,15 @@ export class CKEditorComponent<TEditor extends Editor = Editor> 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:
// `<ckeditor (error)="onError(...)"></ckeditor>`.
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();

Expand All @@ -392,6 +391,8 @@ export class CKEditorComponent<TEditor extends Editor = Editor> implements After
destructor,
sourceElementOrData: element,
config
} ).catch( e => {
emitError( e );
} );

this.watchdog.on( 'itemError', ( _, { itemId } ) => {
Expand All @@ -410,11 +411,12 @@ export class CKEditorComponent<TEditor extends Editor = Editor> 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 );
} );
} );
}
}
Expand Down

0 comments on commit eb8c355

Please sign in to comment.