The event-module provides APIs for working with events through the static Class Event. Emitting and listening to events can be done at the highest level: Event. This is place where all events emit to. Also, any object or instance can emit or listen to an event by merging the propriate methods. Both user-defined events as well as the browser's DOM event system are handled by Event.
The loaderfiles combine event, event-dom and event-mobile all into ITSA.Event.
With nodejs:
Create your project using this package.json. After downloaded, run npm install
and start writing your application:
In the browser:
For browser-usage, ITSA has a predefined loaderfiles. Once included, a global ITSA
object with default features is available. For customized loaderfiles, read: Customized build.
<script src="/itsabuild-min.js"></script>
Events could be used with a:
When events are emitted, they go through 3 phases: before
-, action
- and an after
-phase. However, if the specific event was not defined as a customEvent, the action-phase is skipped. Without any before-subscribers, the before-phase is skipped.
Whenever an event is emitted, all the before
-subscribers will be called first in the order in which they subscribed, unless the subscription took place with prepend=true (see API). The before
-listeners can call any of the methods listed above to alter the execution of the event lifecycle. They also have access to the payload so they can make decisions based on the information in it and can also add or modify information contained in it for later subscribers.
Since any before
-subscriber can interrupt the chain of execution of an event, in this stage, the eventobject should not be used to learn about an event that has happened (because at that point it might not) but to vote on its happening or adding information about it.
If none of the before
-listeners interrupted the chain of processing, the default action (defaultFn
) gets invoked, if present. For custom events, the default action can be defined with the defaultFn()-method
of the defineEvent()
-object (see Defining Custom Events). The defaultFn
method will be called with the payload as modified by any of the previous before
listeners.
Any value the defaultFn returns, will be made available at the event object through the returnValue
-property. Promises may be returned, but the eventcycle does not wait for Promises to resolve. However, they can be inspected in the after-listeners, who can wait for e.returnValue to resolve.
If any before
-listener has interrupted the processing by calling e.preventDefault() on the eventobject, the method passed via the preventedFn()
-method will be called instead of the defaultFn
. See Defining Custom Events how to setup the preventedFn.
Once the event has been executed, the after
-event listeners will be called. The extra methods of the eventobject are no longer be available. Any after
-listener may change the eventobject but that is not recommended as all after
-listeners should see the same eventobject.
Events are identified by their customEvent-name
, which has the syntax: emitterName:eventName. Two emitterNames are reserved:
UI
as emitterName, any name can be choosen for emitterName as well as eventName, as long as it consist of ASCII word characters (regular expression: \w+
).The emitterName
is usually the entity that emits the event. You can setup an instance that can emit events, and that instance is labeled by its emittername. For example: an instance with the emittername: PersonalProfile, might emit events like PersonalProfile:read, PersonalProfile:create and PersonalProfile:update. The emitterName is eventobject's property: e.emitter.
The eventName
is something that has happened to the emitter. It is the second part of the customEvent, after the colon. The eventName tells what action occured, f.e. PersonalProfile:save
tells that save happened. The eventName is eventobject's property: e.type.
Using this technique, Event subscribers can tell apart savings of PersonalProfile record from Picture records, for they are coming from different emitters and have a different emitterName. You can subscribe to 'Picture:save' or 'PersonalProfile:save' to make the distinguish. Usually, the emitterName is the name of the class-instance or object who produced the event, but the developer is free to choose any naming convention (see below).
When subscribing to events, you need to subscribe using the naming convention emitterName:eventName
. However, there are some rules that make subscription easier and very flexible:
*
can replace any or both of the parts as a wildcard. Thus, myMembers.after('*:save') subscribes to save-events of any emitter. Or subscription to all events of the "PersonalProfile"-emitter, can be done by myMembers.after('PersonalProfile:*').Event is meant to be used as a delegated eventsystem: all events bubble up to Event. This means: whatever instance subscribes to any events, subscription just works.
var Members = ITSA.Classes.createClass(null, Event.Listener),
myMembers = new Members();
myMembers.after('PersonalProfile:save', function(e) {
alert('personal profile is saved');
});
var membersproto = Event.Listener,
myMembers = Object.create(membersproto);
myMembers.after('PersonalProfile:save', function(e) {
// this === myMembers
// e.target === the instance of the member who emitted the event
// e.type === 'save'
// e.emitter === 'PersonalProfile'
});
In the example above, the callback
-function is also names the event-subscriber
. This could be an anonymous function, or by reference:
var afterClick = function(e) {
// this === myMembers
// e.target === the instance of the member who emitted the event
// e.type === 'save'
// e.emitter === 'PersonalProfile'
};
myMembers.after('click', afterClick, '#buttongo');
Note: this example assumes event-dom
is included, for that module makes dom-events emit click
to this event-module.
The subscriber always gets invoked with one argument: the eventobject
. This object holds valuable information:
e.status itself is an object with the following properties:
true | false
whether the event got executed (not halted or defaultPrevented)true
if any defaultFn got invokedtrue
if any preventedFn got invokedreason | true
if the event got halted and optional the whyreason | true
if the event got defaultPrevented and optional the whytrue
if the event cannot be made silentvar afterClick = function(e) {
var node = e.target;
// you can do anything with the node now
};
Event.after('click', afterClick, '#buttongo');
Profile.mergePrototypes(Event.Emitter('PersonalProfile'));
var eventobject = myProfile.emit('save');
if (eventobject.status.ok) {
// eventobject.status.halted --> tells if the event got halted and why
}
As the examples above show: the context
within the subscribers equals the object that did the subscription. This becomes very handy when you create Class-instances. The subscribers still get executed within the object-instance's context:
var MyMembers = ITSA.Classes.createClass(
// Constructor:
function (config) {
this.after('click', this.onClick, '#buttonGo');
},
// Prototype members:
{
onClick: function (e) {
alert('personal profile is saved');
// note: context is the object-instance
}
}
).mergePrototypes(I.Event.Listener);
You can easily create one subscription to multiple events. To do so, pass in an array of customEvents instead of one:
var afterClick = function(e) {
var node = e.target;
// you can do anything with the node now
};
myMembers.after(['click', 'dblclick'], afterClick, '#buttongo');
If you create a once-subscriber, the subscriber only gets invoked once at a total. Thus, if the click-event happened, the subscriber gets invoked, but it won't be invoked again on a dblclick-event.
Some events (not all) have a defaultFn
or even a preventedFn
. Typically, some DOM-events come with a defaultFn. For instance: clicking on an anchor-element will navigate to its href-location. HTML-forms will load the page defined by the action-attribute. Both cases are executed by their defaultFn.
DefaultFn's can be prevented by calling e.preventDefault()
in any before-subscriber. In such a case, not the defaultFn gets invoked, but the preventedFn (if defined).
DOM-events have no preventedFn. Custom Events created by #Event.defineEvent() on the other hand can.
The optional filter
-argument is meant to filter before the subscriber gets invoked. This example above makes the subscriber only to be invoked on clicks on an html-element with the id: #buttongo. It doesn't invoke on clicks of other elements.
The filter-argument can be a String-type of Function. In case of a String, a css-selector
is assumed. In case of a function, you are free to set it up any way you like. The function recieves the eventobject as its only argument, which can be inspected to descide how the function returns. Return true to make pass the filter.
Mostly you would subscribe through the after
-subscriber. This subscriber only gets invoked when the event did happen. Indeed, events may be interupted: this is exactly where the before
-subscribers come in the game.
Before-subscribers also get invoked when the event happens, but they get invoked before the event's defaultFn
takes place. Here, you can manipulate the eventobject, or change the event-lifecycle (see below). The following example shows how to manipulate the eventobject:
var beforeClick = function(e) {
e.clientAge = 23;
};
var afterClick = function(e) {
// e.clientAge --> 23
};
myMembers.before('click', beforeClick, '#buttongo');
myMembers.after('click', afterClick, '#buttongo');
After-subscribers always get invoked after the before-subscribers and after the defaultFn (if any).
The eventobject has several methods which can change the event-lifecycle, for instance:
var beforeClick = function(e) {
e.halt();
// stop the event --> no defaultFn and
// no after-listeners get invoked
};
var afterClick = function(e) {
var node = e.target;
// the code will never come here
};
myMembers.before('click', beforeClick, '#buttongo');
myMembers.after('click', afterClick, '#buttongo');
The eventobject
holds these methods that can be invoked (only inside the before-subscribers):
By passing reason
to the method, the eventobject is extended with extra information into its e.status-property. This won't be useful inside any after-subscribers (for they are not invoked), but it could be inside the preventedFn, or when you use the returned object that emit() creates. See the example above.
Sometimes, you only need a subscription the first time the event occurs. You can use onceBefore()
or onceAfter()
instead of before()/after() and the subscriber gets detached automaticly when the event occurs.
You can also detach these subscriptions yourself if you need, which is only effective before the event occured.
Whenever you subscribe to an event, the instance and subscriber are stored inside a private array, so the subscriber can be invoked. When you don't longer need the subscriber, or the instance who subscribred, you must remove the subscription. Otherwise the Garbage Collector cannot cleanup the instance and you'll end up with a memory-leak.
Unsubscribing can be done using detach()
in these 3 different ways:
By using the eventhandler
Whenever you subscribe using .before() or .after(), you get an object in return (the eventhandler
). This object has the method .detach(). This example shows how to use the eventhandler.
Event.detach(instance, customEvent) or Event.detachAll(instance)
Using Event.detach(instance, customEvent)
unsubscribes the supscription to the specified customEvent this instance made. Event.detach(instance)
unsubscribes all subscriptions of this instance.
instance.detach(customEvent) or instance.detachAll()
If you merged Event.Listener to the instance (see Listening for events), then you can use the detach-methods on the instance.
var afterClick = function(e) {
var node = e.target;
};
var clickHandler = myMembers.after('click', afterClick, '#buttongo');
// when no longer needed:
// unsubscribe by calling detach() on the eventhandler:
clickHandler.detach();
Events can be emitted through a Class-instance/object (by merging helper-functions
), or by using Event.emit()
. Both ways have their advantages.
By emitting (or triggering or fireing) an event through the Class-instance or object, you don't need to specify the emitterName and emitterInstance (e.target) all the time. By default, emitterName
is defined by the instance. In order to so so, you should merge Event.Emitter('emitterName') to the Class-instance or object. By doing this, the instances will have the following methods added:
Important note: Merging can be done at the prototype or at the instance itself. If you only have one instance, it doesn't really matter which one to choose. When you have a dozen of them, it is strongly recomended to merge the prototype. This way you keep on working with small instances. The examples below show how merging should be done at the prototype.
Profile.mergePrototypes(Event.Emitter('PersonalProfile'));
var myProfile = new Profile({name: 'Marco'});
myProfile.emit('save');
var profileproto = Event.Emitter('PersonalProfile'),
myProfile = Object.create(profileproto);
myProfile.name = 'Marco';
myProfile.emit('save');
Emitting through the instance is very handy, because you have quick access to the emit-method and you don't need to specify the emitterName on every emission.
Emitting an event can also be done using the emit()-method of Event. This is the suggested way when you have a huge number of objects, which you would like to keep lightweighted, yet they need to be able to emit events. Just keep them as plain objects. As an alternative, you could create these objects having the emitter-helper as prototype (see example above), but sometimes the objects are already created and you can't redefine the prototype:
IO.readObject('http://somedomain.com?groupId=1').then(
function(groupitems) {
// groupitems is an array with objects.
// this already got JSON-parsed
var item = groupitems[25];
item.src = 'newimage.jpg';
// item has a different src --> now we emit the changes:
Event.emit(item, 'image:change');
}
);
Subscribers of the event "PersonalProfile:save" will get the event-object with these properties:
Important note: You could skip passing an object as first argument. By doing so, the event does get fired, but e.target becomes Event itself. This might or might not be what you want.
If you call emit() either thrhough Event.emit() or the instance, you get the eventobject in return if the veent trully happened (not halted or preventDefaulted).
Because of this behaviour, you can emit an event and take action upon this specific emission:
var e = Event.emit('MyProfile:save');
if (e) {
// even did occur
}
Silent events are events that only invoke the defaultFn. No event-subscribers or finalization-subscribers are called. You can make an event silent, by passing the appropriate payload when emitting:
Event.emit('MyProfile:save', {silent: true});
Note: DOM-events can only be subscribed to when the module event-dom
is included.
DOM-events are autmaticly emitted by the UI-interface. But they can also be emitted manually using the .emit()-method.
This is a sort of simulating DOM-events, but not exactly:
A special feature (delivered by the module extra/objectobserve.js
), is the datachanged
-event. This event gives easy and platform-independent Object.observe
. It is meant to observe changes of any data-type that can be stringified. To avoid overloading the system, not all data-types are monitored: you need to specify what you want to observe, and you should unobserve when you don't need to observe any longer.
Starts the observation of a data-object. You need to pass an unique emitterName
as well: this will be used when the eventsystem emits the emitterName:datachanged
-event.
To end observing, use the returned handle.cancel()
, or Event.unobserve()
To end the observation of a data-object, defined with Event.observe
. Needs the emitterName as argument.
var datamodel = {
year: 2015,
members: 100
};
Event.observe('members', datamodel);
Event.after('members:datachanged', function(e) {
var datamodel = e.target,
emitter = e.emitter;
// process the event...
// which will happen when datamodel.members
// is set to 150
});
setTimeout(function() {
datamodel.members = 150; // <-- leads to event 'members:datachanged'
}, 1000);
To end the observation of all observers.
Listening for events can be done using the methods:
For clearness, it is suggested not to use on
and once
. These are aliases to the after
- and onceAfter
-method, and exist to keep consistency with Nodejs. Using after() is more expressive in a way it tells you in what pahse you really are in. These methods are available through Event, or they can be merged into Classes or instances.
The prefered way to listen for events is through the Class-instance or object. This also gives you objectmethods to detach subscribers made by this instance at once.
In order to so so, you should merge Event.Listener to the Class-instance or object. By doing this, the instances will have the following methods added:
Important note: Merging can be done at the prototype or at the instance itself. If you only have one instance, it doesn't really matter which one to choose. When you have a dozen of them, it is strongly recomended to merge the prototype. This way you keep on working with small instances. The examples below show how merging should be done at the prototype.
Members.mergePrototypes(Event.Listener);
var myMembers = new Members();
myMembers.after('PersonalProfile:save', function(e) {
// this === myMembers
// e.target === the instance of the member who emitted the event
// e.type === 'save'
// e.emitter === 'PersonalProfile'
});
var membersproto = Event.Listener,
myMembers = Object.create(membersproto);
myMembers.after('PersonalProfile:save', function(e) {
// this === myMembers
// e.target === the instance of the member who emitted the event
// e.type === 'save'
// e.emitter === 'PersonalProfile'
});
Listening for events could be done through the Event
as well. Using the before()- and after()-methods, where you need to specify the context at the third argument:
var myProfile = new Profile({name: 'Marco'});
Event.after('PersonalProfile:save', function(e) {
// handle the event
}, myProfile);
All event listeners and the default action (defaultFn
) are executed in the context of the listener, so they can have easier access to its own properties and methods. The context can be passed through as the 1st argument (see the API). If you ommit the listener -and give the customEvent as first parameter- then the context will be Event itself. This is what happened in all examples before Example 10.
You probably don't want the subscriber to get invoked on all eventName-events. Therefore subscriber can be filtered by 2 different ways, which can be combined:
This is the most convenient and quickest way. You just listen to the correct customEvent, which is defined as emitterName:eventName
myMembers.after('PersonalProfile:save', function(e) {
...
});
myMembers.after('PersonalActions:save', function(e) {
...
});
The filter-argument can also be used to filter before invocation of the subscriber. Mostly this will be used with DOM-events.
The filter-argument can be a String-type of Function. In case of a String, a css-selector
is assumed. In case of a function, you are free to set it up any way you like. The function recieves the eventobject as its only argument, which can be inspected to descide how the function returns. Return true to make pass the filter.
var MyClass = ITSA.Classes.createClass(
// Constructor:
function (config) {
this.after('Person:save', this.onPersonSave, this.onPersonSaveFilter);
this.after('click', this.onButtonClick, '#buttongo');
// whatever else
},
// Prototype members:
{
someProperty: 1,
someMethod: function () {
},
onButonClick: function (e) {
var buttonnode = e.target;
// do whatever
},
onPersonSave: function (e) {
var personModel = e.target;
// do whatever
},
onPersonSaveFilter: function (e) {
var personModel = e.target;
if ( /*some condition involving personModel*/ ) return true;
}
}
).mergeProperties(Event.Listener);
Emitters can also listen to only themselves. If you would listen using the emitterName:eventName
syntax, the subscriber would get invoked on any event, also of other objects with the same emitterName. By subscribing using this:eventName
, you basicly subscribe to its own emitterName, but also filter on its own instance.
var cb, member1, member2, member3, Member, count = 0;
Member = Object.createClass(
function (name) {
this.name = name;
this.after('this:send', this.afterSend);
},
{
afterSend: function(e) {
// 'PersonalProfile:send' -event was emitted
// this.name === 'itsa' in this example
}
}
);
Member.mergePrototypes(Event.Listener).mergePrototypes(Event.Emitter('PersonalProfile'));
member1 = new Member('a');
member2 = new Member('itsa');
member3 = new Member('b');
member2.emit('send');
Important note: While emitters may omit the emitterName-part when calling the emit()-method if they have previously defined the event through .defineEmitter(), if an event listener omits the emitterName-part, UI
will be assumed.
myMembers.after('save', function(e) {
// the `callback` will never be invoked as the DOM will never emit a `save`-event
});
Though the next example shows two subscribers which both work well and are basicly the same:
myMembers.after('click', function(e) {
// this === myMembers
// e.target === the DOM-node
// e.type === 'click'
// e.emitter === 'UI'
});
myMembers.after('UI:click', function(e) {
// this === myMembers
// e.target === the DOM-node
// e.type === 'click'
// e.emitter === 'UI'
});
The eventsystem can handle both Custom-events as well as DOM-events. They are handled in the same way. To enable DOM-events, the event-dom
-module should be included.
Events can be emitted right out of the box. However, is you want them more powerful, or more specific, you can define them first. By defining a customEvent, you can add make use these fascilities:
Customevents can be defined by using the defineEvent()
-method, which is available on Event or any object that got merged with Event.Emitter. Every customEvent needs to conform the syntax "emitterName:eventName". If you use defineEvent on an instance -which got extended with Event.Emitter-, you don't need to pass the emitterName. When invoking this method, you need to pass the applyable emitterName
as its only parameter. After invocation of defineEvent(), you can invoke more options in a chained way:
Profile = ITSA.Classes.createClass(null, ITSA.Event.Emitter('PersonalProfile'));
myProfile = new Profile({name: 'Marco'});
myProfile.defineEvent('save') // defines "PersonalProfile:save"
.defaultFn(function(e) {
// do something and optionally return
return true; // now available at e.returnValue
})
.preventedFn(function(e) {
// do something
console.warn('prevented');
});
Event.emit(myProfile, 'PersonalProfile:save'); // emits "PersonalProfile:save"
Profile.mergePrototypes(Event.Emitter('PersonalProfile'));
var myProfile = new Profile({name: 'Marco'});
myProfile.defineEvent('save') // defines "PersonalProfile:save"
.defaultFn(function(e) {
// do something and optionally return
return true; // now available at e.returnValue
})
.preventedFn(function(e) {
// do something
console.warn('prevented');
});
myProfile.emit('save'); // emits "PersonalProfile:save"
If you don't need the customEvent-definition anymore, you can undefinei> it by using undefEvent()
, which is available on Event or any object that got merged with Event.Emitter. To undefine all customEvents an instance made, use undefAllEvents()
.
Modules may want to delay customEvent-definitions until it is actually needed. To do that, they can ask the event system to tell them when any other instance has subscribed this particular customEvent. DOM-events also define themselves this way.
var myProfile = new Profile({name: 'Marco'});
Event.notify('PersonalProfile', function(eventinstance, customEvent) {
// customEvent === "PersonalProfile:"+eventName
eventinstance.defineEvent(customEvent)
.defaultFn(function(e) {
// do something and optionally return
return true; // now available at e.returnValue
})
.preventedFn(function(e) {
// do something
console.warn('prevented');
});
});
myMembers.after('PersonalProfile:save', function() {...}); // makes 'PersonalProfile:save' to register its customEvent
To stay high-performant, this eventsystem does not wait for any Promises to return, neither in the subscribers, nor in the defaultFn's. However, you can make the defaultFn to return a Promise and inspect its value in any after-subscriber:
Members.mergePrototypes(Event.Listener);
var myMembers = new Members(),
profileproto = Event.Emitter('PersonalProfile'),
myProfile = Object.create(profileproto),
uri = '/loaddata';
profileproto.defineEvent('load')
.defaultFn(function(e) {
var model = e.target;
return ITSA.IO.getObject(uri+'?id='+model.id).then(
function(response) {
model.merge(response);
return response; // pass it through to any subscriber
}
);
});
myProfile.id = 1;
myProfile.emit('load');
myMembers.after('PersonalProfile:load', function(e) {
var loadpromise = e.returnValue;
loadpromise.then(
function(response) {
// we know myProfile has loaded all its data
// the same data is available inside "response"
}
);
});
Most of the time, the emitter is the object that is labeled by its emitterName. However, in some circumstance, you might want to emit a specific customEvent ("emitterName:eventName") by another instance. Like emitting on behalf of another emitterName. You can do this by fully describe the customEvent, even if the emitter has its own -different- emitterName:
RedProfile.mergePrototypes(Event.Emitter('RedProfile'));
ProfileContainer.mergePrototypes(Event.Emitter('ProfileContainer'));
var myProfile = new RedProfile({name: 'Marco'});
var myProfileContainer = new ProfileContainer();
Event.after('RedProfile:save', function(e) {
// e.target === myProfileContainer
// e.type == 'save'
// e.emitter = 'RedProfile'
});
// force emit with emitterName 'RedProfile' instead of its default 'ProfileContainer'
myProfileContainer.emit('RedProfile:save');
Synthetic Events are very easily created. Just listen for any event under specific circumstances, and fire you new synthetic event:
Event.after(
'click',
function(e) {
// once here, the button was enabled
Event.emit(e.target, 'pureenabledclick', e);
},
window,
function(e) {
// we are only interested in buttons who don't have
// the .pure-button-disabled className
var node = e.target;
return !node.hasClass('pure-button-disabled');
}
);
It is sometimes confusing to separate different terms. Below is a short list of naming-conventions to help here:
payload
Extra object that is passed when an event is emitted. The payload gets merged into the eventobject.
eventobject
The object that passes through the event lifecycle. The eventobject always has some default-properties which are generated at the start of any event. Thse properties can be extended by a emit-payload, or manipulated inside subscribers.
customEvent
Eventdescription defined with the syntax 'emitterName:eventName'. Any defined event is a customEvent: both DOM-events as well as "Custom Made Events".
Custom made event
An customEvent that was created through Event.defineEvent().
eventName
Second part of the customEvent. The eventName is like the action of the event and available at e.type.
emitterName
First part of the customEvent. The emitterName is the identification of the owner of the action (eventName) and available at e.emitter.
e.target
The object that emitted the event. Most of the time e.target will be the instance with the emittename sent. You may also see it as the source. We would have prefered to call it e.src, but the DOM uses e.target so we kept using it.
e.type
Equals the eventName. We would have prefered to call it e.Eventname, but the DOM uses e.type so we kept using it.
subscribe v.s. listen to events
Formally spoken, you can listen to events by create a subscription to them (adding a subscriber).
Event-listener v.s. Event-subscriber
These are the same.
Eventhandle
Handle that you get back when you subscribe to an event. This hande can be used to unsubscribe.
Table of Contents