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:
document.getElementById('foo');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
tabboxholds notes about web pages, onetabpanelfor each page. At any time new notes (and thus panels) can be added. Each panel holds alabelthat displays the page title. How to access thelabelof a certaintabpanel?”
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:
- if the
labelis moved at the beginning of thevboxjust 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; - 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
richlistboxwith manyrichlistitem’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:buttonelements appearing just below the root of the document;//button:buttonelements appearing anywhere in the document;//button[@label="Hello"]:buttonelements appearing anywhere in the document, having thelabelattribute set to “hello”;//[@label="Hello"]: any element, anywhere in the document, having alabelattribute 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.
Thanks for the great writeup. As a XUL “newbie” it’s great to learn from some of the “greybeards” with a couple months of XUL experience.
Post new comment