Monday, December 24, 2007

Javascript Namespaces : Import and Export methods

We have seen how important namespaces are and how to simulate them thru objects. If protecting ourselves and users from collisions is a good thing, writing 4 to 6 names before accessing the desire functions can be a real pain!
we will see how to shortcut things and make our life easier when needed!

Namespaces : the Namespace function


Let's sum up what we have seen in our last entry by taking the example of the JAME namespace (Javascript Animations Made Easy).
We won't put anything directly in the global namespace, that's why we will have to create our first namespace, JAME, without the Namespace function:
JAME = function () {};
JAME = JAME.prototype = function () {};
Now, let's attach the Namespace function onto the JAME namespace.
Let's rename the function to Package, because...
Well, I like Package and it's in the ES4 proposal.
Rename it to whatever you want (I will avoid x345SW2aqefje because it is overused)

JAME.Package = function (sName) {

    //split the name by dots
    var namespaces=sName.split('.') || [sName];
    var nlen=namespaces.length;
       
    var root = window;
    var F    = function() {};

    for(var i=0;i<nlen;i++) {
        var ns = namespaces[i];
        if(typeof(root[ns])==='undefined') {
            root = root[ns] = F;
            root = root.prototype = F;
        }
        else
           root = root[ns];
    }
}

Some packages definition


We have seen how to use it with the utility functions built so far to create the animation function.
Let's see the strings and colors related functions in their package:

  1. JAME.Package('JAME.Util.Color');
  2.  
  3. JAME.Util.Color = {
  4.   rgb2h:function (r,g,b) {
  5.     var Num=JAME.Util.Number;
  6.     return [Num.d2h(r),Num.d2h(g),Num.d2h(b)];
  7.   },
  8.   h2rgb:function (h,e,x) {  
  9.     var Num=JAME.Util.Number;
  10.     return [Num.h2d(h),Num.h2d(e),Num.h2d(x)];
  11.   },
  12.   cssColor2rgb:function (color) {
  13.      if(color.indexOf('rgb')<=-1) {
  14.         return this.hexStr2rgbArray(color);
  15.      }
  16.      return this.rgbStr2rgbArray(color);
  17.   },
  18.   hexStr2rgbArray:function (color) {
  19.      return this.h2rgb(color.substring(1,3),
  20.                        color.substring(3,5),
  21.                        color.substring(5,7)
  22.             );
  23.   },
  24.   rgbStr2rgbArray:function (color) {
  25.      return color.substring(4,color.length-1).split(',');
  26.   }
  27. };

  1. JAME.Package('JAME.Util.String');
  2.  
  3. JAME.Util.String={
  4.   camelize: function(str) {
  5.      return str.replace(/-(.)/g,
  6.                 function(m, l){return l.toUpperCase()}
  7.             );
  8.   },
  9.   hyphenize : function(str) {
  10.      return str.replace(/([A-Z])/g,
  11.                 function(m, l){return '-'+l.toLowerCase()}
  12.             );
  13.   },
  14.   firstToUpperCase : function(str) {
  15.     return str.replace(/^([a-z])/,
  16.                function(m, l){return l.toUpperCase()}
  17.            );
  18.   },
  19.   trim : function(str) {
  20.     return str.replace(/^\s+|\s+$/g, '');
  21.   }
  22. };

How do we use them?
  1. // display backgroundColor
  2. alert(JAME.Util.String.camelize("background-color"));
  3. // display 255,0,255
  4. alert(JAME.Util.Color.hexStr2rgbArray("#FF00FF"));

That's a lot of typing!
We have seen that we could create an alias very easily:
window["camelize"]       = JAME.Util.String.camelize;
window["hex2rgbArray"] = JAME.Util.Color.hex2rgbArray;
Now you can call the camelize and hex2rgbArray directly without having to worry about the namespaces!
But this is still quite cumbersome to type this! And if you want to import everything in your namespace?
Let's build a function that will allow to import everything by just specifying the desired namespace then!

The Import method


Let's see how we would like to use it first:
JAME.Import(JAME.Util.String,JAME.Util.Color,JAME.Util.Number);
So we will just specify the namespace we need and as many as we wish.
In order to allow as many parameters as needed, we are going to use the arguments keyword.
By the way, the arguments keyword is not a real array in that you cannot push, slice....

  1. JAME.Import = function () {
  2.  
  3.     //get the number of parameters
  4.     var nlen=arguments.length;
  5.  
  6.     //loop thru each package/namespace
  7.     for(var i=0;i<nlen;i++) {
  8.  
  9.         // get the actual package
  10.         var package=arguments[i];
  11.  
  12.         //test the existence of an Export method
  13.         if(package.Export) {
  14.  
  15.             //call the Export function
  16.             package.Export();
  17.         }
  18.     }
  19. }

The definition of this function is not very hard.
We just loop thru each package and test for the Export method.
But what is this Export method??

Need some privacy!


The Export function should be defined within the packages and should define what functions can be exported as their exported name.
It is easier and safier to let the package creator defines what he/she thinks can be exported safely.
A function name can be perfectly safe within the boundaries of a package but could be too obvious once in the global scope.
Therefore, renaming the function in order to avoid conflicts by prefixing it should be up to the package author.
So let's see the Export function within the string package :
JAME.Package('JAME.Util.String');

JAME.Util.String={
    camelize: function(str) {
        return str.replace(/-(.)/g,
               function(m, l){return l.toUpperCase()});
    },
    hyphenize : function(str) {
        return str.replace(/([A-Z])/g,
               function(m, l){return '-'+l.toLowerCase()});
    },
    firstToUpperCase : function(str) {
        return str.replace(/^([a-z])/,
               function(m, l){return l.toUpperCase()});
    },
    trim : function(str) {
        return str.replace(/^\s+|\s+$/g, '');
    },
    Export : function() {
        for(var method in this) {
           if(method==="Export") continue;
          window[method]=this[method];
        }    
   }
};
As you can see, in that case, it was pretty straightforward as we just loop thru each method in the string package and alias them onto the window object.
Obviously, we skip the Export method that is internal to the package!
But let's say that you want to rename the trim function to JStrim in order to avoid conflict? You will have to verify if it is the trim method then change it's naming.
Or let's say that you just want to export 2 functions among 5 or 6 functions?
In order to make lifer easier and keep things DRY, we are going to create an Exporter function that will ease the export process!

The Exporter method


Let's see its definition first:
JAME.Exporter = function (oName,sPref) {

    if(!sPref) sPref='';

    for(var method in oName) {
        if(method==="Export"
           || /^_.+/.test(method))
            continue;
        window[sPref+method]=oName[method];
    }
}

The function accepts two parameters:
an object that can be 'this' to export everything or a literal object defining the exported function name and its original version with the package path.
And a string that defines a prefixe to put on the exported function name.
An example will help:
JAME.Util.String= {
...
   _mymethod : function() {...} // will not be exported
   Export : function() {

       JAME.Exporter(this,'JS');
       //or 
       JAME.Exporter({"hyphenize":JAME.Util.String.hyphenize},"JS");
   }
}
As you can see in the exporter function, we skip the Export function and all methods or variables that start with an underscore.
This is just a convention used in many languages to specify private variables.
Even if it is possible to simulate private variables and methods in javascript, thisw won't be the case here, unless you wrap all the code within an other function.
Beware that we do not check against variables or functions already defined in the global namespace, which means that these functions will be overwritten!
It should be easy to add a flag to set it to overwrite mode or not.

The final script


And here we are with a basic namespace simulation and exporting/importing process.
Here is the code that recaps everything seen:
JAME = function () {};
JAME = JAME.prototype = function () {};

JAME.Package = function (sName) {


    var namespaces=sName.split('.') || [sName];
    var nlen=namespaces.length;
       
    var root = window;
    var F    = function() {};

    for(var i=0;i<nlen;i++) {
        var ns = namespaces[i];
        if(typeof(root[ns])==='undefined') {
            root = root[ns] = F;
            root = root.prototype = F;
        }
        else
           root = root[ns];
    }
}

JAME.Import = function () {
     var nlen=arguments.length;

     for(var i=0;i<nlen;i++) {

         var package=arguments[i];

         if(package.Export) {
               package.Export();
         }
     }
}

JAME.Exporter = function (oName,sPref) {

     if(!sPref) sPref='';

     for(var method in oName) {
         if(method==="Export"
            || /^_.+/.test(method))
            continue;
        window[sPref+method]=oName[method];
     }
}

JAME.Export = function () {
    JAME.Exporter({ "Package" : JAME.Package,
                    "Import"  : JAME.Import});
}


Conclusion


These 3 entries allowed us to experiment with namespaces in javascript!
Understanding their importances, object simulation advantages and creating functions to simplify our code and make it cleaner, organised have been seen.
Obviously, this is just a starter and many features could be added or improved.
If you have some ideas, don't hesitate to post them out!
The extension thru inheritance is not assumed here and should be developed though.
If you are interested in packages, namespaces way of managing your code, you can look at some other libraries that offer ways to deal with that:
-AJILE : library offering many import, include,namespaces functions.
-JSAN : a very good project idea that I invite you to collaborate to in anyway you can!
Happy new year!

No comments: