Showing posts with label dojo. Show all posts
Showing posts with label dojo. Show all posts

Tuesday, September 14, 2010

dojo.connect vs. dojo.behavior

Having used dojo for the past few years, I've become very familiar with using dojo.connect to list for events being fired on objects. The API is simple, clear and concise:
var h = dojo.connect(dojo.byId("myNode"), "onclick", myInstance, "reactToNodeClick");

Plenty of documentation already exists on dojo.connect, so I'm not going to rehash it out here. Reading a recent article on dojo.behavior and couldn't help but agree that there are an enormous amount of similarities between dojo.connect and dojo.behavior. Apparently, dojo.behavior uses the dojo.connect plumbing under the covers, so I was curious to figure out why there was a separate dojo API entry-point to this exact same functionality. dojo.behavior must be offering something above and beyond dojo.connect, or at least it must expose new functionality for connecting to dom and custom events. According to comments on this article, dojo.behavior is based on Ben Nolan's plugin which seems to have been deprecated in favor of jquery's live method. Taking a look at the docs for jquery, I took a shot at converting their example to use dojo.behavior:
<!DOCTYPE html>
<html>
<head>
<style>
p {
background: yellow;
font-weight: bold;
cursor: pointer;
padding: 5px;
}
p.over {
background: #ccc;
}
span {
color: red;
}
</style>
<script type="text/javascript" src="dojotoolkit/dojo/dojo.js">
</script>
<script type="text/javascript">
dojo.require("dojo.behavior");
</script>
</head>
<body>
<p>
Click me!
</p>
<span></span>
<script>
dojo.addOnLoad(function() {
dojo.behavior.add({
'p': {
onclick: function(evt) {
dojo.place("<p>Another paragraph!</p>", dojo.query("p")[0]);
}
}
});
dojo.behavior.apply();
});
</script>
</body>
</html>

So, it seems that dojo.bevahior's main advantages over dojo.connect are:
1. behaviors are added to dom elements which have yet to be inserted into the overall document. This allows the developer to organize all the behavior for a page in one place and also allows for quickly adding behavior to dynamically added dom elements. This stackoverflow question has a pretty nice explanation of this.
2. You can add multiple behaviors to elements all in the one statement. Example:
dojo.behavior.add({
'.foo' : {
found: function(node) {
// do something with each node found
}
}, 
// shortcut for found
'.foo2' : function(node) {
// do something with the node
}
});

although, it could be argued that a combination of dojo.forEach and dojo.query would allow a developer to declare multiple dojo.connects in one statement. For example:
dojo.forEach([".foo", ".foo2"], function(item) {
dojo.query(item).connect("onclick", function(node) {
// do something with the node.
});
});

Unless I'm missing something in the dojo.behavior API I don't see a way to disconnect all the previously added behaviors. I use dojo.disconnect quite a bit when using dojo.connect to clean up after myself (phiggins article again). dojo.behavior.add doesn't return an array of handles so you cannot remove behavior once it has been added. A global suspend/resume API might be a useful addition to the dojo.behavior API to allow a developer to stop/start behaviors. It could even look like this:
dojo.behavior.suspend(selector);
dojo.behavior.suspendAll();
dojo.behavior.resume(selector)
dojo.behavior.resumeAll();

// possible usage
dojo.behavior.add({
'p': {
onclick: function(evt) {
dojo.place("<p>Another paragraph!</p>", dojo.query("p")[0]);
}
}
});
dojo.behavior.apply();
// stop all behavior on p elements that were just applied in the previous statement.
dojo.behavior.suspend('p');
// allow behaviors on p elements again
dojo.behavior.resume('p');

I dunno … that seems hackish when compared with dojo.connect/dojo.disconnect.

Even after this exportation into dojo.behavior, I'm not sure I'd ever use it over dojo.connect/disconnect. dojo.connect/dojo.disconnect gives me a level of granularity that I'm comfortable with. Even searching through the dojo source code for references to dojo.behavior, I don't really see it being used at all in internal framework code (dojo.connect/dojo.disconnect is used EVERYWHERE though).

Can you see any other uses for dojo.behavior - why would you chose to use it over dojo.connect/dojo.disconnect?

Some other useful links:
http://paulirish.com/2010/on-jquery-live/
http://forum.jquery.com/topic/jquery-live-jquery-fn-live-discussion

Monday, August 30, 2010

File Promises and dojo drag and drop

When Adobe pushed AIR 2.0 out via auto update, one of the first new API features that I wanted to try to take advantage of is the File Promises API. Armed with Christian Cantrell's article, I went about integrating File Promises into my application.

Drag and drop isn't a new interaction paradigm in the application I work on. Users have always been able to drag and drop items in the application thanks to dojo's drag and drop APIs. With AIR 2.0 and File Promises though, I wanted to give the user the ability to drag files internally in the application and also out of the application and onto their desktop. To allow both internal and external drag and drop operations, I needed to override the default dojo.dnd.Avatar. Overriding the default Avatar for dojo dnd operations is really easy - the constructor for your dnd source should include the following line (basically, you need some way of telling the dnd manager to use a different function when creating the avatar):
dojo.dnd.manager().makeAvatar = dojo.hitch(this, "makeAvatar");

You'll need a makeAvatar function which returns your custom Avatar:
makeAvatar: function() {
return new my.widget.FileListAvatar(dojo.dnd.manager());
},

Once you've told the dojo dnd manager to use your custom dnd avatar, you'll need to figure out when the avatar in question is being dragged close to the edge of the application. See the _handleMouseMove function below:

dojo.declare(
"my.widget.FileListAvatar",
[dojo.dnd.Avatar],
{
_mouseOverConnects: [],

construct: function() {
// override and cancel the autoScroll function to prevent whitespace from appearing
// in the application when the user is dragging files near the border.
dojo.dnd.autoScroll = function(){};
this._mouseOverConnects.push(dojo.connect(this.manager, "onMouseMove", this, "_handleMouseMove"));
}

_handleMouseMove: function() {
var pos, viewport = dojo.window.getBox();
if(this.node) {
pos = dojo.position(this.node);
if(viewport.w <= pos.x + pos.w || pos.x <= 0
|| viewport.h <= pos.y + pos.h || pos.y + pos.h <= 0) {
// at this stage we know that the user is dragging the avatar close to the application's boundaries.
// time to make a switch from an internal dnd operation to an external dnd.
this. _handleExternalDnd();
}
}

The _handleExternalDnd is the point in the application where we start using the AIR 2.0 file promises API. In the following code, all the items in my dnd source have a store and item property which contain information on the url and file name. The url property from each dnd item is passed to a new URLFilePromise object.
_handleExternalDnd: function() {
var f, url, cb = new air.Clipboard(), promises = [], vp = dojo.window.getBox(), isDraggedOut = false;
for(var j = 0, k = this.manager.nodes.length; j < k; j++) {
f = this.manager.source.getItem(this.manager.nodes[j].id).data;
url = f.store.getValue(f.item, "Url");
if(url !== "") {
var fp = new air.URLFilePromise();
fp.request = new air.URLRequest(url);
fp.relativePath = f.store.getValue(f.item, "FileName");
promises.push(fp);
}
}
cb.setData(air.ClipboardFormats.FILE_PROMISE_LIST_FORMAT, promises);
// the next line causes the switch from internal dnd to external dnd
air.NativeDragManager.doDrag(window.htmlLoader, cb);
air.NativeApplication.nativeApplication.activeWindow.stage.addEventListener(window.runtime.flash.events.NativeDragEvent.NATIVE_DRAG_UPDATE, function(evt) {
if(!isDraggedOut && evt.localX > vp.w) {
isDraggedOut = true;
} else if (isDraggedOut && evt.localX < vp.w) {
// TODO - this is the case where the file has been dragged out of the application and the user is dragging the file back in.
// would be nice at this stage to revert to revert back to an internal dnd operation.
}
});
dojo.style(this.node, "display", "none");
},

I've left some commented code in there which might give you hints of what I'd like to do when the user drags files back into the application after initially dragging them out. I haven't figured out how to cancel a native drag and drop operation just yet - if I do, I'll be sure to update this blog post.

That's pretty much it. Once the user drags files out of the application and into their native file system browser, the urls included in the URLFilePromise object will be downloaded. Check out some of the events you can connect to if you want to give your users a progress indicator of where their files are in terms of being downloaded. One point to note when using File Promises - users are only allowed to drag files from an AIR application into the default file browser. For example, the user cannot drag files directly from an Adobe AIR application into iPhoto or Lightroom. They need to drag the files into Finder first, wait for the files to be downloaded and then drag them into the file management application of their choice.

Looking to the future, it looks like this type of functionality will start to become available natively in the browser.

Some useful links which might help with further reading:

Getting started with dojo dnd
dojocampus' dnd articles
Advanced dojo dnd tutorial
CSS Ninja article on drag and drop
CSS Ninja article how gmail's dnd works

Wednesday, August 18, 2010

Migrating to dojo 1.5

The application I work on had been running the 1.3 version of dojo for the past 18 months or so. I procrastinated with upgrading to the 1.4 version as I was busy migrating an existing web based application to an Adobe AIR app. When dojo 1.5 rolled around I had some spare cycles, so I decided to make the jump directly from 1.3 to 1.5. As with any software upgrade there were some hiccups along the way - here's the few issues I stumbled across:

1. dijit.layout.TabContainer's default controllerWidget seems to have switched from dijit.layout.TabController to dijit.layout.ScrollingTabController. This caused some issues for me as I had declared some custom CSS stylings specifically for dijit.layout.TabController which didn't apply to dijit.layout.ScrollingTabController. Pretty simple workaround for this issue though - I just needed to specify a controllerWidget property when programatically creating new dijit.layout.TabContainer widgets.

2. dojox.layout.RotatorContainer/dojox.layout.RotatorPager have been replaced by a combination of dojox.widget.AutoRotator/dojox.widget.rotator.Controller. Given that I had hacked into a lot of 'private' APIs in the 1.3 version of dojox.layout.RotatorContainer I was a little apprehensive about making the move to dojox.widget.AutoRotator. @cb1kenobi on the dojo IRC helped out a lot with the migration and my application code is a lot cleaner looking as a result.

3. dojo.deferred got a makeover in the 1.5 version of dojo. Previously, I had code which looked like this:
dojox.rpc.transportRegistry.register(
"JSONP",
function(str){ return str === "JSONP"; },
{
fire: function(r) {
var headers = [], url, def;
url = r.target + ((r.target.indexOf("?") === -1) ? '?' : '&') + r.data;
// dair.xhr.send returns a new dojo.Deferred
def = dair.xhr.send({
url: url,
method: "POST",
headers: headers,
checkHeaders: true
});
def.addCallback(this, "parseResults");
return def;
},

parseResults: function(obj) {
obj = obj.data;
var result = dojo.fromJson(obj);
if(result && result.Error) {
var errCode = new Error(result.Error.Code);
errCode.displayMsg = result.Error.Message;
return errCode;
}
return result;
},

A short explanation of the code above:

1. Make the network call (dair.xhr.send).
2. Parse the results with a dojo.fromJson call in parseResults.
3. If while parsing the results of a network call I found an Error property on the json object, treat this response as an error.
4. Returning an Error object from the parseResults method resulted in the error callback being invoked.

However, with the 1.5 version of dojo, this doesn't fly. After pestering @novemberborn on the #dojo IRC channel, he was able to point me in the right direction - I needed an extra dojo.deferred (note the promise variable is what I invoke the callback/errback on in the example below):

dojox.rpc.transportRegistry.register(
"JSONP",
function(str){ return str === "JSONP"; },
{
fire: function(r) {
var headers = [], url, def, promise = new dojo.Deferred();
url = r.target + ((r.target.indexOf("?") === -1) ? '?' : '&amp;') + r.data;
def = dair.xhr.send({
url: url,
method: "POST",
headers: headers,
checkHeaders: true
});
def.then(dojo.hitch(this, function(obj) {
this.parseResults(obj, promise);
}));
return promise;
},

parseResults: function(obj, promise) {
obj = obj.data;
result = dojo.fromJson(obj);
if(result &amp;&amp; result.Error) {
resErr = new Error(result.Error.Code);
resErr.displayMsg = result.Error.Message;
promise.errback(resErr);
return;
}
if(promise.fired !== 1) {
promise.callback(result);
}
},


Apart from some other styling bits and bobs, those 3 issues were the only pieces that slowed me down when doing the migration. Having migrated dojo applications from 0.43 -> 0.9 -> 1.0.2 -> 1.3 -> 1.5, I have to say that this migration ranks among the easier of all migrations. This speaks volumes for the dojo dev team - keeping true to an API is definitely changeling at times, but it makes my life a hellava lot easier. I always find that migration time is always a good time to take a second look at how you're using a toolkit. For me, I started using the .set/.get APIs instead of the .attr API. I also converted by dojo.deferred addCallback/addErrback methods to the more concise dojo.then. I also migrated some custom code to use the new dojox dnd BoundingBoxController and Selector APIs (shameless plug there). When the dust settled after the migration, I figured out that I was able to get rid of ~250 lines of custom code as I had found alternatives in the dojo toolkit - I'm always glad to get rid of code. All in all, I reckon the migration took around 4 days of work - not too bad considering the application I work on is ~11000 lines of code.

dojo 1.6 seems to be coming up pretty soon - I reckon I won't leave it another 18 months for my next dojo upgrade. http://bugs.dojotoolkit.org/ticket/8578 looks particularly interesting - I've been looking for some CI implementation for dojo doh testing and this ticket looks like it could prove very useful.

Thursday, March 4, 2010

Javascript testing in Adobe AIR

I'm in the process of migrating a web application to an Adobe AIR application and I finally got around to trying to migrate some of my JavaScript unit tests to run in AIR. I'd previously written some doh tests and wanted to get them up and running in AIR. Turns out it was relatively simple. All you need to do is:

In your apache configuration file, add an entry to point to the base of your AIR project:
<VirtualHost *>
ServerName air-testing.my.domain
DocumentRoot /path/to/my/trunk/htdocs
</VirtualHost>

(The htdocs directory here should have a subdirectory called dojo which has the 4 dojo subdirectories (dojo, dijit, dojox, util)

Then, in the .html file where you previously had a <meta> redirect (lets call this runTests.html - it probably looked something like this)
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="en-US" dir="ltr" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>My Unit Tests</title>
<meta http-equiv="REFRESH" content="0; url=../../dojo/util/doh/runner.html?testModule=my.module.testAll&registerModulePath=mydomain,../../mydomain">
</head>
<body>
Redirecting to D.O.H runner.
</body>
</html>

... change this file to look like this:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html lang="en-US" dir="ltr" xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>My Unit Tests</title>
<script type="text/javascript">
var addOnLoad = function() {
window.location = "http://air-testing.my.domain/dojo/util/doh/runner.html?testModule=my.module.testAll&registerModulePath=mydomain,../../mydomain"";
}
</script>
</head>
<body onload="addOnLoad();">
Redirecting to D.O.H runner.
</body>
</html>

I found it useful to have a testing application descriptor xml file in the same directory as my runTests.html test file:
<?xml version="1.0" encoding="utf-8" ?>
<application xmlns="http://ns.adobe.com/air/application/1.5">
<id>tests</id>
<filename>runTests</filename>
<name>
<text xml:lang="en_US">Run Tests</text>
</name>
<version>0.0001</version>
<description>
<text xml:lang="en_US">Run Tests</text>
</description>
<initialWindow>
<title>TESTS</title>
<content>runTests.html</content>
<systemChrome>standard</systemChrome>
<transparent>false</transparent>
<visible>true</visible>
<width>1250</width>
<height>900</height>
<x>20</x>
<y>20</y>
<minimizable>true</minimizable>
<maximizable>true</maximizable>
<minSize>850 636</minSize>
<resizable>true</resizable>
</initialWindow>
</application>

Running adl on this new test application descriptor file should kick off your doh unit tests.

Saturday, January 9, 2010

Extension to dojo dnd Selector API

I've had the good fortune of using some dojo drag and drop in a recent project and have been very impressed with the flexibility offered by the APIs. The documentation and tests have been a real help for me in getting drag and drop functionality up and running quickly in my project.

After posting a quick question to the stackoverflow forums, I tried my hand at what Eugene suggested. Here's my attempt:

dojo.provide("my.ext.SelectorMixin");

dojo.require("dojo.dnd.Selector");

dojo.declare(
"my.ext.SelectorMixin",
[dojo.dnd.Selector],
{
// summary: a mixin for the dojo.dnd.Selector class which adds functionality
// to shift the currently selected index backwards and forwards. One possible
// use for this mixin would be to allow a user select different dnd items using
// the right and left keys.

shift: function(offset, shiftKey) {
// summary: shifts the currently selected dnd item
// offset: int: the amount to bump the selection by.
// shiftKey: Boolean: whether or not this new selection happened when the user was holding
// down the shift key
var selectedNodes = this.getSelectedNodes();
if(selectedNodes && selectedNodes.length) {
// only delegate to _selectNode if at least one node is selected. If multiple nodes are selected
// assume that we go with the last selected node.
this._selectNode(this._getNodeId(selectedNodes[selectedNodes.length - 1].id, offset), shiftKey);
}
},

_selectNode: function(nodeId, shiftKey) {
// summary: selects a node based on nodeId
// nodeId: String: the id of the node to select
// shiftKey: Boolean: whether or not this new selection happened when the user was holding
// down the shift key
if(!shiftKey) {
// only clear the selection if the user was not holding down the shift key
this.selectNone();
}
this._addItemClass(dojo.byId(nodeId), "Selected");
this.selection[nodeId] = 1;
},

_getNodeId: function(nodeId, offset) {
// summary: selects a node based on nodeId
// nodeId: String: the id of the node to select
// offset: int: the number of nodes to shift the current selection by
var allNodes = this.getAllNodes(), newId = nodeId;
for(var i = 0, l = allNodes.length; i < l; i++) {
var node = allNodes[i];
if(node.id == nodeId) {
// have a match ... make sure we're not at the start or the end of the dnd set
if(!((offset == -1 && i == 0) || (i == l - 1 && offset == 1))) {
// we should be fine to go with the id the user has requested.
newId = allNodes[i + offset].id;
}
break;
}
}
// if we don't get a match, the newId defaults to the currently selected node
return newId;
}
}
);


One very simple use of this extension would be to add key handlers to dojo.doc to listen for right and left keys:

dojo.connect(dojo.doc, "onkeyup", dojo.hitch(this, function(evt) {
if(evt.keyCode == dojo.keys.RIGHT_ARROW) {
this.shift(1, evt.shiftKey);
}
if(evt.keyCode == dojo.keys.LEFT_ARROW) {
this.shift(-1, evt.shiftKey);
}
}));


I'll give a shot at providing some tests for this extension in my next post.

Saturday, November 14, 2009

Disabling Tabs in a dijit.layout.TabContainer

I've started using tab containers a little more recently. Looking through the dojocampus articles and dojo api docs, it looks like there isn't an API for disabling clicks on a particular tab. I'm not sure why this is the case - perhaps it has something to do with accessibility (I've always had intentions of learning more about accessibility on the web, but I've never really had the time)

At any rate, here's a quick and simple solution to disabling tabs in a tab container:
dojo.forEach(dijit.byId("myTabContainer").tablist.getChildren(), dojo.hitch(this, function(item, index, array) {
dojo.attr(item, "disabled", true);
item.onClick = function() {};
}));

Unfortunately, there doesn't seem to be a simple way to re-enable these clicks. Also, it'd be really sweet if you could simply hide some of the tabs in a tab container, but it sounds like you can only add/remove them.

Sunday, September 13, 2009

dojo updates

You gotta give the dojo guys credit. Thanks to @uhop for following up on my previous blog post. Even though my bug was pretty edge case, its great to see the dojo community going the extra mile to fix it. Its small things like this which make all the difference.

On another dojo related note, I attended the DDD at AOL in Mountain View on Thursday. Always good times hanging out with the dojo crew. Some really cool stuff in the pipeline too. It was great to get some samples of the dojo/django collaboration happening with Tobias and Mike's drawing samples were really cool too - I didn't know stuff like that was possible in the browser. Looking forward to dojo 1.4 already. Rob Christiansen from Adobe AIR popped in too - sounds like there's a lot in the pipeline for AIR 2.0. Maybe the dair dojo project will get rejuvenated with the arrival of AIR 2.0 and dojo 1.4

Wednesday, September 2, 2009

dojo drag and drop in AIR

Looking at common.js in the dojo.dnd package, I see this nice utility function:
Paste your text here.dojo.dnd._isMac = navigator.appVersion.indexOf("Macintosh") >= 0;
dojo.dnd._copyKey = dojo.dnd._isMac ? "metaKey" : "ctrlKey";

dojo.dnd.getCopyKeyState = function(e) {
// summary: abstracts away the difference between selection on Mac and PC,
// and returns the state of the "copy" key to be pressed.
// e: Event: mouse event
return e[dojo.dnd._copyKey]; // Boolean
};

However, this doesn't seem to work in AIR (only tested running on mac so far).

Switching the code to look like:
dojo.dnd._isMac = navigator.appVersion.indexOf("Macintosh") >= 0;
dojo.dnd._isAir = navigator.appVersion.indexOf("AdobeAIR") >= 0;
dojo.dnd._copyKey = dojo.dnd._isMac && !dojo.dnd._isAir ? "metaKey" : "ctrlKey";

seems to do the trick

Wednesday, June 3, 2009

Idle Handler with dojo

After reading Nicholas Zakas' blog posting about handling idle listeners with YUI, I decided to take a bash at porting his logic to dojo. It turned out to be pretty simple - here's the code:
dojo.declare("my.IdleListener", [], {

_mouseMoveHandle: null,
_keyDownHandle: null,
_timeout: 30000,

isRunning: function() {
return this._enabled;
},

isIdle: function() {
return this._idle;
},

start: function(newTimeout) {
this._enabled = true;
this._idle = false;
if (typeof newTimeout == "number") {
this._timeout = newTimeout;
}
this._mouseMoveHandle = dojo.connect(dojo.doc, "onmousemove", this, "_handleUserEvent");
this._keyDownHandle = dojo.connect(dojo.doc, "onkeydown", this, "_handleUserEvent");
// set a timeout to toggle state
this._idleTimeout = setTimeout(dojo.hitch(this, "_toggleIdleState"), this._timeout);
},

stop: function() {
this._enabled = false;
// clear any pending timeouts
clearTimeout(this._idleTimeout);
// detach the event handlers
dojo.forEach([this._mouseMoveHandle, this._keyDownHandle], function(item, index, array) {
if(item) {
dojo.disconnect(item);
}
});
},

_handleUserEvent: function() {
// clear any existing timeout
clearTimeout(this._idleTimeout);
if (this._enabled) {
// if the user is just waking us up again, toggle the idle state.
// otherwise, reset the timeout with a new timeout
this._idle ? this._toggleIdleState() : this._idleTimeout = setTimeout(dojo.hitch(this, "_toggleIdleState"), this._timeout);
}
},

_toggleIdleState: function() {
this._idle = !this._idle;
this._idle ? dojo.publish("idle", []) : dojo.publish("active", []);
}
});

To start your idle handler with a default timeout of 30 seconds, all you need to do is:
 var idleListener = new my.IdleListener();
idleListener.start(30000);

Then in whatever parts of your application deal with polling network resources, all you need to do is:
dojo.subscribe("idle", function() {

});
dojo.subscribe("active", function() {

});

For jQuery users, Paul Irish has ported the code. too.

Just posted an update of this code to github. Check it out!

Tuesday, May 19, 2009

Migrating to dojo 1.3

Recently enough I just pushed some software which included an upgrade to dojo 1.3. Prior to this, I was using dojo 1.0.2, so this migration to the latest version of dojo represented around 2 years of work from the dojo guys.

Having previously migrated from dojo 0.4.3 to a dev build of dojo 0.9 to dojo 1.0.2, I've had my fair share of experience with dojo upgrades. Having previously worked on framework code in a prior job, I'm always ready to try out the latest a greatest versions of frameworks. My main reasons behind the migration to dojo 1.3 were:

1. Official support for Chrome and IE 8.

2. Speed speed speed. I'm not sure what the numbers are like in taskspeed for dojo 1.0.2, but the 1.3 numbers stack up pretty well against some of the major JS toolkit vendors on the market at the moment.

3. I wanted to start exploring some of the new APIs available in dojo 1.3 such as dojo.data, BusyButton, dojo.place and dojo.create

4. Start exploring the dair extensions for integration between dojo and Adobe AIR.

5. Testing. One of the areas where the application I work on is definitely deficient is automated testing. I wanted to start looking into DOH and the dojo robot.

So, I had plenty of reasons to migrate and the move couldn't have gone easier. I honestly only got stung once where once of my custom widgets had a member variable called _created which stepped on the _created member variable in dijit._Widget. The vast majority of headaches were more related to svn and importing the 1.3 version of dojo into my repository than actually migrating my code to use the new dojo APIs. Kudos to the dojo dev team for keeping such a clean and consistent API.

Already looking forward to the 1.4 version of dojo ...

Wednesday, May 6, 2009

plugd and Google Maps

I just started looking into integrating Google Maps into an application. Given the breath of features offered by Google Maps, its no surprise that the JavaScript download is pretty hefty -> ~72KB with an empty cache for main.js. Also, including Google Maps in a page brings down around 20 extra images (depending on the default size of your map).

So, I started looking into ways for deterministically loading Google Maps as there are certain sections of the application I work on where Google Maps is not needed. Looking at the Google Ajax APIs, this seems like the easiest way to load Google Maps (and other Google APIs for that matter):


<script type="text/javascript"
src="http://www.google.com/jsapi?key=ABCDEFG"></script>

...

google.load("maps", "2");


Adding plugd into the mix, you can also cut down on loading the Google Ajax API JavaScript using the addScript method:


dojo.addScript("http://www.google.com/jsapi?key=" + key, dojo.hitch(this, function() {
var mapsLoaded = function() {
dojo.publish("/gmaps/loaded", []);
}
var langArray = dojo.locale.split("-");
google.load("maps", "2", {"callback" : mapsLoaded, "language": langArray[0] + "_" + langArray[1].toUpperCase()});
}));


And code which depends on Google Maps being loaded should listen to the "/gmaps/loaded" topic before trying to access any Google Maps APIs.

Tuesday, April 14, 2009

Connecting labels to inputs using dojo

When using dijit, you have to be careful not to include any id elements in your template HTML file. Otherwise, you can end up with elements with the same id in your document which can lead to a lot of head scratching. Having been bitten by the "multiple elements having the same id in my document" bug, I started stripping out all ids from my template files, replacing them with dojoAttachPoint attributes.

All was going fine and I wasn't getting mixed up in id clashing anymore. However, there was one drawback - my labels wouldn't work anymore. Code which previously looked like this:

<li>
<input id="simple" type="radio" dojoattachevent="onclick: simpleClicked"/>
<label for="simple">${statics.i18n.simple}</label>
</li>

no longer worked because it had changed to this:

<li>
<input dojoAttachPoint="simple" type="radio" dojoattachevent="onclick: simpleClicked"/>
<label for="simple">${statics.i18n.simple}
</li>

The label's for attribute expects to reference an id and not a dojoAttachPoint. Previously I was able to toggle the 'simple' checkbox input by clicking on the label. I always tend to click on labels instead of radio/checkboxes (mostly because they're bigger and easier to click), so I saw this move away from id-based inputs as a degradation in the usability of the web application.

So, I hacked together a piece of code which is invokable after every widget is set up and appended to the dom:

connectLabelsToInputs: function(query, widget) {
dojo.query(query, widget.domNode).forEach(function(item, index, array) {
if(widget[item.getAttributeNode("for").value] && (widget[item.getAttributeNode("for").value].type == "checkbox" || widget[item.getAttributeNode("for").value].type == "radio")) {
var attrs = widget[item.getAttributeNode("for").value].attributes;
for(var i = 0; i < attrs.length; i++) {
if(attrs[i].value.indexOf("onclick") != -1) {
dojo.addClass(item, "clickable");
dojo.connect(item, 'onclick', widget, dojo.trim(attrs[i].value.substring(attrs[i].value.indexOf(":") + 1, attrs[i].value.length)));
}
}
if(!dojo.hasClass(item, "clickable")) {
dojo.addClass(item, "clickable");
dojo.connect(item, "onclick", dojo.hitch(widget, function() {
widget[item.getAttributeNode("for").value].checked = widget[item.getAttributeNode("for").value].checked == false ? true : false;
}));
}
}
}, widget);
}

This code makes the assumption that you have a class called 'clickable' which looks something like this:

.clickable {
cursor: pointer;
}

So, if I know that a widget I just created contains labels and checkboxes or radio buttons, all I need to do is invoke:

common.connectLabelsToInputs("fieldset ol li label", this);

and my labels are clickable again.

Friday, March 13, 2009

Listening for pasted input

A lot of the input forms I work on defaults the "Submit" button to disabled. Added to client side validation, this default usually prevents the user from submitting forms without required fields. I usually attach onkeyup events to each of the form fields to see whether I should enable the "Submit" button for a particular form.

However, onkeyup doesn't catch everything. If the user pastes input into a field using their mouse, onkeyup doesn't catch that event. So, the user is left with an input field which appears filled, but the "Submit" button remains disabled.

The solution is pretty simple. You can attach something like this to your input fields:



<input type="text" dojoAttachEvent="onkeyup: checkSubmitButton, oninput: checkSubmitButton, onpaste: checkSubmitButton">



More on oninput and onpaste

Friday, March 6, 2009

dojo builds and regexp

Just saw a tweet fly by which made me think of a possible blog entry.

I'm more inclined to use Firebug and console.* statements to debug my client side dojo code. I find it easier than rebuilding my code base every time I want to make a change. The one downside to this is my client side code has console statements scattered throughout the code, thus making my the download heavier.

So, as part of my build process, I use the following code to strip out all console.* statements and replace them with a semi-colon.


my @generated_files = (
"htdocs/my-build/$revision/dojo/dojo/main.js",
"htdocs/my-build/$revision/dojo/dojo/registration.js",
"htdocs/my-build/$revision/dojo/dojo/poller.js",
"htdocs/my-build/$revision/dojo/dojo/extras.js"
);
foreach my $generatedFileName (@generated_files) {
my $in_file = $generatedFileName;
my $out_file = $generatedFileName . ".stripped.js";
open DATA, "$in_file" or die "can't open $in_file $!";
my @file_lines = ;
close (DATA);
open DATAOUT, ">$out_file" or die "can't open $out_file $!";
foreach my $line (@file_lines) {
$line =~ s/(?<!:)console\.(?:warn|log|error|con|info)(\((?:(?>[^()]+)|(?{1}))*\));/;/gi;
print DATAOUT "$line";
}
close (DATAOUT);
}


The good people over at http://regexadvice.com/ helped me out with that expression and saved me ~5KB after GZIP compression - not too shabby.

This regular expression does not filter every console.* statement out of your code. For example if the regexp parser encounters:


test ? console.warn("Success") : console.warn("Failure");


it will not strip it down to:


test ? ; : ; ;


As that isn't valid JavaScript.

I'm pretty sure this problem could be fixed for the latest and greatest versions of dojo with a new parameter which you can pass automatically to the build script, but I'm still on 1.0.2 so I'll continue with this until I upgrade to 1.3.

Monday, March 2, 2009

Catching dijit's Dialog close event

I tend to use dijit Dialogs whenever I need the user to make a decision before proceeding with any other actions. They're especially useful for server error conditions and asking the user how they'd like to react to something like an API failure.

However, popping up dialogs can be annoying to users. I know I'm guilty of scanning dialog text and just hitting the 'x' button and ignoring the message. There are two ways you can prevent the user from doing this with dojo.

1. The most obvious solution: Hide the 'x' button. This is easily done with some simple CSS:


#myDialog .dijitDialogTitleBar .dijitDialogCloseIcon {
display:none;
}


The one drawback to this is that the user can hit the escape button and that'll close the dialog.

2. Attach the following two callbacks to your dialog:


this._userClosedDialogHandle = dojo.connect(dijit.byId("myDialog"), "hide", this, "_handleCloseDialog");
this._userClosedEscDialogHandle = dojo.connect(dijit.byId("myDialog").containerNode, 'onkeypress', function (evt) {
key = evt.keyCode;
if(key == dojo.keys.ESCAPE) {
this._handleCloseDialog();
}
});


The one caveat with this method is that you need to disconnect both handles in the _handleCloseDialog method. Otherwise, you'll find yourself in an infinite loop pretty quickly.

Sunday, February 8, 2009

dojo and the enter key

Attaching a listener for the enter key on a form can help a lot from a usability perspective. If your users are more familiar with tabbing through a forms using only keys, asking them to use the mouse just to submit the form can be a little irritating. dojo comes to the rescue with a simple dojoattachevent. All you need to do is add the a dojoAttachEvent to your template file:


dojoattachevent="onkeyup: checkForEnter"


Then, in your widget logic, all you need to do is:


checkForEnter: function(keyEvent) {
if(keyEvent.keyCode == 13) {
if(!dojo.isFF) {
if(this._validateForm()) {
this._createAccount();
}
}
}
}


Oddly enough, Firefox is able to recognize the enter key and submits the form all by itself.

dojo campus has quite a few keys defined so you can capture a lot of what your users are trying to achieve with your web application.

Monday, January 19, 2009

dojo internationalization: easy as 1 .. 2 .. 7

The internationalization APIs in dojo are pretty similar to the internationalization support in Java - at least I found more similarities between dojo i18n and Java than I did between dojo i18n and PHP's gettext. However, getting up and running with dojo i18n support can be a little tricky. Here's a few simple steps I followed to get started with dojo internationalization.

1. Include the list of locales you want to support in your build script. I use Perl to build my dojo application, so it ended up looking a like this:


system("sh build.sh profileFile=$trunk/build/dojo/profile.js releaseDir=../../../build/$revision/ action=release version=$revision localeList=en-us,ja-jp");


2. Set the locale in the djConfig at the top of your page:


var djConfig = {
parseOnLoad: true,
usePlainJson: true,
locale: determineUserLocale()};


The determineUserLocale method here simply returns a locale in the format "en-us", "ja-jp" based on the user's preferences.

3. Wherever your dojo.require's usually are, add the following


dojo.requireLocalization("my.class", "i18n");


This is responsible for downloading a resource bundle called i18n.js which is located under the my.class module.

4. Create an I18n base class (any widget I use which needs internationalization support needs to extend from this widget):


dojo.provide("my.class.I18n");

dojo.require("dijit._Widget");
dojo.requireLocalization("my.class", "i18n");

dojo.declare(
"my.class.I18n",
[dijit._Widget],
{
statics: { i18n: {}},

preamble: function() {
this.statics.i18n = dojo.i18n.getLocalization("my.class", "i18n");
},

translate: function(key, params) {
params = dojo.isArray(params) ? params : [params];
return !params ? this.statics.i18n[key] : dojo.string.substitute(this.statics.i18n[key], params);
}
}


5. Included my.class.I18n in any widget which required internationalization support:


dojo.provide("my.class.TopNav");

dojo.require("my.class.I18n");

dojo.declare(
"my.class.TopNav",
[my.class.I18n, dijit._Templated],
{
templatePath: dojo.moduleUrl("my.class", "templates/TopNav.html"),

postCreate: function() {
...
},

onClick: function() {
...
},

_postApiCall: function(res) {
this.successNode.innerHTML = this.translate("success", [res]);
}


6. Altered my TopNav.html file to be automatically populated by internationalized strings:


${statics.i18n.homeLink}


7. Included internationalized strings in a file called i18n.js under the my.class directory:


homeLink: "Home",
success: "Success ${0} !",


Allowing different languages in the same application should be easy from here on in. All you need to do is set up your directory structure as follows:


my
class
nls
en-us
i18n.js
ja-jp
i18n.js


One option you have for switching between languages is to refresh the full page with the new locale passed in as a query string variable:


loadNewLanguage: function(newLocale) {
var oldSearch = "";
if(window.location.search.length > 0) {
if(window.location.search.indexOf("?Locale=") != -1) {
var localeToRemove = window.location.search.substring(window.location.search.indexOf("?Locale="), (window.location.search.indexOf("?Locale=") + 14));
oldSearch = "&" + window.location.search.replace(localeToRemove, "");
} else {
oldSearch = "&" + window.location.search.substring(1, window.location.search.length);
}
}
window.location = ((window.location.href.substring(0, window.location.href.indexOf(window.location.search)) + "?Locale=" + newLocale + oldSearch + window.location.hash));
}


The determineUserLocale() function mentioned at the top of this article will need to be able to prioritize and Locale query string parameters.

While these steps should get you up and running with dojo's internationalization APIs, there's plenty of i18n and l10n resources available directly on the dojo website. I found these links useful: