Usable directory listings with a little Dojo April 29th, 2008 at 12:03 am by Sam Foster
I think we’ve all seen Apache directory listings? They are a list of links + icons that detail the contents of the directory. You can go wild with a custom handler to format directory listing requests however you want. But for most cases they work just fine out of the box. They are kind of tedious to browse through though: scroll, scroll, click, or - worse - tab, tab, tab (tab, tab,) enter. A little Dojo magic might go a long way here.
This tutorial shows you how to upgrade those plain vanilla pages to make getting around a little faster and along the way introduce you to some of the most useful bits of Dojo, and practical techniques for working with them. We’ll touch on: dojo.query, dojo.data, the dojo parser and dijit (specifically the FilteringSelect widget.)
Our final goal looks like this: Index of labs/code/dirindex/data
We’re adding a keyboard-aware suggest-box control to directory index page. The keyboard interaction is hugely improved: tab, u, enter takes you straight to the “United States of America” page.
Getting Started
If you want to follow along, you’ll need access to an Apache web server that allows you to configure indexes - via the main configuration file or a .htaccess file in the directory of your choice. We’re going to create a new header file (see the HeaderName directive) - which is a file that will get included at the top of the generated html that represents each directory listing.
Step 1: Get Configured
Here’s the incantations you’ll need in your .htaccess file:
Options +Indexes IndexIgnore _header.html HeaderName /_header.html
IndexIgnore directive allows us to exclude the header file from the listing. You could also just use a dot-prefixed filename (but then your OS will likely hide it too.)Create your _header.html file, and to make sure we’re on track, start with something simple like:
<div id="indexHeader"> Here's my custom directory index header </div>

View Screenshot
You should see something like the directory structure in this screenshot.
Step 2: Adding Dojo
Now the fun begins. We’ll need some JavaScript and CSS to add style to our suggest box. You can host your own build of Dojo, but for simplicity, we’ll use one from AOL’s CDN.
<script type="text/javascript" src="http://o.aolcdn.com/dojo/1.1.0/dojo/dojo.xd.js"></script> <div id="indexHeader"> Here's my custom directory index header. </div>

View Screenshot
In Firebug, we can now see there’s a “dojo” object ready and waiting:
The suggest box we want to use comes from Dijit, Dojo’s widget system, and is called the dijit.form.FilteringSelect widget. It’s designed with a clean separation of the presentation and behavior layer from the actual data it is tasked with displaying. First, let’s get it on the page, before we worry about how we’re going to feed data to it.
<link rel="stylesheet" type="text/css" media="all" href="http://o.aolcdn.com/dojo/1.1.0/dijit/themes/tundra/tundra.css"/> <style type="text/css" media="all"> #indexHeader { position: fixed; top: 0; right: 0; border: 1px solid #666; background-color: #f9f9f9; padding: 6px 1em 4px 1em; font-family: Tahoma MS, Chicago, arial, sans-serif; } .dj_ie #indexHeader { /* ie7 can do position:fixed, but only in standards mode, which we dont control here */ position:absolute; } .dj_gecko #indexHeader { -moz-border-radius: 4px; margin-top: -4px; margin-right: -4px; } /* dijit moves the id to the inner input, and provides the container element with an prefixed id */ #widget_fileSuggestBox { width: 14em; } </style> <style type="text/css" media="print"> #indexHeader { display:none; } </style>
We style the indexHeader, and arrange for the suggest-box to sit in the top-right corner of the page. The .dj_ie and .dj_gecko classes are a little Dijit magic - it adds a class to the <html> element which identifies the browser and makes this kind of CSS branching really easy and hack-free.
<script type="text/javascript" src="http://o.aolcdn.com/dojo/1.1.0/dojo/dojo.xd.js"></script> <script type="text/javascript" src="http://o.aolcdn.com/dojo/1.1.0/dijit/dijit.xd.js"></script> <script type="text/javascript" language="javascript"> dojo.require("dijit.form.FilteringSelect"); dojo.require("dojo.parser"); </script> <div id="indexHeader"> <label for="fileSuggestBox">Shortcut: </label> <div dojoType="dijit.form.FilteringSelect" store="linksStore" id="fileSuggestBox" searchAttr="id" labelAttr="label" invalidMessage="No matching file in this directory" > </div> </div>
We’ve added a script tag to pull in the dijit.xd.js file. That’s a “layer” in Dojo build terminology, and it rolls up a lot of the common dependencies for working with widgets into one optimized file. We add the requires to pull in the specific widget we want to use, and ensure the parser module is loaded. We add some CSS and markup, and add the dojoType to the element that’s destined to become our FilteringSelect widget.
To configure the widget, we need to define some attributes:
- store=”linksStore”
- where to get its data. Should be a reference to a data store that supports at minimum the Read api
- id=”fileSuggestBox”
- so we can easily get a reference to it later
- searchAttr=”id”
- what data item attribute should we be matching against when we search with each keypress
- labelAttr=”label”
- what data item attribute should we use as the displayed option label in the dropdown
- invalidMessage=”No such file in this directory”
- message to display when no matches are found

View Screenshot
After doing all that, now we have a container but no combo box. Eh?
Step 3: Tame the parser
The Dojo parser allows us to add directives right into the markup where they make most sense. The dojoType attribute is there for the parser to find, but we haven’t told it to run yet. In Dojo 1.* auto-parsing of the document for dojoType’d elements is off by default. That’s good. Blindly crawling the entire DOM of a potentially large page isn’t something you want to do unless you need to for performance reasns. If you want to have it run automatically when the page loads, you can set djConfig.parseOnLoad to true, either with a djConfig attribute in the dojo.js script tag itself, or by defining a djConfig variable before the dojo script tag, and setting it there.
As it happens, we know where any widgets will be - in our indexHeader div. So, we kick off the parser, and pass it the container element we’re working with:
<script type="text/javascript" language="javascript"> dojo.require("dijit.form.FilteringSelect"); dojo.require("dojo.parser"); dojo.addOnLoad(function() { // there's no need to parse the whole page for widgets dojo.parser.parse( dojo.byId("indexHeader") ); }); </script>
Did that? Right now you’re looking at an error that says linksStore is not defined. It is time to feed the widget.
Step 4: Wire it up
Let’s create that missing linksStore:
<div style="display:none" jsId="linksStore" dojoType="dojo.data.ItemFileWriteStore"> <script type="dojo/method" event="preamble" args="params"> params.data = { identifier: "id", items: [] } </script> </div>
The dojo.data.ItemFile*Store classes are fairly general purpose data store implementations that accept json data as input. We require dojo.data.ItemFileWriteStore - we’re going to be dynamically adding items to this store, and this store implements the Write API which gives us an easy way to do that.
We add a hidden div to have the parser trigger creation of the store (We could have done this with a statement in the script too - but this puts everything in one place).
The jsId attribute allows us to provide an identifier for the store object - it should match the store attribute on the FilteringSelect instance.
Now that we’ve got a store (albeit empty) our FilteringSelect widget works. While we’re here we’ll define what we want to happen when the value changes (i.e. the user types a value and hits enter, or selects an item from the dropdown.)
<div dojoType="dijit.form.FilteringSelect" store="linksStore" id="fileSuggestBox" searchAttr="id" labelAttr="label" invalidMessage="No matching file in this directory" > <script type="dojo/method" event="onChange" args="itemId"> linksStore.fetchItemByIdentity({ identity: itemId, onItem: function(item) { window.location.href = linksStore.getValue(item, "url"); }, onError: function() { console.warn("no store item for: " + itemId); } }); </script> </div>
We use the dojo/method script block to override the onChange method of the widget. It is directly equivalent to the following code block:
new dijit.form.FilteringSelect({ onChange: function() { .. } }, node);
onChange method will be passed the new value.Using the “args” attribute we assign the arguments[0] of this function to a “itemId” variable, that will be scoped to the function. We look up the item in the store, and redirect to the url the item provides.
Step5: Populating the Store
Ok. To finish this up, all we need now is data, and that means populating the store with items that have the same attributes as the links in our directory listing. You can imagine a lot of ways to do this. You could write a new index handler say in PHP that could return the listing in a json format to populate the dropdown list. Ugh, that’s a lot of work. Or you could write that data inline into an array in a script block in the header file.
But it is work that’s already been done by the built-in mod_autoindex module in Apache. The data we need is in the page, and it is there in a readily queryable format (DOM) with a little help from Dojo.
dojo.require("dijit.form.FilteringSelect"); dojo.require("dojo.data.ItemFileWriteStore"); dojo.require("dojo.parser"); dojo.addOnLoad(function() { var count=0; // to pick up the theme on the combobox drop-down // we need this class on the body dojo.addClass(dojo.body(), "tundra"); // there's no need to parse the whole page for widgets dojo.parser.parse( dojo.byId("indexHeader") ); // the query selected all "a" elements whose href // property doesn't begin with '?'. This filters out // the column sorting links that the // mod_autoindex's FancyIndexing option creates var query = 'a[href]'; dojo.query(query).forEach(function(elm) { var url = elm.href; var linkLabel = elm.text || elm.innerText; // use the index as the item id in the store var itemId = count++; // create the data object to create an item from var itemData = { label: itemId + ": " + linkLabel, url: url, // the link text is a filename, so it is // guaranteed to be unique. // we use it for the id, which is also // the field used to search on when // typing in the select box id: linkLabel }; linksStore.newItem(itemData); }) });
To get that we use dojo.query, to find all <a> elements that have an href attribute - that’s the query variable - a standard CSS3 selector. Then, using the forEach method of the dojo.NodeList that query returns, we iterate over the results.
Each node (<a> element) in the NodeList will be passed into the anonymous function we supply to forEach. We build up a data object with the properties we want to be able to retrieve later, and an (arbitrary) id, and call the Write API-standard newItem on our store to add the data, and make it available to the widget.
Step 6: Add Polish
There’s a couple of finishing touches needed:
#indexHeader { visibility: hidden; ... }
We can avoid any distracting flicker as it loads and populates, by setting visibility initially to hidden, and only show when we’re ready
dojo.addOnLoad(function() { var uniqueLinks = {}; ... // to avoid any flicker, our header is initially display block, but hidden dojo.style( dojo.byId("indexHeader"), "visibility", "visible"); // the query selected all "a" elements whose href property doesnt begin with '?'. // this filters out the column sorting links that the mod_autoindex's // FancyIndexing option creates var query = 'a:not(a[href^="?"])'; dojo.query(query).forEach(function(elm) { var url = elm.href; // dont list the same link twice if(uniqueLinks[url]) { return; } else { uniqueLinks[url] = elm; } ... }) });
Depending on how you’ve got your directory listings configured, the icons might also be linked, so you’d end up with duplicate entries for each member of the directory. So we use an object as a lookup to only add each item once (we could have also queried the store itself.)
Also, we can refine that query a bit more. We can exclude the column headers that allow sorting of the directory listing with a not clause in the CSS3 selector that is our query, as those links are distinguished by having a querystring (”?“) in the href attribute.
Summary
So there you have it. We extended and augmented a page to make it faster and easier to use, leveraging out-of-the-box goodness to make it all happen with just about 40 (nicely formatted) lines of code.
Next steps might be to allow sorting of the listing without a page refresh, and perhaps a Tree widget to allow us to drill deep into a directory structure without even leaving the page. But that’s for another day.



Posted April 29th, 2008 at 8:10 am
w00t! Great stuff, Sam. Keep it coming.
Posted April 30th, 2008 at 11:37 am
I tried the tab u enter it in Opera 9.27, on Windows XP. It did not work :-(.
Posted April 30th, 2008 at 12:24 pm
Nice article.
One question (isn’t there always?) though: There’s no way for the store itself to keep the values unique instead of doing it outside of the store?
Posted April 30th, 2008 at 2:27 pm
[…] Sam Foster has written up an example of using Dojo to create directory listings with keyboard shortcuts. […]
Posted April 30th, 2008 at 3:54 pm
In response to Seth … with ItemFile*Store, you don’t have to specify an identifier attribute. What the store will do if you have not specified one, is generate an identifier for each item anyway. Basically, it just numbers the items from 0 … N. In other words, as long as your initial dataset leaves identifier undefined, the ItemFile*Store will go into ‘auto-id’ mode and make up one for you. It won’t be accessible as an attribute of course, but is always accessible via store.getIdentity(item).
Posted April 30th, 2008 at 4:19 pm
@Karl - yeah :(, Opera is not officially a supported browser for the dijit project (though it is for Dojo core). In most cases it just work, but not here unfortunately. We’d love to add Opera support - its a matter of (wo)manpower to accomodate the extra testing necessary.
@Seth - I could give each item an id, but the store would just overwrite the previous item with that id IIRC. It seems faster and cheaper to pre-filter, and let the store only do its heavy-lifting where necessary
Posted May 4th, 2008 at 3:50 am
What you did is already implemented in firefox/well, not so fancy/. Try this:
1. hit “/”(quick find)
2. type part of the link text
3. Hit enter
Voila!