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

Building your own Javascript library (part 1)

November 30th, 2009

Goal

There are two programming languages I like: PHP and Javascript. In this mini series, I will explore Javascript and jQuery, and build my own library/framework. I won’t re-invent the wheel here. I’ll just make a collection of methods and functionality I need, making use of some functionality in jQuery.

The goal of the first part of my mission is to create a small, lightweight, self-contained, easily extensible base for my framework. Recently I created my own little company, called “The Analog Guy”. I’ll just call my library “TAG”.


Creating the base

One thing I have learned from using other libraries, is that I don’t like initializing an object in Javascript. I just want to include the script on my page, and it has to work out of the box. I’ll use a self-executing anonymous function to encapsulate my class, initialize it, and add some functionality. We start with the blueprint of the self-executing anonymous function:

(function($){
    // rest goes here
})(jQuery)

This is an anonymous function that will be executed immediately. As a parameter to this anonymous function, I give the jQuery object. This makes sure that inside my anonymous function, $ refers to the jQuery object. Even if on the rest of the page, another library is used that defines $. That’s called the scope of the object.

So well, next thing I want to do, is reserve my own TAG namespace. So I’ll add a little code for that:

(function($){
    if (typeof window.Tag == "undefined"){
        var Tag = window.Tag = function(){
            // rest goes here
        }
    }
})(jQuery)

If another script uses the “Tag” namespace in the global scope, then that might indicate that the user has accidentally included the tag.js file twice on the same page. I’ve made sure that it the namespace only gets defined just once.

In my goal, I said I was going to build this library on top of jQuery. I want to make sure that $ is defined, and that it is jQuery. I don’t want to support other libraries at this moment.

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

Now that I know that jQuery is available, the next thing I want to add, is extensibility. I want to keep the base lightweight: only add the functionality that is needed. All the other functionality, that is non-essential, should be added via plugins or extensions. This makes it possible:

(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");
            }
 
            // Object FN will contain 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...");
                    }
                }
            };
        }
    }
})(jQuery)

The extend method has two arguments: the name of the namespace the plugin wants to use, and then the plugin itself. First there is a check if that namespace is still available. If it is, the first thing I do, is add some basic functionality to the plugin, that will make life easier for the developer of that plugin. Don’t worry, we’ll see immediately where I add that core functionality. A next step is to inject the plugin into the base class. For this, I use the “this.fn” object, because that makes it easier to detect which plugins are loaded. As a “bonus”, I also inject that same plugin in the base namespace. That makes it more convenient to use the plugin. “Tag.fn.myPlugin” can be referred to as “Tag.myPlugin”. Lastly, if that plugin needs some automagical setup, it can define the “init” method, which will get triggered when loading the plugin.

This provides basic extensibility for my framework. The next method I need, is a utility method. I cannot place it on a plugin, because it is something basic: a method that recursively sets a value inside an object

(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");
            }
 
            // Object FN will contain 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];
                  }
               }
            };
        }
    }
})(jQuery)

I know jQuery.extend() exists. I just don’t think it can do what I want: set some properties of an object, without touching other existing properties. The method “setObjectData” I just created will do just that. It recursively loops through a set of values, and sets these values on the given object. This method will be used later, for setting values in a plugin configuration.

For now, I’m happy with that functionality. I don’t have an immediate use for other functionality. So we can initialize our class now. Since this takes only 1 line of code, I’ll immediately add some more functionality.

There are certain methods, I want to be available in every plugin, without the plugin having to know the name of my base class. I think it’s a basic need of the plugins, to be as agnostic as possible about the rest of the world. The fewer the dependencies, the better. If I ever decide to rename my company, and I rename my base library. Then the plugin should still work with little or no effort. These methods are “setConfig”, to set values in a plugin config, and “toString”, to automagically determine the name of the plugin.

(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");
            }
 
            // Object FN will contain 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 = {
            /**
             * 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() {
                for(var k in window.Tag.fn) {
                    if (window.Tag.fn[k] === this) {
                        return 'Tag.' + k;
                        break;
                    }
                }
                alert('Could not determine the name of the plugin');
                return '';
            }
        };
    }
})(jQuery);

That’s it for the moment. I’ll certainly need more functionality inside the base class, but I will build it was I need it.

Usage

Well, it’s a basic class really, there is no real usage for now. The strength will come from the plugins. To include the library in your files, simply include script tags which point to the correct JS file.

Writing your own plugins

Since I’m emphasizing plugins, I’ll give you the basic structure of how a plugin should be built. In future parts, I’ll build at least two plugins that will further extend the functionality: a “core” plugin, and a “pubsub” plugin. Here’s the template:

(function(){
    var myPlugin = {
        /**
         * @var object settings - The configuration of this library
         */
        settings:{},

        /**
         * Will be called as soon as this library is loaded
         * into the main Tag library
         *
         * @return void
         */
        init:function(){},

        // other methods go below
    };
    Tag.extend("myPlugin",myPlugin);
})();

That’s all there is to it. Just an Object Literal notation of an object, again inside a self-executing anonymous function. Include your JS file in your HTML, after you included the Tag library, and your plugin will be available as “Tag.myPlugin”.

Next

In the next part of this mini series, I’ll create the “core” plugin. This plugin will create functionality that is quite basic, but not essential to the library. Usually the methods will be utility methods which can be used by other plugins or regular Javascript code.

In the last part of the series, I’ll introduce my “pubsub” plugin. It’s a plugin that serves as a dispatcher for events. With it, it is possible to send out event notifications. Other parts of your code will be able to (un-)subscribe to events, and will get triggered when the event is fired.

If you have any remarks, please let me know. I’m open to suggestions and improvements. My code is available at http://www.codaset.com/theanalogguy/the-analog-guy-javascript-library.

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

Tom JQuery, Javascript

  1. December 1st, 2009 at 10:34 | #1

    very cool & good tip, thank you very much for sharing.

    Can I share this post on my JavaScript library?

    Awaiting your response. Thank

  2. December 1st, 2009 at 22:12 | #2

    Yes, you can share anything you want, as long as you reference back to this site. The library is available under MIT license, and can be downloaded from my Codaset mentioned in the post.

  3. December 2nd, 2009 at 11:08 | #3

    Hello,

    really really really good article, thanks & keep up the good work !

    I looked at your pubsub plugin ( http://www.codaset.com/theanalogguy/the-analog-guy-javascript-library/source/master/blob/tag/tag.pubsub.js ) and want to ask you :

    to notify you run the subscribed anonymous function like so :

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

    However this does not tell the function to run in the plugin namespace: in other words “this” does not point to the plugin object that is notified. Just upgrading to :

    this.subscriptions[path][i].call( //some code to find the plugin inside window.Tag// );

    will automagically set “this” to the notified plugin. Note that Jquery does it :

    $(‘#foo’).click(function() {
    //this is the jquery object $(‘#foo’)
    });

    Mickael

  4. December 2nd, 2009 at 11:15 | #4

    Hi Mickael,

    If you look at the commit history of the pubsub plugin, you’ll see that at one point, I actually did what you suggested:

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

    But there were two things with that:
    1. This refered to the pubsub scope, which isn’t good
    2. This way, callbacks that reside in the global namespace, would have their scope changed to the one of the plugin that notified the event.

    At the moment, I’m still thinking about how to solve this. Besides that, I also want to be able to return parameters to the subscribed functions. I’ll find a solution later today (I hope), and you’ll read about it in the next article.

    Thanks for your feedback!

  5. December 2nd, 2009 at 12:08 | #5

    @Tom
    Ok I understand ! The first argument to your subscribe method is the plugin that’ll send the notification, not the plugin that actually subscribes to be notified later…

    In my logic, the “this” context should be :

    - the plugin that subscribed to the notification if set
    - nothing if it’s a global namespace subscription

    This will surely ask some work, some proxy function in the “core” class to be able to tell:

    this.subscribeToEvent(plugin,event, function);

    and inside :

    this.subscribeToEvent = function (plugin,event,function) {
    me.getPubsubProvider().subscribe(plugin,event,function,this);
    };

    and of course :

    subscribe:function(plugin, evt, fn,context){
    var path = this._constructPath([plugin, evt]);
    if (typeof this.subscriptions[path] === ‘undefined’) {
    this.subscriptions[path] = [];
    }

    var subscription_id = this.subscriptions[path].length;
    this.subscriptions[path].push({
    path:path,
    index: subscription_id,
    context: context,
    callback: fn
    });

    return (subscription_id);
    }

    and finally:

    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].callback === 'function') {
    if ( this.subscriptions[path][i].context ) {
    this.subscriptions[path][i].callback.call(context);
    } else {
    this.subscriptions[path][i].callback();
    }
    }
    }
    }
    },

    However I'm not good enough in javascript to be sure this is the cool & good way to do it.

  6. December 2nd, 2009 at 16:28 | #6

    Hi Mickael, thanks for your feedback.
    I have thought about it for some time, and my conclusion was that it is not the job of the pubsub provider to make sure that the context is correct.

    For this reason, I have created a new function in the core plugin: “hitch”. The name and a good deal of the logic came from the dojo.hitch function.

    I won’t go into details right here. I’m going to start writing my new article in a couple of minutes. There I’ll explain everything in detail. I’m happy with the solution I came up with, and I hope you’ll like it too.

  7. Nizzy
    July 8th, 2010 at 03:41 | #7

    Tom, thanks for sharing your work.

    I was wondering how you could implement a debug pattern that allows me to catch any errors from plugins within Tag obj.

    Best
    Nizzy

  8. July 13th, 2010 at 11:33 | #8

    Hi Nizzy,

    You could try to add the onError event handler to the Tag object. More info can be found here: http://api.jquery.com/error/
    If that isn’t enough, you can also look to place try/catch statements where appropriate (haven’t really looked into that yet, so I don’t have a recommendation where to put those)

    Tom

  1. December 1st, 2009 at 12:17 | #1
  2. December 1st, 2009 at 18:16 | #2
  3. December 2nd, 2009 at 11:32 | #3
  4. December 2nd, 2009 at 19:00 | #4
  5. August 18th, 2010 at 21:14 | #5
  6. August 18th, 2010 at 21:35 | #6