Dojo Widgets: (Slightly) Beyond Hello World
There are quite a few good bare-bones introductions to Dojo widgets out there, but not a lot beyond the Hello World stuff. Here is a bit, as I find it and figure some of it out...
The recent 0.3 release added an amazingly convenient method for creating new
widgets, dojo.widget.defineWidget
, which I'll use for all my widget
making -- it is just that much nicer =)
dojo.widget.defineWidget("skife.sample.CommentPane", dojo.widget.HtmlWidget, {
templatePath: "http://morphy.skife.org/src/dojo-samples/CommentPane.html",
templateCssPath: "http://morphy.skife.org/src/dojo-samples/CommentPane.css"
})
Is barebones usage fora widget which doesn't actually do much. This defines a widget
named skife.sample.CommentPane
which extends dojo.widget.HtmlWidget
.
It has both HTML and CSS templates. When the widget is defined by this method it will
be registered under the name CommentPane
, which is kind of inconvenient as
it basically strips all the namespacing out. C'est la vie.
The method takes a couple more arguments though, so we could make it look more like
dojo.widget.defineWidget("skife.sample.CommentPane", dojo.widget.HtmlWidget, {
templatePath: "http://morphy.skife.org/src/dojo-samples/CommentPane.html",
templateCssPath: "http://morphy.skife.org/src/dojo-samples/CommentPane.css"
}, "html", function() {
this._secretIdentity = dojo.dom.getUniqueId();
});
The third argument, html
declares what contexts the following argument
applies to, I think. Frankly, I have only ever done html widgets, no SVG or otherwise for me yet, so
I mostly leave it blank (it applies to anything) or use "html"
to specify
that it is html specific.
The last argument is a function to be used as a constructor. Kind of handy, though to be honest I have found I rarely use them when making dojo widgets. Weird as I love my ctors in most programming. It could be purely aesthetic, this function is both super handy and butt ugly if you use anything beyond the second argument =) Anyway, I digress.
Fully static templates are pretty boring, though. The next step up is some variable interpolation. If we change our template to be (the still boring, but hey)
<div class="skife-comment-pane">
<h1>${title}</h1>
</div>
we can interpolate some values. Now, Dojo's templating is pretty abyssmal, and for anything complicated I have been using TrimPath's JST instead. It is not tough to make JST play nicely with dojo's packaging and compressing system (though to make the compression not break you need to apply a minor patch) but for fairly simple stuff, Dojo's works great.
Dojo's template processor looks for a field called strings
on the
widget to pull values from. The keys are properties on the strings
object, and the keys are, in fact, used as keys, there is no kind of expression language.
To support a default title, let's change the widget definition to add a title field:
dojo.widget.defineWidget("skife.sample.CommentPane", dojo.widget.HtmlWidget, {
templatePath: "http://morphy.skife.org/src/dojo-samples/CommentPane.html",
templateCssPath: "http://morphy.skife.org/src/dojo-samples/CommentPane.css",
/* Default Property Values */
title: "I am a widget named Bob"
});
While the widget has a title
our template cannot find it. To handle
this we will start to make use of the widget's lifecycle hooks. The useful one here
is postMixInProperties
which is invoked after the initial properties
are set on the widget. We'll use this to set up our strings for interpolation into
the template.
dojo.widget.defineWidget("skife.sample.CommentPane", dojo.widget.HtmlWidget, {
templatePath: "http://morphy.skife.org/src/dojo-samples/CommentPane.html",
templateCssPath: "http://morphy.skife.org/src/dojo-samples/CommentPane.css",
/* Default Property Values */
title: "I am a widget named Bob",
postMixInProperties: function() {
this.strings = {
title: this.title
}
}
})
This will be invoked before the template is processed, but after any properties have been set, so we can now change the title either by setting it in the declarative style:
<div dojo:type="CommentPane" title="Stuff about Dojo" />
Or programmatically if we are creating our widget that way:
var widget = dojo.widget.createWidget("CommentPane", {
title: "More Stuff about Dojo"
});
In both cases the title provided will be used instead of the default. It is worth mentioning, as this caught me initially, that if you use the declarative style you must have a default value assigned to the property in order to have the one from the xml node copied over.
Variable interpolation, in 0.3, is flat. I have heard rumour that it will support traversal of object graphs a la JEXL in 0.3.1, and a much more powerful templating system is "on it's way, sometime."
Ajax, not DOM Scripting, is the hot shiznit right now, so let's go ahead
and fetch some remote data for our CommentPane
-- the existing
comments. The first step is to put somewhere in the template for us to
display these comments:
<div class="skife-comment-pane">
<h1>${title}</h1>
<dl dojoAttachPoint="commentListNode"></dl>
</div>
This uses the standard dojoAttachPoint
to attach the dl
DOM node to our widget. We still need to put something into it, however. To do this
we hook into a different lifecycle hook, fillInTemplate
and add another
parameter to our widget, commentsUrl
, which is a url to GET the existing
comments from a remote service, in JSON format,
looking like this.
dojo.widget.defineWidget("skife.sample.CommentPane", dojo.widget.HtmlWidget, {
templatePath: "http://morphy.skife.org/src/dojo-samples/CommentPane.html",
templateCssPath: "http://morphy.skife.org/src/dojo-samples/CommentPane.css",
/* Default Property Values */
title: "I am a widget named Bob",
commentsUrl: "!",
/* lifecycle hooks */
postMixInProperties: function() {
this.strings = {
title: this.title
}
},
fillInTemplate: function() {
var list = this.commentListNode;
dojo.io.bind({
url: this.commentsUrl,
mimetype: "text/json",
load: function(type, data, evt) {
dojo.lang.forEach(data.comments, function(comment) {
var dt = document.createElement("dt");
dt.innerHTML = dojo.string.escape("html", comment.author);
var dd = document.createElement("dd");
dd.innerHTML = dojo.string.escape("html", comment.text);
list.appendChild(dt);
list.appendChild(dd);
})
}
});
}
});
The fillInTemplate
function is invoked after the template has
been translated to a DOM tree, and the various nodes have been attached. We
fetch a list of existing comments from a remote service and populate them into
the DOM dynamically. The comments won't appear until after the widget
renders, but that is okay, better to show the user something as soon as possible.
Properly we should also include an error handler in this, making the bind
look like:
dojo.io.bind({
url: this.commentsUrl,
mimetype: "text/json",
load: function(type, data, evt) {
dojo.lang.forEach(data.comments, function(comment) {
var dt = document.createElement("dt");
dt.innerHTML = dojo.string.escape("html", comment.author);
var dd = document.createElement("dd");
dd.innerHTML = dojo.string.escape("html", comment.text);
list.appendChild(dt);
list.appendChild(dd);
})
},
error: function() {
var dt = document.createElement("dt");
dt.innerHTML = "Skife"
var dd = document.createElement("dd");
dd.innerHTML = "Unable to retrieve existing comments, sorry."
list.appendChild(dt);
list.appendChild(dd);
}
});
The URL used to fetch the comments is a parameter so that this widget can be used in different places, providing it is just adding another param in either style of creating widgets, here is the declarative one:
<div dojo:type="CommentPane"
commentsUrl="http://morphy.skife.org/src/dojo-samples/existing-comments.json"
title="Stuff about Dojo" />
It is important to be able to add comments as well, but I'll leave that as an exercise for the reader, or until next time! If you cannot stand the wait, look at how the sample code in this works =)
All the code from this is available here if you want to muck around with it.
Update!
Scott J. Miles was kind enough to
post about upcoming changes to
defineWidget
which address my "aesthetically, it's butt ugly" comment.
Without further ado, Scott's comments pulled out from the comments and properly formatted:
Scott J. Miles:
Also, the constructor function (init argument) can now be specified before the properties, hopefully improving readability.
dojo.widget.defineWidget("skife.sample.CommentPane", dojo.widget.HtmlWidget, function() {
this._secretIdentity = dojo.dom.getUniqueId()
},{
templatePath: "http://morphy.skife.org/src/dojo-samples/CommentPane.html",
templateCssPath: "http://morphy.skife.org/src/dojo-samples/CommentPane.css"
});
If you don't like that either, there are actually two other ways to specify the constructor. I will have some examples online shortly.