I've been mucking with Dojo widgets a lot lately, and in writing a new base widget (for doing inplace widget creation using existing annotated dom nodes rather than templates) I finally ground my teeth against browser-refresh based testing enough to do something. I was stupid, automated testing of dojo widgets is easy!
Take this example:
dojo.hostenv.setModulePrefix("skife", "/dojo-stuff/skife");
dojo.require("skife.test");
dojo.require("skife.widget.InplaceWidget");
var create = function(s) {
var node = document.createElement("div");
node.innerHTML = s;
var parser = new dojo.xml.Parse();
var frag = parser.parseElement(node, null, true);
dojo.widget.getParser().createSubComponents(frag);
return node;
}
skife.test.suite("Testing org.skife.InplaceWidget", function(s) {
s.test("Widget is instantiated declaratively", function(t) {
var called = false;
dojo.widget.defineWidget("skife.test.DummyWidget1", skife.widget.InplaceWidget, {
postAttachDom: function() {
called = true;
}
});
var div = create("<div dojoType='DummyWidget1'></div>");
t.assert("postAttachDom was not called", called)
});
s.test("In place nodes are attached", function(t) {
dojo.widget.defineWidget("skife.test.DummyWidget2", skife.widget.InplaceWidget, {
postAttachDom: function() {
t.assertNotNull("hi node not attached", this.hi)
}
});
var div = create("<div dojoType='DummyWidget2'>" +
"<span dojoAttachPoint='hi'>Hi</span></div>");
});
})
In this case I need to use declarative widget creation, so the hardest part was figuring out how to get the parser to run over the dom tree I had just made. The Dojo FAQ had two (different) ways of doing it, neither of which work anymore. Luckily the kindly folks in #dojo pointed me to ContentPane which does this (see the create
function earlier).
Anyway, this served as a good reminder to me that not testing because you think something will be hard to test is stupid. Once you try and prove it is hard to test, then you have some legs to make excuses on, until then, kick yourself in the ass and go test some more :-)
Along the way I started cleaning up the JS test harness (re-namespaced into something less likely to have collisions!) I've been mucking with. I've been n the insane side of busy lately so pretty much everything not directly involved with $goal had fallen by the wayside for a bit. Fun to have some breathing room to pick things up!
0 writebacks [/src/javascript/dojo] permanent link
Sat, 03 Jun 2006
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.