Search code examples
backbone.jsrequirejsbackbone-viewsamd

How to track the number of models in a collection and stay in sync with the server changes Backbone.js using AMD


Backbone.js newbie here. General question: What is the best practice to track the number of models in a collection in order to display it on the UI? My use cases can involve changes on the server side so each time the collection is sync'd I need to be able to update the UI to the correct number from storage.

I'm using Backbone.js v1.0.0 and Underscore v1.4.4 from the amdjs project and Require.js v2.1.6.

Specific example: Simple shopping cart showing "number of items in the cart" that continually updates while the user is adding/removing items. In this example I'm almost there but (1) my code is always one below the real number of models and (2) I feel that there is a much better way to do this!

Here's my newbie code. First, I have a collection of items that the user can add to their cart with a button. (NOTE: all AMD defines and returns are removed in code examples for brevity.)

var PackagesView = Backbone.View.extend({
el: $("#page"),
events: {
  "click .addToCart": "addToCart"
},
initialize: function(id) {

  this.collection = new PackagesCollection([],{id: id.id});
  this.collection.fetch({
    reset: true
  });

  this.collection.on("reset", this.render, this);

},
render: function(){

//other rendering stuff here
..............

  //loop through models in collection and render each one
  _.each(this.collection.models, function(item){
    that.renderPackages(item);
  });

}

renderPackages: function(item){
  var packageView = new PackageView({
    model: item
  });
  this.$el.append(packageView.render().el);
},

Next I have the view for each individual item in the cart PackageView which is called by the PackagesView code above. I have a "add to cart" button for each Package that has a "click" event tied to it.

var PackageView = Backbone.View.extend({
tagName:"div",
template:$(packageTemplate).html(),

events: {
  "click .addToCart": "addToCart"
},

render:function () {

    var tmpl = _.template(this.template);

    this.$el.html(tmpl(this.model.toJSON()));
    return this;
},

addToCart:function(){

  cartView = new CartView();

  cartView.collection.create(new CartItemModel(this.model));

}

Finally, I have a CartView that has a collection of all the items in the cart. I tried adding a listenTo method to react to changes to the collection, but it didn't stay in sync with the server either.

var CartView = Backbone.View.extend({
el: $("#page"),

initialize:function(){

  this.collection = new CartCollection();
  this.collection.fetch({
    reset: true
  });


  this.listenTo(this.collection, 'add', this.updateCartBanner);

  this.collection.on("reset", this.render, this);

},

render: function(){

  $('#cartCount').html(this.collection.length);

},

updateCartBanner: function(){

  //things did not work here. Just putting this here to show something I tried.

}

End result of specific example: The .create works correctly, PUT request sent, server adds the data to the database, "reset" event is called. However, the render() function in CartView does not show the right # of models in the collection. The first time I click a "add to cart" button the $('#cartCount') element does not get populated. Then anytime after that it does get populated but I'm minus 1 from the actual count on the server. I believe this is because I have a .create and a .fetch and the .fetch is happening before the .create finishes so I'm always 1 behind the server.

End result, I'm not structuring this the right way. Any hints in the right direction would be helpful!


Solution

  • Found an answer to my question, but could definitely be a better method.

    If I change my code so instead of a separate view for each model in the collection as I have above, I have one view that iterates over all the models and draws then it will work. I still need to call a .create followed by a .fetch with some unexpected behavior, but the end result is correct. Note that in this code I've completely done away with the previous PackageView and everything is drawn by PackagesView now.

    var PackagesView = Backbone.View.extend({
    el: $("#page"),
    events: {
    
      "click .addToCart": "addToCart"
    
    },
    initialize: function(id) {
    
      this.collection = new PackagesCollection([],{id: id.id});
      this.collection.fetch({
        reset: true
      });
    
      this.collection.on("reset", this.render, this);
    
    },
    render: function(){
    
      var that = this;
    
      var tmpl = _.template($(packageTemplate).html());
    
      //loop through models in collection and render each one
      _.each(this.collection.models, function(item){
        $(that.el).append(tmpl(item.toJSON()));
      });
    
    },
    
    addToCart:function(e){
    
      var id= $(e.currentTarget).data("id");
      var item = this.collection.get(id);
    
      var cartCollection = new CartCollection();
    
      var cartItem = new CartItemModel();
    
      cartCollection.create(new CartItemModel(item), {
        wait: true,
        success: function() {
          console.log("in success create");
          console.log(cartCollection.length);
        },
        error:function() {
          console.log("in error create");
          console.log(cartCollection.length);   
        }
    
      });
    
      cartCollection.fetch({
        wait: true,
        success: function() {
          console.log("in success fetch");
          console.log(cartCollection.length);
          $('#cartCount').html(cartCollection.length);
        },
        error:function() {
          console.log("in error fetch");
          console.log(cartCollection.length);   
        }
    
      });  
    

    Result: The $('#cartCount') in the .fetch callback injects the correction number of models. Unexpectedly, along with the correct .html() value the Chrome console.log return is (server side had zero models in the database to start with):

    in error create PackagesView.js:88
    0 PackagesView.js:89
    in success fetch PackagesView.js:97
    1 
    

    And I'm getting a 200 response from the create, so it should be "success" for both callbacks. I would have thought that the Backbone callback syntax for create and fetch were the same. Oh well, it seems to work.

    Any feedback on this method is appreciated! Probably a better way to do this.

    Incidentally this goes against the general advice here, although I do have a "very simple list" so perhaps its OK in the long run.