Contribute to this guide

guideEvent system

Emitters are objects that can fire events. They also provide means to listen to other emitters’ events.

Emitters are heavily used throughout the entire editor architecture. They are the building blocks for mechanisms such as the observables, engine’s view observers, and conversion.

Any class can become an event emitter. All you need to do is mix the Emitter into it:

import { EmitterMixin, mix } from '@ckeditor/ckeditor5-utils';

class AnyClass {
    // Class's code.
    // ...
}

mix( AnyClass, EmitterMixin );

# Listening to events

Adding a callback to an event is simple. You can listen directly on the emitter object and use an anonymous function:

emitter.on( 'eventName', ( eventInfo, ...args ) => { /* ... */ } );

However, a function object is needed if you want to be able to remove the event listener:

emitter.off( 'eventName', handler );

There is also another way to add an event listener – by using listenTo(). This way one emitter can listen to events on another emitter:

foo.listenTo( bar, 'eventName', ( eventInfo, ...args ) => { /* ... */ } );

Now you can easily detach the foo from bar simply by stopListening().

// Stop listening to a specific handler.
foo.stopListening( bar, 'eventName', handler );

// Stop listening to a specific event.
foo.stopListening( bar, 'eventName' );

// Stop listening to all events fired by a specific emitter.
foo.stopListening( bar );

// Stop listening to all events fired by all bound emitters.
foo.stopListening();

The on() and off() methods are shorthands for listenTo( this, /* ... */ ) and stopListening( this, /* ... */ ) (the emitter is bound to itself).

# Listener priorities

By default, all listeners are bound on the normal priority, but you can specify the priority when registering a listener:

this.on( 'eventName', () => { /* ... */ }, { priority: 'high' } );
this.listenTo( emitter, 'eventName', () => { /* ... */ }, { priority: 'high' } );

There are 5 named priorities:

  • highest
  • high
  • normal
  • low
  • lowest

Listeners are triggered in the order of these priorities (first highest, then high, etc.). For multiple listeners attached on the same priority, they are fired in the order of the registration.

Note: If any listener stops the event, no other listeners including those on lower priorities will be called.

It is possible to use relative priorities priorities.get( 'high' ) + 10 but this is strongly discouraged.

# Stopping events and returned value

The first argument passed to an event handler is always an instance of the EventInfo. There you can check the event name, the source emitter of the event, and you can stop() the event from further processing.

emitter.on( 'eventName', ( eventInfo, data ) => {
    console.log( 'foo' );
    eventInfo.stop();
} );

emitter.on( 'eventName', ( eventInfo, data ) => {
    console.log( 'bar' ); // This won't be called.
} );

emitter.fire( 'eventName' ); // Logs "foo" only.

Listeners can set the return value. This value will be returned by fire() after all callbacks are processed.

emitter.on( 'eventName', ( eventInfo, data ) => {
    eventInfo.return = 123;
} );

emitter.fire( 'eventName' ); // -> 123

# Listening on namespaced events

The event system supports namespaced events to give you the possibility to build a structure of callbacks. You can achieve namespacing by using : in the event name:

this.fire( 'foo:bar:baz', data );

Then the listeners can be bound to a specific event or the whole namespace:

this.on( 'foo', () => { /* ... */ } );
this.on( 'foo:bar', () => { /* ... */ } );
this.on( 'foo:bar:baz', () => { /* ... */ } );

This way you can have more general events, listening to a broader event ('foo' in this case), or more detailed callbacks listening to specified events ('foo:bar' or 'foo:bar:baz').

This mechanism is used for instance in the conversion, where thanks to events named 'insert:<elementName>' you can listen to the insertion of a specific element (like 'insert:p') or all elements insertion ('insert').

Note: Listeners registered on the same priority will be fired in the order of the registration (no matter if listening to a whole namespace or to a specific event).

# Firing events

Once you mix the Emitter into your class, you can fire events the following way:

this.fire( 'eventName', argA, argB, /* ... */ );

All passed arguments will be available in all listeners that are added to the event.

Note: Most base classes (like Command or Plugin) are emitters already and fire their own events.

# Stopped events

It is sometimes useful to know if an event was stopped by any of the listeners. There is an alternative way of firing an event just for that:

import { EventInfo } from '@ckeditor/ckeditor5-utils';

// Prepare the event info...
const eventInfo = new EventInfo( this, 'eventName' );

// ...and fire the event.
this.fire( eventInfo, argA, argB, /* ... */ );

// Here you can check if the event was stopped.
if ( eventInfo.stop.called ) {
    // The event was stopped.
}

Note that EventInfo expects the source object in the first parameter as the origin of the event.

# Event return value

If any handler set the eventInfo.return field, this value will be returned by fire() after all callbacks are processed.

emitter.on( 'eventName', ( eventInfo, ...args ) => {
    eventInfo.return = 123;
} );

const result = emitter.fire( 'eventName', argA, argB, /* ... */ );

console.log( result ); // -> 123

# Delegating events

The Emitter interface also provides the event delegation mechanism, so that selected events are fired by another Emitter.

# Setting events delegation

Delegate specific events to another emitter:

emitterA.delegate( 'foo' ).to( emitterB );
emitterA.delegate( 'foo', 'bar' ).to( emitterC );

You can delegate events with a different name:

emitterA.delegate( 'foo' ).to( emitterB, 'bar' );
emitterA.delegate( 'foo' ).to( emitterB, name => `delegated:${ name }` );

It is also possible to delegate all the events:

emitterA.delegate( '*' ).to( emitterB );

Note: Delegated events are fired from the target emitter no matter if they were stopped in any handler on the source emitter.

# Stopping delegation

You can stop delegation by calling the stopDelegating() method. It can be used at different levels:

// Stop delegating all events.
emitterA.stopDelegating();

// Stop delegating a specific event to all emitters.
emitterA.stopDelegating( 'foo' );

// Stop delegating a specific event to a specific emitter.
emitterA.stopDelegating( 'foo', emitterB );

# Delegated event info

The delegated events provide the path of emitters that this event met along the delegation path.

emitterA.delegate( 'foo' ).to( emitterB, 'bar' );
emitterB.delegate( 'bar' ).to( emitterC, 'baz' );

emitterA.on( 'foo', eventInfo => console.log( 'event', eventInfo.name, 'emitted by A; source:', eventInfo.source, 'path:', eventInfo.path ) );
emitterB.on( 'bar', eventInfo => console.log( 'event', eventInfo.name, 'emitted by B; source:', eventInfo.source, 'path:', eventInfo.path ) );
emitterC.on( 'baz', eventInfo => console.log( 'event', eventInfo.name, 'emitted by C; source:', eventInfo.source, 'path:', eventInfo.path ) );

emitterA.fire( 'foo' );

// Outputs:
//   event "foo" emitted by A; source: emitterA; path: [ emitterA ]
//   event "bar" emitted by B; source: emitterA; path: [ emitterA, emitterB ]
//   event "baz" emitted by C; source: emitterA; path: [ emitterA, emitterB, emitterC ]

# View events bubbling

The view.Document is not only an Observable and an emitter but it also implements the special BubblingEmitter interface (implemented by BubblingEmitterMixin). It provides a mechanism for bubbling events over the virtual DOM tree.

It is different from the bubbling that you know from the DOM tree event bubbling. You do not register listeners on specific instances of the elements in the view document tree. Instead, you can register handlers for specific contexts. A context is either a name of an element, or one of the virtual contexts ('$capture', '$text', '$root', '$document'), or a callback to match desired nodes.

# Listening to bubbling events

Listeners registered in the context of the view element names:

this.listenTo( view.document, 'enter', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: 'blockquote' } );

this.listenTo( view.document, 'enter', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: 'li' } );

Listeners registered in the virtual contexts:

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: '$text', priority: 'high' } );

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: '$root' } );

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: '$capture' } );

Listeners registered in the context of a custom callback function:

import { isWidget } from '@ckeditor/ckeditor5-widget';

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: isWidget } );

this.listenTo( view.document, 'arrowKey', ( evt, data ) => {
    // Listener's code.
    // ...
}, { context: isWidget, priority: 'high' } );

Note: Without specifying the context, events are bound to the '$document' context.

# Bubbling events flow

Bubbling always starts from the virtual '$capture' context. All listeners attached to this context are triggered first (and in the order of their priorities).

Then, the real bubbling starts from the selection position (either its anchor or focus – depending on what is deeper).

If text nodes are allowed at the selection position, then the first context is '$text'. Then the event bubbles through all elements up to the '$root' and finally '$document'.

In all contexts listeners can be registered at desired priorities. If a listener stops an event, this event is not fired for the remaining contexts.

# Examples

Assuming the given content and selection:

<blockquote>
    <p>
        Foo[]bar
    </p>
</blockquote>

Events will be fired for the following contexts:

  1. '$capture'
  2. '$text'
  3. 'p'
  4. 'blockquote'
  5. '$root'
  6. '$document'

Assuming the given content and selection (on a widget):

<blockquote>
    <p>
        Foo
        [<img />]	 // enhanced with toWidget()
        bar
    </p>
</blockquote>

Events will be fired for the following contexts:

  1. '$capture'
  2. 'img'
  3. widget (assuming a custom matcher was used)
  4. 'p'
  5. 'blockquote'
  6. '$root'
  7. '$document'

An even more complex example:

<blockquote>
    <figure class="table">	 // enhanced with toWidget()
        <table>
            <tr>
                <td>
                    <p>
                        foo[]bar
                    </p>
                </td>
            </tr>
        </table>
    </figure>
</blockquote>

Events that will be fired:

  1. '$capture'
  2. '$text'
  3. 'p'
  4. 'td'
  5. 'tr'
  6. 'table'
  7. 'figure'
  8. widget (assuming a custom matcher was used)
  9. 'blockquote'
  10. '$root'
  11. '$document'

# BubblingEventInfo

In some events the first parameter is not the standard EventInfo, but BubblingEventInfo. This is an extension that provides the current eventPhase and currentTarget.

Currently, this information is available for the following events:

Hence the events from the above example would be extended with the following eventPhase data:

  1. '$capture' - capturing
  2. '$text' - at target
  3. 'p' - bubbling
  4. 'td' - bubbling
  5. 'tr' - bubbling
  6. 'table' - bubbling
  7. 'figure' - bubbling
  8. widget - bubbling
  9. 'blockquote' - bubbling
  10. '$root' - bubbling
  11. '$document' - bubbling

And for the example with the widget selected:

<blockquote>
    <p>
        Foo
        [<img />]	 // Enhanced with toWidget().
        bar
    </p>
</blockquote>

Events that will be fired:

  1. '$capture' - capturing
  2. 'img' - at target
  3. widget - at target (assuming a custom matcher was used)
  4. 'p' - bubbling
  5. 'blockquote' - bubbling
  6. '$root' - bubbling
  7. '$document' - bubbling