Search code examples
javascriptbackbone.jsbackbone-events

Binding events to views - Firing uneven number of times


I have UserModel: UserView and UserCollection: UserCollectionView. With this, I am trying to bind a click event to the UserView (I am doing this in UserView). So, this is the code I have:

var root = 'http://localhost:5000/api/v1';
var app = {};

// Backbone Model
app.UserModel = Backbone.Model.extend({
    defaults: {
        // urlRoot: root + '/users',
        name: 'Default Name',
        email: '30',
        username: 'default_username'
    },

    initialize: function() {
        this.set({
            id: this.get('username')
        });
        console.log('User model \'' + this.id + '\' has been initialized.');
    },

    // parse: function(data) {
    //     console.log('Model parse funciton called');
    //     return data;
    // }
});

// Backbone Model View
app.UserView = Backbone.View.extend({
    // el: '#users-list',
    // tagName: 'div',
    el: '.user-box-wrapper',

    events: {
        'click .user-data': 'userClicked'
    },

    userClicked: function(ev) {
        console.log("User selected");
        // console.log(ev.currentTarget);
    },

    template: _.template($('#connections-user-template').html()),

    initialize: function() {
        this.render();
    },

    render: function() {
        $('#users-list').append(this.template(this.model.toJSON()));
        // this.$el.append( this.template( this.model.toJSON()));
        console.log('User view is rendered');
    }
});

// Backbone Collection
app.UserCollection = Backbone.Collection.extend({
    model: app.UserModel,
    url: root + '/users',

    initialize: function() {
        // this.fetch();
    },

    parse: function(data) {
        // console.log(data.data);
        return data.data;
    }
});

// Backbone Collection View
app.UserCollectionView = Backbone.View.extend({
    el: '#users-list',

    template: _.template($('#connections-template').html()),

    initialize: function() {
        this.connections = new app.UserCollection();
        var self = this;
        this.connections.fetch().done(function() {
            self.render();
        });
    },

    render: function() {
        console.log('User collection view is rendered');
        this.$el.html(this.template());
        // this.$el.append( this.template( this.model.toJSON()));
        this.connections.each(function(user) {
            console.log('User : ' + user.get('id'));
            var userView = new app.UserView({
                model: user
            });
            // userView.model.fetch();
            // userView.render();
        });
    }
});
var connectionsView = new app.UserCollectionView();

The JSON data actually returns 14 objects (or UserModels in this case). The problem is, if I click the first user view, it is triggered 13 times, and the second view click is triggered 12 times and so on, the last view click event not being triggered at all when clicked.

The individual UserViews are rendered once each, however (that's what I think at least). Can someone please explain what the problem here is and what exactly is happening here?

P.S. - I am aware of the workaround of binding the events in the CollectionView.

Edit 1

This is the DOM structure:

<!doctype html>
<html>
  <head>
    <title>Hey there</title>
    <link rel="stylesheet" href="../static/css/normalize.css">
    <link rel="stylesheet" href="../static/css/index.css">
    <script src="/static/js/jquery-2.2.0.js"></script>
    <script src="/static/js/underscore-1.8.3.js"></script>
    <script src="/static/js/backbone-1.2.3.js"></script>
  </head>

  <body>
    <header id="top">
      <div id="logo-wrapper">
        <img src="../static/img/logo.png" alt="Logo" id="logo">
      </div>
      <div id="top-links">
        <div id="top-profile-box" class="toplink">
          <div id="top-profile-data-box">
            <div id="top-profile-data-name">Kevin Isaac</div>
            <div id="top-profile-data-passion">Writer</div>
          </div>
          <img id="top-profile-image" src="../static/img/user1.jpg" alt="">
        </div>
        <div id="notification-icon" class="toplink"></div>
        <div id="top-message-icon" class="toplink"></div>
        <div id="logout-icon" class="toplink"></div>
      </div>
    </header>
    <div id="middle">
      <nav id="side-nav">
        <div id="side-nav-top">
          <div class="side-nav-link" id="side-nav-home-link">
            <div class="side-nav-link-img"></div>
            <div class="side-nav-link-title">Home</div>
          </div>
          <div class="side-nav-link" id="side-nav-profile-link">
            <div class="side-nav-link-img"></div>
            <div class="side-nav-link-title">Profile</div>
          </div>
          <div class="side-nav-link" id="side-nav-messages-link">
            <div class="side-nav-link-img"></div>
            <div class="side-nav-link-title">Message</div>
          </div>
          <div class="side-nav-link" id="side-nav-account-link">
            <div class="side-nav-link-img"></div>
            <div class="side-nav-link-title">Account</div>
          </div>
        </div>
      </nav>
      <div id="main-content">
        <!-- Start of page specific HTML -->

        <div id="content-title">
          <div class="content-subtitle" id="connections">Connections</div>
          <div class="content-subtitle" id="followers">Followers</div>
          <div class="content-subtitle" id="followings">Followings</div>
        </div>

        <div id="content-body">
          <div id="users-box">
            <div id="users-list">No connection</div>
              <!-- Backbone Template Starts --> 
              <script type="text/template" id="connections-template"></script>
              <script type="text/template" id="connections-user-template">
                <div class="user-box-wrapper">
                  <div class="user-box">
                    <div class="user-pic-wrapper">
                      <img src="/static/img/user1.jpg" alt="">
                    </div>
                    <div class="user-data" id="boox">
                      <div class="user-name"><%= name %></div>
                      <div class="user-passion"><%= username %></div>
                      <div class="user-city"><%= email %></div>
                    </div>
                  </div>
                </div>
              </script>
              <!-- Backbone Template Ends --> 


            <div id="users-side-box">
              <div id="users-box-search">
                <input id="user-search" type="text" name="">
              </div>
              <div id="user-metadata">
                <div id="metadata-user-top-box">
                  <div id="metadata-user-image-wrapper">
                    <img src="/static/img/user1.jpg" alt="">
                  </div>
                  <div id="metadata-user-name-box">
                    <div id="metadata-name">Name's Bond</div>
                    <div id="metadata-passion">Assassin</div>
                  </div>
                </div>
                <div id="metadata-user-bottom-box">
                  <div class="metadata-user-attribute">
                    <span class="metadata-property">Studied at: </span>
                    <span class="metadata-value">Karunya University </span>
                  </div>
                  <div class="metadata-user-attribute">
                    <span class="metadata-property">Native City: </span>
                    <span class="metadata-value">London</span>
                  </div>
                  <div class="metadata-user-attribute">
                    <span class="metadata-property">Website: </span>
                    <span class="metadata-value">www.007.com</span>
                  </div>
                </div>
              </div>
            </div>
          </div>
        </div>

        <!-- End of page specific HTML -->
      </div>
      <aside id="main-aside">
        Aside one two therr
      </aside>
    </div>

    <script src="../static/js/index.min.js"></script>
  </body>
</html>

Solution

  • There are many issues with your code.

    Main problem is that, all your userView's are pointing to the same selector, and the element matching this selector .user-box-wrapper is inside the view template - So whenever you create a new userView, it'll add a new event listener to .user-box-wrapper present in all existing userViews, but not to itself. So your first userView will have n-1 events handlers registered to it while last one has none (n being total number of userViews). View element is not supposed to be part of template, template is supposed to be added to the view element.

    When your view will have multiple copies of itself, don't define el option, let backbone create a new element for each instance of the view. You can customize the properties of this element.

    Another issue is that you are not appending the userView template to your userView element, but something else outside it ($('#users-list'). So clicking the template won't trigger the event handler (in your case this is why you're last userView doesn't fire it's event. It was bound to all other existing views because common selector was provided by el)

    You should try to avoid global selectors like $('#users-list') from within a view. In this case you can append the userView to #users-list from within userCollectionView who's el points to #users-list.

    Your code should be:

    var root = 'http://localhost:5000/api/v1';
    var app = {};
    
    // Backbone Model
    app.UserModel = Backbone.Model.extend({
      defaults: {
        // urlRoot: root + '/users',
        name: 'Default Name',
        email: '30',
        username: 'default_username'
      },
    
      initialize: function() {
        this.set({
          id: this.get('username')
        });
        console.log('User model \'' + this.id + '\' has been initialized.');
      }
    });
    
    // Backbone Model View
    app.UserView = Backbone.View.extend({
      className: 'user-box-wrapper',
      /*--^-- creates a new div with this class for each user*/
      template: _.template($('#connections-user-template').html()),
      events: {
        'click .user-data': 'userClicked'
      },
      initialize: function() {
        this.render();
      },
      render: function() {
        this.$el.append( this.template( this.model.toJSON()));
        console.log('User view is rendered');
      },
      userClicked: function(ev) {
        console.log("User selected");
        // console.log(ev.currentTarget);
      }
    });
    
    // Backbone Collection
    app.UserCollection = Backbone.Collection.extend({
      model: app.UserModel,
      url: root + '/users',
      initialize: function() {
        // this.fetch();
      },
      parse: function(data) {
        // console.log(data.data);
        return data.data;
      }
    });
    
    // Backbone Collection View
    app.UserCollectionView = Backbone.View.extend({
      el: '#users-list',
      template: _.template($('#connections-template').html()),
      initialize: function() {
        this.connections = new app.UserCollection();
        var self = this;
        this.connections.fetch().done(function() {
          self.render();
        });
      },
      render: function() {
        this.$el.html(this.template());
        console.log('User collection view is rendered');
        this.connections.each(function(user) {
          console.log('User : ' + user.get('id'));
          var userView = new app.UserView({
            model: user
          });
          this.$el.append(userView.el); /*append child view here*/
        },this);
      }
    });
    var connectionsView = new app.UserCollectionView();