Dojo Build string interning revelation
I made an interesting random discovery today which opened a door for a whole new breed of Templated Dojo widgets (aka: Dijits). The Dijit portion of the Dojo Toolkit really could be broken into two parts: A Collection of pre-fabricated UI components and a well thought out framework for creating your own UI components.
At the heart of Dijit are two modules used everywhere: dijit._Widget and dijit._Templated, but those pull in a number of _base modules used to power the framework. dijit.registry, dijit.byId, dijit.typeomatic, and all the various accessibility and utility functions used internally exposed. The overall size of the Dijit framework is about 10k gzipped, including dijit._Widget and dijit._Templated, so it hardly a drop in the bucket on the wire.
dijit._Templated does a cool thing and does a ton of caching, loading and managing of templates for widgets allowing a developer to define either a templateString or templatePath to use for the rendering:
dojo.provide("my.TemplatedWidget"); dojo.require("dijit._Widget"); dojo.require("dijit._Templated"); dojo.declare("my.TemplatedWidget", [dijit._Widget, dijit._Templated],{ templatePath: dojo.moduleUrl("my", "templates/TemplatedWidget.html") });
Basic example using templatePath, which the widget will fetch as necessary using XHR. The interesting part happens after running a Dojo build. As Neil had previously investigated, any matched templatePath variables will have their remote markup inlined into the javascript source as a templateString member.
I often run into a situation where I want to use the Dijit framework to create widgets that have children, but seldom need to make the children classes themselves. Delegating a number of identical items underneath the outer most widget instance is a great way to squeeze performance out of your app, especially when you don't need the children to compartmentalize any of their own behavior.
One solution is to handle this manually, and define some kind of childtemplate member, creating the children as you go:
dojo.declare("my.Thinger", [dijit._Widget, dijit._Templated], { // the containerNode in Thinger.html is an unordered-list templatePath: dojo.moduleUrl("my", "templates/Thinger.html"), childtemplate: " <li>${foo}</li> ", addChild:function(/* Object */data){ // render a child in this widget from the passed data. dojo.place(dojo.string.substitute(this.childtemplate, data), this.containerNode); } });
But this immediately breaks the separation of concerns and leaves us with markup in our JavaScript, which is never a good idea. I wanted to specify an external template for the children items, but still get the advantage of the string-interning the build provides. On a whim, I decided to test how accurately the build was matching "templatePath" for the conversion, and to see if I would be able to pull something like this off:
dojo.declare("my.Thinger", [dijit._Widget, dijit._Templated], { // the containerNode in Thinger.html is an unordered-list templatePath: dojo.moduleUrl("my", "templates/Thinger.html"), childtemplatePath: dojo.moduleUrl("my", "templates/Thinger-child.html") });
and have the resulting built version contain a "childtemplateString" equaling the contents of my now external Thinger-child.html template.
My whim proved accurate, though if this is a bug or a feature is yet to be determined. Taking advantage of this behavior, I know I can shim in some extra code creating a dijit._Templated variant which allows for the 'childtemplate' pattern I seek.
We're going to create a "subclass" of dijit._Templated, hooking into the buildRendering method and do some templatePath handling of our own. We need to support three things:
- A childtemplateString and childtemplatePath should work transparently together. String should take precedence over Path.
- The developer should not have to know which is being used, so make the normalized string version available as "childtemplate"
- The template member should be configurable, optional, and default to "off" (maintaining backwards compatibility)
The task was incredibly easy. I chose using a "subTemplate" member as indication a child template exists, and to avoid ambiguity in naming selections. If no subTemplate is passed, nothing new happens. If a subTemplate string is passed, that string dictates where the template will be available. Eg: subTemplate:"foo" creates this.footemplate from either footemplateString or footemplatePath.
dojo.provide("beer._Templated"); dojo.require("dijit._Templated"); (function(){ var cachedString = {}; dojo.declare("beer._Templated", dijit._Templated, { // subTemplate: String // An identifier for this childtemplate relationship. eg: Set to 'item', // which allows for "itemtemplateString" or "itemtemplatePath" usage as // you would a normal `dijit._Templated`. After `buildRendering`, an // instance member is provided based on this `subTemplate` name as well. // eg: this.itemtemplate will always be available in `postCreate` for // use internally. subTemplate:null, buildRendering:function(){ // hook into _Templated's buildRendering, cache templates on a // per-class basis. if(this.subTemplate){ var identifier = this.subTemplate + "template", s = "String"; // there is a lot of repetition between cache and this[something], // but it only happens once, so isn't so bad (vs more code?) if(!cachedString[this.declaredClass]){ if(!this[identifier + s]){ // assume we have an itemtemplatePath in the absence // of an itemtemplateString dojo.xhrGet({ url: this[identifier + "Path"], sync: true, // we should be only doing this once load: dojo.hitch(this, function(template){ this[identifier + s] = dojo.trim(template); }) }); } // cache the string for the template cachedString[this.declaredClass] = this[identifier + s]; } this[identifier] = cachedString[this.declaredClass]; } // always call native buildRendering this.inherited(arguments); } }); })();
Now you can declare classes inheriting from beer._Templated, and have all the support of dijit._Templated in addition to a nice shorthand/custom templating solution. To use:
dojo.provide("my.Thing"); dojo.require("dijit._Widget"); dojo.require("beer._Templated"); dojo.declare("my.Thing", [dijit._Widget, beer._Templated], { // core: templatePath: dojo.moduleUrl("my","templates/Thing.html"), // extra: childtemplatePath: dojo.moduleUrl("my", "templates/Sub-Thing.html"), subTemplate:"child", // using it: addChild: function(data){ var n = dojo.place(dojo.string.substitute(this.childtemplate), data), this.containerNode); dojo.style(n, "opacity", 0); dojo.fadeIn({ node: n }).play(200); } });
The only problem I have with this so far is the naming convention. "childtemplatePath" should be written as "childTemplatePath", as per the Dojo style guidelines. A quick (3 line) adjustment to the build utility, and that support can be added as well.
The 'beer' namespace comes from me discovering this tidbit while working on twitterverse, which [for whatever reason] is using 'beer' for the code.
MARION:
Buy:Mega Hoodia.Nexium.100% Pure Okinawan Coral Calcium.Zyban.Actos.Petcam (Metacam) Oral Suspension.Prevacid.Synthroid.Valtrex.Prednisolone.Accutane.Human Growth Hormone.Lumigan.Zovirax.Arimidex.Retin-A….
21 July 2010, 10:50 am