Append items to a sorted collection in Backbone.js

Jun 30, 2015
javascript, tech
When an item is added to a collection in Backbone.js it should automatically be added to the corresponding list. When using a sorted collection it would be nice to have it appear in place, especially when not refreshing the entire list. That's what I'll explore in this post.

I won’t cover all the boiler plate code but you can view that at JSFiddle
The project is a ListItem model and a corresponding ListCollection. There is a ListItemView which is compiled into a ListView to create an ordered list. There is a FormView used for adding items to the collection.

The first component of our code is the comparator in the collection which keeps the list sorted by name.

1
2
3
4
5
6
var ListCollection = Backbone.Collection.extend({
  model: ListItem,
  comparator: function(item) {
    return item.get('name').toLowerCase();
  }
});

With this a simple render method will always have the list in order but it needs to redraw the list every time the collection is updated.
Simply bind the add event to this.render and you’re done.

1
2
3
4
5
6
7
8
9
10
11
12
13
//...
  initialize: function() {
    this.listenTo(this.collection, 'add', this.render);
  },
  render: function() {
    var items = [];
    this.collection.each(function(item) {
      items.push((new ListItemView({model: item})).render().el);
    });
    this.$el.html(items);
    return this;
  }
//...

What if we have a list that is more complicated or we want to display the item being added. For this we need a couple of things.

  1. Split the creation of the item view out into its own factory method
  2. Call the factory method when building the initial list within render
  3. Create a new addItem method which will append the item to the list
  4. Change our event binding to this.addItem
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//...
  initialize: function() {
    this.listenTo(this.collection, 'add', this.addItem);
  },
  render: function() {
    var self = this;
    var items = [];
    this.collection.each(function(item) {
      items.push(self.buildItemView(item).render().el);
    });
    this.$el.html(items);
    return this;
  },
  addItem: function(item) {
    var $view = this.buildItemView(item).render().$el;
    this.$el.append($view.hide().fadeIn());
  },
  buildItemView: function(item) {
    return new ListItemView({model: item});
  }
//...

The problem now is that we’re using jQuery’s append which adds the item view to the end of the list negating the work of the comparator in our Backbone collection. What we need now is a way to insert the new item into the list at the correct index. For that we’ll need at add an insertAt method to jQuery.
This new method will take an index and an element and it will place it into the childNodes collection at the correct index.

1
2
3
4
5
6
7
8
9
10
11
$.fn.extend({
  insertAt: function(index, element) {
    var lastIndex = this.children().size();
    if(index < lastIndex) {
      this.children().eq(index).before(element);
    } else {
      this.append(element);
    }
    return this;
  }
});

Now we can update our addItem method to calculate the index of the new item and then add it into the list at that index.

1
2
3
4
5
6
7
8
9
10
//...
  addItem: function(item) {
    // Get the index of the newly added item
    var index = this.collection.indexOf(item);
    // Build a view for the item
    var $view = this.buildItemView(item).render().$el;
    // Insert the view at the same index in the list
    this.$el.insertAt(index, $view.hide().fadeIn());
  }
//...

The final working product is embedded here:

comments powered by Disqus