NOTE: Please see Bob Leurck's example for some updates on this code.
The last time I posted about dojo we looked at creating a simple dojo widget. The final aim was to produce a web-oriented progress bar.
Well, now it's finished. Head over to the demo page for XML Manager (my latest product) to try it out. It's a web progress bar because it adapts to the amount of time a server request is expected to take. Each little box takes a little bit longer than the last one, so that it can cover short wait periods just as well as long wait periods.
This post will take a look at the JavaScript, HTML and CSS code to implement this progress bar as a dojo widget. In the last post the code I gave was actually for dojo 0.1. Since dojo 0.2 is the latest version, I've moved to that for the final implementation. There aren't really any big changes.
On thing I did learn about dojo &mdash don't use your own onload
method. This really screws up dojo and prevents it from running properly. Instead you need to go native:
dojo.event.connect(window, "onload", yourFunction);
This actually makes your HTML cleaner and you get the benefits of dojo's memory-leak-avoiding event-system (that would be one of the big reasons for using dojo in the first place).
OK, let's look at some JavaScript first. Here's the JavaScript you use on the page where you want a widget to appear. This is just normal JavaScript code appearing in the script element or in an external JavaScript file referenced using the src
attribute of the script
element. Personally I prefer using exernal JavaScript files as it keeps everything nicely separated. So, here are the relevant bits from the demo.js
external JavaScript file, as used by the XML Manager demo mentioned above.
demo.js
dojo.require("dojo.widget.*"); dojo.hostenv.setModulePrefix('ricebridge', 'ricebridge'); dojo.widget.manager.registerWidgetPackage('ricebridge.widget'); dojo.require("ricebridge.widget.RicebridgeProgress"); function startProgress() { var progress = dojo.widget.manager.getWidgetById("progress"); if( progress ) { progress.startProgress(); } } function stopProgress() { var progress = dojo.widget.manager.getWidgetById("progress"); if( progress ) { progress.stopProgress(); } }
Argh! What's all that dojo gunk at the top? Well the dojo documentation about creating your own widgets only shows how you to create them in the dojo.widget
package. For production use, you'll really want to put them in your own package. In this case, I've chosen the package ricebridge.widget
. The first line at the top is a standard dojo incantation to include the dojo.widget
package. The next two lines set up the ricebridge.widget
package, so that dojo will look for it in a ricebridge
folder in the same folder as the dojo.js
file. For the moment, I'm just accepting that this little invocation works and getting on with it. The last line just tells dojo to include our new package. It's the same idea as the first line.
The two functions startProgress
and stopProgress
are the main control points for the widget. They use the dojo widget manager to find the progress bar wiget object, and start and stop it, by calling methods that we will define in the progress widget itself. In the XML Manager demo, these methods are called when a server request is sent, and then when it returns, to start and stop the progress bar display.
Here's how you use the widget in your HTML code:
demo.htm
<div dojoType="RicebridgeProgress" widgetId="progress" numboxes="30" width="300" height="20" multiplier="10" basecolor="#ccc" oldcolor="#666" hicolor="#00f"> </div>
That's nice and easy, isn't it? The dojoType
attribute places this div
under the control of the dojo widget system, which will turn it into our progress bar using an HTML template (coming up!). We've defined the name RicebridgeProgress
to point to our progress bar widget. We also use the dojo-specific widgetId
attribute to identify the widget object. This widget identifier is used by the getWidgetById
method of the widget manager to find the widget. We used this method in the demo.js
file above. You'll notice that there are a lot of custom attributes on this div
as well. This is how dojo enables a widget to have a custom set of parameters. For our web progress bar, we use the following attributes to set these parameters:
Attribute | What it does |
---|---|
numboxes | Number of indicator boxes in the progress bar. |
width | width of the bar in pixels |
height | height of the bar in pixels |
multiplier | time in millis to increase the wait period of the next box by |
basecolor | color of inactive box |
oldcolor | color of box reached in previous operation |
hicolor | color of box reached in current operation |
So how does dojo know what to show for a widget? Where is the widget HTML defined? dojo uses an HTML and CSS template system for this purpose. Let's look at those files now. In the case of the progress bar they're pretty simple as we build part of the widget HTML dynamically use the JavaScript DOM API.
The other question is where to put these files. They go in the ricebridge/widget
folder, corresponding to the ricebridge.widget
dojo package. This folder structure is placed in the same root folder as the dojo.js
file as per the dojo package discovery convention.
Here's the widget HTML:
progress.htm
<div dojoAttachPoint="main" class="rbprogress"> <table border="0"><tr dojoAttachPoint="bar"></tr></table> </div>
We're going to build the boxes using html td
elements. I'm using a table because it makes it easy to get the boxes all the same size. You could use different elements, such as an HTML list. The td
elements will be created dynamically, based on the numboxes
parameter.
The other thing to notice are the dojoAttachPoint
attributes. These will be used by the widget JavaScript object to access parts of the widget HTML. We'll see this in action in a minute.
Each widget can also have some CSS associated with it. Here's the CSS for the progress bar:
progress.css
div.rbprogress { border: 1px solid #666; margin: 0px; padding: 0px; } div.rbprogress table { width: 100%; height: 100%; border: 0px; margin: 0px; padding: 0px; } div.rbprogress td { border: 1px solid #666; margin: 2px; }
This is fairly standard stuff. At the moment I am not sure how to customise this for each widget instance on an HTML page. It seems one would have to set the required styles dynamically using JavaScript. It is something I will be looking at in the future.
OK. That's all that out of the way. Let's get to the meat. Here is the JavaScript code that defines the widget itself. There's a good bit here, so we'll go through it one step at a time. This JavaScript file is placed in the same folder as the progress.htm
and progress.css
files.
At the top of the file, we inform the dojo package system of the objects we are “providing”. Notice that we provide two objects, HtmlRicebridgeProgress
and RicebridgeProgress
. Based on the dojo widget example this is just the way that you do it, so cut, paste and don't ask too many questions! After the “provides”, we “require” any additional dojo stuff that we need to implement the widget. Obviously we'll need the dojo.widget
package to get at the dojo widget support (our widget inherits from the generic dojo widget object), and we need dojo.lang
for the dojo.inherits
method, used at the end of the file.
RicebridgeProgress.js
dojo.provide("ricebridge.widget.HtmlRicebridgeProgress"); dojo.provide("ricebridge.widget.RicebridgeProgress"); dojo.require("dojo.widget.*"); dojo.require("dojo.lang.*"); ricebridge.widget.HtmlRicebridgeProgress = function() { dojo.widget.HtmlWidget.call(this); this.templatePath = dojo.uri.dojoUri("ricebridge/widget/progress.htm"); this.templateCssPath = dojo.uri.dojoUri("ricebridge/widget/progress.css"); this.widgetType = "RicebridgeProgress"; this.numboxes = 10; this.multiplier = 100; this.width = 500; this.height = 50; this.basecolor = '#ccc'; this.hicolor = '#00f'; this.oldcolor = '#666'; var running = false; var box = 0; this.fillInTemplate = function() { this.main.style.width = this.width+'px'; this.main.style.height = this.height+'px'; for( var bI = 0; bI < this.numboxes; bI++ ) { var td = document.createElement('td'); td.style.backgroundColor = this.basecolor; this.bar.appendChild(td); } } this.startProgress = function() { running = true; this.markNext(); } this.markNext = function() { if( running && box < this.numboxes ) { this.bar.childNodes[box].style.backgroundColor = this.hicolor; var _this = this; dojo.lang.setTimeout(function(){_this.markNext()},this.multiplier*(1+box)); box++; } } this.stopProgress = function() { running = false; for( var bI = 0; bI < box; bI++ ) { this.bar.childNodes[bI].style.backgroundColor = this.oldcolor; } for( var bI = box; bI < this.numboxes; bI++ ) { this.bar.childNodes[bI].style.backgroundColor = this.basecolor; } box = 0; } } dojo.inherits(ricebridge.widget.HtmlRicebridgeProgress, dojo.widget.HtmlWidget); dojo.widget.tags.addParseTreeHandler("dojo:RicebridgeProgress");
So let's start by defining an object for our widget. This object will be called ricebridge.widget.HtmlRicebridgeProgress
. The Html
prefix seems to be required by dojo - I have not tried to do this without it. The Ricebridge
prefix is there because we need a way to avoid conflicts with the names of existing widgets when we use the dojoType
attribute. Um, isn't that what the ricebridge.widget
package is for? Yeah, I thought so too, but apparently not. Again, this seems the safest bet at the moment until I find a better way.
The first few lines of the widget definition are taken from the dojo widget article. We call the dojo HtmlWidget
constructor, as the first step in inheriting from it (the second step occurs at the end of the file, using the dojo.inherits
method). The next three lines set up some standard dojo widget properties, indicating the location of the HTML and CSS template files, and the widget type name, as used by the dojoType
attribute.
These lines are pretty much boilerplate. Next we have the object-specific properties. Dojo checks these names against the attributes of the widget div
in your HTML page and sets any that match. This is how the parameterisation discussed above works. It's a nice bit of dojo magic.
After these public fields, we define two private fields, running
and box
, to hold the current progress bar state. I'm using Douglas Crockford's suggestions for JavaScript field encapsulation.
Next comes the fillInTemplate
method. This is a dojo interface method that should be in every widget object. It does the final work of building the widget DOM. In our case we create the correct number of td
elements, corresponding to the number of boxes requested in the widget div
numboxes
parameter. Notice that dojo has converted the dojoAttachPoint
references into widget object properties of the same name. Thus, this.main
and this.bar
refer to the correct elements in the HTML template. Another handy dojoism.
The startProgress
and stopProgress
methods do exactly what they say. These are the methods called by our main JavaScript file, as mentioned above. All they do is update the color of each table cell as time passes. We use a utility method, markNext
, to handle the setTimeout
callback. Notice that this uses the dojo.lang.setTimeout
method, and not the normal JavaScript setTimeout
method (doing things the "dojo way" keeps you out of trouble). Also, we use the _this
variable to maintain a reference to the widget object in the anonymous function passed as an argument to the setTimeout
method.
And that's pretty much it. You can check out the web progress bar in action over on the XML Manager demo page.
So should you use dojo? Technically it must be one of the most advanced JavaScript frameworks out there. It's a bit weak on the documentation side, but you can always read the source, which is nice and neat and pretty easy to follow. After this little evaluation exercise, I'm going to go with it. I have an opportunity at the moment to put dojo through it's paces in a client project and I'm going to work it hard. I'll let you guys know how it goes.
One more thing, Alex Russell, the dojo maintainer, was nice enough to comment on my previous dojo post. That indicates that the project leaders are pretty committed and that the dojo developers actually care about their users. Not a bad sign at all!