Brian's Waste of Time

Thu, 07 Sep 2006

Testing Dojo Widgets

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.


5 writebacks [/src/javascript/dojo] permanent link