Composer.js

Router

The router makes it easy to map the current browser URL to an object/function. It supports regular expression group parameters (routes are written in regular expressions) and provides some convenience functions.

Note: the Composer.Router object requires History.js in order to function properly. It can be loaded without it, but History.js must be present to use it.

Events

These are the events fired by routers.

statechange

Triggered when History.js fires its ‘statechange’ event (happens when using History.pushState et al). Arguments passed:

  • the URL we routed to
  • the force parameter (whether or not we’re forcing a route load)

destroy

Triggered when a router object is destroyed. No arguments.

fail

Triggered when there is a problem routing. Arguments passed:

  • description: a JS object that contains the url of the route that failed, the route that was found (if any), whether or not the handler_exists for the route that was found, and whether or not the action_exists for the route that was found.

preroute

Triggered right before a route actually happens. This event is unique because it allows modifying the URL that the route will be triggred with (without changing the URL in the browser bar). This essentially gives you URL rewriting abilities (think mod_rewrite in Apache). Arguments passed:

  • boxed path: an object in the format {path: url_path_str}. If you change the path key in the boxed object, the router will use the path you provided to route on instead of the on in the URL bar.

route

Triggered when a route happens in the router. The router itself listens to this event to determine when to run the route and its matching code. This happens before the matching route (if any) is run. Arguments passed:

  • The (string) path being routed on

route-success

Triggered when a route happens successfully (no errors, found a matching route). Note that this happens just before the found route function is called. Arguments passed:

  • the matching route object

Composer.Router

The main router class. Extends Composer.Base, giving it access to all of Base’s abilities.

It’s important to note that unlike most other Composer objects, the router has global state (in order to bind to various URL events). As such, it is advised to only create one instance of the Composer.Router object at a time.

routes :: attribute({})

Holds the route’s routing table. Although routes are generally passed to the router’s constructor, we’ll document the format here because it deserves its own section.

Routes are specified in the {"/url/regex/specifier": ['object', 'function_name']} format:

var routes = {
    '/': ['pages', 'home'],
    '/users/([0-9]+)': ['users', 'load'],
    // easy way to do a catch-all
    '.*': ['pages', 'not_found']
};

Routes follow some basic rules:

  • Routes are loaded in the order they are specified in the routing table. This is important, because it allows you to do what we did above: create a catch-all route at the bottom of the table that runs if no other routes match.
  • URL specifications (the object keys) are always enclosed between ^ and $, so the route your specify, by default, must be a full match. A URL specifier of “/” will only match a URL of “/”, not match “/mypage” (but “/.*” will match “/mypage”).
  • The value of the URL specifier key is an array with exactly two elements: a top-level object name and a key within that object that points to a function to run.
  • The URL specifiers can use regular expression groups to capture values out of the routed URL and will pass the values in order to the routing function.

So given our above routing table, we might set up our code like so:

var pages = {
    home: function()
    {
        new HomePageController();
    },

    not_found: function()
    {
        new ErrorController({error: 404});
    }
};

var users = {
    load: function(user_id)
    {
        // parameters are always passed as strings
        user_id = parseInt(user_id);
        var user = users_collection.find_by_id(user_id);
        new UserDisplayController({ model: user });
    }
};

Note that our top-level objects are usually just that: simple objects. You can create them as controllers, but it might introduce unneeded complexity. You really only need to use controllers if a particular object is displaying something, which we’re not doing in this case, we’re just loading other objects!

options :: attribute({…})

options: {
    suppress_initial_route: false,
    enable_cb: function(url) { return true; },
    process_querystring: false,
    base: false,
    default_title: ''
}

suppress_initial_route tells the router that when it’s created it should not try to route the current URL (it will try to do so by default). So if your browser is at the URL “/users” the router will try to load the route for “/users” when it’s created unless you set this option to true.

enable_cb is a function that will cancel the current route if it returns a value other than true. It is called with the URL being routed on. This lets you do simple per-URL routing logic within your app depending on its state.

process_querystring tells the router that we want to take the querystring parameters into account when routing. This makes writing your routes a lot trickier because you have to account for URL query parameters now, but also gives you the power to route on them (and capture their values) if that’s what your app requires.

base tells the router that when you call route, you want it to prepend the string options.base to the URL when its passed to pushState. Note that if you set a base, it is removed post-URL-change, and will be ignored by your routing table. So you write your routes as if base isn’t in the URL. This is for applications where you absolutely need all resources to fall under a certain path. While this seems useful, it is mainly for apps that run in a container such as a Firefox or Node-webkit app that needs all pushState URLs to fall under a predetermined folder (ie /content) that otherwise would cause a SecurityError. In other words, don’t use base unless you really need it. It won’t hurt things, but it might make them harder to debug.

default_title is used by route() to set the brwoser window’s title on route if one is not given to it directly.

initialize :: function(routes, options)

Router’s constructor. routes is your routing table, and options is an object containing some or all of the allowed router options.

window.pages = {
    router_docs: function()
    {
        alert('Routed to the Router docs!');
    },

    not_found: function()
    {
        alert('Uh oh');
    }
};
var routes = {
    '/composer.js/docs/router': ['pages', 'router_docs'],
    '/.*': ['pages', 'not_found']
};
var router = new Composer.Router(routes);
router.destroy();

destroy :: function()

Destroys the router and any bindings it has (either other objects’ bindings to it or its bindings to the History object).

get_param :: function(search, key)

Convenience function to get the value of a URL query parameter out of the current URL by its key.

search is the query string (preusmably you got this from window.location.search.

route :: function(url, options)

Route to a URL. This function is provided as an abstraction around setting your URL directly via pushState. Note that if base is set to a string in your options, that base is prepended to the URL that’s routed. So a base of “/content” would case router.route('/users') to change the URL in the address bar to “/content/users”. However you would still write your routing table as though “/content” didn’t exist.

// this
window.location = '/users/69';

// is now
var router = new Composer.Router({});
router.route('/users/69');

It’s advised that when using the router object, you use this function to change URLs.

options can contain the following items:

  • replace_state - If true, will call History.replaceState instead of History.pushState to change the URL.
  • state - An object that is passed as the state parameter to History.pushState or History.replaceState.
  • title - A string title to push to the browser’s header. If not specified, Router.options.default_title is used instead (otherwise a blank string).

find_matching_route :: function(url, routes)

Although this function is mostly used internally, you may also sometimes need to match a URL against your routing table manually. This lets you do that:

window.users = {
    list: function() {},
    display: function(id) {}
};
var routes = {
    '/users': ['users', 'list'],
    '/users/([0-9]+)': ['users', 'display'],
    '.*': ['pages', 'not_found']
};
var router = new Composer.Router(routes, {suppress_initial_route: true});
alert('Route: '+ JSON.stringify(router.find_matching_route('/users/10', routes)));

last_url :: function()

Convenience function that returns the last successfully routed URL. Can be useful for managing various state within your application.

This function makes all links in your app behave as pushState links that change the browser’s URL without actually reloading the page. bind_links uses event delegation to bind an event to the window document that listens for clicks on <a> tags. When it detects a click, it routes the href of the element via History.pushState. This lets you write your links in your app as normal links like you would in a static app, but then use pushState to change pages.

options can contain the following items:

  • do_state_change - A function that takes one argument (the <a> tag element that was clicked) that if it returns false, will cancel the route and let the browser perform the default action.
  • filter_trailing_slash - If true, will remove trailing slashes from <a> tags before routing on their href attribute. Note that this doesn’t affect the element itself, just the URL after it’s pulled from the element.
  • global_state - Any object that is used as the state value in the pushState call.
  • selector - Use a specific selector when binding to links. Note that the selector can be anything, not just links. So “ul.trees li” would bind to any <li> within a <ul class=”trees”> tag.
  • exclude_class - A classname used to exclude specific links from pushState navigation. So a value of “nolink” would exclude any <a class=”nolink”> elements from being bound. This option is ignored if the selector options is provided.
  • rewrite - An optional function that is given the final URL to be routed on by a link bound with bind_links. The value it returns is used as the new URL passed to route.

bind_links has a few criteria it must meet before triggering a route/History.pushState:

  • Control/Shift/Alt keys must not be pressed, otherwise the browser is allowed to do its default action (open a new tab, for instance).
  • The <a> tag must not have a target="_blank" attribute, otherwise the browser default is allowed (new tab/window opened).
  • The <a> tag must have an href must either be a relative URL, or must have a host that matches the current host the app is on. In other words, if your app is hosted on “http://myapp.com” and you click a link in the app to “http://slizzard.com”, bind_links will detect this and treat it like a normal external link (the browser switches pages normally).

These criteria ensure that your users have a smooth experience and can expect accepted conventions while using your app. I cannot count how many apps completely break control+click for opening a link in a new tab because they want to bind every link on the site (and suck at doing so).

Composer’s router does it right. If you experience any problems where the router breaks conventions, please let us know!