The events model is a powerful and straightforward way to communicate between and inside components. This article will describe how the events model works on our frontend application level implementation.
An event is a channel through what messages can be dispatched to several endpoints at once. Components can use events to notify or to be notified, hence they can be a good support for an extensive API, asynchronous by design.
Each event can be seen as a channel, being processed as a pipeline, containing
three steps: before, main, after. By design, the main
step is the
one that is targeted by the default event listener API, the two others allow
to prepend or append actions on the same event. In each step, as many as needed
listeners can be added, they will be called in serie. From each step, a listener
may interrupt the pipeline and prevents the remaining steps to be processed.
However, inside a step nothing ensures a particular listener can be prevented
nor a particular listener can be called before another particular one. Only the
remaining steps can be prevented, and the steps are the only way to ensure a
listener is called before or after another one.
Events don't have to be registered prior being able to use them: an eventified object can trigger any events, even if no listener is bound to it, and a client can listen to any events, even if the listened to event is never triggered. This way it is possible to extend the API without having to change the signatures nor the implementation, as there is no fixed pool of available events.
eventifier API
The events manager engine is provided by a core module, called eventifier
.
This is a mixin that will augment any target object with a set of API bringing
the ability to manage events.
To use the eventifier
you need to import the module core/eventifier
:
define(['core/eventifier'], function (eventifier) {
'use strict';
// ...
});
To turn a given object into an event manager you need to apply the eventifier
factory on it:
// apply the mixin on the "emitter" object
eventifier(emitter);
Note: the emitter object is returned by the factory, so it is possible to create a new eventified object on the fly:
// create an "emitter" object and apply the mixin on it
var emitter = eventifier({
doSomething: function doSomething() {
// ...
}
});
Then the events manager API is available on this object:
// do something before the "message" event is processed
emitter.before('message', function(e, msg) {
console.log('message to be processed:', msg);
setTimeout(function() {
console.log('This will be processed after the event...');
}, 250);
// remaining steps can be prevented synchronously
if (someCondition) {
return false;
}
});
// listen to the "message" event
emitter.on('message', function(msg) {
console.log('received message:', msg);
// remaining steps can be prevented synchronously
if (someCondition) {
return false;
}
});
// do something after the "message" event has been processed
emitter.after('message', function(msg) {
console.log('processed message:', msg);
// does nothing specifically, there is no step after this one
return false;
});
// emit the "message" event
emitter.trigger('message', 'hello');
The event steps are ran linearly: firstly the before
step, then the main
and
finally the after
. By default, the steps are synchronous, that means each step
much be completed before moving to the next, but if a listener needs to delay
some stuff, remaining listeners might be processed before this stuff is
completely processed. For this reason, asynchronous event listeners are also
supported: a listener can return a Promise
to put the next event step on hold.
// listen to the "message" event, but delay the "main" listener by 250ms
emitter.before('message', function(e, msg) {
console.log('hold on received message:', msg);
return new Promise(function(resolve, reject) {
setTimeout(function() {
// remaining steps can be prevented
if (someCondition) {
reject();
} else {
resolve();
}
}, 250);
});
});
// listen to the "message" event, but delay the "after" listener by 100ms
// this listener should be delayed 250ms from the "before"
emitter.on('message', function(msg) {
console.log('received message:', msg);
return new Promise(function(resolve) {
setTimeout(function() {
// remaining steps can be prevented
if (someCondition) {
reject();
} else {
resolve();
}
}, 100);
});
});
// listen to the "message" event, once all main listeners have been processed
// this listener should be delayed 100ms from the "main" and at least 350ms from
// the "before"
emitter.after('message', function(msg) {
console.log('processed message:', msg);
// does nothing more than simply delay an action.
// there is no step after this one, so it won't put the event on hold
return new Promise(function(resolve, reject) {
setTimeout(function() {
// remaining steps can be prevented
if (someCondition) {
reject();
} else {
resolve();
}
}, 50);
});
});
// emit the "message" event
emitter.trigger('message', 'hello');
The available API is described here.
trigger(event)
Emits an event. All listeners registered for this event will be called. However, if a namespace is used, only the listeners registered with this particular namespace will be called. Several events can be emitted at the same time, simply separate each name with a space.
Note: the lexical scope of the listener will be bound to the emitter object.
// emit click event, all listeners will be triggered
emitter.trigger('click');
// emit namespaced click event, only listeners registered under this particular
// namespace will be triggered
emitter.trigger('click.foo');
// emit several events at once, with or without namespace
emitter.trigger('show enable foo.bar');
// emit event with parameters
emitter.trigger('foo', param1, param2);
on(event, listener)
Attaches a listener to an event. Calling on
with the same event's name
multiple times add callbacks: they will all be executed. The event's name may be
namespaced, to ease the later management. It could also be a list of events,
separated by spaces, so the listener will be called if any of the listed event is
emitted. A Promise
can be returned, and then the listeners registered on the
after
step will be delayed until the promise is resolved.
Note: the lexical scope of the listener will be bound to the emitter object.
// listen to click event
emitter.on('click', function() {
// The lexical scope is bound to the emitter
this.doSomething();
});
// listen to event with parameters
emitter.on('click', function(param1, param2) {
// The lexical scope is bound to the emitter
if (param1) {
this.doSomething(param2);
}
});
// listen to click event, scope the listener to a namespace
emitter.on('click.foo', function() {
// The lexical scope is bound to the emitter
this.doSomething();
});
// listen to several events, with or without namespace
emitter.on('show enable foo.bar', function() {
// The lexical scope is bound to the emitter
this.doSomething();
});
// synchronously break the events chain
emitter.on('click', function() {
return false;
});
// hold the event chain (async mode)
emitter.on('click', function() {
return new Promise(function(resolve, reject) {
// continue to next step
resolve();
// break the events chain, prevent the remaining steps
reject();
});
});
off(event)
Removes all listeners for an event. To reduce the scope of the removal, a namespace can be used. If only the name is given, all listeners registered under this namespace will be deleted.
// remove all listeners bound to the click event
emitter.off('click');
// remove all listeners bound to the namespaced click event
emitter.off('click.foo');
// remove all listeners bound to any events with the .foo namespace
emitter.off('.foo');
// remove listeners for several events at once, with or without namespace
emitter.off('click show foo.bar');
removeAllListeners()
Removes all registered listeners.
before(event, listener)
Registers a callback that is executed before the given event name. That provides
the opportunity to cancel the execution of the event if one of the returned
value is false. A particular parameter is provided to each listener, on the
first position, to give the ability to enter in async mode. However, this is a
legacy feature, and returning a Promise
should be preferred instead.
Note: the lexical scope of the listener will be set on the emitter object.
The before
step has a particularity other steps don't have: an event
parameter is prepended to the arguments list.
// listen to click event
emitter.before('click', function() {
// The lexical scope is bound to the emitter
this.doSomething();
});
// listen to event with parameters
emitter.before('click', function(e, param1, param2) {
// The lexical scope is bound to the emitter
if (param1) {
this.doSomething(param2);
}
});
// listen to click event, scope the listener to a namespace
emitter.before('click.foo', function() {
// The lexical scope is bound to the emitter
this.doSomething();
});
// listen to several events, with or without namespace
emitter.before('show enable foo.bar', function() {
// The lexical scope is bound to the emitter
this.doSomething();
});
// synchronously break the events chain
emitter.before('click', function() {
return false;
});
// hold the event chain (async mode)
emitter.before('click', function() {
return new Promise(function(resolve, reject) {
// continue to next step
resolve();
// break the events chain, prevent the remaining steps
reject();
});
});
after(event, listener)
Registers a callback that is executed after the given event name. All listeners should be executed at this stage, as long as the previous steps were not cancelled.
Note: the lexical scope of the listener will be set on the emitter object.
// listen to click event
emitter.after('click', function() {
// The lexical scope is bound to the emitter
this.doSomething();
});
// listen to event with parameters
emitter.after('click', function(param1, param2) {
// The lexical scope is bound to the emitter
if (param1) {
this.doSomething(param2);
}
});
// listen to click event, scope the listener to a namespace
emitter.after('click.foo', function() {
// The lexical scope is bound to the emitter
this.doSomething();
});
// listen to several events, with or without namespace
emitter.after('show enable foo.bar', function() {
// The lexical scope is bound to the emitter
this.doSomething();
});
stopEvent(name)
If triggered into a sync listener, this immediately cancels the execution of all
following listeners regardless of their category. If triggered asynchronously,
this will only cancel the next category of listeners, in an async context, you
can also reject a Promise
with the same results:
.on()
and.after()
if triggered during a.before()
listener.after()
if triggered during a.on()
listener- nothing if triggered during a
.after()
listener
emitter.before('click', function(e) {
// will prevent the next steps to be ran: "on" and "after"
this.stopEvent(e.name);
});
emitter.on('click', function() {
// will prevent the next steps to be ran: "after"
this.stopEvent('click');
});
emitter.after('click', function() {
// will do nothing
this.stopEvent('click');
});
spread(destination, event)
Spreads events to another eventifier
object. So when an event is triggered on
the current target, it gets triggered on the destination too. Be careful, the
forward will be triggered only if the event reaches the on
step (it can be
canceled by a before
).
// listen to regular click event on targetEmitter
targetEmitter.on('click', function() {
// The lexical scope is bound to the targetEmitter
this.doSomething();
});
// listen to regular click event on emitter
emitter.on('click', function() {
// The lexical scope is bound to the emitter
this.doSomething();
});
// forward all click events to the targetEmitter
emitter.spread(targetEmitter, 'click');
// emit click event on both emitter and targetEmitter
emitter.trigger('click');
// emit click event only on targetEmitter
targetEmitter.trigger('click');