Defining a $ function for Dojo
I generally prefer using full namespace names when using JavaScript libraries. It helps avoid confusion when passing around code or copying/pasting code that may be used in mixed-library environments. However, if you have contained environment, you may prefer extreme brevity over namespace robustness.
So, how can we define a function named $ that works with the base Dojo functionality (the stuff you get in dojo.js: DOM querying, event handling, XHR, style, basic effects, JSON, array, language and object hierarchy helpers). Ideally, $ would be mapped to dojo.query, to allow easy node selection, but then any other dojo method, like dojo.connect() would be available via $.connect().
Here is something that works:
$ = function(){
return dojo.query.apply(dojo, arguments);
};
dojo.mixin($, dojo);
That does the basics: defines a function $ that returns the result of a dojo.query() call, then copies all the properties of dojo to the $ function object (via dojo.mixin()).
This allows for making these sorts of calls:
//Set on onclick listener for all divs in the body
$("div").onclick(function(){
alert("div clicked.");
});
//Do an XMLHttpRequest POST using data from
//a form with a DOM ID of "myFormId"
$.xhrPost({
form: $("#myFormId")[0]
});
Setting a prototype for a function
So that is nice if all I want is dojo base. However, Dojo has a neat module loader, and ideally I want to be able to do something like:
dojo.require("dojo.io.script");
Then later in the code just do a $.io.script.get(). However, this will not work if you define the $ function before doing the dojo.require() call. dojo.mixin() only copies over object properties that exist at the time of the dojo.mixin() call.
So, ideally, I want to set the prototype lookup for $ to be dojo. That way, as new things are added to the dojo object, they will be automatically available via $.
At this point I hit a wall, at least with my current understanding of JavaScript. I was hoping I could do something like this:
var Dollar = function(){
return function(){
return dojo.query.apply(dojo, arguments);
};
};
Dollar.prototype = dojo;
$ = new Dollar();
In JavaScript constructor functions, you do not have to return a value. If you do not, whatever the "this" value was inside the constructor function gets returned as the result of the call the "new" call.
However, if you return a value from the constructor, that is what is returned as the result of the "new" call. This is what Dollar does above.
What I was hoping would happen is that the returned function (that calls dojo.query()) would have its __proto__ property set to Dojo, but I think the __proto__ binding happens before the constructor function is called, so that you can access methods on "this" inside the constructor.
Suppose there was a constructor function called ConstructorFunction, and you called "new ConstructorFunction()". Here is some very crude psuedo-code for what I think the "new" call is doing:
var temp = {};
temp.__proto__ = ConstructorFunction.prototype;
return ConstructorFunction.apply(temp, arguments);
If this is what is going on, it makes sense that by returning a value from the constructor function call, I lose out on the __proto__ binding.
So why not just set the $.__proto__ value directly after $ is defined? Accessing __proto__ is strongly discouraged. It is an implementation's private implementation detail, subject to change. Also, doing that only works right now for Firefox 2 and Safari 3 (failed for IE 6 and Opera 9.24). So stop thinking about it.
I also tried something like this:
Function.prototype = dojo;
$ = new Function("return dojo.query.apply(dojo, arguments);");
but that fails for what I believe to be similar reasons as my first prototype attempt.
I am very interested if someone can suggest how I can modify the $ function's prototype lookup, so that it uses the dojo object. It is neat to think about defining an object that also supports the () operator on it (with a custom prototype).
I want something that works with today's browsers, not ECMAScript 4. Although seeing an ECMAScript 4 implementation might be instructional for the future.
So, really simply: We just add the ability to have something like djConfig.dojo that lets you define what "dojo" is created as. By default, it's {}. So you make it use a function instead, and have a pointer from $ to dojo.
ReplyDeletepottedmeat: That would solve the immediate example for Dojo, but I am interested in a general technique, something that could be added after the fact, without requiring internal Dojo code changes.
ReplyDeleteIt seemed like an interesting thought exercise on functions and prototypes. I do not see a generic way of getting the result I want, but maybe someone else has an idea.
Instead of:
ReplyDeleteFunction.prototype = dojo;
$ = new Function("return dojo.query.apply(dojo, arguments);");
try:
$ = new Function("return dojo.query.apply(dojo, arguments);");
Function.prototype = dojo.prototype;
Then anything added to the dojo's prototype will automatically be added to $'s prototype
Sorry, I meant:
ReplyDelete$ = new Function("return dojo.query.apply(dojo, arguments);");
$.prototype = dojo.prototype;
Shane
shaneosullivan: I see two issues with that approach:
ReplyDelete1) The dojo module loader attaches properties directly to the dojo object, not on dojo.prototype.
2) I also do not want to modify the Function.prototype since that will affect all Function derivatives. I did that in my example code above, just to see if it would work, but I would not want that change for the "final" solution.
shaneosullivan: oops, I did not address your second post: setting $.prototype will not work in that case, since $ is already created.
ReplyDeleteSetting $.prototype would only help things that use $ in their prototype chain (as I understand it).
And for what it is worth, I did try an example like that, but it did not work.
What I really want to do in that line is something like $.__proto__ = dojo, but I consider that verboten, and does not work cross browser.
$ = dojo = dojo.mixin(function(){
ReplyDeletereturn dojo.query.apply(dojo, arguments);
}, dojo);
// And test
$.addOnLoad(function(){
console.debug($("div"));
});
pottedmeat: very nice. It did not occur to me to replace the original object with a modified one.
ReplyDeleteI liked the idea of modifying the prototype for $, but the mixin and replace solution is nice and can work in a generic case. It can even be collapsed down to a one-liner, so bonus points for that.
James,
ReplyDeletesetting non-standard __proto__ is quite simple. All you need is to use a "proxy" constructor (which Crockford advertises as a Object.beget method):
function F(){}
F.prototype = bar;
foo = new F;
// in Mozilla the following will be true
foo.__proto__ == bar;