A typical server-side web framework today includes three main components: a URL dispatching to some controller object scheme, a template engine, and a data mapping facility. Currently in Dojo, you’ll find that the latter two items already exist. dojox.dtl provides the first one, and dojo.data provides the second.

We provide SitePen Support customers with a custom-built user interface for managing their support account. When we started working on this customer interface, we opted to make the entire user interface driven by JavaScript. The server-side provides data and data manipulation functionality via JSON-RPC, but does not dynamically serve up pages as a typical web app would.

All along, we’ve had code to differentiate the URLs coming into the Support customer interface. For example, /?login would take you to the login part of the app, and /?signup-Startup would take you to the signup page with the Startup support plan selected. These were handled in a very ad hoc manner up until our latest update to the app. In the new version, we needed to add a couple more URL patterns that would be used in email messages to our customers. Our ad hoc URL handling was getting ugly and we knew that there are still further parts of the app that we’d like to be able to link directly to, so the time had arrived to make a better solution.

Reinhardt is Born

Given that Dojo already offers an implementation of the Django template engine, and that regular expression-based URL dispatch was a good fit for our problem at hand, I decided to base our URL dispatch on the model provided by Django.

Reinhardt is the name I gave to this client-side web framework. At this point, it simply offers URL dispatch. I can imagine a client-side framework offering tools to help you organize large Dojo-based applications in easy to understand, simple to grow manner. The built-in URL dispatch could also provide automatic history and back button support. For now, however, it is just a small bit of code to make client-side URL dispatch clean and simple.

I should note at this point that Reinhardt is more of a demo than a real open source project. However, it is a straightforward implementation and includes unit tests, so you can confidently use it for real URL dispatch work.

An Example App

I’ve created a sample application that provides a database of movies starring the late, great Paul Newman. This application allows you to navigate directly to any particular movie or function of the application simply by changing the URL. It also shows how easy it is to make links that work both within your application without a page reload and as external links. The nice thing about the way the links are set up in this application is that, unlike in a typical Ajax application, the users can simply copy a link and bookmark it or e-mail it to someone.

How Does the Example Work?

The index.html file is very simple. It sets up the navigation and provides a landing zone for the content. The more interesting file to look at is js/main.js.

At the top of the file, we set pagecount to 0. This counter is used to highlight the difference between complete page loads and incremental loads of content into the content area. The next part is the focus of the article:

var patterns = reinhardt.urldispatch.patterns("demo.code",
  [/^list\/(\d*)$/, "showMovieList", ["year"]],
  [/^search$/, "search"],
  [/^movie\/(\d+)\/$/, "whatyear", ['id']],
  [/^movie\/([\w ]+)\/$/, "whatyear", ["name"]],
  [/^luke$/, "whatyear", [], {name: "Cool Hand Luke"}]
);

In the first line, we’re getting a new PatternList object. A PatternList is an Array subclass that adds a couple of methods for performing the dispatch. The first parameter given to the patterns call is a prefix. This says that all of the function names we refer to should have “demo.code” prepended.

The remainder of the arguments to patterns() are the patterns to match and information about what to do if there is a match. Each pattern is specified by an Array with up to 4 items. The first is the regular expression to attempt to match. The second element is the name of the function to call. This needs to be fully qualified (once the prefix is added). The optional third element is an Array that gives names to the groups found by the regular expression. The values of those groups will be passed in to the function when it’s called. The optional fourth and final element is a set of static parameters that will be passed into the function when it’s called.

Let’s walk through the first entry. /^list\/(\d*)$/ will match URLs like index.html?list/ and index.html#list/1969. Note that both ? and # are supported in the URL, and you can choose which ones you want to accept at dispatch time. The second parameter says that we’re going to call demo.code.showMovieList when we get a match. showMovieList will take one argument, which is an object with all of the parameters for the call. In the regular expression, (\d*) defines a group of zero or more digits. Whatever fits in that group will be put into the “year” attribute on the argument to showMovieList. showMovieList can then display the movies just from that year.

The second entry just looks for “search” and will call demo.code.search.

The next two entries show that the patterns are matched in order. movie/10/ would match both of the expressions. But, movie/Cars/ will only match the second. If the first one is a match, demo.code.whatyear is called with an ‘id’ attribute on the parameters object. If the second matches, there will be a ‘name’ attribute passed to that same function.

The final entry provides a URL “luke” that just goes directly to the display for Cool Hand Luke, and demonstrates how you can provide static arguments to your destination function.

Differences between Django’s URL dispatch and Reinhardt’s URL dispatch

There are a number of differences between the current version of URL dispatch in Reinhardt and Django’s URLconf:

  1. Positional parameters to the handlers are not supported,
  2. Pattern regular expressions cannot contain named groups, which is why the extra parameter that maps the regex groups is used,
  3. There is no support for nesting PatternLists, and
  4. There is no facility for generating URLs.

In Django’s URLconf, an expression like /(\w+)-(\w+)/ matching a URL with foo-bar would call the handler function like: handler("foo", "bar"). This is certainly possible to do with JavaScript. However, JavaScript’s function calls are not quite as powerful as Python’s. In Python, you can mix and match calls with named parameters and calls with positional parameters to the same function (there’s some discussion of this in Chapter 8 of The Definitive Guide to Django, under the heading “Keyword Arguments vs. Positional Arguments”). The behavior of combining positional parameters with the optional static options that can be specified in the pattern is far less obvious in JavaScript than it would be in Python. Rather than introducing confusing behavior, I opted to just not include support for positional parameters.

JavaScript regular expressions do not support named groups as Python’s do. Adding named groups would likely be a fairly simple extension to the syntax, but is not something that I’ve done. At some point in the future, there may be the option to pass in a string that can include named groups rather than just a standard regex object. Similarly, since Reinhardt does not do any parsing of regular expressions, it has no way of putting values back into a URL pattern to generate a URL.

Django supports nesting of URL patterns so that one set of patterns can match the one part of the URL, and then another set can continue the match from there. This is particularly useful when you’re including an app in a larger site. This would not be difficult to support in Reinhardt, but has not been a need of ours to date.

Making the Call

With the patterns defined, let’s drop to the bottom of the file:

dojo.addOnLoad(
  function() {
    if (!patterns.dispatch()) {
      renderContent("../../default.dtl", 
                    {movieCount: demo.database.movies.length});
    }
  }
);

Once the page is done loading, we call patterns.dispatch(). This method is the one responsible for figuring out what to call and actually calling it. dispatch() takes an optional parameter that tells it where to look: “both” (the default), “hash” or “search”. “hash” tells the function to look in window.location.hash (whatever appears after the # in the URL) and “search” means window.location.search (whatever appears after the ? in the URL). dispatch() returns true if it successfully matched a pattern and made the call.

After calling dispatch(), if no pattern matched we will render out the default content. The renderContent function appears closer to the beginning of main.js:

function renderContent(templatename, variables) {
  var template = new dojox.dtl.Template(new dojo.moduleUrl("demo",
                                      templatename));
  var context = new dojox.dtl.Context(variables);
  dojo.byId("content").innerHTML = template.render(context);
  pagecount++;
  dojo.byId("pagecount").innerHTML = pagecount;
}

We’re using Django template language (DTL) templates to display all of the information we’re showing. In the default case, we’re passing in a templatename of “../../default.dtl” which will be located relative to the “demo” JavaScript module. For the variables, we’re passing in an object with a “movieCount” attribute. This gets turned into a DTL Context object for use in rendering the template. In other words, the values available on that object act as the data being plugged into the template. Finally, we render the template and place the result on the page. This is also where we update our pagecount.

The template that is rendered is simple enough:

The Paul Newman Movie Database

There are {{movieCount}} movies in the database.

Though this example application is entirely JavaScript, it should seem somewhat familiar to people who have used Django.

Links in the Sample App

All of the navigation in the sample application is done with standard <a&gr; tags. Though the application is written as a completely JavaScript-driven app, you can navigate around using standard browser navigation.

This is quite inefficient, however. The JavaScript code, and our little “database”, would have to be evaluated by the browser after each link you follow. If you click the “Navigate without reloading” radio button, this function is called:

function specialLoaderOn() {
	dojo.query("#navmenu a").forEach(function(node) {
	node.onclick = navClick
	});
}

This function rips through all of the <a> tags in the navigation menu and replaces the onclick with a function called navClick. Here is that function:

function navClick(event) {
	var url = event.target.href;
	var index_file = url.indexOf("index.html");
	var mypath = url.substring(index_file + 11);
	if (!patterns.dispatchTo(mypath)) {
		renderContent("../../default.dtl", 
					{movieCount: demo.database.movies.length});
	}
	event.preventDefault();
}

This function looks at the href attribute of the link that the user clicked on. It chops off everything up to and including the “index.html?” and looks at the path that remains. It then uses the dispatchTo method which takes a path string and transfers control to a handler, just as if the user had come in at that URL. The event.preventDefault() call ensures that the <a> tag’s normal behavior is not executed.

Navigating this way is much more efficient, because the only work the browser needs to do is render the template and place it in the content area. The JavaScript and the rest of the page remain intact.

With this implementation, the browser back button does not work as the user might wish and the browser’s location bar doesn’t change when you navigate this way. If the features of dojo.back were tied in to either the dispatchTo method or in the navClick function, it would be possible to make navigating without reloading just as convenient and familiar as standard browser navigation, but without the sacrifice in navigation speed.

The URL handler functions

The URL handler functions that a PatternList dispatches to are very straightforward. For example, here is the showMovieList function that is responsible for showing the basic movie list and also for displaying any search results.

showMovieList: function(params) {
	var movies = demo.database.movies;
	if (params.year) {
		movies = dojo.filter(movies, function(movie) {
			return movie.year == params.year;
		});
	}
	
	if (params.q) {
		var search = params.q.toLowerCase();
		movies = dojo.filter(movies, function(movie) {
			var name = movie.name.toLowerCase();
			return name.indexOf(search) > -1;
		});
	}
	
	renderContent("templates/movielist.dtl", {movies:movies});
},

The movie database itself is hardcoded in demo/database.js. It contains a simple array of objects with each object representing a single movie. For each movie, we have the name of the movie and the year in which the movie was made. showMovieList will first filter the movie list based on whether or not the year matches the passed in year. Next, it will do a case insensitive search based on the movie name. The final step is to render the filtered movie list using the movielist.dtl template.

There is nothing Reinhardt specific about this particular function. The only special thing about it is that it accepts one parameter which is an object containing the real parameters used for this function to work.

Search Engine Friendliness

One significant difference between this style of URL dispatch and server-side URL dispatch is that these URLs are only properly interpreted by JavaScript. In fact, even the contents of these pages are only displayed by JavaScript. That means that until the search engines understand how to run JavaScript, anything presented in this application is completely invisible to them.

I don’t think anybody would want to use this for a content-driven web site. This approach is useful when you’re building an application, like the SitePen Support customer interface, and you need to provide deep links into parts of the application. This mechanism is convenient and easy to use for scenarios such as that one.

Getting the Code

You can download the complete sample application to try it out for yourself!