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.
Comments
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?
@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.
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.
There’s a mistake in the link “own routing. Missing after http…;)
@jibiboily: Thanks for the head’s up, fixed it.
@Jonathan Castello:
You are correct! I was being overly protective against / and ?. I’ll update it.
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
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
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.