Creating Your Own $

Creating Your Own $
by Neil Roberts

The bling, one of the best global variables in JavaScript. A tool which has come to mean, as a function, a way to locate a node or set of nodes. And, as a namespace, a simple way to access often-used functionality. “This can’t be done with Dojo,” you insist. But you’re wrong, it’s really easy. The ideas behind this little symbol are quite simple and I’m going to show you how to create your own customized version of it.

 

I’m going to look to jQuery for inspiration. Its fanbase is huge, its fans declare its syntax beautiful and expressive, and its “plugin” syntax is easy to use.

Your first decision is how you want to start your $ off. dojo.query will give us our node-locating abilities. Do you want it to support jQuery’s plugin syntax? Do you want it to contain all of the functions and properties of the normal dojo object?

The second decision is a little trickier. If the dojo object is updated during the life of your page, do you want the changes from the dojo object to make it into your $. This would generally happen after a dojo.require statement (which imports new code). Something like a dojo.require("dojo.behavior"); call that adds the behavior field to the dojo object. Another situation, though less likely, is that one of the properties on the dojo object might change. The tradeoff depends on whether or not you plan on overriding any of the fields that exist in the dojo object. If updates to dojo go from dojo to $, it means updates to $ go from $ to dojo.

Putting it into Action

  $ = dojo.query;

Here’s the $ at it’s simplest. You can now call $("#title p") and it will work without complaint. Adding your own functions and properties to $ would also add them as properties of dojo.query (since they both point to the same place). This may or may not cause problems, but it’s better to be safe than sorry, so we’ll make it a separate function.

  $ = function(){ return dojo.query.apply(this, arguments); }

You’ll notice the use of a special variable: this. Using the .call(context, a, b, c); or .apply(context, [a, b, c]); functions allows us to change what object this variable points to. In this above example, we’re going to set the value of this in the function we’re calling to the current value. Basically, we’re sending it through unchanged.

Using apply(this, arguments) allows the dojo.query function to get called with no side-effects. This means that when dojo.query is called, it will retain both the context it was used with, and the arguments it was passed. We also have to make sure that we return the results.

In order to get all of the dojo object’s fields and properties into $, we can just use dojo.mixin. This function modifies the first argument we give it by “mixing in” all the properties from the second argument. Since dojo.mixin returns this first argument, we can keep this all in a simple syntax.

  $ = dojo.mixin(function(){ return dojo.query.apply(this, arguments); }, dojo);

And to add our plugin architecture, we’ll want to mix in our plugins to the results of our dojo.query call. Normally, you’d add a plugin to dojox.NodeList.prototype, but it’s likely we’ll run into naming conflicts and things that expect the dojo.query result to work in a certain way might be very confused. Either way, the method we’ll be using to add our plugins is basically the same as the way that dojo.query does.

  $ = dojo.mixin(function(){ return dojo.mixin(dojo.query.apply(this, arguments), $.fn); }, dojo);
  $.fn = {};

What this means is that everything hanging off of the $.fn object will get added to the result of our $() call. Since we’ve been pretty concise before, and keeping in the spirit of jQuery, I’m going to add the fn variable declaration to our mixin call.

  $ = dojo.mixin(function(){ return dojo.mixin(dojo.query.apply(this, arguments), $.fn); }, dojo, { fn: {} });

And finally, are we worried about naming collisions between $ and dojo? Do we want to have our $ updated every time dojo is? If we do, it’s as easy as this:

  dojo =  $;

If you decide not to do this, but you still want to try to keep $ in sync with dojo when the dojo.require function is used, there’s a pretty good fix.

  dojo.connect(dojo, "require", function(require){
    var parts = require.split(".");
    if(parts.length >= 2){
      if(parts[0] == "dojo" && typeof $[parts[1]] == "undefined"){
        $[parts[1]] = dojo[parts[1]];
      }
    }
  });

This function will get called every time dojo.require is called. It will get passed a string matching the string passed to dojo.require. We split it up by periods, check to see that it starts with “dojo”, make sure that it’s not already set in $ and, if everything goes well, add it to $.

Plugins

I’m going to look at the jQuery Plugins/Authoring page for some inspiration. We’re going to make that first code block work by the time we’re done.

First of all, a lot of jQuery’s functions set the context of their functions equal to the current node. Writing your own plugin to emulate this is extremely easy. Since the plugin example we’ll be writing uses the each function, we’ll start there.

  $.fn.each = function(callback){
    dojo.forEach(this, function(node, i){
      callback.call(node, i);
    });
    return this;
  }

Note that the internals here are actually the same as they would be when writing a jQuery plugin. The value assigned to this is the result of the $() call. We can loop through the results from the $() call, and call the passed function for each. The call(node, i) format means that the value of this in the passed function will be the node, and that it will receive a single parameter: the value of i.

Things are actually a little different with events. We still want to set the context to the current node (if applicable), but doing this is a little different (and still easy). If you’re worried we might be overriding a built-in function, don’t be. Since we’ve written $ the way we have, anything that uses a normal dojo.query call will still get the old version.

  $.fn.click = function(callback){
    dojo.forEach(this, function(node){
      dojo.connect(node, "onclick", function(e){
        callback.call(e.target, e);
      });
    });
    return this;
  }

dojo.connect will use the “onclick” event of the node to call our function that, in turn, calls our callback, using the event target as its context, and with the event object as its parameter.

And yes, dojo.query does accept a plain node as an argument, which means that you can have:

  $("li.expandable").click(function(e){
    $(this).addClass("expanded");
  });

Now we get to the good stuff. Let’s see if we can implement those plugins from the Plugins/Authoring page.

  $.fn.debug = function() {
    return this.each(function(){
      alert(this);
    });
  };
  $.log = function(message) {
    if(window.console) {
       console.debug(message);
    } else {
       alert(message);
    }
  };

Without any modification, these will both work. The debug function will be added to the $() call, and the log function will be added onto $.

And as a final tidying, many people are familiar with $.ready as their page-loading notifier of choice. Easy again!

  $.ready = dojo.addOnLoad;

You shouldn’t use these techniques to reinvent the wheel. If you’re going to just recreate all of jQuery’s syntax, you should just use jQuery. But if you love Dojo, your coding focuses on behavior driven development, and you want to write a ton of custom plugins without having to touch dojo.NodeList, here’s your solution.

Quick Start

Now that I’ve told you all about it, you can try this code out for yourself without having to download a single file. We’ll be using the version of Dojo hosted by AOL. To use the code seen on this page, simply add these 2 script tags to the head of an HTML document.

<script type="text/javascript" src="http://o.aolcdn.com/dojo/1.0.2/dojo/dojo.xd.js"></script>
<script type="text/javascript" src="http://dojotoolkit.org/~pottedmeat/bling.js"></script>

你可能感兴趣的:(JavaScript,jquery,prototype,dojo,Go)