Module-documentation

version 1.0.0
module: itsa-event version 0.0.2
size-min gzipped: 3.9 kb (incl. dependencies: kb)
dependencies: polyfill/polyfill-base.js, js-ext/lib/function.js, js-ext/lib/object.js
maintanance: Marco Asbreuk
home


all modules

Custom Events

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.

Getting Started

With nodejs:

Create your project using this package.json. After downloaded, run npm install and start writing your application:

var ITSA = require('itsa'),
    Event = ITSA.Event;

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>
<script>
    var Event = ITSA.Event;
</script>

The Basics

Event lifecycle

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.

Before phase

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.

Default action

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.

Prevented action

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.

After phase

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.

emitterName:eventName

Events are identified by their customEvent-name, which has the syntax: emitterName:eventName. Two emitterNames are reserved:

emitterName

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.

eventName

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.

usage

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:

Event-subscription

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.

eventsubscription using Classes

var Members = ITSA.Classes.createClass(null, Event.Listener),
    myMembers = new Members();
myMembers.after('PersonalProfile:save', function(e) {
    alert('personal profile is saved');
});

eventsubscription using Objects

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'
});

Callback

In the example above, the callback-function is also names the event-subscriber. This could be an anonymous function, or by reference:

subscribe 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.

Event-object

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:

examine the eventobject inside an after-subscriber

var afterClick = function(e) {
    var node = e.target;
    // you can do anything with the node now
};
Event.after('click', afterClick, '#buttongo');

examine the emit-method

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
}

Context

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);

Combined subscriptions

You can easily create one subscription to multiple events. To do so, pass in an array of customEvents instead of one:

create combined subscriptions

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.

defaultFn and preventedFn

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.

Special features

The filter-argument

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.

Before-subscribers and after-subscribers

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:

manipulating the eventobject inside before-subscriber

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).

e.preventDefault() and e.halt()

The eventobject has several methods which can change the event-lifecycle, for instance:

halt the event inside the before-subscriber

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.

One time subscriptions onceBefore() and onceAfter()

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.

Keep the memory healthy: detach()

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:

detach subscriber using the evenhandler

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();

Emitting events

Events can be emitted through a Class-instance/object (by merging helper-functions), or by using Event.emit(). Both ways have their advantages.

Using .emit() on the Class-instance/object

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.

emitting event through a Class-instance

Profile.mergePrototypes(Event.Emitter('PersonalProfile'));
var myProfile = new Profile({name: 'Marco'});
myProfile.emit('save');

emitting event through plain object

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.

Using Event.emit()

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:

emitting event on behalf of plain object

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.

emit() returns eventobject

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).

Take one-time action

Because of this behaviour, you can emit an event and take action upon this specific emission:

emitting event and take action

var e = Event.emit('MyProfile:save');
if (e) {
    // even did occur
}

Silent events

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:

emitting event silently

Event.emit('MyProfile:save', {silent: true});

DOM-events

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:

datachanged-event

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.

Event.observe()

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()

Event.unobserve()

To end the observation of a data-object, defined with Event.observe. Needs the emitterName as argument.

Example datachanged-event

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);

Event.unobserveAll()

To end the observation of all observers.

Listening for events

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.

Using .before() and .after() on the Class-instance/object

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.

subscribe to an event through a Class-instance

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'
});

subscribe to an event through plain object

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'
});

Using Event.before() and Event.after()

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:

listening for events using Event.after()

var myProfile = new Profile({name: 'Marco'});
Event.after('PersonalProfile:save', function(e) {
    // handle the event
}, myProfile);

Listener context

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.

Filtering the subscriber

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:

Filter by emitter

This is the most convenient and quickest way. You just listen to the correct customEvent, which is defined as emitterName:eventName

Listening to specific emitter

myMembers.after('PersonalProfile:save', function(e) {
    ...
});

myMembers.after('PersonalActions:save', function(e) {
    ...
});

Filter by filter-argument

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.

Listening to specific emitter

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);

Listening with emitterName "this"

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.

listening to an event that never occurs

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');

Careful when listening without the emitterName

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.

listening to an event that never occurs

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:

successfully listening to DOM-events

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'
});

DOM-events

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.

Defining customEvents

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:

define a customEvent through Event

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"

define a customEvent through an instance

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"

Undefine customEvent definitions

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().

Delayed defineEvent() using "subscriber notification"

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.

delayed eventDefine()

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

Handling asynchronicity

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:

take action after Promise-result

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"
        }
    );
});

Emitter doesn't need to be the owner of emmiterName

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:

emitting event with 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');

Creating Synthetic Events

Synthetic Events are very easily created. Just listen for any event under specific circumstances, and fire you new synthetic event:

creating "pureenabledclick"-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');
    }
);

Naming conventions

It is sometimes confusing to separate different terms. Below is a short list of naming-conventions to help here:

API Docs