Hybrid Routing With Backbone.js

Last time, I wrote about how to get super fast page loads with PJAX.

Sometimes though, more control is needed than what PJAX offers. Maybe you want to override the behavior of a url and process the request entirely on the client-side in Javascript. Or maybe, you want to do something before or while a url is loading.

To do this, I came up with a technique dubbed “hybrid routing,” using Backbone.js.

Basically how it works is to forward each and every link click action to a Backbone.js router class. The router class can then either override the processing of the url and do something special, or it can just fallback on a default behavior and load the url from server as partial HTML much like jquery-pjax.

Let’s take a look at an example router class that handle this.

This app has a #main-view container where the top-level app views will be loaded. It also has an #overlay container where we’ve decided we want to load all secondary level views. These views will overlay the #main-view, sort of like a lightbox or modal.

class App.Router extends Backbone.Router

  initialize: (options) ->
    # keep track of the first route event so that
    # loadNormally doesn't get stuck in an infinite
    # redirect loop
    @isFirstRoute = true
    @once 'route', =>
      @isFirstRoute = false

    # replace main view
    '' : 'replaceMainView'

    # load any url to the admin normally
    'admin*path' : 'loadNormally'

    # load all urls in an overlay by default
    '*default' : 'presentOverlay'

  loadNormally: ->
    # Don't redirect if this is the first route event
    # (the page has just loaded)
    window.location = @getUrl() unless @isFirstRoute

  replaceMainView: ->
    @loadUrl @getUrl(), (data) ->

  presentOverlay: ->
    @loadUrl @getUrl(), (data) ->

  getUrl: ->

  loadUrl: (url, callback) ->
    $.get url, { partial: true }, (data) =>

To hook this up, we need a link click behavior that works similarly to how jquery-pjax works.

$(document).on 'click', 'a', (e) ->
  $link = $(e.currentTarget)
  href   = $link.attr('href')

  # we only care about links with http protocol
  isHttp = $link.prop('protocol').indexOf('http') == 0

  # we only care about links to html pages
  allowedFormats = ['html']
  format = getFormatFromUrl(href)
  isAllowedFormat = format in allowedFormats

  # allow this behavior to be disabled with data-pushstate=false
  isPushState = $link.data('pushstate') != false

  # ignore links with data-remote=true
  isRemote = $link.data('remote')

  if !e.isDefaltPrevented() && !isRemote && isHttp && isAllowedFormat && isPushState

    # trigger the router and make sure the route event is triggered
    app.navigate(href, trigger: true)

# This helper function returns the format of a given url
# (what comes after the '.')
getFormatFromUrl = (url) ->
  dotIndex = url.indexOf('.')
  if dotIndex > 0
    return url.slice(dotIndex+1)
    return 'html'

Finally, we kick off the router somewhere with:

jQuery ->
  window.app = new App.Router()
  Backbone.history.start(pushState: true)

With this in place, any link click that is an http get link and is not overridden by any other behaviors will be forwarded to the Backbone router, where we can determine what to do based on the URL.

The big win here is that now you can control how every url loads in your application from a central location, rather than having to sprinkle behaviors throughout your templates.

comments powered by Disqus