Search code examples
javascripteventsbackbone.js

Backbone.js - Update specific attribute on Model does not want to fire "change" event


In the following code,

this.listenTo(this.model, 'change:voteCount', this.changeVoteCount);
this.listenTo(this.model, 'change', this.renderTemplate);

If "voteCount" attribute updated, I want to trigger "this.changeVoteCount" method but not trigger "this.renderTemplate".

I already googling about 2 hours and couldn't find solution. How can I do for this requirement?

Update

(just the explanation for why I choose @try-catch-finally answer)

For people who also meet this problem, I want to clear out the confusion which answer should be chosen as their solution.

I will describe some details to explain why I choose this answer. The following is my model structure:

{
   'name': 'some string',
   'location': 'some string',
   'link': 'some string',
   'voteCount': 0    }

Otherwise 'voteCount' attribute updated, I want to trigger "change" event binded method (in my case, this.renderTemplate).

So I tried the first approach,

this.listenTo(this.model, 'change:name change:location change:link', this.renderTemplate);

In this approach, if I update 'location' and 'name' at once in one place, then the this.renderTemplate method will trigger two times. I want this.renderTemplate method trigger just one time whenever 'location', 'name' or 'link' attributes updated separately or together at the same time.

So finally, I tried the following approach and succeed.

this.listenTo(this.model, 'change', function(model) {
            if (_.isEmpty(_.intersection(_.keys(model.changed), ["voteCount"]))) {
                this.renderTemplate.apply(this, arguments);
            }
        });

Solution

  • Beside listing all attributes you want to listen for their change mentioned by @antejan, you could do this programatically too.

    This way you'd examine the model's changed attribute which contains all attributes that have been changed.

    this.listenTo(this.model, 'change', function(model) {
         if (!_.contains(_.keys(model.changed), "voteCount") {
              this.renderTemplate.apply(this, arguments);
         }
    });
    

    When blacklisting multiple keys you could also do:

    if (_.isEmpty(_.intersection(_.keys(model.changed), ["attr1", "attr2", ...])) { ...
    

    As long as your model does only have a few attributes I'd prefer @antejans way of listing all attributes explicitly since its more clear whats going on:

    this.listenTo(this.model, "change:attr1 change:attr2 ...", this.renderTemplate);
    

    Note that there might be a performance penalty when listing multiple change:attr events since Backbone will call the listeners in a for-loop. See the annotated Backbone source of set():

      ...
    
      if (!silent) {
        if (changes.length) this._pending = options;
        for (var i = 0, l = changes.length; i < l; i++) {
          this.trigger('change:' + changes[i], this, current[changes[i]], options);
        }
      }
    
      // You might be wondering why there's a `while` loop here. Changes can
      // be recursively nested within `"change"` events.
      if (changing) return this;
      if (!silent) {
        while (this._pending) {
          options = this._pending;
          this._pending = false;
          this.trigger('change', this, options);
        }
      }