Monday, 1 June 2015

'add' and 'reset' events from Backbone Collections

In one of my current projects I'm using Backbone to lazy load and render data in a hierarchical structure (a treeview in this case). The data that I'm displayed is returned from the backend in database order, so I set the comparator property on the Backbone Collection to ensure a more friendly sort order. And this is where I encountered a slight 'gotcha' (it wouldn't be fair to call it a bug) which I thought it might be worth documenting in case it tripped anyone else up.

I've created a JS Bin to demonstrate the gotcha which I'll talk you through here:

1. Create a basic HTML page to hold everything


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
<!DOCTYPE html>
<html>
<head>
<meta name="description" content="JS Bin to demonstrate the fact that Backbone collections that have a comparator to ensure a certain sort order raise 'add' events in the unsorted order when multiple models are added via the 'Collection.set' method">
<script src="https://code.jquery.com/jquery-2.1.1.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/backbone.js/1.1.2/backbone-min.js"></script>
  <meta charset="utf-8">
  <title>JS Bin</title>
</head>
<body>
  <h1>List appears below</h1>
  <ol>
    
  </ol>
</body>
</html>

2. Define a basic Collection and View


 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var ExampleCollection = Backbone.Collection.extend({
      comparator: 'title',
      model: Backbone.Model
    }),
    ExampleView = Backbone.View.extend({
      tagName: 'li',
      render: function() {
        this.$el.text(this.model.get('title'));
        return this;
      }
    });

I haven't defined a Model as I'm going to use the base Backbone.Model function to create my model objects. As you can see above:

  • ExampleCollection objects will sort their models by their 'title' attribute.
  • ExampleView objects will render a list item that will contain the 'title' attribute of their model. Note that the render function is not responsible for adding the view to the DOM (this is a fairly common approach in Backbone).

3. Create an ExampleCollection object and attach event handlers 

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
var col = new ExampleCollection(),
  $container = $('ol');

col.on('add', function(model, collection) {
  $container.append(new ExampleView({model: model, collection: collection}).render().$el);
});
col.on('reset', function(collection) {
  collection.each(function(model) {
    $container.append(new ExampleView({model: model, collection: collection}).render().$el);
  });
});

As you can see the two event handlers attached to 'add' and 'reset' ensure between them that whenever a model is added to the collection (whether it is added by a 'set' or by a 'reset') an ExampleView object will be created and appended to the only <ol> on the page.

4. Add some models to our collection and watch what happens!


1
2
3
4
5
6
7
col.set([new Backbone.Model({
  id: 1, title: 'Zebra'
}),new Backbone.Model({
  id: 2, title: 'Adder'
}),new Backbone.Model({
  id: 3, title: 'Porcupine'
})], {sort: true});

As we are passing {sort: true} to the set method you might expect that we will see a sorted list of models on screen, but what we actually see is this:

List appears below

  1. Zebra
  2. Adder
  3. Porcupine

Changing the line that populates the collection from col.set to col.reset results in what we expected to see first time; the models are now correctly sorted:

List appears below

  1. Adder
  2. Porcupine
  3. Zebra

Why is this? The answer is that when multiple models are added to the collection in the first example (using col.set([model1,model2], {sort: true})) although the collection is sorted correctly (you can verify this by debugging if you want) the 'add' events are raised using the original ordering of the models, not the sorted ordering. When multiple models are added using a 'reset' then the 'reset' event is passed the full, sorted collection, and so we add the view to the HTML in sort order.

You could grumble about this but I think such complaints are misplaced for two reasons:

  1. If you're populating an empty collection using multiple models then 'reset' is really the correct method to use rather than 'set'.
  2. The 'set' method is extremely versatile; it can be used to merge models, add models or to both merge and add models as part of a single operation; it can be called on sorted or unsorted collections; it can be used to populate an empty collection or make updates to a collection that is already populated. When 'set' is used in some of these scenarios it would not be meaningful or a worthwhile use of CPU time to raise the 'add' events in the sort order.
So that's why I think that this behaviour is a 'gotcha' rather than a bug. Something else worth noting is that this means that when populating collections by calling 'fetch' you need to use {reset: true} in the options to ensure that the 'reset' method is called and a 'reset' event raised.

1 comment: