Standalone Javascript Routing

A recent project has us using spine.js as well as a few other JavaScript libraries. Though spine.js comes with its own routing, it conflicts with pjax. The solution was to roll our own.

The requirements are simple, we should be able to give it a set of routes and their corresponding callbacks and we should be able to process the browsers url upon request. The CoffeeScript for that looks like this:

class Router
  @add: (path, callback) ->
    @routes ||= []
    @routes.push { 
      path: path, 
      callback: callback 
    }
  @process: ->
    for route in @routes
      params = window.location.pathname.match(route.path)
      if params?
        route.callback(params)
        return

This code works exactly as we want it too, but there are a few things I’d like to make better. The first of which is that we have to provide a regex as the path so our routes would look something like this:

Route.add /\/blog\/(\w*)/, blogCallback

It would be much easier to read and maintain if it followed more of a rails-routing-style semantic like this:

Route.add "/blog/:id", blogCallback

We can make this possible by processing the path string into its regex counterpart inside Route.add()

The first thing we need to do is escape the forward slashes ( / )

path.replace(/\//g, "\\/")

Then we need to change everything starting with a colon into a capture field

path.replace(/:(\w*)(?!(\w))/g,"(\\w*)")

And then we convert the fancied up string into a RegExp and save it as the path for our route. I’ve chained all of the replaces() together for simplicity.

path = new RegExp(path.replace(/\//g, "\\/").replace(/:(\w*)/g,"(\\w*)"))

The updated class looks like this:

class Router
  @add: (path, callback) ->
    @routes ||= []
    @routes.push { 
      path: new RegExp(path.replace(/\//g, "\\/").replace(/:(\w*)/g,"(\\w*)")), 
      callback: callback 
    }  
  @process: ->
    for route in @routes
      params = window.location.pathname.match(route.path)
      if params?
        route.callback(params)
        return

Then in our project, we have the following setup code.

$ ->
  Route.add "/blog/:id", blogCallback
  $(document).on "ready end.pjax", ->
    Route.process()

Whenever our document is ready or a pjax link has completed, our routes get matched against the url and the appropriate callback gets fired.

tim@collectiveidea.com

Comments

  1. zee@zacharyspencer.com
    Zee
    January 25, 2012 at 12:21 PM

    I ran into some issues with backbone routing in regards to having get variables at the end of a url (i.e. for sorting, etc)

    It appears your regex also struggles with that?

  2. January 25, 2012 at 13:13 PM

    @Zee:

    In the project we are working on, we have a separate class that handles the query string. So this code ignores it. I may extend it in the coming days to provide them as well.

  3. January 25, 2012 at 13:53 PM

    Is the negative lookahead in /:(\w)(?!(\w))/g necessary? \w should be greedy by default, so it will always consume as many characters as it can.

  4. j@jipi.ca
    jipiboily
    January 25, 2012 at 13:52 PM

    There’s a mistake in the link “own routing. Missing after http…;)

  5. January 25, 2012 at 14:24 PM

    @jibiboily: Thanks for the head’s up, fixed it.

  6. January 25, 2012 at 14:32 PM

    @Jonathan Castello:

    You are correct! I was being overly protective against / and ?. I’ll update it.

  7. ej@campbell.name
    EJ
    January 25, 2012 at 16:32 PM
    Rather than escape the / in your original example, you should use a different start character like or !. Also, doesn’t your code drop the string “id”‘unlike ruby. It seems confusing to label if it’s not used.
  8. January 25, 2012 at 21:25 PM

    Rolling your own routing is a great exercise in programming. Flatiron has it’s own stand-alone Router which supports pushState or hash-based routing:

    http://flatironjs.org/#routing
    http://github.com/flatiron/director

  9. January 27, 2012 at 4:13 AM

    I din’t thought of making it rails-style. You could also use this to your advantage in order to make a fake named groups system. You should really put some default arguments and override arguments. They are quite handy.
    Anyway thanks for the good tip/idea. :D

  10. February 18, 2012 at 23:47 PM

    One modification I made to my implementation of this is to do a secondary check to see if the route actually equals location before calling the callback.  I did this because I was having problems with using the location for a route that was ‘/’.  ’/’ is qualified therefore those scripts will shot gun through the app and be called on every route.