Search code examples
backbone.jsjsfiddle

Backbone.js: this.model in event is first model rendered, instead of the model associated with DOM element


JSFiddle: http://jsfiddle.net/umgauper/jnf8B/743/

I'm trying to create a basic shopping cart app. When an item is clicked, I want its title and price added to the shopping cart table.

I am creating a separate instance of the view for each model. However, when an item is clicked, the table data that is added is always the model data for the item1 model, instead of the model data I would expect to be associated with the clicked DOM element. What do I have to change in order to get the model data of the clicked element?

var app = app || {};
var Item = Backbone.Model.extend({
    defaults: {
        order: '',
        title: '',
        price: 0.01,
        inCart: false
            }
});


var item1 = new Item({order: 1, title: "shoes", price: 30.00});
var item2 = new Item({order: 2, title: "socks", price: 5.00});
var item3 = new Item({order: 3, title: "shirt", price: 20.00});

var ItemsCollection = Backbone.Collection.extend({model: Item});

var items = new ItemsCollection([item1, item2, item3]);

app.ItemView = Backbone.View.extend({
    template: _.template($("#item-template").html()),
    initialize: function() {
        this.render()
    },
    render: function() {
        $("#display").append(this.template(this.model.attributes));
    },
    el: "#container",
    events: {
        'click .title': 'moveToCart'
    },
    moveToCart: function(e) {
        e.stopImmediatePropagation();
        var title = this.model.get("title");
        var price = this.model.get("price");
        $("table").append("<tr><td>" + title + "</td><td>" + price + "</td></tr>");
    }
});


var itemView1 = new app.ItemView({model: item1});
var itemView2 = new app.ItemView({model: item2});
var itemView3 = new app.ItemView({model: item3});
 <div id="container">
  <div id="display">

  </div>
  <table id="cart">
      <thead>
        <th>Title</th>
        <th>Price</th>
      </thead>

  </table>
  </div>
  <script type="text/template" id="item-template">
    <div>
        <p class="title"><%= title %></p>
        <p class="price">$<%= price %></p>
    </div>
  </script>


Solution

  • Your problem is that all your app.ItemViews share the same el:

    app.ItemView = Backbone.View.extend({
        //...
        el: "#container",
    

    and the events are bound to the el through delegation. The result is that your events are all confused because everyone is trying to listen for the same event on the same element.

    The easiest solution is to move the <div> out of your #item-template template:

    <script type="text/template" id="item-template">
        <p class="title"><%= title %></p>
        <p class="price">$<%= price %></p>
    </script>
    

    and make it the el for the ItemView. A Backbone view creates an empty <div> as its el if you don't specify the el any other way so you can just drop the el declaration from ItemView. You'd also need to adjust render to account for the new structure:

    render: function() {
        this.$el.append(this.template(this.model.toJSON()));
        $('#display').append(this.el);
        return this;
    }
    

    The return this at the end of render is standard practice so that you can say things like:

    $x.append(view.render().el);
    

    Moving the $('#display').append(...) call up one level would also be common and would help keep the view self contained. Also notice that I switched to this.model.toJSON() instead of this.model.attributes; pretending that attributes doesn't exist is generally a good idea so that you don't accidentally go behind Backbone's back and confuse things.

    Updated fiddle: http://jsfiddle.net/ambiguous/cad0kxee/