Better interface maintenance and element addressing

This is the first in a series of articles on aspects of designing XUL applications.

Developing user interfaces incrementally while at the same time scripting them with DOM functions can quickly become a hindrance to experimentation (and thus to the chance to end up with a good enough interface) as more and more code has to be adjusted, when moving interface parts around, just to keep things running. Here I show some techniques to improve on this situation.

Two approaches are most often seen to address interface parts:

  1. document.getElementById('foo');
  2. someContainerElement.firstChild.firstChild;

Referencing elements through their id is resistant to rearrangements of interface parts: one can move the foo element to the other end of the window, or several levels deep into a different container, and the code that references it will not need to be changed.

On the other hand, an id should be unique. How would one deal with the following situation?

“A tabbox holds notes about web pages, one tabpanel for each page. At any time new notes (and thus panels) can be added. Each panel holds a label that displays the page title. How to access the label of a certain tabpanel?”

Not all labels can be given an id of “title”. id’s could be generated dynamically so as to be unique:


label.setAttribute('id', 'label-of-' + page.url);

It does not look much of a robust solution, though. To avoid generating many id’s, the second approach is used: navigating the DOM. Assuming the label is the last child of a vbox, that in turn is the first child of the tabpanel, one could write:


tabpanel.firstChild.lastChild.value = 'test';

There are at least two problems with that:

  1. if the label is moved at the beginning of the vbox just so that it looks better at the top (so, for a presentation purpose), the code needs to be rewritten; concerns about appearance and content are no longer separated;
  2. by looking at the code above only, it is difficult to understand what element is referenced.

Below, two little used citizens of the Mozilla space are introduced, that help overcoming the limitations of the approaches shown so far: nsIDOMXULElement.getElementsByAttribute() and XPath.

nsIDOMXULElement.getElementsByAttribute()

Assume that the label representing the title of the web page has no id. Instead, it is assigned an identifier that is unique within a narrower scope: the role attribute within the tabpanel’s descendants:


<tabpanel>
  <vbox>
    <!-- other elements here -->
    <label role="title"/>
  </vbox>

  <!-- other elements here -->
</tabpanel>

It is easy to select the label given the tabpanel element:


tabpanel.getElementsByAttribute('role', 'title')[0].value = page.title;

getElementsByAttribute() will look through all the descendants of the element on which it is called.

It is also easy to see that the label can be moved anywhere within the tabpanel, without the rewriting code.

(The class attribute could have been chosen instead of role. However, in many places throughout XUL it is already used for presentational purposes, e.g. class="small-margin", class="menuitem-iconic".)

The technique can also be used to reference the tabpanel within the context of the tabbox. Each tabpanel represents notes about a web page, so it could be identified by the web page URL. Addressing the title of the tabpanel about http://www.mozilla.org then becomes:


var tabbox = document.getElementsById('notes');
tabbox
    .getElementsByAttribute('url', 'http://www.mozilla.org')[0]
    .getElementsByAttribute('role', 'title')[0]
    .value = 'Mozilla rocks!';

XPath

getElementsByAttribute() is fine for basic queries. However, consider this scenario:

“There is a richlistbox with many richlistitem’s, each representing a user subscription to a service. Select the item representing joe’s subscription to http://www.therubymine.com.”

The richlistbox might be something like this:


<richlistbox id="subscriptions">
  <richlistitem user="mary" service="http://blog.hyperstruct.net">
    <label value="Subscription nr. 18758"
  </richlistitem>
  <richlistitem user="joe" service="http://www.therubymine.com">
    <label value="Subscription nr. 87293"
  </richlistitem>
  <richlistitem user="joe" service="http://blog.hyperstruct.net">
    <label value="Subscription nr. 99153"
  </richlistitem>
</richlistbox>

Using getElementsByAttribute(), a list of Joe’s subscriptions could be retrieved, as well as a list of the users subscribed to http://www.therubymine.com. To nail down joe’s subscription to http://www.therubymine.com, though, both queries would have to be performed and results to be crossed.

Enter XPath.

XPath is a little language (or Domain-Specific Language) to perform queries in XML documents. One writes a string representing the “path” of the wanted elements within the XML document, and passes it to an evaluator that finds them.

A few examples before applying it to the problem above:

  • /button: button elements appearing just below the root of the document;
  • //button: button elements appearing anywhere in the document;
  • //button[@label="Hello"]: button elements appearing anywhere in the document, having the label attribute set to “hello”;
  • //[@label="Hello"]: any element, anywhere in the document, having a label attribute set to “hello”.

(For more information see XPath at developer.mozilla.org)

Logical operators are available to build more powerful queries. The query that solves the problem above is:


  //richlistbox[@id="subscriptions"]/richlistitem[@user="joe" and @service="http://www.therubymine.it"]

If the data model is pretty stable but the user interface is only temporary, and you think you might use a sequence of vbox in the future to represent subscriptions, there is no need to depend on richlistbox and richlistitem:


  //*[@id="subscriptions"]/*[@user="joe" and @service="http://www.therubymine.it"]

Perhaps the elements representing the sequence of subscriptions might end up deeper than direct descendants of the “subscriptions” element, as in:


<hbox id="subscriptions">
  <vbox>
    <label class="header" value="Subscriptions"/>
  </vbox>
  <vbox>
    <vbox user="mary" service="http://blog.hyperstruct.net">
      <label value="Subscription nr. 18758"
    </vbox>
    <vbox user="joe" service="http://www.therubymine.com">
      <label value="Subscription nr. 87293"
    </vbox>
    <vbox user="joe" service="http://blog.hyperstruct.net">
      <label value="Subscription nr. 99153"
    </vbox>
  </vbox>
</richlistbox>

To prepare for that case, the query can be become:


  //*[@id="subscriptions"]//*[@user="joe" and @service="http://www.therubymine.it"]

(Notice the double slash in the middle, meaning “descendants” rather than “children”.)

Below is a basic utility to retrieve the first element matching an XPath query. It assumes a XUL document. It can (and in the upcoming articles, will) be made more general and useful.


function getByPath(path) {
    function resolver() { return null; }

    return document.evaluate(
        path, document, resolver, XPathResult.ANY_UNORDERED_NODE_TYPE, null).
        singleNodeValue;
}

Summary

getElementById() is good for interface maintenance because it selects parts of the interface regardless of their position, but does not handle all needs.

If we assign meaningful attributes that are unique within a context narrower than the whole document, the same advantages of getElementById() can be provided by getElementsByAttribute() for simple queries and by XPath for more complex ones.

Share

Almost there…

Share

Exchanging Data Between Chrome and Content

Scenario: a chrome and a content application are aware of their respective existence and wish to communicate. Communication should be able to flow both ways.

One possibility for chrome→content communication is for chrome to invoke Javascript functions defined in content. However, this would only work for DOM objects and properties (and for good reason), unless XPCNativeWrappers are disabled.

One possibility for chrome←content communication is for content to ask the user to grant it expanded privileges and then invoke chrome functions by itself. This opens a door much wider than necessary, increases the coupling between the remote and the local side, and nags the user.

Another possibility is described here.

Let there be two invisible <div> elements in content: <div id="for-chrome"> and <div id="for-content">.

Code living in content writes what it wants to be sent to chrome into <div id="for-chrome">; code living in chrome writes what it wants to be sent to content to <div id="for-content">; both register event listeners that tell them when the <div> they’re interested in gets new data.

Example of content XHTML, content.html:


<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <title>Sample</title>
    <script type="text/javascript" src="content.js"/>
  </head>
  <body onload="init(event);">
    <div id="for-content"/>
    <div id="for-chrome"/>
  </body>
</html>

Example of content Javascript, content.js:


function init(event) {
    document.getElementById('for-content').addEventListener(
        'DOMNodeInserted', function(event) {
            receivedFromChrome(event.target.textContent);
        }, false);
}

function receivedFromChrome(data) {
    alert('Content received: ' + data);
}

function sendToChrome(data) {
    document.getElementById('for-chrome').textContent = data;
}

Example of chrome Javascript:


content.document.getElementById('for-chrome').addEventListener(
    'DOMNodeInserted', function(event) {
        receivedFromContent(event.target.textContent);
    }, false);

function sendToContent(data) {
    document.getElementById('for-content').textContent = data;
}

Going a step further and sending/expecting XML data is easy and very low overhead, courtesy of E4X. content.js then becomes:


function init(event) {
    document.getElementById('for-content').addEventListener(
        'DOMNodeInserted', function(event) {
            receivedFromChrome(new XML(event.target.textContent));
        }, false);
}

function receivedFromChrome(data) {
    alert('Content received: ' + data.toXMLString());
}

function sendToChrome(data) {
    document.getElementById('for-chrome').textContent =
        typeof(data) == 'xml' ? data.toXMLString() : data;
}

chrome.js:


content.document.getElementById('for-chrome').addEventListener(
    'DOMNodeInserted', function(event) {
        receivedFromContent(new XML(event.target.textContent));
    }, false);

function sendToContent(data) {
    document.getElementById('for-content').textContent =
        typeof(data) == 'xml' ? data.toXMLString() : data;
}


Where will one want this? Probably, in scenarios where the chrome application is expecting data from the content application that could come at any time, not just as a result of a chrome-initiated query, and one doesn’t want to sign scripts or nag users with requests for extra content privileges. The communication channel is still opt-in, although it’s the chrome code (which is trusted already) that opens it by registering the event listener, and it’s a much narrower channel with regard to security: an attacker would have to get hold of the content application and to craft data specific to the chrome-content protocol and the chrome code handling the protocol would have to contain security holes in the first place.

Share

Your First Javascript XPCOM Component in 10 Minutes

Steps

  • Prepare the skeleton for an extension. (The Firefox extension wizard can do it for you. If you use it, call the extension “myextension” otherwise the code below won’t work.)
  • Only if you used the extension wizard: create a components/ directory in your tree and change the ROOT_DIRS line in config_build.sh so that it reads:

ROOT_DIRS="defaults components"

  • In the components/ directory, create MyComponent.idl:

#include "nsISupports.idl"

[scriptable, uuid(86939149-65a1-4777-8133-b97506250963)]
interface nsIMyComponent: nsISupports
{
    void reload();
    void sayHello();
};

  • In the components/ directory, create MyComponent.js:

/* ---------------------------------------------------------------------- */
/* Component specific code.                                               */

const CLASS_ID = Components.ID('{86939149-65a1-4777-8133-b97506250963}');
const CLASS_NAME = 'My XPCOM Component';
const CONTRACT_ID = '@example.org/mycomponent;1';
const SOURCE = 'chrome://myextension/content/xpcom.js';
const INTERFACE = Components.interfaces.nsIMyComponent;

/* ---------------------------------------------------------------------- */
/* Template.  No need to modify the code below.                           */

const Cc = Components.classes;
const Ci = Components.interfaces;
const Cr = Components.results;
const loader = Cc['@mozilla.org/moz/jssubscript-loader;1']
    .getService(Ci.mozIJSSubScriptLoader);

function Component() {
    this.wrappedJSObject = this;
}

Component.prototype = {
    reload: function() {
        loader.loadSubScript(SOURCE, this.__proto__);
    },

    QueryInterface: function(aIID) {
        if(!aIID.equals(INTERFACE) &&
           !aIID.equals(Ci.nsISupports))
            throw Cr.NS_ERROR_NO_INTERFACE;
        return this;
    }
};
loader.loadSubScript(SOURCE, Component.prototype);

var Factory = {
    createInstance: function(aOuter, aIID) {
        if(aOuter != null)
            throw Cr.NS_ERROR_NO_AGGREGATION;
        var component = new Component();
        if(typeof(component.init) == 'function')
            component.init();

        return component.QueryInterface(aIID);
    }
};

var Module = {
    _firstTime: true,

    registerSelf: function(aCompMgr, aFileSpec, aLocation, aType) {
        if (this._firstTime) {
            this._firstTime = false;
            throw Components.results.NS_ERROR_FACTORY_REGISTER_AGAIN;
        };
        aCompMgr = aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
        aCompMgr.registerFactoryLocation(
            CLASS_ID, CLASS_NAME, CONTRACT_ID, aFileSpec, aLocation, aType);
    },

    unregisterSelf: function(aCompMgr, aLocation, aType) {
        aCompMgr = aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
        aCompMgr.unregisterFactoryLocation(CLASS_ID, aLocation);
    },

    getClassObject: function(aCompMgr, aCID, aIID) {
        if (!aIID.equals(Ci.nsIFactory))
            throw Cr.NS_ERROR_NOT_IMPLEMENTED;

        if (aCID.equals(CLASS_ID))
            return Factory;

        throw Cr.NS_ERROR_NO_INTERFACE;
    },

    canUnload: function(aCompMgr) { return true; }
};

function NSGetModule(aCompMgr, aFileSpec) { return Module; }

  • Generate an UUID that will identify the component uniquely and use it to replace 86939149-65a1-4777-8133-b97506250963 in MyComponent.idl and MyComponent.js. (In Debian-based Linux distros, the uuidgen tool is found in the the e2fsprogs package. A Firefox Extension/Theme GUI Generator is also available on the web.)
  • Compile the IDL. Have the paths of the xpidl tool and of the Mozilla IDL directory handy. Below, those found in the mozilla-dev package from Debian-based Linux distros are used.

$ /usr/lib/mozilla/xpidl -m typelib -w -v -I /usr/share/idl/mozilla -e MyComponent.xpt MyComponent.idl
  • In the content/ directory create xpcom.js:

function init() {
    this._message = 'Hello, XPCOM world!';
}

function sayHello() {
    Components
        .classes["@mozilla.org/embedcomp/prompt-service;1"]
        .getService(Components.interfaces.nsIPromptService)
        .alert(null, 'Greeting...', this._message);
}

  • In the content/ directory create a test file test.xul:

<?xml version="1.0"?>
<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
  <script type="application/x-javascript">
    function sayHello() {
        Components
            .classes['@example.org/mycomponent;1']
            .getService(Components.interfaces.nsIMyComponent)
            .sayHello();
    }
  </script>

  <button label="Test" oncommand="sayHello();"/>
</window>

  • Register the extension, either by packaging and installing it or by creating a pointer to its directory.
  • (Re)start Firefox and open chrome://myextension/content/test.xul. Clicking on the button should have a greeting pop up. Done!

Explanation

This technique moves the parts common to Javascript XPCOM components (factory, registration, etc.) to a single place that can be left alone (second part of MyComponent.js), the ones related to XPCOM bookkeeping (class ID, contract ID, etc.) to another that can be customized (first part of MyComponent.js), and the code that actually does something to yet another place (xpcom.js) which is where the 99% of the writing is done.

If an init() function is provided in xpcom.js, it will be run each time an instance is created (or, in the case of a service, the first time a reference is acquired).

Customizations

  • In MyComponent.js, changing the first five lines should be enough for most situations.
  • Add more method definitions to MyComponent.idl and method implementations in xpcom.js.
  • Of course, none of the MyComponent.idl, MyComponent.js, and xpcom.js files needs to be named that way, choose what is appropriate.

Optimizations

  • When developing XPCOM services (i.e. components that are only instantiated the first time, then reused), particularly with MozRepl, the reload() method will come in handy; with reload() you can make changes to xpcom.js and reload it without restarting Firefox. Sample MozRepl session:

> var c = Components.classes['@example.org/mycomponent;1'].getService(Components.interfaces.nsIMyComponent);
> c.sayHello();
> // Modify and save xpcom.js
> c.reload();
> c.sayHello();

Changes to the MyComponent.idl will still require an IDL recompilation and a restart.

  • For non-service components, a trick to avoid restarting Firefox is to cause a fresh component definition to be loaded each time an instance is created. Delete the function Component() { ... } definition and the loader.loadSubscript(SOURCE, Component.prototype) instruction from global scope and replace Factory with the following code:

var Factory = {
    createInstance: function(aOuter, aIID) {
        if(aOuter != null)
            throw Cr.NS_ERROR_NO_AGGREGATION;

        function Component() {
           this.wrappedJSObject = this;
        }
        loader.loadSubScript(SOURCE, Component.prototype);
        var component = new Component();
        component.init();

        return component.QueryInterface(aIID);
    }
};

Loading the definition each time is a performance hit, you’ll want to only use this during development or on components not too frequently instantiated.

  • Here is a Makefile to automate compilation of the IDL. Put it in the components/ directory and change IDLC, INC and XPTS according to your setup. Whenever you change the IDL, just issue make.

IDLC=/usr/lib/firefox/xpidl
INC=/usr/share/idl/mozilla/
XPTS=MyComponent.xpt

all: $(XPTS)

%.xpt: %.idl
    $(IDLC) -m typelib -w -v -I $(INC) -e $(@) $(<)

.PHONY: all

Share

MozLab released

A few days ago I released MozLab. It’s the by-product of months of work with Mozilla and Javascript, where I recreated an enviroment to support the development styles I’m used to: strongly interactive and test-driven development.

By interactive development I mean that you don’t have to demolish a building and rebuild it just to see how a new table fits in your living room, and you don’t have to quit a program and restart it just to see if two new lines of code do their job. You just send those lines to your program while it’s running and they go live immediately. By strongly interactive development I mean that you can, to a point, even modify the building itself, not just its contents, without bringing it down and then up again.

MozRepl takes care of this. (Screencast here.)

By test-driven I refer to that (unhappily named) style of development, where you start by writing a small specification of your program using code instead of words. Then you write the code that lets the specification (itself a program) be executed without errors; then you write some more of the specification, then some more code to have the specification execute correctly, and so on. Your code turns out better because you wrote it with a continuous focus on what it’s required to do, rather than getting lost in the implementation, and you can re-use the specification later as a test suite to check whenever you make a change that the program still runs correctly.

MozUnit provides tools for this. (Screencast here.)

The rest of MozLab consists of libraries I wrote along the way, such as a ModuleManager that lets you load code on-demand from Javascript, instead of XUL <script> tag, and without mixing it with the current namespace. Or the finite state machine that I wrote about some weeks ago that puts some sanity back into the implementation of stateful network protocols.

MozLab seems to be doing well. While I’m writing this, its page at Mozilla Addons , where it was made available eight days ago, reports 647 downloads for the first release (thus no upgrades). If you write Mozilla or AJAX applications and want to give it a try, I’d be happy to hear your thoughts!

Share