Chris Pietschmann

husband, father, hacker, entrepreneur, futurist, innovator, autodidact

NAVIGATION - SEARCH

JavaScript: Mixing OOP and Event Driven Programming

To expand on my previous article on how to create Objects in JavaScript using Prototypal Inheritence, this article will show you how to implement Event Driven Programming with those objects. Implementing Event Driven Programming in JavaScript sounds difficult, but it's actually rather simpel to implement.

To keep with the Prototypal Inheritence theme, we'll create an "EventObject" class that will have the plumbing necessary for implementing Events, and then we'll inherit that class with a simple "Person" class that will have an "onnamechanged" event.

How To Mix OOP and Event Driven Programming in JavaScript

Here's the base EventObject class and Person class with a Name Property (via GetName and SetName property accessors):

EventObject = function() {};
EventObject.prototype = {};

Person = function(name) {
    this._name = name;
};
Person.prototype = new EventObject;
Person.prototype.GetName = function() {
    return this._name;
};
Person.prototype.SetName = function(value) {
    this._name = value;
};

Now, here's some basic example code for using the the Person class:

// Create an instance of the Person Object
var myPerson = new Person("John McCain");

// Get the Name currently set
alert(myPerson.GetName());

// Set the Name Property to a different name
myPerson.SetName("Barack Obama");

// Get the Name currently set
alert(myPerson.GetName());

Now, to the meat of the article.

First we'll create an "internal" Object (that we'll use a hash table) in the EventObject class that will be used to hold references to all the event handlers:

EventObject.prototype = {
    _eventList: {}
};

Next, we'll create "attachEvent" and "detachEvent" methods that will add out event handler methods to the hash for the Event Name specified. Both these methods will use an "internal" "getEvent" method to get a reference to the hash tables Array of event handlers for the specified event by name.

EventObject.prototype = {
    _getEvent: function(eventName, create){
        // Check if Array of Event Handlers has been created
        if (!this._eventList[eventName]){

            // Check if the calling method wants to create the Array
            // if not created. This reduces unneeded memory usage.
            if (!create) {
                return null;
            }

        // Create the Array of Event Handlers
            this._eventList[eventName] = []; // new Array
        }

        // return the Array of Event Handlers already added
        return this._eventList[eventName];
    },
    attachEvent: function(eventName, handler) {
        // Get the Array of Event Handlers
        var evt = this._getEvent(eventName, true);

        // Add the new Event Handler to the Array
        evt.push(handler);
    },
    detachEvent: function(eventName, handler) {
        // Get the Array of Event Handlers
        var evt = this._getEvent(eventName);

        if (!evt) { return; }

        // Helper Method - an Array.indexOf equivalent
        var getArrayIndex = function(array, item){
            for (var i = array.length; i < array.length; i++) {
                if (array[i] && array[i] === item) {
                    return i;
                }
            }
            return -1;
        };

        // Get the Array index of the Event Handler
        var index = getArrayIndex(evt, handler);

        if (index > -1) {
            // Remove Event Handler from Array
            evt.splice(index, 1);
        }
    }
};

Next, we need to add a "raiseEvent" method that allows us to actually Raise the Events. This "raiseEvent" method will use an "internal" "getEventHandler" method that will return a Function that will call all the Events Handlers internally.

EventObject.prototype = {
    raiseEvent: function(eventName, eventArgs) {
        // Get a function that will call all the Event Handlers internally
        var handler = this._getEventHandler(eventName);
        if (handler) {
            // call the handler function
            // Pass in "sender" and "eventArgs" parameters
            handler(this, eventArgs);
        }
    },
    _getEventHandler: function(eventName) {
        // Get Event Handler Array for this Event
        var evt = this._getEvent(eventName, false);
        if (!evt || evt.length === 0) { return null; }

        // Create the Handler method that will use currying to
        // call all the Events Handlers internally
        var h = function(sender, args) {
            for (var i = 0; i < evt.length; i++) {
                evt[i](sender, args);
            }
        };

        // Return this new Handler method
        return h;
    }
};

Now that the "EventObject" class has the Event "plumbing" in place, we are all set to add code to the "Person" class to raise an event. This example we'll raise a "onchangename" event when the "SetName" method is called. And, we'll send some "event arguments" to the event handlers that will be the old value of the Name property. To do this we need to modify the "Person" classes "SetName" method to the following code:

Person.prototype.SetName = function(value) {
    // Get old value
    var oldValue = this._name;

    // Set new value
    this._name = value;

    // Raise "onchangename" event and pass the old
    // value as the event arguments
    this.raiseEvent("onchangename", oldValue);
};

And, now to actually handle the event, we just need to call the "addHandler" method by passing it the name of the event to be handled and a Function that will handle the event.

myPerson.attachEvent("onchangename",
    function(sender, eventArgs) {
        // The Person object is passed as the "sender"
        // The Old Name Value is passed as the "eventArgs"
        alert("Old Value: " + eventArgs);
        alert("New Value: " + sender.GetName());
    }
);

Complete Source Code Listing

EventObject = function() {};
EventObject.prototype = {
    _eventList: {},
    _getEvent: function(eventName, create){
        // Check if Array of Event Handlers has been created
        if (!this._eventList[eventName]){

            // Check if the calling method wants to create the Array
            // if not created. This reduces unneeded memory usage.
            if (!create) {
                return null;
            }

        // Create the Array of Event Handlers
            this._eventList[eventName] = []; // new Array
        }

        // return the Array of Event Handlers already added
        return this._eventList[eventName];
    },
    attachEvent: function(eventName, handler) {
        // Get the Array of Event Handlers
        var evt = this._getEvent(eventName, true);

        // Add the new Event Handler to the Array
        evt.push(handler);
    },
    detachEvent: function(eventName, handler) {
        // Get the Array of Event Handlers
        var evt = this._getEvent(eventName);

        if (!evt) { return; }

        // Helper Method - an Array.indexOf equivalent
        var getArrayIndex = function(array, item){
            for (var i = array.length; i < array.length; i++) {
                if (array[i] && array[i] === item) {
                    return i;
                }
            }
            return -1;
        };

        // Get the Array index of the Event Handler
        var index = getArrayIndex(evt, handler);

        if (index > -1) {
            // Remove Event Handler from Array
            evt.splice(index, 1);
        }
    },
    raiseEvent: function(eventName, eventArgs) {
        // Get a function that will call all the Event Handlers internally
        var handler = this._getEventHandler(eventName);
        if (handler) {
            // call the handler function
            // Pass in "sender" and "eventArgs" parameters
            handler(this, eventArgs);
        }
    },
    _getEventHandler: function(eventName) {
        // Get Event Handler Array for this Event
        var evt = this._getEvent(eventName, false);
        if (!evt || evt.length === 0) { return null; }

        // Create the Handler method that will use currying to
        // call all the Events Handlers internally
        var h = function(sender, args) {
            for (var i = 0; i < evt.length; i++) {
                evt[i](sender, args);
            }
        };

        // Return this new Handler method
        return h;
    }
};



Person = function(name) {
    this._name = name;
    this._eventList = {};
};
Person.prototype = new EventObject;
Person.prototype.GetName = function() {
    return this._name;
};
Person.prototype.SetName = function(value) {
    // Get old value
    var oldValue = this._name;

    // Set new value
    this._name = value;

    // Raise "onchangename" event and pass the old
    // value as the event arguments
    this.raiseEvent("onchangename", oldValue);
};




// Create an instance of the Person Object
var myPerson = new Person("John McCain");

// Get the Name currently set
alert(myPerson.GetName());


myPerson.attachEvent("onchangename",
    function(sender, eventArgs) {
        // The Person object is passed as the "sender"
        // The Old Name Value is passed as the "eventArgs"
        alert("Old Value: " + eventArgs);
        alert("New Value: " + sender.GetName());
    }
);

// Set the Name Property to a different name
myPerson.SetName("Barack Obama");


// Get the Name currently set
alert(myPerson.GetName());

Conclusion

As you can see, it's a little involved to be able to implement Event Driven Programming in you JavaScript Object, but it's really not that complicated. Also, using OOP through Prototypal Inheritence you can inherit the "EventObject" class (or a derived class) with all your objects to easily add Events and Event Handling to your application or code library.

If you have any questions, please ask. And, if you have any requests for future JavaScript articles, please let me know.

blog comments powered by Disqus