Monthly Archives: September 2006

Modular interfaces

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

Displaying repetitive data in XUL interfaces, such as a list of people, a set of messages, or any other collection of things whose number may vary at runtime, is usually done by painstakingly creating those document parts with createElement() and placing them with appendChild() and insertBefore().

For example, to add an person item to a richlistbox representing an addressbook, one might have:


function addPerson(firstname, lastname, telephone) {
    var xulPerson = document.createElement('richlistitem');
    item.setAttribute('role', 'person');
    item.setAttribute('orient', 'vertical');

    var xulName = document.createElement('hbox');

    var xulFirstName = document.createElement('label');
    xulFirstName.setAttribute('role', 'firstname');
    xulFirstName.setAttribute('value', firstname);
    xulName.appendChild(xulFirstName);

    var xulLastName = document.createElement('label');
    xulLastName.setAttribute('role', 'lastname');
    xulLastName.setAttribute('value', lastname);
    xulName.appendChild(xulLastName);

    xulPerson.appendChild(xulName);

    var xulTelephone = document.createElement('label');
    xulTelephone.setAttribute('role', 'telephone');
    xulTelephone.setAttribute('value', telephone);

    xulPerson.appendChild(xulTelephone);

    document.getElementById('people').appendChild(xulPerson);
}

After calling:


addPerson('ford', 'prefect', '0042-42-42');

The richlistbox will look like:


<richlistbox id="people">
  <richlistitem role="person" orient="vertical">
    <hbox>
      <label role="firstname" value="ford"/>
      <label role="lastname" value="prefect"/>
    </hbox>
    <label role="telephone" value="0042-42-42"/>
  </richlistitem>
</richlistbox>

That a function almost twenty lines long generates an XML fragment not longer than ten lines probably needs no comment.

From a wider perspective, taking structural information out of the XML document and putting it in the Javascript code means no longer having a single place where to look for it.

There is a very simple technique to address this.

  1. Let the document provide examples or “blueprints” of instances of repetitive data;
  2. tell the interface renderer not to show them;
  3. clone them as needed and insert the cloned elements in the right places.

The same example, reworked using this technique, is shown below.

Document:


<richlistbox id="people"/>

<box id="blueprints" hidden="true">
  <richlistitem role="person" orient="vertical">
    <hbox>
      <label role="firstname" value=""/>
      <label role="lastname" value=""/>
    </hbox>
    <label role="telephone" value=""/>
  </richlistitem>

  <!-- Other blueprints -->
</box>

Code:


function addPerson(firstname, lastname, telephone) {
    var xulPerson = document.getElementById('blueprints')
        .getElementsByAttribute('role', 'person')[0].cloneNode(true);

    xulPerson.getElementsByAttribute('role', 'firstname')[0]
        .setAttribute('value', firstname);
    xulPerson.getElementsByAttribute('role', 'lastname')[0]
        .setAttribute('value', lastname);
    xulPerson.getElementsByAttribute('role', 'telephone')[0]
        .setAttribute('value', telephone);

    document.getElementById('people').appendChild(xulPerson);
}

(For a description of the technique used to access descendants of an element by their role see Better interface maintenance and addressing.)

Function body has become half the previous one and the description of the structure has gone back to the XML document.

Summary

Building more than trivial interface parts with DOM functions is time-consuming and disperses information about structure. Keeping “blueprints” in a specific document area and cloning them through cloneNode is quicker and keeps information about structure inside the document.

Related readings

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.