Home > JQuery, Javascript > Building your own Javascript library (part 3)

Building your own Javascript library (part 3)

December 2nd, 2009

Goal

In this final part of the series, I’m going to create a Publish/Subscribe provider for my new framework. With this so-called pubsub provider, it will be possible for any function to create or fire an event. Any other function may then listen for that event, and will get triggered when the event occurs. My main goal is to make sure that this provider is easy to use, not only for plugins, but also for regular functionality. Besides that, I also want to have my code as loosely coupled as possible. For that, I’m going to use a little bit of Dependency Injection + lazy loading. All this to make sure that plugins are as agnostic as possible about the outside world. If you want to know more, please read on!


Updates to the base library

I’m going to start with some amends to the functionality I have already created in the first part of the series. Don’t know if you remember, but there I created functionality that is copied inside each plugin that registers itself into the base class. One of these methods is called “toString”. During my testing, I have discovered that using that name is not such a good idea. When using Firebug, the “toString” method is constantly called when logging a method. That’s why I decided to rename the method to “_toString”.

Besides the name change, I have also added a local cache in the method. This is to make sure that the expensive for loop is executed only once. The result is then put in a local cache. On each subsequent call of “_toString”, the local cache is used.

(function($){
    if (typeof window.Tag == "undefined"){
        var Tag = window.Tag = function(){
            // We have a dependency on jQuery:
            if (typeof $ === "undefined" || $ !== window.jQuery){
                alert("Please load jQuery library first");
            }
 
            /*
             * @var Object - Contains all plugins
             */
            this.fn = {};
 
            /**
             * Basic extend functionality. Each new plugin should extend
             * the TAG library, and become part of its namespace
             *
             * @param namespace String - Namespace of the new plugin
             * @param obj Object - The new plugin
             *
             * @return void
             */
            this.extend = function(namespace,obj){
                if (typeof this[namespace] == "undefined"){
                    if (typeof this.fn[namespace] === "undefined" && typeof this[namespace] === "undefined"){
                        // extend each namespace with core functionality
                        $.extend(obj,this.fn.extFN);
 
                        // load the new plugin in the namespaces:
                        this.fn[namespace] = obj;
                        this[namespace] = this.fn[namespace];
 
                        // initialize the new library if necessary
                        if (typeof this[namespace].init === "function"){
                            this[namespace].init();
                        }
                    } else {
                        alert("The namespace '" + namespace + "' is already taken...");
                    }
                }
            };
 
            /**
             * Recursively set values in an object
             *
             * Method allows to set individual settings, without overwriting
             * existing other values in the settings.
             *
             * @param object Object - The object to modify
             * @param data Object - New values for settings
             *
             * @return Object - Current plugin for chainability
             */
            this.setObjectData = function(object, data) {
               var path = (typeof arguments[2] !== "undefined")?arguments[2]:[];
               for(var k in data){
                  path.push(k);
                  if (typeof data[k] === "object"){
                      this.setObjectData(object, data[k], path);
                  } else {
                      var tmpObject = object;
                      for(var i=0,l=path.length;i<l-1;i++){
                          tmpObject = tmpObject[path[i]];
                      }
                      tmpObject[k] = data[k];
                  }
               }
            };
        };
        window.Tag = new Tag();
 
        window.Tag.extFN = window.Tag.fn.extFN = {
            /*
             * @var Object - Cache object
             */
            _cache:{},
 
            /**
             * Set the settings of a plugin
             *
             * @param settings Object - New values for settings
             *
             * @return Object - Current plugin for chainability
             */
            setConfig:function(settings) {
               window.Tag.setObjectData(this.settings, settings);
               return this;
            },
 
            /**
             * Automagically determines correct name of a plugin
             *
             * @return String
             */
            _toString:function() {
                // Because a loop is costly, we'll only do it once, and cache
                // the result of the loop
                if (typeof this._cache['_toString'] === 'undefined') {
                    // the name is not cached, so we need to cache it:
                    this._cache['_toString'] = '';
                    for(var k in window.Tag.fn) {
                        if (window.Tag.fn[k] === this) {
                            this._cache['_toString'] = 'Tag.' + k;
                            break;
                        }
                    }
                }
 
                // notify the user that this is an unknown plugin
                if (this._cache['_toString'] === '') {
                    alert('Could not determine the name of the plugin');
                }
 
                // return the cached name:
                return this._cache['_toString'];
            }
        };
    }
})(jQuery);

While I was busy refactoring the “toString” method, I added a “getParent” method too. This method should be used when you want to refer to the Tag base class from within a plugin, without having to know the name. The advantage is that when Tag gets renamed, the plugins still need very little change in their code.

            //... inside the extFN part of the base object:
 
            /**
             * Returns the base object
             *
             * @return Object
             */
            getParent:function() {
                return window.Tag;
            },

For such a small method, I’m not going to repeat the entire base class again. Saves me some space, because this will be a long article ;)

Creating the Pubsub plugin

Now for the new code. We start of with our basic template again:

(function(){
    var pubsub = {
        /**
         * @var object settings - The configuration of this library
         */
        settings:{}
    };
 
    // Let's put this in the 'pubsub' namespace of TAG:
    Tag.extend("pubsub",pubsub);
})();

Here I do need the settings. It will be filled up a bit later. A Pubsub provider will have 3 basic methods. In other implementations, they may have different names than what I’ll use, but that’s not very important. The methods I need are these:

  • A method to listen for an event: “subscribe”
  • A method to stop listening for an event: “unsubscribe”
  • A method to fire off an event: “notify”

I’ll start with subscribing to an event. The premise here is that a method registers itself as a listener for a particular event. So I need to know: what event? what should happen when that event occurs?

I have to keep track of that information. For this purpose, I’m going to create 2 properties in the plugin: One for keeping track of what method should be called on what event. And another one to make sure a subscription can easily be undone.

(function(){
    var pubsub = {
        /*
         * @var Object - Contains all subscriptions
         */
        subscriptions:{},
 
        /*
         * @var Object - Contains all subscription ID's
         */
        subscriptionIDs:[],
 
        /**
         * @var object settings - The configuration of this library
         */
        settings:{
            separator:'.'
        },
 
        /**
         * Create a subscription to a given notification
         *
         * @param plugin String - Name of the plugin that will send out the notification
         * @param evt String - Name of the notifiation event
         * @param fn Function - Callback function to be executed on notification
         *
         * @return integer - The id of the subscription
         */
        subscribe:function(plugin, evt, fn){
            var path = this._constructPath([plugin, evt]);
            if (typeof this.subscriptions[path] === 'undefined') {
                this.subscriptions[path] = [];
            }
 
            this.subscriptions[path].push(fn);
            this.subscriptionIDs.push({
                path:path,
                index:(this.subscriptions[path].length - 1)
            });
 
            return (this.subscriptionIDs.length - 1);
        },
 
        /**
         * Construct a path from the arguments
         *
         * @return String
         */
        _constructPath:function(list) {
            return list.join(this.settings.separator);
        }
    };
 
    // Let's put this in the 'pubsub' namespace of TAG:
    Tag.extend("pubsub",pubsub);
})();

Let me walk you through the code. The properties for keeping track are added and I have introduced a setting. This setting will be used in the method “_constructPath”. The “subscribe” method is added, and takes 3 parameters: the plugin that will fire the event, the event name, and the function that should be executed when that event takes place.

First, I create a ‘path’ from the plugin name and the event name. If the plugin is ‘Tag.autosuggest’, and the event is ‘gotResponse’, then the path will be ‘Tag.autosuggest.gotResponse’. I have created a method just to create the path. If I ever find out that the path should change, then I only need to change it in that one method. That’s called keeping your code DRY.

Second, the properties are filled up with the necessary data. Each path will be stored in the “subscriptions” property, along with a list of functions that will be executed when that event occurs. If two listeners subscribe to an event, then the index of that event will have a list of 2 functions.

Third, to make it’s easy to unsubscribe from an event, a second list is kept. Each subscription gets an entry in that list. The index of that subscription is then returned at the end of the “subscribe” method. With this ID, it’s possible to unsubscribe without having to specify the plugin and event.

(function(){
    var pubsub = {
        /*
         * @var Object - Contains all subscriptions
         */
        subscriptions:{},
 
        /*
         * @var Object - Contains all subscription ID's
         */
        subscriptionIDs:[],
 
        /**
         * @var object settings - The configuration of this library
         */
        settings:{
            separator:'.'
        },
 
        /**
         * Create a subscription to a given notification
         *
         * @param plugin String - Name of the plugin that will send out the notification
         * @param evt String - Name of the notifiation event
         * @param fn Function - Callback function to be executed on notification
         *
         * @return integer - The id of the subscription
         */
        subscribe:function(plugin, evt, fn){
            var path = this._constructPath([plugin, evt]);
            if (typeof this.subscriptions[path] === 'undefined') {
                this.subscriptions[path] = [];
            }
 
            this.subscriptions[path].push(fn);
            this.subscriptionIDs.push({
                path:path,
                index:(this.subscriptions[path].length - 1)
            });
 
            return (this.subscriptionIDs.length - 1);
        },
 
        /**
         * Cancels a subscription to a given notification
         *
         * @param id Integer - ID of the subscription
         *
         * @return Object - Pubsubhub plugin for chainability
         */
        unsubscribe:function(id) {
            if (typeof this.subscriptionIDs[id] !== "undefined" && this.subscriptionIDs[id] !== null) {
                // don't remove it with splice, or other id's will be FUBAR
                this.subscriptions[this.subscriptionIDs[id].path][this.subscriptionIDs[id].index] = null;
                this.subscriptionIDs[id] = null;
            } else {
                alert('Unable to remove this subscription');
            }
            return this;
        },
 
        /**
         * Construct a path from the arguments
         *
         * @return String
         */
        _constructPath:function(list) {
            return list.join(this.settings.separator);
        }
    };
 
    // Let's put this in the 'pubsub' namespace of TAG:
    Tag.extend("pubsub",pubsub);
})();

The “unsubscribe” method removes the entries from our lists. Via the given ID, it looks up where the callback function is listed in the “subscriptions” property. It is very important that we don’t physically remove the entry from the array. Instead, I just overwrite what was there with “null” values.

If we remove the entries via “Array.splice”, then the array will be re-indexed. This will invalidate all other ID’s, so we can’t use “Array.splice”. Using the regular “delete” method…. well, let’s just say that in this case it would work, but I’m not too fond of using “delete” with arrays. It doesn’t actually delete the entry, it just writes null values to that index. Yeah, that’s exactly what I’m doing manually. But that’s not called “deleting an entry”, so let’s make sure we don’t get confused. We don’t want to end up refactoring “delete” with “Array.splice”, because we forgot that we don’t want an actual delete.

(function(){
    var pubsub = {
        /*
         * @var Object - Contains all subscriptions
         */
        subscriptions:{},
 
        /*
         * @var Object - Contains all subscription ID's
         */
        subscriptionIDs:[],
 
        /**
         * @var object settings - The configuration of this library
         */
        settings:{
            separator:'.'
        },
 
        /**
         * Create a subscription to a given notification
         *
         * @param plugin String - Name of the plugin that will send out the notification
         * @param evt String - Name of the notifiation event
         * @param fn Function - Callback function to be executed on notification
         *
         * @return integer - The id of the subscription
         */
        subscribe:function(plugin, evt, fn){
            var path = this._constructPath([plugin, evt]);
            if (typeof this.subscriptions[path] === 'undefined') {
                this.subscriptions[path] = [];
            }
 
            this.subscriptions[path].push(fn);
            this.subscriptionIDs.push({
                path:path,
                index:(this.subscriptions[path].length - 1)
            });
 
            return (this.subscriptionIDs.length - 1);
        },
 
        /**
         * Cancels a subscription to a given notification
         *
         * @param id Integer - ID of the subscription
         *
         * @return Object - Pubsubhub plugin for chainability
         */
        unsubscribe:function(id) {
            if (typeof this.subscriptionIDs[id] !== "undefined" && this.subscriptionIDs[id] !== null) {
                // don't remove it with splice, or other id's will be FUBAR
                this.subscriptions[this.subscriptionIDs[id].path][this.subscriptionIDs[id].index] = null;
                this.subscriptionIDs[id] = null;
            } else {
                alert('Unable to remove this subscription');
            }
            return this;
        },
 
        /**
         * Send out a notification to all subscribers
         *
         * @param plugin String - Name fo the plugin
         * @param evt String - Name of the event
         *
         * @return Object - Pubsubhub plugin for chainability
         */
        notify:function(plugin, evt) {
            var path = this._constructPath([plugin, evt]);
            if (typeof this.subscriptions[path] !== 'undefined') {
                for(var i=0,l=this.subscriptions[path].length;i<l;i++){
                    // make sure we only execute functions, not strings, integers, null's, ...
                    if (typeof this.subscriptions[path][i] === 'function') {
                        this.subscriptions[path][i]();
                    }
                }
            }
        },
 
        /**
         * Construct a path from the arguments
         *
         * @return String
         */
        _constructPath:function(list) {
            return list.join(this.settings.separator);
        }
    };
 
    // Let's put this in the 'pubsub' namespace of TAG:
    Tag.extend("pubsub",pubsub);
})();

With the “notify” method, you fire off an event. There are two arguments: name of the plugin and name of the event. From these two arguments, the path is constructed again. Then I look up in the “subscriptions” list if that entry exists. If it does, then all functions listed there will be executed.

The “notify” method works as a fire-and-forget system. If there are no listeners for an event, then nothing will happen. There is no communication back to the method that fired the event. That’s not necessary. It’s not its task to know if the event will be correctly dispatched.

At the moment, “notify” doesn’t transmit a payload to the subscribers. I’m sure I will need that functionality eventually. I’ll add it later, when the need is actually there.

The problem of scope

When a method subscribes to an event, and then gets executed, there will be a problem with the scope of the method. The Pubsub plugin doesn’t know anything about the callback function, other than it is a function. It can either be an anonymous function, or it can be part of an object. Pubsub doesn’t know. Hence, when the function is called, there won’t be a scope. The scope is the function itself. For an anonymous function, that’s good. For a method that’s part of an object, that’s not good

Possible solution 1: Just execute the callback in the scope of the Pubsub provider, like this:

// ...
this.subscriptions[path][i].call(this);
// ...

This is not a good solution, because anonymous functions suddenly have a scope and methods from an object suddenly have a scope that isn’t theirs. If the method is ‘Tag.autosuggest.updateSuggestions’, then the scope should be ‘Tag.autosuggest’, and not ‘Tag.pubsub’.

Possible solution 2: Execute the callback in the scope of the notifier. Via some code, it should be easy to determine the scope of the notifier. But again, my same objections apply as with the first solution: Anonymous functions shouldn’t have a scope, and I don’t want to change the scope of methods from an existing object

Possible (and accepted) solution 3: Make sure the callback already has the correct scope before executing it. (see below)

Hitching a ride

(I told you this was going to be a long article) The idea comes from “dojo.hitch”: Via another method making sure that a given function is executed in a given scope. So I created “Tag.core.hitch” for this purpose. The name comes from Dojo, and the implementation is derived from dojo’s implementation.

        // inside the Tag.core plugin:
        /**
         * Runs fn in the given scope
         *
         * @param scope Object - The scope fn should run in
         * @param fn Function|String - The function to run
         *
         * Extra parameters are optional, and will be passed on to fn
         *
         * @return Function
         */
        hitch:function(scope, fn) {
            // arguments looks like an Array, but is an Object
            // here we convert it to an array
            var args = Array.prototype.slice.call(arguments);
            if (typeof scope === 'undefined' || typeof fn === 'undefined') {
                // both scope & fn should be defined
                alert('Tag.hitch: both scope and function should be defined');
                return null;
            }
            if(typeof fn === 'string'){
                if (typeof scope[fn] === 'undefined') {
                    alert('The function is not defined inside the given scope');
                    return null;
                }
                return function(){ return scope[fn].apply(scope, args.slice(2)); };
            }
            return function(){ return fn.apply(scope, args.slice(2)); };
        }

This method returns an anonymous function. Inside that anonymous function, the scope of the given function is set to the scope that’s defined in the arguments. Quite simple actually. But very effective.

With the hitch method, a subscriber is now in charge of making sure that the callback function is executed in the correct scope.

Dependency Injection

Before I come to the usage examples, there is one more thing I’d like to implement. You know how I like keeping my plugins agnostic. Well, instead of letting a plugin use the pubsub provider like this: “Tag.pubsub.notify(…)”, I want to make sure that it’s not important that the pubsub provider is called “Tag.pubsub”. In fact, if someone comes along and writes a better provider, it should be possible to use that provider without having to change the code of the plugins.

This can be accomplished with a little Dependency Injection. (DI via getters and setters to be precise). I’ll extend core plugin with 2 methods: “getPubsubProvider” and “setPubsubProvider”. With the getter, the Pubsub provider is returned. Via he setter it’s possible to specify another Pubsub provider. As the icing on the cake, I’ll add some lazy loading: if no pubsub provider is specified, then “getPubsubProvider” defaults to “Tag.pubsub”.

(function($){
    if (typeof window.Tag == "undefined"){
        var Tag = window.Tag = function(){
            // We have a dependency on jQuery:
            if (typeof $ === "undefined" || $ !== window.jQuery){
                alert("Please load jQuery library first");
            }
 
            /*
             * @var Object - Contains all plugins
             */
            this.fn = {};
 
            /**
             * Basic extend functionality. Each new plugin should extend
             * the TAG library, and become part of its namespace
             *
             * @param namespace String - Namespace of the new plugin
             * @param obj Object - The new plugin
             *
             * @return void
             */
            this.extend = function(namespace,obj){
                if (typeof this[namespace] == "undefined"){
                    if (typeof this.fn[namespace] === "undefined" && typeof this[namespace] === "undefined"){
                        // extend each namespace with core functionality
                        $.extend(obj,this.fn.extFN);
 
                        // load the new plugin in the namespaces:
                        this.fn[namespace] = obj;
                        this[namespace] = this.fn[namespace];
 
                        // initialize the new library if necessary
                        if (typeof this[namespace].init === "function"){
                            this[namespace].init();
                        }
                    } else {
                        alert("The namespace '" + namespace + "' is already taken...");
                    }
                }
            };
 
            /**
             * Recursively set values in an object
             *
             * Method allows to set individual settings, without overwriting
             * existing other values in the settings.
             *
             * @param object Object - The object to modify
             * @param data Object - New values for settings
             *
             * @return Object - Current plugin for chainability
             */
            this.setObjectData = function(object, data) {
               var path = (typeof arguments[2] !== "undefined")?arguments[2]:[];
               for(var k in data){
                  path.push(k);
                  if (typeof data[k] === "object"){
                      this.setObjectData(object, data[k], path);
                  } else {
                      var tmpObject = object;
                      for(var i=0,l=path.length;i<l-1;i++){
                          tmpObject = tmpObject[path[i]];
                      }
                      tmpObject[k] = data[k];
                  }
               }
            };
        };
        window.Tag = new Tag();
 
        window.Tag.extFN = window.Tag.fn.extFN = {
            /*
             * @var Object - Cache object
             */
            _cache:{},
 
            /*
             * @var Object - Pubsub provider
             */
            pubsubProvider:null,
 
            /**
             * Set the settings of a plugin
             *
             * @param settings Object - New values for settings
             *
             * @return Object - Current plugin for chainability
             */
            setConfig:function(settings) {
               window.Tag.setObjectData(this.settings, settings);
               return this;
            },
 
            /**
             * Inject another Pubsub Provider
             *
             * @param obj Object - The new Pubsub Provider
             *
             * @return Object - Current plugin for chainability
             */
            setPubsubProvider:function(obj) {
                // verify if necessary functionality is present:
                var fns = ["subscribe","unsubscribe","notify"];
 
                var validObject = true;
                $.each(fns,function(){
                    if (typeof obj[this] !== "function"){
                        validObject = false;
                        return;
                    }
                });
 
                if (validObject) {
                    this.pubsubProvider = obj;
                }
 
                return this;
            },
 
            /**
             * Get the Pubsub Provider
             *
             * @return Object - Pubsub Provider
             */
            getPubsubProvider:function() {
                // lazy loading:
                if (null === this.pubsubProvider) {
                    this.pubsubProvider = window.Tag.pubsub;
                }
 
                return this.pubsubProvider;
            },
 
            /**
             * Returns the base object
             *
             * @return Object
             */
            getParent:function() {
                return window.Tag;
            },
 
            /**
             * Automagically determines correct name of a plugin
             *
             * @return String
             */
            _toString:function() {
                // Because a loop is costly, we'll only do it once, and cache
                // the result of the loop
                if (typeof this._cache['_toString'] === 'undefined') {
                    // the name is not cached, so we need to cache it:
                    this._cache['_toString'] = '';
                    for(var k in window.Tag.fn) {
                        if (window.Tag.fn[k] === this) {
                            this._cache['_toString'] = 'Tag.' + k;
                            break;
                        }
                    }
                }
 
                // notify the user that this is an unknown plugin
                if (this._cache['_toString'] === '') {
                    alert('Could not determine the name of the plugin');
                }
 
                // return the cached name:
                return this._cache['_toString'];
            }
        };
    }
})(jQuery);

That’s the entire base class for now. As you can see, “setPubsubProvider” verifies if the new provider has all three required methods. I added this so that the plugins don’t have to be rewritten. If a new Pubsub Provider is introduced, with different names for the methods, then all you have to do is write an adapter for it, and set it via “setPubsubProvider”.

Usage

A lot of code has been written and added. Here are some examples how all this can be used. For most examples, I’ll use a fictive “Tag.autosuggest” plugin:

// inside the Tag.autosuggest plugin, we throw an event:
this.getPubsubProvider().notify(this._toString(),'thresholdReached');

(you see, no ‘Tag’ mentioned here. I like!)

Inside another plugin, we listen for the above event:

var id = this.getPubsubProvider().subscribe(this.getParent().autosuggest._toString(),'thresholdReached',function(){
    alert('Treshold has been reached');
});

(Still no mention of ‘Tag’ here. True, we have to write a lot of code, but it’s future proof)

Instead of an anonymous function, I want to execute a method of an object. We have to keep scope here:

var id = this.getPubsubProvider().subscribe(this.getParent().autosuggest._toString(),'thresholdReached',this.getParent().core.hitch(this,this.callbackFunction));

(“callbackFunction” will be executed in the correct scope)

An alternative to the above is:

var id = this.getPubsubProvider().subscribe(this.getParent().autosuggest._toString(),'thresholdReached',this.getParent().core.hitch(this,"callbackFunction"));

(Here, “callbackFunction” is specified as a string. Other than that, the exact same will happen as the previous example)

Unsubscribe from an event:

var id = this.getPubsubProvider().subscribe(this.getParent().autosuggest._toString(),'thresholdReached',this.getParent().core.hitch(this,this.callbackFunction));
 
this.getPubsubProvider().unsubscribe(id);

All above examples implied another plugin. But the code can also be used in regular Javascript, provided that Tag and Tag.pubsub are available on the page.

function doCalculation (a, b) {
    Tag.pubspub.notify('doCalculation','beforeCalculation');
    var result = a + b;
    Tag.pubspub.notify('doCalculation','afterCalculation');
    return result;
}
 
var id = Tag.pubsub.subscribe('doCalculation','afterCalculation',function(){
    alert('Calculation complete');
});
 
Tag.pubsub.unsubscribe(id);

Instead of the plugin name, I provided the function name to the “notify” and “subscribe” methods.

Conclusion

Finally, the end of a lengthy article. With the pubsub plugin, it should be easy to make your plugins event-driven, and loosely coupled. Already, a shortcoming is visible: It should be possible to transmit data from the notifier to the subscriber. In the coming days or maybe next week, I’ll refactor the code to allow that. Once that feature is added, it will be possible to write your application as a blackboard system, with all functionality independent from each other.

In these articles, I have introduced some principles from the OO world, like Dependency Injection, modular design, lazy loading, DRY, … If you want to read them again, the first article is available here, and the second article is available here. The code of this library is under constant development. I’ve made it available under the MIT license on my Codaset account on http://www.codaset.com/theanalogguy/the-analog-guy-javascript-library. Feel free to fork it, add change requests or bug reports. If you want to use the code, and you create plugins for it, please let me know. I’m curious if and how this will be used by other people.

Thank you for reading my article. Hope you enjoyed it.

Share and Enjoy:
  • DZone
  • del.icio.us
  • StumbleUpon
  • Digg
  • Ma.gnolia
  • Technorati
  • TwitThis

Tom JQuery, Javascript

  1. No comments yet.
  1. No trackbacks yet.