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:

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

no longer worked because it had changed to this:

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

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.

Sunday, April 12, 2009

dijit.Dialog's underlay

dijit.Dialogs are really handy for getting the user's attention. However, they can be a little intrusive if they're overused (think annoying JavaScript popups) as they take control away from the user.

Its always advisable to give the user a way out of the dialog. A cancel button, or the little x in the top right hand corner of the dialog should be visible at all times. For whatever reason though (more than likely my CSS skills suck), the little x at the top of the dialog box sometimes disappears behind a scroll bar in IE after I resize the dialog.

One escape route which should always be visible to the user is the underlying web page. Should they click on the underlay, you could interpret that interaction as the user wanting the dialog to disappear. Here's some dojo code I cooked up to allow that interaction to happen:

handleOverlayClick: function(dialogName) {
if(!this._dialogHandles) {
this._dialogHandles = new dojox.collections.Dictionary();
if(!this._dialogHandles.item(dialogName)) {
this._dialogHandles.add(dialogName, dojo.connect(dojo.byId(dialogName + "_underlay"), "onclick", dijit.byId(dialogName), "hide"));
dojo.connect(dijit.byId(dialogName), "hide", dojo.hitch(this, function() {
// cleanup

Thanks to @phiggins on the dojo IRC channel for help with this.