Hi, welcome to Word of Mike, my little corner of the internet. I am a Software/Web Developer working in North Yorkshire. I mainly write about programming but my other passion is politics so beware. (click to hide)

2013-06-04 21:15:27 UTC

Rendering Ordered Backbone.js Collections


The dilemma that I had today was in writing a Backbone callback for "add" on my collection, which rendered my collection in order. The difficulty being that calling .fetch() on the collection triggered the "add" callback for all the items in the order that they are returned from the API, rather than the order specified in my collection's comparator function.


Update: I've been reliably informed that it isn't a great idea to use this technique on page load, as it's causing a browser reflow for every object it inserts, which will apparently suck for big collections. It still works nicely for adding to the collection, but the whole collection should be bootstrapped at load anyway, I think.


First things first, I've been learning Backbone for about three days, so in another couple of days I'll probably realise there's a way better way to accomplish the same thing.

I could simply and easily render the entire collection every time it changes, but it feels wasteful when with a bit of work I can insert only the new record into the page, or delete the deleted one. With a bit of help I quickly understood that you could use indexOf() to get the objects position in the sorted collection, so, take the following example:

var Foo = Backbone.Model.extend();

var FooCollection = Backbone.Collection.exend({
  model: Foo,
  comparator: function(foo) {
    // order by 'created_at' DESC
    return -(new Date(foo.get('created_at'))).getTime();
  }
});

var FooView = Backbone.View.extend({
  el: $("#foo-list"),
  initialize: function() {
    this.listenTo(this.collection, "add", this.addOne);
    this.collection.fetch();
  },
  addOne: function(foo) {
    index = this.collection.indexOf(foo);
    // Now we need to render the object into
    // the DOM at the specified index in our UL
    // or table or whatever (UL in this example)
  }
});

The awkward thing that arises here is that, as explained, we don't necessarily know what order our objects will come to the addOne function, so if the first item needs to go in index 4, for example, we don't have four other elements to insert it after in our DOM. We need some way of keeping it's index that we calculated so that we can compare the next item to it to know whether it goes before or after.

I built a quick JS example, mainly to just get my head around what I was trying to achieve, but it demonstrates adding elements the DOM arbitrarily but maintaining their sorting:

<ul class="collection"></ul>
var indexes = [4,1,3,2,5,0],
    as_is = new Array();
for(i = 0; i<indexes.length; ++i) {
  var $collection = $(".collection"),
      $children = $collection.children(),
      index = indexes[i];
  if (as_is.length == 0) {
    as_is.push(index);
    $collection.append("<li>" + index + "</li>");
  } else {
    as_is.push(index);
    as_is.sort(function(a,b){return (a-b)});
    var ins = as_is.indexOf(index);
    var pos = $children.eq(ins);
    if (pos.length > 0) {
      $children.eq(ins).before("<li>" + index + "</li>");
    } else {
      $collection.append("<li>" + index + "</li>");
    }
  }
}

What this code actually does is keep a representation of which indexes we've inserted into the DOM thus far, so step by step:

  1. Add the index to our array (as_is)
  2. Sort the array
  3. Find the new, sorted, position of the element we added using indexOf()
  4. Insert the actual DOM element into our list at that position
So, applying that to our addOne function, we arrive at:
var Foo = Backbone.Model.extend();

var FooCollection = Backbone.Collection.extend({
  model: Foo,
  comparator: function(foo) {
    // order by 'id' descending
    return -foo.get('id');
  }
});

var FooView = Backbone.View.extend({
  tagName: 'li',
  render: function() {
    this.$el.html(this.model.get('id'));
    return this;
  }
});

var AppView = Backbone.View.extend({
  el: $("#foo-list"),
  initialize: function() {
    this.domList = new Array();
    this.listenTo(this.collection, "add", this.addOne);
  },
  addOne: function(foo) {
    var $children = this.$el.children(),
        index = this.collection.indexOf(foo),
        view = new FooView({ model: foo });
    if (this.domList.length == 0) {
      this.domList.push(index);
      this.$el.append(view.render().el);
    } else {
      this.domList.push(index);
      this.domList.sort(function(a,b){return (a-b)});
      var ins = this.domList.indexOf(index)
      var pos = $children.eq(ins);
      if (pos.length > 0) {
        pos.before(view.render().el);
      } else {
        this.$el.append(view.render().el);
      }
    }
  }
});

var foos = new FooCollection();
var app_view = new AppView({collection: foos});
foos.add({ id: 2 });
foos.add({ id: 3 });
foos.add({ id: 5 });
foos.add({ id: 4 });
foos.add({ id: 1 });

Here's the fiddle!

As you can see, we're adding the Foo objects arbitrarily but we're inserting the items in the correct place in the DOM each time, and it will work with fetch() if you're linking up to an API (thats the whole point!).