Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Model change events in nested collections not firing as expected

I'm trying to use backbone.js in my first "real" application and I need some help debugging why certain model change events are not firing as I would expect.

If I create a collection from a JSON array from the server, then fetch() it at set intervals, I do not get notified if an individual model in the collection has changed. The backbone documentation suggests that such notifications should be generated. All I seem to get is a refresh notification on each fetch, which is not useful. OTOH, if I create a model from a JSON object from the server, then fetch() the model at set intervals, I do get a change notification when an attribute changes. Any ideas?

Details

My web service at /employees/{username}/tasks returns a JSON array of task objects, with each task object nesting a JSON array of subtask objects. For example,

[{
  "id":45002,
  "name":"Open Dining Room",
  "subtasks":[
    {"id":1,"status":"YELLOW","name":"Clean all tables"},
    {"id":2,"status":"RED","name":"Clean main floor"},
    {"id":3,"status":"RED","name":"Stock condiments"},
    {"id":4,"status":"YELLOW","name":"Check / replenish trays"}
  ]
},{
  "id":47003,
  "name":"Open Registers",
  "subtasks":[
    {"id":1,"status":"YELLOW","name":"Turn on all terminals"},
    {"id":2,"status":"YELLOW","name":"Balance out cash trays"},
    {"id":3,"status":"YELLOW","name":"Check in promo codes"},
    {"id":4,"status":"YELLOW","name":"Check register promo placards"}
  ]
}]

Another web service allows me to change the status of a specific subtask in a specific task, and looks like this: /tasks/45002/subtasks/1/status/red [aside - I intend to change this to a HTTP Post-based service, but the current implementation is easier for debugging]

I have the following classes in my JS app:

Subtask Model and Subtask Collection

var Subtask = Backbone.Model.extend({});

var SubtaskCollection = Backbone.Collection.extend({
  model: Subtask
});

Task Model with a nested instance of a Subtask Collection

var Task = Backbone.Model.extend({
  initialize: function() {

    // each Task has a reference to a collection of Subtasks
    this.subtasks = new SubtaskCollection(this.get("subtasks"));

    // status of each Task is based on the status of its Subtasks
    this.update_status();

  },
  ...
});

var TaskCollection = Backbone.Collection.extend({
  model: Task 
});

Task View to renders the item and listen for change events to the model

var TaskView = Backbone.View.extend({

  tagName:  "li",

  template: $("#TaskTemplate").template(),

  initialize: function() {
    _.bindAll(this, "on_change", "render");
    this.model.bind("change", this.on_change);
  },

  ...

  on_change: function(e) {
    alert("task model changed!");
  }

});

When the app launches, I instantiate a TaskCollection (using the data from the first web service listed above), bind a listener for change events to the TaskCollection, and set up a recurring setTimeout to fetch() the TaskCollection instance.

...

TASKS = new TaskCollection();

TASKS.url = ".../employees/" + username + "/tasks"

TASKS.fetch({
  success: function() {
    APP.renderViews();
  }
});


TASKS.bind("change", function() {
  alert("collection changed!");
  APP.renderViews();
});


// Poll every 5 seconds to keep the models up-to-date.
setInterval(function() {
  TASKS.fetch();  
}, 5000);

...

Everything renders as expected the first time. But at this point, I would expect either (or both) a Collection change event or a Model change event to get fired if I change a subtask's status using my second web service, but this does not happen.

Funnily, I did get change events to fire if I added one additional level of nesting, with the web service returning a single model, for example:

"employee":"pkaushik", "tasks":[{"id":45002,"subtasks":[{"id":1.....

But this seems klugey... and I'm afraid I haven't architected my app right. I'll include more code if it helps, but this question is already rather verbose.

Thoughts?

like image 962
Pallavi Anderson Avatar asked Jan 01 '11 22:01

Pallavi Anderson


1 Answers

You are listening to the events on your tasks, not your sub tasks. If you want your task to carry the events for your subtasks as well you should do something like this in your Task.initialize:

var self = this;    
this.subtasks.bind("refresh", function(){ self.trigger("refresh:subtask")});
this.subtasks.each(function(st){ 
  st.bind("change", function(){ self.trigger("change:subtask:" + st.id, st) });
});

You might want to refresh the bindings after a refresh event also. This way when a change event occurs on the subtask model, the task model will trigger a change model as well.

Backbone will only send a refresh event after a fetch you will not be informed of a change in a specific model. Look at the fetch function of collection:

options.success = function(resp) {
  collection[options.add ? 'add' : 'refresh'](collection.parse(resp), options);
  if (success) success(collection, resp);
};

It will call refresh by default. Here is the refresh function:

refresh : function(models, options) {
  models  || (models = []);
  options || (options = {});
  this.each(this._removeReference);
  this._reset();
  this.add(models, {silent: true});
  if (!options.silent) this.trigger('refresh', this, options);
  return this;
}

It will add all models to the collection silently (without events) and then trigger a refresh event, but it will not identify changed elements.

like image 152
Julien Avatar answered Oct 18 '22 18:10

Julien