Search code examples
javascripteventsbackbone.jsviewamd

Event in child view not firing - Backbone.js


I'm developing a small project in an attempt to learn backbone.js and I'm using AMD (recommended in a tutorial). I have a child view for an "Athlete Control", basically a view I plan to reuse on other pages/views. The click event on the child view (AthleteView) doesn't fire.

The AthleteView is rendered in a div within the TimerView and the TimerView is rendered in a div with id "page" on the index page. I've included everything below, as I suspect it could how I'm setting things up that is the problem.

Main View:

define([
  'jquery',
  'underscore',
  'backbone',
  'views/SettingsView',
  'views/AthleteControlView',
  'text!templates/timerTemplate.html',
  'timer'
], function($, _, Backbone, SettingsView, AthleteView, timerTemplate){

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

    render: function(){      
      this.$el.html(timerTemplate);
      var settingsView = new SettingsView();
      settingsView.render();

      var athleteView = new AthleteView();
      athleteView.render();

      console.log('rendered Timer');

    }
  });

  return TimerView;

});

Child View:

define([
  'jquery',
  'underscore',
  'backbone',
  'text!templates/AthleteControlTemplate.html',
  'jqueryui'
], function($, _, Backbone, keypadTemplate){

  var AthleteView = Backbone.View.extend({

    el: $("#AthleteControl"),

    events: {
          'click': 'clickHandler'
    },

    render: function(){
      $("#AthleteControl").html(keypadTemplate);
      console.log('rendered AthleteControl');
    },


    clickHandler: function()
    {
      alert('click');
      console.log('click');
    }

  });

  return AthleteView;

});

ROUTER.JS

define([
  'jquery',
  'underscore',
  'backbone',
  'views/TimerView'
], function($, _, Backbone, TimerView) {

  var AppRouter = Backbone.Router.extend({
    routes: {
      '*actions': 'defaultAction'
    }
  });

  var initialize = function(){
    var app_router = new AppRouter;
    app_router.on('route:defaultAction', function (actions) {
        var timerView = new TimerView();
        timerView.render();
    });

    Backbone.history.start();
  };
  return { 
    initialize: initialize
  };
});

Index.html

<html>
<head>
    <script data-main="js/main" src="js/libs/require/require.js"></script>
</head>
<body>
  <div id="page">
    Loading....
  </div>
  <div id="footer"></div>
</div> 

</body>
</html>

TIMER TEMPLATE:

<div class = "Controls">
     <button id="Start">Start</button>
     <button id="Stop">Stop</button>
      <button id="Reset">Reset</button>
 </div>
 <div id="AthleteControl"></div>

Solution

  • So the id="AthleteControl" element comes from inside timerTemplate.html. That means that $('#AthleteControl") won't find anything when this code is executed:

    var AthleteView = Backbone.View.extend({
      el: $("#AthleteControl"),
    

    That means that AthleteView will end up behaving as though you said:

    var AthleteView = Backbone.View.extend({
      el: undefined,
    

    If you don't specify a view's el explicitly, the view will create an empty <div> as the el. Things like:

    el: $("some-selector")
    

    are almost always a mistake due to the timing problems. Instead, just use the selector string and let Backbone convert it to a DOM node when it needs to:

    el: "some-selector"
    

    The quick fix should be to un-jQuery the els:

    el: '#page'
    
    el: '#AthleteControl'
    

    As far as the "let the views create their own els" is concerned, you let the views create their own DOM nodes to use as their el rather than trying to bind views to els that are already on the page. This approach:

    1. Makes your views more self-contained.
    2. Helps to reduce event binding conflicts as your application gets larger and involves more views.
    3. Helps to reduce memory leaks because each view has one DOM node (and vice versa) while the application is running.
    4. Removing a view is a simple view.remove() call rather than manually cleaning up the event bindings and HTML. If you have subviews then you'd override remove in your view to call remove on the subviews before chaining to the default remove implementation.

    This approach (without the AMD/require.js stuff for clarity) would look more like the following.

    HTML:

    <script id="timerTemplate" type="text/x-underscore">
        <div class="Controls">
            <button id="Start">Start</button>
            <button id="Stop">Stop</button>
            <button id="Reset">Reset</button>
        </div>
    </script>
    
    <div id="page">
    </div>
    

    JavaScript:

    var AthleteView = Backbone.View.extend({
        id: 'AthleteControl',
        render: function() {
            this.$el.html('Athlete control stuff goes here.');
            return this; // This is standard practice, you'll see why below.
        }
    });
    
    var TimerView = Backbone.View.extend({
        render: function () {
            var tmpl = _.template($('#timerTemplate').html());
            this.$el.html(tmpl());
    
            var athleteView = new AthleteView();
            this.$el.append(athleteView.render().el);
            return this;
        }
    });
    
    var timer_view = new TimerView;
    $('#page').html(timer_view.render().el);
    

    Note the return this in the render methods and note how this.$el.html and this.$el.append are used to build and combine the views.

    Demo: http://jsfiddle.net/ambiguous/r4tsA/1/