Custom Event Management in JavaScript

by

If you have used any JavaScript libraries before, you are most likely quite familiar with the concept of binding functions to specific events within an object, but have you ever considered creating your own custom events for your JavaScript classes to allow users even more flexibility in implementing your code? Binding listeners to user events (such as click or mouseover) is a necessity for robust coding, but what happens when I want to allow developers to execute a specific bit of logic only when my library element has been rendered to the page? I need to build my code in such a way as to give “hooks” to the coder in the form of events for which they can listen.

Some libraries like ExtJS already support quite extensive custom events, primarily on their UI elements. It is this behavior for which I was seeking in a current project. Quickly becoming tired of implementing the same addListener(), removeListener and fireEvent() methods on all my framework elements, I began to devise a more generic way of enabling and implementing custom events in my classes. The challenge was not blurring the line of context too much between the event management system and the actual object firing the events themselves. I wanted a system that would allow for object level interaction with events without having to duplicate the code over and over again. Enter fn.call() and fn.apply().

JavaScript call() and apply()

Those of you well versed in the specifics of executing JavaScript functions and providing a context in which they are to execute, feel free to jump ahead to the next section, but an understanding of this principle is paramount for fully understanding how this design works. In concept, the execution is very straight forward: I want to define a function that can be called from within the scope of a separate object. Basically, I want to create a function foo() and get it to behave as though it were part of a class – Foo.bar().

This is where call() comes in. By passing our context object (or scope) to call(), the function is executed in that context:

function bar() {
    alert(this.name);
}

var Foo = function() {
    this.name = 'Foo';
};
var o = new Foo();
bar.call(o);

As you can see, our function is written to reference this.name from within the context in which it is executed. If you were to execute it directly in the global context (bar()this.name is not defined. However, by calling the function from the context of a Foo object, we can resolve the name and alert it successfully.

Additional parameters that are passed to call() will serve as parameters for the function definition itself:

function bar(name) {
    alert('Object name: ' + this.name);
    alert('Parameter name: ' + name);
}

bar.call(o, 'baz');

This code will first alert “Foo”, as it is the member variable for the object on which bar() is executed. Secondly, “baz” will be alerted, since this is passed as the parameter to the function itself. Any number of additional parameters can be passed into the call() method, and all will be passed along to the underlying function call.

However, occasionally, you may have a need for a variable length of parameters to be passed along, so rather than duplicating code, we can use the apply() method instead. Rather than accepting an indefinite number of parameters, apply() takes the context for the first argument and an array as the second. This array will be expanded and passed along as individual parameters to the executed code:

function move(x, y) {
    this.x = x;
    this.y = y;
}

var Point = function(x, y) {
    this.x = x;
    this.y = y;
    this.getPos = function() {
        return this.x + ', ' + this.y;
    };
};
var p = new Point(10, 10);
alert(p.getPos());
move.apply(p, [25, 25]);
alert(p.getPos());

As you might expect, this snippet will allow you to change the coordinates of the Point object by passing in an array of new coords.

Now that we have covered the basics behind the core JavaScript implementation we’re going to leverage, let’s see how this can be applied to custom event management.

Setting up the Event Manager

Our manager is fairly simple in design. It expects that an object on which it executes will have a hash of listener arrays keyed by the event to which they are bound. In this way, if we fire a “render” event, all the listeners should be stored under this.listeners.render. By looping over and triggering whatever exists in that portion of the hash, we have successfully executed our event listeners.

var Events = {
    fireEvent : function(ev, args) {
        if (!!this.listeners[ev]) {
            for (var i = 0; i < this.listeners[ev].length; i++) {
                // Execute in the global scope (window), though this could also be customized
                this.listeners[ev][i].apply(window, args);
            }
        }
    }
};

With this alone, we already have something functional we can use on an object to fire a custom event. By calling fireEvent on the context of an object, that object will be checked for any internal listeners for the event being fired:

var o = MyObj();
o.listeners = {
    render : [function() { alert('fired!'); }]
};
Events.fireEvent.call(o, 'render');

Now that we are able to trigger events within our individual contexts, we want to set up a way to bind and release listeners from those contexts. To do so, let's set up addListener() and removeListener() methods respectively:

// Inside the Events declaration
addListener : function(ev, fn) {
    // Verify we have events enabled
    Events.enable.call(this, ev);

    if (!this.listeners[ev]) {
        this.listeners[ev] = [];
    }

    // Verify a function is being added
    if (fn instanceof Function) {
        this.listeners[ev].push(fn);
    }
},

removeListener : function(ev, fn) {
    if (!!this.listeners[ev] &&
        this.listeners[ev].length > 0) {
        // If a listener is provided
        if (!!fn) {
            var fns = [];
            for (var i = 0; i < this.listeners[ev].length; i++) {
                if (fn != this.listeners[ev][i]) {
                    fns.push(this.listeners[ev][i]);
                }
            }
            this.listeners[ev] = fns;
        } else { // No listener, so remove them all
            this.listeners[ev] = [];
        }
    }
}

This might look like a significant bit of code, but it's very straight forward. If we call addListener() and provide both an event name and a function, the function will be bound to the event name we provide. Of course, we want to do a little sanity checking, so we want to validate that the second parameter is indeed a Function instance. Likewise, if we provide an event to removeListener(), we can clear out a specific action from the assigned behavior. Consider the following:

function bar() {
    alert('fired!');
}
var o = new Foo();
Events.addListener.call(o, 'render', bar);
Events.fireEvent.call(o, 'render'); // Will alert "fired!"
Events.removeListener.call(o, 'render', bar);
Events.fireEvent.call(o, 'render'); // Will do nothing

While functional, this is still not ideal. For one thing, the average JavaScript coder who picks up your class to use may not understand the gist of using call() for all the assignments and firing of events. In order to simplify this a bit, we do a little bit of voodoo that will set up object methods for each class in which we want to use events that can be used as standard methods. By creating an enable() method, we can take the context from the user once and implement the other Events calls without the need for intricate JavaScript knowledge. Consider the following code (our last method within the Events object):

enable : function() {
    var self = this;
    if (!self.listeners) {
        self.listeners = {};
    }

    self.fireEvent = function(ev, args) {
        Events.fireEvent.call(self, ev, args);
    };

    self.addListener = function(ev, fn) {
        Events.addListener.call(self, ev, fn);
    };

    self.removeListener = function(ev, fn) {
        Events.removeListener.call(self, ev, fn);
    };
}

All we're doing is creating wrappers for our Events methods, but we're creating a mechanism by which we pass along the context of the current object. So, once we have enabled event management for a class (within its initialization logic is ideal), we can simply call the management methods directly on the object itself:

var Foo = function() {
    Events.enable.call(this);
};

function bar() {
    alert('fired!');
}

var o = new Foo();
o.addListener('render', bar);
o.fireEvent('render'); // alerts "fired!"
o.removeListener('render', bar);
o.fireEvent('render'); // does nothing

To see the fully functional and documented definition for the Events object, feel free to view the source here (opens in new window).

Implementation and Usage

Now that we have a fairly robust events management system, let's put it to good use. By defining the API for each event that is fired, we can provide very nice interaction and customization to our users. Let's take a very basic use case and look at how we might want to use some event management for logging or debugging purposes. Let's build a simple Car object with accel() and decel() methods that modify a member variable this.speed. We will enable event mapping and fire the "accel" and "decel" events upon successful execution of each method:

var Car = function(sp) {
    this.speed = (sp || 55);
    Events.enable.call(this);

    this.accel = function(amount) {
        this.speed += amount;
        this.fireEvent('accel', [this.speed]);
    };

    this.decel = function(amount) {
        this.speed -= amount;
        this.fireEvent('decel', [this.speed]);
    };
};

Notice that we're adding in some callback parameters here. In this case, we're providing the current speed as a parameter to the listeners when we fire the event. By defining the API of the event listeners within our own classes, we can very easily instruct our users on what information is available to them at which time. In a case like this, we could easily build a new Car object and slap on a listener to the "accel" event to check that we're not going too fast:

var c = new Car(); // default speed is 55
c.addListener('accel', function(sp) {
    // Log to FF console
    console.log('Current speed: ' + sp);
    if (sp >= 100) {
        alert('Car is going way too fast!');
    }
});
c.accel(40); // Current speed: 95
c.accel(10); // Current speed: 105 ALERT!

Of course, this is a very basic example to show the power behind creating your own events within your own JavaScript classes. You can see how attaching events to the render mechanism for advance UI elements or to specific actions within a complex Ajax communication would come in handy. If you were to tack on a few listeners that simply did some debug logging, you could more easily trace and identify problems in your code by seeing where execution failed.

Additionally, be aware that the addListener() method simply pushes the new listener onto an array, so you can add multiple listeners from different places in your code. Each will be executed and passed the same set of parameters.

I hope this article has been clear and helpful. I have tested each code snippet to assure it works, but if you have questions or recommendations that would enhance this concept, please feel free to contact me.

-GH


15 Comments »

  1. Hi Garth,
    I like your solution, but wanted to point out that your check to verify that events are enabled in the ‘addListener’ function will erase the existing listeners object. The result is that the last listener added becomes the only active one.

    I’ve wrapped the line with the following if statement to solve the issue.

    if (!this.listeners) {
        // Verify we have events enabled
        Events.enable.call(this, ev);
    }   
    

    Cheers

    Comment by alan borthwick — 22 Feb 2012 @ 9:30 am

  2. Thanks, Alan. I actually did notice that in an application on which I was working, but I forgot to update the code here on the blog. I’ll do that now, thanks!

    Post has now been updated with the check. Additionally, I have added a check to the addListener method that verifies events have been enabled. This lets you apply events to previously un-enabled objects.

    Comment by obsidian — 22 Feb 2012 @ 10:05 am

  3. Garth,

    I love this article. But I have a question. As I see it, the object responds to its own events. How do I get disperate objects to respond to an event. Case in point: I want an object to capture a resize event. Now, I coud hard code all the objects that are affected by the resize event in the callback function, but that means every new object I make, I will have to go back into the function and add this object. I was hoping to use this method you have here to make it so I would not have to do this, but I cant see the path. I guess what Im hopping for is something like this:

    myObject.addListener(‘windowChange’, function(){…some code here…})

    $(window).resize(function(event) {
    fireEvent(‘windowChange’);
    })

    I know what I have here is gibberish, but illustrates the need. Any thoughts (if you have time)? Cheers

    Jodah

    Comment by jodah — 20 Aug 2012 @ 10:11 am

  4. If I understand the question, you want to trigger multiple events on different objects when a single overarching event is fired. I can see a couple possible solutions, one of which would be supported with this code. Basically, you would have to register each of your “child” or trickle down objects as listeners to the parent event:

    var o1, o2, o3;
    myObj.addListener('windowChange', function() { o1.triggerEvent('childEvent'); });
    myObj.addListener('windowChange', function() { o2.triggerEvent('childEvent'); });
    myObj.addListener('windowChange', function() { o3.triggerEvent('childEvent'); });
    
    myObj.triggerEvent('windowChange');
    

    Not sure if this is what you’re after or not.

    Comment by obsidian — 20 Aug 2012 @ 10:43 am

  5. Hello Gart,

    I’m very impressed by the simplicity of your code, and would like to thank you for it.
    However, as you are mentionning sanity check, here how I rewrote the add and remove functions. I did this also because performance matters, so, no need to add a same function twice. Also, for the removal, i’m not iterating over the whole array. I’m finding the position of the function to remove, and just use splice().
    Check this and tellme what you think:

    addListener : function(ev, fn) {
    Events.enable.call(this, ev);
    if (!this.listeners[ev]) this.listeners[ev] = [];
    if (!(fn instanceof Function)) return;
    for (var i in this.listeners[ev])
    if (this.listeners[ev][i] === fn) return;
    this.listeners[ev].push(fn);
    },
    removeListener : function(ev, fn) {
    if (!this.listeners[ev]) return;
    if (!(this.listeners[ev].length > 0)) return;
    if (!fn) { this.listeners[ev] = []; return; }
    for (var i in this.listeners[ev]) {
    if (this.listeners[ev][i] === fn) {
    this.listeners[ev].splice(i, 1);
    return;
    }
    }
    }

    Comment by toxcct — 20 Aug 2012 @ 2:09 pm

  6. I think this is exactly what I was looking for. Thank you!

    Comment by jodah — 20 Aug 2012 @ 3:03 pm

  7. Jodah, great to hear. Glad it was a help!

    Comment by obsidian — 21 Aug 2012 @ 6:02 am

  8. @toxcct, thanks for the comment. I agree that running those checks is definitely essential for the production version, though I intentionally left some of the complexity out for sake of understanding. Your code is a good example of how to filter out duplication of logic, though there is one thing I would challenge you with: rather than running your if statements and loops onto one line, let your code breathe and then minify it instead of worrying excessively on keeping things so compact for the original code. Also, I personally prefer to not have multiple return points in a method, but rather break out of a loop and always return from the same location. One other thing in your code I have to point out that wouldn’t pass code review on my team: you should never use a for-in loop on an Array, since it loops over the prototype of the element rather than simply the values. Just use a simple for loop instead. The following code also has a bit more optimization built in with regards to dereferencing, etc.

    addListener : function(ev, fn) {
        Events.enable.call(this, ev);
        var list = (this.listeners[ev] || []);
    
        // Notice the positive check here
        if (fn instanceof Function) {
            var exists = false;
            for (var i = 0; i < list.length; i++) {
                if (list[i] === fn) {
                    exists = true;
                }
            }
    
            if (false === exists) {
                list.push(fn);
            }
        }
    
        this.listeners[ev] = list;
    }
    

    Comment by obsidian — 21 Aug 2012 @ 6:14 am

  9. Hi Gart,

    Thanks for your answer; I didn’t know the difference between the “for in” and the “classical for” constructions (is this a standard feature, or simply that all the browsers don’t have the same behavior ?).

    For the performance instructions, I still agree partly with you.
    Actually, I’m the first to claim that code should be readable, hence using a single return at the end, and such coding good practices. However, when coming to optimizing your code (and only then, don’t get me wrong), then using such early “return” statements can save some cycles, rather than just using breaks and ifs, which will still proceed the execution cursor till the end.

    BTW, i’m not properly from the JS world (C/C++ ruled my youth :-) ), so i’m still learning, but I know these assertions remain true anyway.

    Comment by toxcct — 21 Aug 2012 @ 8:58 am

  10. @toxcct, if you run a for versus for-in on an Array object, the intrinsic output is the same, but if you have a 3rd party library – or if you yourself – have modified the prototype of the Array (say, to add a new method), you will begin to get the additional prototype elements back in a for-in loop as well.

    I agree that returning from a method early does indeed save some cycles, though I would argue that, in the scope of the webpage, the more readable code outweighs the few additional cycles in this situation (at least in my opinion). Again, this is where the artistry of development comes into play: each is entitled to execute as they see fit. There is a balance between maintainable code and optimized code, and each person has to determine where that line is for themselves. I’m not down on anyone who draws it a little further one way or the other from where my line is at all, but in this case, I would personally take the hit of a couple cycles of a for loop in order to have a single entry (and exit) point to a method like this.

    Just some thoughts, but GREAT points, thanks!

    Comment by obsidian — 21 Aug 2012 @ 10:11 am

  11. @toxcct, I just threw together a quick Fiddle to show of the dangers of using a for-in loop on an Array. There are those out there who will argue that it’s not worth worrying about, but in the world we live in with shared code and 3rd party resources, you have to account for other people’s bad practices, too: http://jsfiddle.net/GRCXY/.

    Comment by obsidian — 21 Aug 2012 @ 10:35 am

  12. Thanks for pointing out this last detail. Actually, now we talk about it, you’re right, I already met this issue when I add to add a “filter()” function to the prototype of Array.
    But I then wondered why the for-in loop in IE was iterating over that extra element, while Chrome wasn’t.
    Now I understand that Chrome wasn’t because I was actually extending the prototype conditionally :
    if (!Array.prototype.filter) {…}

    Now, I believe I was scared about the “classical for” because the “length” property not always reflects the exact number of elements in the Array. for instance, this would fail:
    var arr = []; //length == 0
    arr[15] = “foo”; // length now equals 15, with a single element in the array…

    So how to do then…?

    Comment by toxcct — 21 Aug 2012 @ 11:40 am

  13. This might help you: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/length

    Remember that when you assign an element to an array with an explicit index like that, the preceding indexes will be padded with undefined, so you do have the spacial representation for 16 elements in your example (16 due to the fact that it’s a zero based index). If you are truly wanting something that behaves like a hash map, you might be better off creating your own data type that inherits the properties you wish from the native Array and Object objects.

    Comment by obsidian — 21 Aug 2012 @ 12:04 pm

  14. Hello Garth,

    Sorry to spoil your article again, but back on the subject, I think I’m encountering an issue with your code. Not sure if it’s a bug in your code or in IE though.
    Yeah, I said it. Bloody Internet Explorer (prior to version 9) crashes my page, which BTW works perfectly in every other browsers, IE9 included, with a kind of obscure message to me :
    > SCRIPT5007: Function.prototype.apply : the argument is null or undefined.
    > events.js, Line 21 Character 5
    The line pointed to by the error in the Console is in the fireEvent() function, at line “fn.apply(window, args);”
    The argument reported as null appears to be “args” (which is null indeed, because not -always- passed), and when I change the line into “fn.apply(window)”, IE doesn’t report any error anymore.
    I could let it like that, but 1) I need to pass arguments to some events sometimes, and 2) it’s not right to have your code dictated by IE (^___^).

    Do you have an idea of why this happens, and how to counter it ?

    Thanks very much.

    Comment by toxcct — 22 Aug 2012 @ 12:15 pm

  15. Well, in this case, you could always just check the args first to be sure it exists:

    if (!!args) {
        this.listeners[ev][i].apply(window, args);
    } else {
        this.listeners[ev][i].call(window);
    }
    

    Comment by obsidian — 22 Aug 2012 @ 12:33 pm

RSS feed for comments on this post. TrackBack URL

Leave a comment