Search code examples
javascriptjquerybackbone.jsbackbone-events

Backbone DOM events firing multiple times


I am building a backbone app for the first time - and it is going great.

However, I don't think I am creating views for my collection of models in the correct way and when I bind events they fire for every view when I only want them to fire for one.

Here is my backbone code (a snippet):

        (function(){

        Series = Backbone.Model.extend({
            defaults:{
                active:false
            }
        });

        SeriesGridItemView = Backbone.View.extend({
            el:'#model-grid',
            defaults: {
                active : false
            },
            initialize: function(){
                this.render();
                this.listenTo(this.model, 'change', this.setState);
            },
            render: function(){
                this.template = _.template( $('#template-model-grid-item-view').html() );
                this.view = $(this.template(this.model.toJSON())).appendTo(this.$el);
            },
            setState: function(){
                this.active = this.model.get('active');
                this.view.toggleClass('active',this.active);
            },
            events: {
                'click':'toggle'
            },
            toggle: function(e){
                e.stopPropagation();
                e.preventDefault();
                console.log('clicked');
                return false;
            }

        });

        SeriesCollection = Backbone.Collection.extend({
            model: Series,
            setPrice : function(p){
                this.forEach(function(m){
                    var active = 0;
                    _.each(m.get('vehicles'),function(v){
                        if(v.price <=p){
                            v.active = true;
                            active++;
                        }
                        else{
                            v.active = false;
                        }
                    });
                    m.set('active',active>0);
                });
            }
        });

        series = new SeriesCollection(window.BMW.data.series);
        series.forEach(function(m,i){
            var c = i+1;
            if(c > 3){
                c%=3;
            }
            m.set('column','1');
            new SeriesGridItemView({model:m});
        });
    })();

And here is the JSON that constructs the models:

window.BMW.data.series = [
    {
        seriesId:1,
        name:'1 Series',
        slug:'1-series',
        order:0,
        vehicles:[
            {
                seriesId:1,
                price:200
            },
            {
                seriesId:2,
                price:300
            }
        ]
    },
    {
        seriesId:2,
        name:'3 Series',
        slug:'3-series',
        order:1,
        vehicles:[
            {
                seriesId:1,
                price:400
            },
            {
                seriesId:2,
                price:500
            }
        ]
    },
    {
        seriesId:3,
        name:'4 Series',
        slug:'4-series',
        order:3,
        vehicles:[
            {
                seriesId:1,
                price:100
            },
            {
                seriesId:2,
                price:300
            }
        ]
    },
    {
        seriesId:4,
        name:'6 Series',
        slug:'6-series',
        order:4,
        vehicles:[
            {
                seriesId:1,
                price:100
            },
            {
                seriesId:2,
                price:300
            }
        ]
    },
    {
        seriesId:6,
        name:'X3',
        slug:'x3',
        order:5,
        vehicles:[
            {
                seriesId:1,
                price:500
            },
            {
                seriesId:2,
                price:800
            }
        ]
    }
];

And here is my template for the views

<script type="text/template" id="template-model-grid-item-view">
    <div id="series-<%=seriesId%>" class="grid-item-view column-<%=column%>">
        <div class="label"><%= name %></div>
        <div class="thumbnail">
            <img src="/Content/themes/BMW/img/series/small/<%= slug %>.png"/>
        </div>
    </div>
</script>

The views assemble correctly, but when I click one view, the event fires on all of the views. Can someone please point me in the right direction?


Solution

  • Since you omitted the selector in your events object of your views the following applies

    Per the Backbone Documentation: Omitting the selector causes the event to be bound to the view's root element (this.el).

    The problem is each of the SeriesGridItemView's bind click events to #model-grid and each view is a child of #model-grid. In your example, you register 5 click events and when you click on any of your views, all 5 events are triggered.

    Without changing any of your other code, one solution is to setup you events object to return a function so you can specify an id selector for each of your views.

    events: function() {
      var selector = '#series-' + this.model.get('seriesId'); // this id corresponds to your template id
      var ev = {};
      ev['click ' + selector] = 'toggle';
      return ev;
    },
    

    Another option and the one I prefer, is to not specify #model-grid as your root element for all your views. It would end up looking like: demo

    SeriesGridItemView = Backbone.View.extend({
      // remove the following line
      el:'#model-grid', 
    
      // .. all else is the same ../
    });
    
    series = new SeriesCollection(window.BMW.data.series);
      series.forEach(function(m,i){
        var c = i+1;
        if(c > 3){
          c%=3;
        }
        m.set('column','1');
        $('#model-grid').append(new SeriesGridItemView({model:m}).el);
    });
    

    A side suggetion

    1. In your render function, there's no need to create variable, you can access your element by using $:

      render: function(){
        this.template = _.template( $('#template-model-grid-item-view').html() );
        this.$el.append(this.template(this.model.toJSON())));
      },
      setState: function(){
        this.active = this.model.get('active');
        this.$el.toggleClass('active',this.active);
      },