• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

ckeditor / ckeditor5-angular / 572

pending completion
572

push

travis-ci-com

pomek
Added a new property that allows disabling the two-way data binding mechanism in the CKEditor component.

31 of 33 branches covered (93.94%)

Branch coverage included in aggregate %.

8 of 8 new or added lines in 2 files covered. (100.0%)

150 of 153 relevant lines covered (98.04%)

23.22 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.59
/src/ckeditor/ckeditor.component.ts
1
/**
2
 * @license Copyright (c) 2003-2023, CKSource Holding sp. z o.o. All rights reserved.
3
 * For licensing, see LICENSE.md.
4
 */
5

6
declare global {
7
        interface Window {
8
                CKEDITOR_VERSION?: string;
9
        }
10
}
11

12
import type {
13
        AfterViewInit, OnDestroy } from '@angular/core';
14
import {
15
        Component,
16
        Input,
17
        Output,
18
        NgZone,
19
        EventEmitter,
20
        forwardRef,
21
        ElementRef
22
} from '@angular/core';
23

24
import EditorWatchdog from '@ckeditor/ckeditor5-watchdog/src/editorwatchdog';
25
import { first } from 'rxjs/operators';
26

27
import uid from './uid';
28

29
import type {
30
        ControlValueAccessor } from '@angular/forms';
31
import {
32
        NG_VALUE_ACCESSOR
33
} from '@angular/forms';
34

35
import { CKEditor5 } from './ckeditor';
36

37
const ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID = 'Lock from Angular integration (@ckeditor/ckeditor5-angular)';
1✔
38

39
export interface BlurEvent {
40
        event: CKEditor5.EventInfo<'blur'>;
41
        editor: CKEditor5.Editor;
42
}
43

44
export interface FocusEvent {
45
        event: CKEditor5.EventInfo<'focus'>;
46
        editor: CKEditor5.Editor;
47
}
48

49
export interface ChangeEvent {
50
        event: CKEditor5.EventInfo<'change:data'>;
51
        editor: CKEditor5.Editor;
52
}
53

54
@Component( {
55
        selector: 'ckeditor',
56
        template: '<ng-template></ng-template>',
57

58
        // Integration with @angular/forms.
59
        providers: [
60
                {
61
                        provide: NG_VALUE_ACCESSOR,
62
                        // eslint-disable-next-line @typescript-eslint/no-use-before-define
63
                        useExisting: forwardRef( () => CKEditorComponent ),
6✔
64
                        multi: true
65
                }
66
        ]
67
} )
68
export class CKEditorComponent implements AfterViewInit, OnDestroy, ControlValueAccessor {
1✔
69
        /**
70
         * The reference to the DOM element created by the component.
71
         */
72
        private elementRef!: ElementRef<HTMLElement>;
73

74
        /**
75
         * The constructor of the editor to be used for the instance of the component.
76
         * It can be e.g. the `ClassicEditorBuild`, `InlineEditorBuild` or some custom editor.
77
         */
78
        @Input() public editor?: CKEditor5.EditorConstructor;
79

80
        /**
81
         * The configuration of the editor.
82
         * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editorconfig-EditorConfig.html
83
         * to learn more.
84
         */
85
        @Input() public config: CKEditor5.Config = {};
45✔
86

87
        /**
88
         * The initial data of the editor. Useful when not using the ngModel.
89
         * See https://angular.io/api/forms/NgModel to learn more.
90
         */
91
        @Input() public data = '';
45✔
92

93
        /**
94
         * Tag name of the editor component.
95
         *
96
         * The default tag is 'div'.
97
         */
98
        @Input() public tagName = 'div';
45✔
99

100
        /**
101
         * The context watchdog.
102
         */
103
        @Input() public watchdog?: CKEditor5.ContextWatchdog;
104

105
        /**
106
         * Allows disabling the two-way data binding mechanism. It can boosts performance for large documents if set to `true`.
107
         *
108
         * When a component is connected using the [(ngModel)] or [formControl] directives then all data will be never synchronized.
109
         *
110
         * An integrator must call `editor.getData()` manually once the application needs the editor's data.
111
         * An editor instance can be received in the `ready()` callback.
112
         */
113
        @Input() public disableTwoWayDataBinding = false;
45✔
114

115
        /**
116
         * When set `true`, the editor becomes read-only.
117
         * See https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html#member-isReadOnly
118
         * to learn more.
119
         */
120
        @Input() public set disabled( isDisabled: boolean ) {
121
                this.setDisabledState( isDisabled );
15✔
122
        }
123

124
        public get disabled(): boolean {
125
                if ( this.editorInstance ) {
6✔
126
                        return this.editorInstance.isReadOnly;
4✔
127
                }
128

129
                return this.initiallyDisabled;
2✔
130
        }
131

132
        /**
133
         * Fires when the editor is ready. It corresponds with the `editor#ready`
134
         * https://ckeditor.com/docs/ckeditor5/latest/api/module_core_editor_editor-Editor.html#event-ready
135
         * event.
136
         */
137
        @Output() public ready = new EventEmitter<CKEditor5.Editor>();
45✔
138

139
        /**
140
         * Fires when the content of the editor has changed. It corresponds with the `editor.model.document#change`
141
         * https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_model_document-Document.html#event-change
142
         * event.
143
         */
144
        @Output() public change: EventEmitter<ChangeEvent> = new EventEmitter<ChangeEvent>();
45✔
145

146
        /**
147
         * Fires when the editing view of the editor is blurred. It corresponds with the `editor.editing.view.document#blur`
148
         * https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_document-Document.html#event-event:blur
149
         * event.
150
         */
151
        @Output() public blur: EventEmitter<BlurEvent> = new EventEmitter<BlurEvent>();
45✔
152

153
        /**
154
         * Fires when the editing view of the editor is focused. It corresponds with the `editor.editing.view.document#focus`
155
         * https://ckeditor.com/docs/ckeditor5/latest/api/module_engine_view_document-Document.html#event-event:focus
156
         * event.
157
         */
158
        @Output() public focus: EventEmitter<FocusEvent> = new EventEmitter<FocusEvent>();
45✔
159

160
        /**
161
         * Fires when the editor component crashes.
162
         */
163
        @Output() public error: EventEmitter<void> = new EventEmitter<void>();
45✔
164

165
        /**
166
         * The instance of the editor created by this component.
167
         */
168
        public get editorInstance(): CKEditor5.Editor | null {
169
                let editorWatchdog = this.editorWatchdog;
87✔
170

171
                if ( this.watchdog ) {
87✔
172
                        // Temporarily use the `_watchdogs` internal map as the `getItem()` method throws
173
                        // an error when the item is not registered yet.
174
                        // See https://github.com/ckeditor/ckeditor5-angular/issues/177.
175
                        editorWatchdog = this.watchdog._watchdogs.get( this.id );
5✔
176
                }
177

178
                if ( editorWatchdog ) {
87✔
179
                        return editorWatchdog.editor;
72✔
180
                }
181

182
                return null;
15✔
183
        }
184

185
        /**
186
         * The editor watchdog. It is created when the context watchdog is not passed to the component.
187
         * It keeps the editor running.
188
         */
189
        private editorWatchdog?: CKEditor5.EditorWatchdog;
190

191
        /**
192
         * If the component is read–only before the editor instance is created, it remembers that state,
193
         * so the editor can become read–only once it is ready.
194
         */
195
        private initiallyDisabled = false;
45✔
196

197
        /**
198
         * An instance of https://angular.io/api/core/NgZone to allow the interaction with the editor
199
         * withing the Angular event loop.
200
         */
201
        private ngZone: NgZone;
202

203
        /**
204
         * A callback executed when the content of the editor changes. Part of the
205
         * `ControlValueAccessor` (https://angular.io/api/forms/ControlValueAccessor) interface.
206
         *
207
         * Note: Unset unless the component uses the `ngModel`.
208
         */
209
        private cvaOnChange?: ( data: string ) => void;
210

211
        /**
212
         * A callback executed when the editor has been blurred. Part of the
213
         * `ControlValueAccessor` (https://angular.io/api/forms/ControlValueAccessor) interface.
214
         *
215
         * Note: Unset unless the component uses the `ngModel`.
216
         */
217
        private cvaOnTouched?: () => void;
218

219
        /**
220
         * Reference to the source element used by the editor.
221
         */
222
        private editorElement?: HTMLElement;
223

224
        /**
225
         * A lock flag preventing from calling the `cvaOnChange()` during setting editor data.
226
         */
227
        private isEditorSettingData = false;
45✔
228

229
        private id = uid();
45✔
230

231
        public constructor( elementRef: ElementRef, ngZone: NgZone ) {
232
                this.ngZone = ngZone;
45✔
233
                this.elementRef = elementRef;
45✔
234

235
                const { CKEDITOR_VERSION } = window;
45✔
236

237
                // Starting from v34.0.0, CKEditor 5 introduces a lock mechanism enabling/disabling the read-only mode.
238
                // As it is a breaking change between major releases of the integration, the component requires using
239
                // CKEditor 5 in version 34 or higher.
240
                if ( CKEDITOR_VERSION ) {
45✔
241
                        const [ major ] = CKEDITOR_VERSION.split( '.' ).map( Number );
44✔
242

243
                        if ( major < 34 ) {
44✔
244
                                console.warn( 'The <CKEditor> component requires using CKEditor 5 in version 34 or higher.' );
1✔
245
                        }
246
                } else {
247
                        console.warn( 'Cannot find the "CKEDITOR_VERSION" in the "window" scope.' );
1✔
248
                }
249
        }
250

251
        // Implementing the AfterViewInit interface.
252
        public ngAfterViewInit(): void {
253
                this.attachToWatchdog();
40✔
254
        }
255

256
        // Implementing the OnDestroy interface.
257
        public async ngOnDestroy(): Promise<void> {
258
                if ( this.watchdog ) {
45✔
259
                        await this.watchdog.remove( this.id );
4✔
260
                } else if ( this.editorWatchdog && this.editorWatchdog.editor ) {
41✔
261
                        await this.editorWatchdog.destroy();
21✔
262

263
                        this.editorWatchdog = undefined;
21✔
264
                }
265
        }
266

267
        // Implementing the ControlValueAccessor interface (only when binding to ngModel).
268
        public writeValue( value: string | null ): void {
269
                // This method is called with the `null` value when the form resets.
270
                // A component's responsibility is to restore to the initial state.
271
                if ( value === null ) {
17✔
272
                        value = '';
7✔
273
                }
274

275
                // If already initialized.
276
                if ( this.editorInstance ) {
17✔
277
                        // The lock mechanism prevents from calling `cvaOnChange()` during changing
278
                        // the editor state. See #139
279
                        this.isEditorSettingData = true;
6✔
280
                        this.editorInstance.setData( value );
6✔
281
                        this.isEditorSettingData = false;
6✔
282
                }
283
                // If not, wait for it to be ready; store the data.
284
                else {
285
                        // If the editor element is already available, then update its content.
286
                        this.data = value;
11✔
287

288
                        // If not, then wait until it is ready
289
                        // and change data only for the first `ready` event.
290
                        this.ready
11✔
291
                                .pipe( first() )
292
                                .subscribe( ( editor: CKEditor5.Editor ) => {
293
                                        editor.setData( this.data );
11✔
294
                                } );
295
                }
296
        }
297

298
        // Implementing the ControlValueAccessor interface (only when binding to ngModel).
299
        public registerOnChange( callback: ( data: string ) => void ): void {
300
                this.cvaOnChange = callback;
11✔
301
        }
302

303
        // Implementing the ControlValueAccessor interface (only when binding to ngModel).
304
        public registerOnTouched( callback: () => void ): void {
305
                this.cvaOnTouched = callback;
10✔
306
        }
307

308
        // Implementing the ControlValueAccessor interface (only when binding to ngModel).
309
        public setDisabledState( isDisabled: boolean ): void {
310
                // If already initialized.
311
                if ( this.editorInstance ) {
16✔
312
                        if ( isDisabled ) {
2✔
313
                                this.editorInstance.enableReadOnlyMode( ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID );
1✔
314
                        } else {
315
                                this.editorInstance.disableReadOnlyMode( ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID );
1✔
316
                        }
317
                }
318

319
                // Store the state anyway to use it once the editor is created.
320
                this.initiallyDisabled = isDisabled;
16✔
321
        }
322

323
        /**
324
         * Creates the editor instance, sets initial editor data, then integrates
325
         * the editor with the Angular component. This method does not use the `editor.setData()`
326
         * because of the issue in the collaboration mode (#6).
327
         */
328
        private attachToWatchdog() {
329
                const creator = async ( element: HTMLElement, config: CKEditor5.Config ) => {
40✔
330
                        return this.ngZone.runOutsideAngular( async () => {
42✔
331
                                this.elementRef.nativeElement.appendChild( element );
42✔
332

333
                                const editor = await this.editor!.create( element, config );
42✔
334

335
                                if ( this.initiallyDisabled ) {
42✔
336
                                        editor.enableReadOnlyMode( ANGULAR_INTEGRATION_READ_ONLY_LOCK_ID );
2✔
337
                                }
338

339
                                this.ngZone.run( () => {
42✔
340
                                        this.ready.emit( editor );
42✔
341
                                } );
342

343
                                this.setUpEditorEvents( editor );
42✔
344

345
                                return editor;
42✔
346
                        } );
347
                };
348

349
                const destructor = async ( editor: CKEditor5.Editor ) => {
40✔
350
                        await editor.destroy();
28✔
351

352
                        this.elementRef.nativeElement.removeChild( this.editorElement! );
28✔
353
                };
354

355
                const emitError = () => {
40✔
356
                        this.ngZone.run( () => {
3✔
357
                                this.error.emit();
3✔
358
                        } );
359
                };
360

361
                const element = document.createElement( this.tagName );
40✔
362
                const config = this.getConfig();
40✔
363

364
                this.editorElement = element;
39✔
365

366
                // Based on the presence of the watchdog decide how to initialize the editor.
367
                if ( this.watchdog ) {
39✔
368
                        // When the context watchdog is passed add the new item to it based on the passed configuration.
369
                        this.watchdog.add( {
4✔
370
                                id: this.id,
371
                                type: 'editor',
372
                                creator,
373
                                destructor,
374
                                sourceElementOrData: element,
375
                                config
376
                        } );
377

378
                        this.watchdog.on( 'itemError', ( _, { itemId } ) => {
4✔
379
                                if ( itemId === this.id ) {
2✔
380
                                        emitError();
1✔
381
                                }
382
                        } );
383
                } else {
384
                        // In the other case create the watchdog by hand to keep the editor running.
385
                        const editorWatchdog: CKEditor5.EditorWatchdog = new EditorWatchdog( this.editor );
35✔
386

387
                        editorWatchdog.setCreator( creator );
35✔
388
                        editorWatchdog.setDestructor( destructor );
35✔
389
                        editorWatchdog.on( 'error', emitError );
35✔
390

391
                        this.editorWatchdog = editorWatchdog;
35✔
392

393
                        this.editorWatchdog.create( element, config );
35✔
394
                }
395
        }
396

397
        private getConfig() {
398
                if ( this.data && this.config.initialData ) {
40✔
399
                        throw new Error( 'Editor data should be provided either using `config.initialData` or `data` properties.' );
1✔
400
                }
401

402
                const config = { ...this.config };
39✔
403

404
                // Merge two possible ways of providing data into the `config.initialData` field.
405
                const initialData = this.config.initialData || this.data;
39✔
406

407
                if ( initialData ) {
39✔
408
                        // Define the `config.initialData` only when the initial content is specified.
409
                        config.initialData = initialData;
16✔
410
                }
411

412
                return config;
39✔
413
        }
414

415
        /**
416
         * Integrates the editor with the component by attaching related event listeners.
417
         */
418
        private setUpEditorEvents( editor: CKEditor5.Editor ): void {
419
                const modelDocument = editor.model.document;
42✔
420
                const viewDocument = editor.editing.view.document;
42✔
421

422
                modelDocument.on( 'change:data', ( evt: CKEditor5.EventInfo<'change:data'> ) => {
42✔
423
                        this.ngZone.run( () => {
11✔
424
                                if ( this.disableTwoWayDataBinding ) {
11!
425
                                        return;
×
426
                                }
427

428
                                if ( this.cvaOnChange && !this.isEditorSettingData ) {
11✔
429
                                        const data = editor.getData();
1✔
430

431
                                        this.cvaOnChange( data );
1✔
432
                                }
433

434
                                this.change.emit( { event: evt, editor } );
11✔
435
                        } );
436
                } );
437

438
                viewDocument.on( 'focus', ( evt: CKEditor5.EventInfo<'focus'> ) => {
42✔
439
                        this.ngZone.run( () => {
3✔
440
                                this.focus.emit( { event: evt, editor } );
3✔
441
                        } );
442
                } );
443

444
                viewDocument.on( 'blur', ( evt: CKEditor5.EventInfo<'blur'> ) => {
42✔
445
                        this.ngZone.run( () => {
4✔
446
                                if ( this.cvaOnTouched ) {
4✔
447
                                        this.cvaOnTouched();
2✔
448
                                }
449

450
                                this.blur.emit( { event: evt, editor } );
4✔
451
                        } );
452
                } );
453
        }
454
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc