0

In my backbone app I have a View backed by a Model and I'm storing the models in a Collection. There are several of these views on the page and only one can be selected at a time. Therefore, I have another View that contains a collection of these Views. When the user clicks on an unfocused view, the View sets the Model's "focused" attribute to true. The set on the model triggers a change event which is handled by the view itself (adds a focused class to its $el) and the ViewCollection view, which loops through all of the Views in the collection and unfocuses any of them that are focused (other than the one that was just clicked).

This all works great when I click at a moderate speed. However, when I click very rapidly I am seeing inconcistencies in the Model's set behavior. Namely, when I set the model's "focused" attribute to true, the next click event shows that that Model's "focused" attribute is still false.

Code:

    var M = Backbone.Model.extend({
            defaults : {
                            "name" : "",
                "focused" : true
            }
    }),

    L = Backbone.Collection.extend({
            model : M
    }),

    V = Backbone.View.extend({
    events : {
        "click .button" : "focus"
    }
    initialize: function() {
      this.model.bind("change:focused", this.handleFocusChange, this)
    },
    focus: function() {
                console.log("Focus: " + this.model.get("name") + ".focused = " + this.model.get("focused"));

                if (this.model.get("focused")) {
                    return;
                }

                this.model.set({focused: true});  // triggers change event
                this.model.save(this.model);
                console.log("Focus: " + this.model.get("name") + ".focused = " + this.model.get("focused"));
            },
            unfocus: function() {
                console.log("Unfocus: " + this.model.get("name") + ".focused = " + this.model.get("focused"));
                if (!this.model.get("focused")) {
                    return;
                }

                this.model.set({focused: false});  // triggers change event
                this.model.save(this.model);
                console.log("Unfocus: " + this.model.get("name") + ".focused = " + this.model.get("focused"));
            },
            handleFocusChange: function(model, focused) {
                if (focused) {
                    console.log("Handling focus change on " + this.model.get("name") + ", " + this.model.cid);
                    this.$('.button').addClass("focused");
                } else {        
                    console.log("Handling unfocus change on " + this.model.get("nickname") + ", " + this.model.cid);
                    this.$('.button').removeClass("focused");
                }
            }
    }),

    ViewCollection = Backbone.View.extend({
    initialize : function(options) {
                _.bindAll(this, 'initViews');

                this._views = [];

                this.collection.each(this.initViews);
            },
            initViews : function(model) {
                var view = new V({
                    model : model
                });

                this._views.push(view);

                view.model.bind("change:focused", this.focusChanged, this);
            },
            focusChanged: function(changed, focused) {
                if (!focused) {
                    return;
                }

                _.each(this._views, function(view) {
                    if (view.model.id !== changed.id) {
                        view.unfocus();
                    }
                });
            }
    })

Here is the output from the log statements above:

Click
Focus: A.focused = false
Handling focus change on A, c10
Unfocus: B.focused = false
Unfocus: C.focused = false
Unfocus: D.focused = true
Handling ufocus change on D, c7
Unfocus: D.focused = false
Focus: A.focused = true  <-- good!
Click
Focus: C.focused = false
Handling focus change on C, c9
Unfocus: B.focused = false
Unfocus: A.focused = false  <-- bad!
Unfocus: D.focused = false
Focus: C.focused = true 

As you can see, I clicked first on A and then on C. The changed event was fired and both the View and the Collection behaved appropriately. However, on the second click, A's "focused" attribute is false when it should be true. Again, this only happens when I click on the views very rapidly. I cannot reproduce this issue when I click at a normal speed.

Any help would be greatly appreciated!

threejeez
  • 2,314
  • 6
  • 30
  • 51

1 Answers1

1

This is a lot to process but if I understand your setup correctly.... it looks like the ajax call from model.save() is failing.

model.save() is asynchronous -- it fires a change event before the save call returns. If the save fails, the changes are undone. Try one/some of the following:

  • Look at what happens in Firebug, Fiddler, or whatever...
  • Try putting your logging function as a success or error callback and see which fires.
  • Add the wait param (this.model.save(this.model, {wait:true});) and see how your logging changes.
EBarr
  • 11,826
  • 7
  • 63
  • 85
  • Ok, I think you might be on to something here. I removed the save calls entirely and I couldn't reproduce the issue. However, when I added it back I added error/success handlers and, while the issue did return, all save callbacks went to success. The wait param wouldn't be appropriate here, I don't think. It is very likely that the user clicks on another view *before* the response comes back from the server. Therefore, I need the "focused" attribute to be set immediately so I know to unfocus that view when the next view is clicked. – threejeez Aug 01 '12 at 21:23
  • Another thing about the wait parameter: in this case, it does't really matter how fast the server gets the update; all that matters is that it gets there in order. So for this model I've overridden the sync function and implemented an ajax queue that sends one request at a time. The js code could wind up waiting quite a long time before the change event is fired. – threejeez Aug 01 '12 at 21:32
  • My point about `wait` wasn't for final code ---but to point us to what is actually happening. So Firebug shows ajax success? If you post a "working" fiddle, with HTML & all ..it might be easier to see what's happening. – EBarr Aug 01 '12 at 21:51
  • Gotcha. I'll give the wait a try. Yes, firebug shows xhr success and the callback that's fired when save returns is the one attached to success. – threejeez Aug 01 '12 at 21:53