Wednesday, November 5, 2008

A reasonably complicated custom Dojo widget example


I get a lot of questions from people on how to do this and that with Dojo, sometimes very specific and sometimes about how to approach problems in general. I don't really consider myself experienced to speak about all things Dojo, I'm actually just more of a fanboy.

Most of my answers, though, boil down to some basic things, were the most common is : make a custom widget. This is key, but seemingly missing from many frameworks/toolkits today. I feel like there has been three kind of 'generations' in JavaScript usage in the browser in recent years, and many of the flame-wars and misunderstandings might be due to the fact that people have a lot of misaligned assumptions when speaking of JavaScript programming. In my view the different generations or stages has been the following;

1. JavaScript inlined in the page - Netscape 4/ IE3 kludgery [Animal House - foodfight scene]
2. Clean html with consistent and meaningful styling and classifications with all JavaScript logic in a couple of separate files which operate on said markup, transforming it, attaching event handlers, et.c. - jQuery [2001 - space shuttle approach to twin-wheel space station].
3. Client-side hierarchical components with modularized logic and templates - Dojo [Basic Instict - the single crotch frame].

First of all I'd like to say that jQuery might have a modularized templating system, where you separate widget markup from logic - I'm not experienced enough in it (yet) to tell. Please comment if you have some good references. Then I'd like to say that both Ext and JavascriptMVC has excellent templating systems for their components - in a very similar vein to what Dojo has (again, AFAIK). But I had to choose one good example out of each group, or at least it felt that way.

What do you gain by writing custom components all the time? It seems awfully complicated, doesn't it?

OK. what do you gain (in Dojo). Let's see;

1. You get an enforced structure that help you separate view and logic inside the widget.
2. You get opinionated support from the framework for automatic id generation, coupling markup elements to widget references and widget lifecycle management (certain named functions get called at certain times).
3. You get _guarantees_ that the widget is hermetically sealed, unless you do something completely stupid. This means you can take it out from one place and put it somewhere else, or change your mind about having three and putting in four instead. No colissions, no overlaps.
4. Widgets markup html snippet templates can themselves (at least in Dojo) contain other generally arbitrary widgets,. Turtles, all the way down, basically.
5. The whole mélange can be expressed in the HTML file that is actually loaded by the user with one (1) div.


But other than that, I suppose, not much.

Since I really only know Dojo, I will be using that in my example. Let's say that I want to have a widget with dynamically generated JavaScript-only 2D charts, where the charts can be dragged and dropped and reordered just like iGoogle or similar pages. Wouldn't that be cool?

Let's start with the target HTML file, which look like this:





I begin by loading some Dojo CSS stuff and the actual toolkit base itself from AOL's CDN (Google would have worked as well, of course). Then I have to configure the djConfig variable a bit so that Dojo find the local files for the widget referenced later, even though it is loaded cross-domain.

The dojo.require statements check if the referenced classes are available, and if not resolves them and load them (since the custom component 'multichart.main' is not part of dojo, I needed to point out where to find it in the modulePaths setting in djConfig earlier.

Then as just one, albeit fat, div tag, I define the draggable multichart container. As you see dojo uses custom HTML properties to let its parser instantiate widgets declared in markup (All widgets can also be created programmatically in the classical style, of course). the dojoType property declare a widget which must be loaded already. All other properties after that are inserted as 'this' properties of the instantiated widget class, if it has declared those names already. Works smoothly, truly.

I basically just pass one argument which is an associative array of names and values. The idea is that the names become title string on the charts, and the values point out urls where json data is provided to generate the charts.

The test file foo.txt looks like this:

{
series:
{
series1: [{x: 1, y: 0.2}, {x: 2, y: 0.5}, {x: 3, y: 1.2}, {x: 4, y: 0.3}],
series2: [{x: 1, y: 0.5}, {x: 2, y: 1.0}, {x: 3, y: 0.9}, {x: 4, y: 1.7}]
}
}

So nothing magic, just a json object with a property 'series' and one or more series of numbers, following the standard way of feeding the dojox.chart API.

But back to the custom widget. Since I declared that Dojo should look for widgets beginning with 'multichart' in the directory of the same name in the directory that the HTML file was loaded from, I place a file called 'main.js' there. Which looks like this;


dojo.provide("multichart.main");

dojo.require("dijit._Templated");
dojo.require("dijit._Widget");

dojo.require("dijit.layout.ContentPane");
dojo.require("dojox.layout.GridContainer");

dojo.require("multichart.chart");

dojo.declare("multichart.main", [ dijit._Widget, dijit._Templated ],
{
templatePath : dojo.moduleUrl("multichart","templates/main.html"),
widgetsInTemplate : true,

content : {}, // name - url pairs to make draggable charts out of. Must be passed from calling script / page
columns : 3, // How many columns we want to have in the GridContainer

constructor: function()
{
console.log("constructor for multichart.main called");
},

postCreate: function()
{
console.log("postCreate for multichart.main called mycontainer is "+this.test);
this.gc = new dojox.layout.GridContainer(
{
nbZones: this.columns,
opacity: 0.7, // For avatars of dragged components
allowAutoScroll: true,
hasResizableColumns: false,
isAutoOrganized : true,
withHandles: true,
acceptTypes: ["multichart.chart"] // This property must always be present, and can take any kind of widget, including your own
}, this.test);
this.gc.startup(); // When creating some dojox widgets programmtaically, you must manually call the startup() function, to make sure it's properly intialized
var i = 0; // Count for which column we'll place each chart in
var p = 0; // Count for which row we'll place a chart in
for(var name in this.content)
{
var url = this.content[name];
var chart = new multichart.chart({name: name, url:url}); // Create a custom chart with the given name and url
console.log("adding chart "+chart+" to zone "+i+", place "+p);
this.gc.addService(chart, i, p); // Add the chart to the GridContainer (This function will be called addChild in the future, for conformance with similar containers.
i++;
if (i > this.columns-1) // Go to next row if we've hit the end of the columns
{
i = 0;
p++;
}
}
}

});

It begins by telling Dojo that it contain the class 'multichart.main', then follow a lot of requiremenets which otherwise would have to be present in the main HTML file. Then follows the widget class declaration.

As you can see it inherits from two superclasses; _Widget which contain the widget subsystem logic and _Templated, which managed html template snippets.

Note the 'content' and 'columns' properties which, since they are defined here, can be passed from a markup declaration(or from a programmatic creation where the arguments gets passed in an object literal as first argument to the constructor). This also gives me a good place to put default values which will stand if not overwritten (such as 'columns').

postCreate gets called when the widget is ready for action, and here is where I usually put most of my init code. What this widget does is to create a Dojo component which mimics an iGoogle page and let the user drag, drop and rearrange all other Dojo widgets inside it. Then it loops over the argument and create any number of yet another custom component, 'multichart.chart', which is also present in the same directory.

Each widget which derive from _Tenplated can also define a html template inside a string or in an external file. I usually use external files when developing, and then use DOjo's offline build system to inline templates, compress, concatenate files inot one, et.c.

If you want to se the widget in action for yourself, you can download it as an archive here. You might also be interested in my upcoming book "Learning Dojo" where it it explained more fully, along with numerous other examples :) OK, I admit it, this whole post was mostly a shameless plug for the book, but partly I wanted to define the playing field a bit as well.

Cheers,
PS

7 comments:

The First Step said...

Thanks so much...This example was what I needed

Mike Warren said...

thanks, I was struggling with this, but then noticed you've provided the code which is a big help. Think other examples on dojocampus etc. I've been looking at where for earlier versions of dojo

Peter Svensson said...

@Mike Warren: Good, then the article worked as it should. Please let me know if you have any other question concerning Dojo and widgets.

Cheers,
PS

huy said...

First off, thank you for the script. It has helped me speed up my development so much. I'm wondering if you know a way to identify which zone has what widget? I'm in need of this so i can save the user layout preference.

thanks so much

Peter Svensson said...

@huy: Thanks! I think I would start looking at the property grid in the class.

If you create a new GridContainer object and call it gc, you will reach it like gc.grid

Each zone has an integer value, and getting the zone gc.grid[z] will get you the zone object.

If you the use firebug and console.dir() you will be able to peer into that object to see properties that let you identify which widget it is.

Sorry to net be more specific at the moment, but we're on vacation :)

Cheers,
PS

huy said...

Thanks Peter. It works as expected.

Lily said...

I've seen many problems like that while I was logged on Viagra Online but I didn't think that it was for that reason I think I need to check my computer.Generic Viagra Buy Viagra