Eventually Consistent

Dispatches from the grey area between correct and working

Binding a backbone model to the query string

It's very common to use backbone's router to drive application state with the window's url. The basic idea is that popstate or hashchange events are matched to a hash of routes and invoke a handler function. This approach works well for changing views, but not so well for setting application state.

It’s very common to use backbone’s router to drive application state with the window’s url. The basic idea is that popstate or hashchange events are matched to a hash of routes and invoke a handler function. This approach works well for changing views, but not so well for setting application state. For example, consider these routes:

var routes = {
  '(/)'           : 'index',
  'posts'         : 'showPostList',
  'posts/:postId' : 'showPost'
}

This works great if you want to show a list of posts and an individual posts. But what if on the posts list page you want to filter by author. You might be tempted to write a route like this:

var routes = {
  'posts?author=:authorName'
}

This wont work. Backbone’s router doesn’t read query string parameters. To get these values, I suggest making a backbone model that is binds to the query string and automatically updates its attributes on popstate changes. Going a step further, updating the model should update the query string.

While it would be great to use the HTML5 pushState api, its actually “disabled” on iOS. As far as I can tell, this essentially means its not supported at all. So instead of get into the nitty-gritty of hacking a solution for iOS I will just use this library for reading and writing to the url.

The basic model looks like this:

var createHhistory = require( 'history' ).createHistory;
var useQueries     = require( 'history' ).useQueries;

var history = useQueries( createHistory )();

var QueryStringModel = Backbone.Model.extend( {
  
  initialize: function ( opts ) {
    // only track attributes configured 
    this.track = opts.track;
    // flag for prevent infinite loops from occurring
    this.isPopstate = false;
    // setup a listener to url changes
    history.listen( this._pop );
    // setup a listener for model changes
    this.listenTo( this, 'change', this._push );
  },
    /**
     * Handles history change events
     */
  _pop: function ( location ) {
    // history will invoke this callback on POP, REPLACE, and PUSH, s
    // so short-circuit on all but POP events
    if ( location.action !== 'POP' ) {
      return;
    }
    // compile an object of attributes to update
    var params = location.query;
    var toUpdate = _.pick( params, this.track );
    // set isPopstate flag to true
    this.isPopstate = true;
    // update the model
    this.set( toUpdate );
    // reset isPopstate flag to false
    this.isPopstate = false;
  },

  _push: function () {
    // short circuit if flag is true (ie, this model change event was
    // triggered by this._pop)
    if ( this.isPopstate ) {
      return;
    }
    // pick the tracked attributes
    var params = _.pick( this.attributes, this.track );
    // push them up
    history.pushState(null, window.location.pathname, params );
  }
} );

Using the model would like this:

var myAppModel = new QueryStringModel( {
  page: 1,
  sort: 'date'
}, {
  track: [ 'page', 'date' ]
} );

Or as a base model to extend From

var AppModel = QueryStringModel.extend( {
  defaults: {
    page: 1,
    sort: 'date'
  }
} );

var myAppModel = new AppModel( {}, {
  track: [ 'page', 'date' ]
} );

Now you can listen to changes on this model and render your views accordingly.

Thanks for reading! If you found this helpful, consider sharing it.