A Dojo Plugin Pattern
I created a new widget last night based on a couple small functions in base plugd: dojo.twit ... It is a very simple widget with a minimal 2k overhead and illustrates just how truly "plug-able" and flexible Dojo is.
For the horribly impatient: checkout the twitter feed on the sidebar (another degradable example of Dojo) or download the full built twit.js (requires Dojo 1.3)
There are three existing patterns in Dojo that are loosely related:
- Acting upon a node - eg: dojo.style(node, {}), dojo.anim(node, {})
- Acting upon a list of nodes - eg: dojo.query(".nodes").style({})
- Converting a node into a "widget" - eg: new my.Widget({}, node)
The first two go hand-in-hand. Anything you can do to one node you should be able to repeat across a list of nodes in the same way with only a marginal cost for the iteration. The functions to act upon a node follow a very simple API:
some.func = function(/* String|DomNode */node, /* Object? */props){ // summary: do something to node. // node: Can be a DOMNode reference, or string ID or a DOMNode to use // props: Object-hash of properties and parameters for this function call node = dojo.byId(node); // now do something to node. }
The functions that act upon a list of nodes are simply mixed into dojo.NodeList:
// new in Dojo 1.3.0: dojo.NodeList.func = dojo.NodeList._adaptAsForEach(some.func); // old Dojo 1.0 - 1.2.x way: dojo.extend(dojo.NodeList, { func: function(props){ // summary: run `some.func` for each of the nodes in this list return this.forEach(function(n){ some.func(n, props); }); } });
To use the some.func function either way:
// one node some.func("someId", { arg1: "a" }); // all with class="bar" dojo.query(".bar").func({ arg1: "a" });
Note the above use of a new Dojo 1.3 API _adaptAsForEach ... These are private API's put in place to allow little to no duplication between Dojo's own following of this pattern. The direct forEach method will always work as shown, as they are public APIs, though _adaptAsForEach may change. (I'm taking the gamble they will remain in place in some form for quite some time, and enjoy the savings they provide)
By following this pattern, Dojo is able to provide incredibly fast low-level APIs, then map them directly into the Selector engine (aka: dojo.query) for bulk operations.
The last item ("Widgets") actually has two use cases: programatic and declarative. The programatic v. declarative argument has been going on for a long time, and many people still cry standards-foul when referring to Dojo -- this is entirely unnecessary. The declarative way is the optional method of the "plain old JavaScript" way of instantiating some class[like] object:
// programatic: new dijit.TitlePane({ title:"The Title" }, "nodeOrId"); <!-- declarative --> <div dojoType="dijit.TitlePane" id="someId" title="The Title"></div>
The pattern here is simple. The dojoType attribute maps to some object/function (in the example: dijit.TitlePane). The dojo.parser is the only thing in Dojo that understands the dojoType attribute. The Parser's sole job is to:
- find nodes with a dojoType attribute. eg: dojoType="Thinger"
- read the attributes on the found nodes, and convert them to an object.
- call new Thinger(thatObject, foundNode) for each of the nodes
The attributes which are read by the parser aren't entirely arbitrary. For performance reasons, the attributes must be defined in the class you plan to instantiate in order for the parser to find them:
dojo.declare("Thinger", null, { exampleAttrib:"default-value" constructor: function(args, node){ // generic ctor function dojo.mixin(this, args); this.node = dojo.byId(node); } })
The above "Thinger" example will only attempt to locate an "exampleAttrib" parameter on a passed node:
<div dojoType="Thinger" exampleAttrib="overridden" ignoredAttrib="useless"></div>
So the widget pattern is nearly identical to the Dojo pattern (only reversed). Again together:
var args = { prop:"b" }; // functions go node, args my.func("someNode", args); dojo.query("#someNode").func(args); // widgets go args, node new Thinger(args, "someNode"); <p dojoType="Thinger" prop="b">
The reasoning behind the parameter order difference is simple: with widgets the node is optional and with functions the node is explicit. If you are programatically creating a Thinger, you can omit the node (at least in the case of Dijit) as one will be created for you. You must place the node in the DOM manually in this case.
new dijit.TitlePane({ title:"Title", content:" The inner content " }).placeAt(dojo.body());
The parser simply passes the node with the dojoType, and no new nodes are created.
All of this explanation is leading into my twit.js code, albeit an incredibly shortened version of the final file. The above use cases exist in Dojo, and are more or less a constant across the toolkit. I wanted to make my little tweeter plugin work for all of them, without any duplication.
First things first: define the dojo.twit() and dojo.NodeList functions as skeletons. We're making this a full module, so we must include the dojo.provide() call.
dojo.provide("plugd.twit"); dojo.require("dojo.string"); // required for template substitution dojo.require("plugd.script"); // simple/tiny dojo.io.script replacement (function(d){ // the defaults we can override var defaults = { "user":"phiggins", "count":"7", "template":" <p" + ">${text}</" + "p>" } // the main function d.twit = function(node, args){ node = d.byId(node); // mix in the defaults with the args var opts = d.mixin({}, defaults, args); /* the rest of the twit code */ } // 1.3+ only. use this.forEach method for 1.0 - 1.2 d.NodeList.prototype.twit = d.NodeList._adaptAsForEach(d.twit); })(dojo);
This, so far, allows us the two functional examples following the Dojo pattern. We need to fill in some code in the core dojo.twit() function to have this complete, but the keys are there:
- follows the same API pattern of accepting a string ID or domNode
- accepts an object hash of properties to mix over the defaults
- and works transparently with dojo.query
Before we move into making this work as a typical widget would, I should point on special thing out. The "template" member in the defaults is using dojo.string.substitute for basic variable replacement. When we fetch the list of tweets for the opts.user, we will apply this template to each data item returned. No extra work on my part was needed:
d.twit = function(node, args){ node = d.byId(node); // mix in the defaults with the args var opts = d.mixin({}, defaults, args); /* the rest of the twit code: in pseudo-code */ fetch(twitterUrl, function(data){ dojo.forEach(data, function(tweet){ // append a new DOM to this node based on the passed template: dojo.place(dojo.string.substitute(opts.template, tweet), node) }) }) }
The substitute function simply mixes the tweet data into opts.template string, eg: ${text} becomes whatever tweet.text (the tweet content) is. By passing this result directly to dojo.place we are creating the new dom (this is also new in 1.3 -- not place(), but place() acting as a dom-creation API), and appending it to the node we targeted.
This is awesome until we go to implement the Class-based / dojo.parser version. The template:"" option needs to be an HTML snippet, and it would be incredibly ugly to support a pattern like:
<ul dojoType="dojo.Twitter" template="<li>${text}</li>"></ul>
That makes me cringe, and I'm ok with the general use of a dojoType. I needed a better solution. I decided it would be simple enough to use the content of the widget node as the template. For example:
<ul dojoType="dojo.Twitter" user="phiggins"> <li>${text}</li> </ul>
To do this we'll need to read the innerHTML at the time of instantiation, then empty the node (to clear out the template):
d.Twitter = function(args, node){ node = d.byId(node); d.twit(node, d.mixin(args, { template: n.innerHTML })); d.empty(node); } // mix in the defaults into the .prototype, making parser recognize them: d.extend(d.Twitter, defaults);
Now, officially, if dojo.twit does what is is supposed to (and it does), all the following API's are available with almost no repetition. Pick your style:
// functions: dojo.twit("nodeId", { user:"phiggins", template:" ${text} " }) dojo.query("ul.foo").twit({ template:" <li>${text}</li> " }); // plain class new dojo.Twitter({ user:"foo" }, "nodeId") // and the infamous dojoType <ul dojoType="dojo.Twitter" user="phiggins"> <li>${text}</li> </ul>
You can see this in action in the sidebar. Entirely degradable. If no JS, no "Twitter" label is shown. Two lines in my global.js trigger the rest:
dojo.addOnLoad(function(){ dojo.style("tweets", "display", "block"); // show the block, js is enabled dojo.query("#twitter").twit({ template:" <li> ${text} </li> "}); // make it a list });
I love JavaScript, and Dojo 1.3 makes JavaScript that much more fun. Even if you don't agree with the library, and functionality deemed important and common enough for the base dojo.js -- there are years of professional-grade coding living in there, available to learn from and grow upon.