chris Frisina
chris Frisina

Reputation: 19688

Backbone click event binding not being bound to DOM elements

I have these divs called beats, and I want to register when they are clicked. However, I can't seem to get them to register a click at all, either by clicking them, or calling a JQuery click event on a specific div in the console. Either way, nothing is getting registered.

The measureView.js creates this beatView, which creates a beat inside the parent measure.

beatView.js :

//filename: views/beats/beatView.js
/*  This is the view for a single beat, which is contained in a measure view. */
define([ 'jquery', 'underscore',      'backbone',      'backbone/models/beat',      'text!backbone/templates/measures/audioMeasures.html',      'text!backbone/templates/beats/linearBarBeats.html',      'text!backbone/templates/beats/linearBarSVGBeats.html',      'text!backbone/templates/beats/circularPieBeats.html',      'app/dispatch',      'app/log'
], function($, _, Backbone, BeatModel, audioMeasuresTemplate, linearBarBeatsTemplate, linearBarSVGBeatsTemplate, circularPieBeatsTemplate, dispatch, log){
  return Backbone.View.extend({
    //registering backbone's click event to our toggle() function.
    events : {
      'click' : 'toggle'
    },

    //The constructor takes options because these views are created by measuresView objects.
    initialize: function(options){
      if (options) {
        console.log('options :');
        console.warn(options);
        this.model = options.model;
        // this.parentEl should be measure.cid
        this.measureBeatHolder = options.parentElHolder;
      } else {
        console.log('never really getting here');
        this.model = new BeatModel;
      }    
      this.render();
    },

    //We use css classes to control the color of the beat. A beat is essentially an empty div.
    render: function(toggle){
      var state = this.getSelectionBooleanCSS();
      if (toggle) {
        $('#beat'+toggle).removeClass(state);
        $('#beat'+toggle).addClass(this.switchSelectionBooleanValue());
      } else {
        var compiledTemplate = _.template(this.representations[this.currentBeatRepresentation], {beat: this.model, beatAngle: this.beatAngle, state: state});
        $(this.measureBeatHolder).append( compiledTemplate );
        return this;
      }
    },

    getSelectionBooleanCSS: function(){
      if (this.model.get("selected")) {
        return "ON";
      } else {
        return "OFF";
      }
    },

    switchSelectionBooleanValue: function(){
      if (this.model.get('selected') == true) {
        this.model.set('selected', "false");
      } else {
        this.model.set('selected', "true");
      }
      return this.model.get('selected');
    },

    /*
      This is called when a beat is clicked.
      It does a number of things:
      1. toggles the model's selected field.
      2. re-renders the beat.
      3. prints a console message.
      4. tells log to send a log of the click event.
      5. triggers a beatClicked event.
    */

    toggle: function(){
      console.log('getting to toggle function');
      var selectedBool = this.model.get("selected");
      this.model.set("selected", !selectedBool);
      var newBool = this.model.get("selected");
      this.render(this.model.cid);
      dispatch.trigger('beatClicked.event');
    }
  });
});

For referencing:

beatModel:

//filename: models/beat.js
/*
  This is the beat model.
  It only knows about whether or not it
  is selected.
*/
define([
  'underscore',
  'backbone'
], function(_, Backbone) {
  var beatModel = Backbone.Model.extend({
    defaults: {
      selected: false,
      state: 'OFF'
    },
    initialize: function(){
    },
    getStyleClass: function() {
      if (this.selected) {
        return 'ON';
      }
      else {
        return 'OFF';
      }
    }
  });

  return beatModel;
});

measureModel:

//filename: models/measure.js
/*
  This is the measure model.
  A component has a collection of these models.
  these models have a collection of beats.
*/
define([
  'underscore',
  'backbone',
  'backbone/collections/beats'
], function(_, Backbone, beatsCollection) {
  var measureModel = Backbone.Model.extend({
    defaults: {
      label: '0/4',
      beats: beatsCollection,
      numberOfBeats: 0,
      divisions: 8
    },
    initialize: function(){     
    }

  });

  return measureModel;
});

measureView.js:

// Filename: views/measures/measuresView.js
/*
  This is the MeasuresView.

  This is contained in a ComponentsView.
*/
define([
  'jquery',
  'underscore',
  'backbone',
  'backbone/collections/measures',
  'backbone/collections/beats',
  'backbone/models/measure',
  'backbone/views/beats/beatView',
  'text!backbone/templates/measures/audioMeasures.html',
  'text!backbone/templates/measures/linearBarMeasures.html',
  'text!backbone/templates/measures/linearBarSVGMeasures.html',
  'text!backbone/templates/measures/circularPieMeasures.html',
  'app/dispatch',
  'app/state',
  'app/log'
], function($, _, Backbone, MeasureModel, BeatsCollection, MeasuresCollection, beatView, audioMeasuresTemplate, linearBarMeasuresTemplate, linearBarSVGMeasuresTemplate, circularPieMeasuresTemplate, dispatch, state, log){
  return Backbone.View.extend({
    // el: $('.component'),

    // The different representations
    representations: {
      "audio": audioMeasuresTemplate,
      "linear-bar": linearBarMeasuresTemplate,
      "linear-bar-svg": linearBarSVGMeasuresTemplate,
      "circular-pie": circularPieMeasuresTemplate
    },

    currentMeasureRepresentation: 'linear-bar',

    //registering click events to add and remove measures.
    events : {
      'click .addMeasure' : 'add',
      'click .delete' : 'remove'
    },

    initialize: function(options){
      //if we're being created by a componentView, we are
      //passed in options. Otherwise we create a single
      //measure and add it to our collection.
      if (options) {
        this.measuresCollection = options.collection;
        this.parent = options.parent;
        this.el = options.el;
      }
      // else {
      //   this.measure = new BeatsCollection;

      //   for (var i = 0; i < 4; i++) {
      //     this.measure.add();
      //   }

      //   this.measuresCollection = new MeasuresCollection;
      //   this.measuresCollection.add({beats: this.measure});
      // }

      if (options["template-key"]) {
        this.currentBeatRepresentation = options["template-key"];
      }

      //registering a callback for signatureChange events.
      dispatch.on('signatureChange.event', this.reconfigure, this);
      //Dispatch listeners
      dispatch.on('measureRepresentation.event', this.changeMeasureRepresentation, this);

      this.render();

      //Determines the intial beat width based on the global signature. Has to be below this.render()
      this.calcBeatWidth(this.parent.get('signature'));
    },

    changeMeasureRepresentation: function(representation) {
      this.currentMeasureRepresentation = representation;
      this.render();      
    },

    render: function(){
      $(this.el).html('<div class="addMeasure">+</div>');
      var measureCount = 1;
      //we create a BeatsView for each measure.
      _.each(this.measuresCollection.models, function(measure) {
        // when representation button changes, the current representation template will get updated
        var compiledTemplate = _.template( this.representations[this.currentMeasureRepresentation], {measure: measure, beatHolder:"beatHolder"+measure.cid, measureCount:measureCount, measureAngle: 360.0 } );
        $(this.el).find('.addMeasure').before( compiledTemplate );
          console.log('measure beats: ');
          console.warn(measure.get('beats').models);
            _.each(measure.get('beats').models, function(beat) {
              // console.warn("#beat"+beat.cid.toString());
              new beatView({model:beat, parentElHolder:'#beatHolder'+measure.cid, parentCID:measure.cid, singleBeat:"#beat"+beat.cid});
            }, this);
        measureCount ++;
      }, this);
      return this;
    },

    /*
      This is called when the user clicks on the plus to add a new measure.

      It creates a new measure and adds it to the component.
      It generates a string representing the id of the measure and the ids of
      its beats and logs the creation.

      Lastly, it triggers a stopRequest, because we can't continue playing until
      all the durations get recalculated to reflect this new measure.
    */
    add: function(){
        console.log('add measure');
        var newMeasure = new BeatsCollection;

        for (var i = 0; i < this.parent.get('signature'); i++) {
          newMeasure.add();
        }

        this.measuresCollection.add({beats: newMeasure});

        //Logging
        name = 'measure' + _.last(this.measuresCollection.models).cid + '.';
        _.each(newMeasure.models, function(beats) {
          name = name + 'beat'+ beats.cid + '.';
        }, this);
        log.sendLog([[3, "Added a measure: "+name]]);

        //Render
        this.render();
        //Dispatch
        dispatch.trigger('stopRequest.event', 'off');
    },

    /*
      This is called when the user clicks on the minus to remove a measure.
    */
    remove: function(ev){
      if ($('#measure'+this.measuresCollection.models[0].cid).parent()) {
        //removing the last measure isn't allowed.
        if(this.measuresCollection.models.length == 1) {
          console.log('Can\'t remove the last measure!');
          return;
        }
        console.log('remove measure');

        //we remove the measure and get its model.
        var model = this.measuresCollection.get($(ev.target).parents('.measure').attr('id').replace('measure',''));
        this.measuresCollection.remove(model);

        //send a log event showing the removal.
        log.sendLog([[3, "Removed a measure: measure"+model.cid]]);

        //re-render the view.
        this.render();

        //trigger a stop request to stop playback.
        dispatch.trigger('stopRequest.event', 'off');
        dispatch.trigger('signatureChange.event', this.parent.get('signature'));
      }
    },
    // This is triggered by signatureChange events.
    reconfigure: function(signature) {
      console.log('MeasureView.reconfigure(signature) : signature=' +signature);
      /* if the containing component is selected, this
         triggers a request event to stop the sound.

         Then this destroys the beat collection and creates
         a new collection with the number of beats specified
         by the signature parameter.
      */
      if ($(this.parent).hasClass('selected')) {
        dispatch.trigger('stopRequest.event', 'off');
        this.measure.reset();

        for (var i = 0; i < signature; i++) {
          this.measure.add();
        }
        //re-render the view.
        this.render();

        //recalculate the widths for each beat.
        this.calcBeatWidth(signature);
        dispatch.trigger('signatureChange.event', this.parent.get('signature'));

      }
    },

    //This determines the width of each beat based on the
    //number of beats per measure or 'signature'.
    calcBeatWidth: function(signature) {
      if ($(this.el).hasClass('selected')) {
        var px = 100/$('.measure').css('width').replace(/[^-\d\.]/g, '');
        var beatWidth = (100 - ((signature*1+1)*px))/signature;

        $(this.el).children('.beat').css({
          'width' : beatWidth+'%'
        });
      }
    }
  });
});

Upvotes: 2

Views: 1982

Answers (3)

Bob Dowling
Bob Dowling

Reputation: 11

Actually... All you need to do is define the events in your view's initialize function.

Upvotes: 0

chris Frisina
chris Frisina

Reputation: 19688

Thanks to @AlexMcp and @PaulHoenecke for their contributions.

I ended up passing the context to itself via the JQuery proxy function in the render

    // add click handler to this beat
    $("#beat"+this.model.cid).click($.proxy(this.toggle, this));

Complete beatView.js file:

//filename: views/beats/beatView.js
/*
  This is the view for a single beat, which
  is contained in a measure view.
*/
define([
  'jquery',
  'underscore',
  'backbone',
  'backbone/models/beat',
  'text!backbone/templates/measures/audioMeasures.html',
  'text!backbone/templates/beats/linearBarBeats.html',
  'text!backbone/templates/beats/linearBarSVGBeats.html',
  'text!backbone/templates/beats/circularPieBeats.html',
  'app/dispatch',
  'app/log'
], function($, _, Backbone, BeatModel, audioMeasuresTemplate, linearBarBeatsTemplate, linearBarSVGBeatsTemplate, circularPieBeatsTemplate, dispatch, log){
  return Backbone.View.extend({

    /* TODO still issues with this
      el: '.beat',
      registering backbone's click event to our toggle() function.
       events : {
         'click' : 'toggle'
       },
    */

    // The different representations
    representations: {
      "audio": audioMeasuresTemplate,
      "linear-bar": linearBarBeatsTemplate,
      "linear-bar-svg": linearBarSVGBeatsTemplate,
      "circular-pie": circularPieBeatsTemplate
    },
    currentBeatRepresentation: 'linear-bar',
    beatAngle: 90,

    //The constructor takes options because these views are created
    //by measuresView objects.
    initialize: function(options){
      if (options) {
        // TODO: need to take in an option about currentBeatRep
        // TODO: maybe need to respond to a representation changed event (change this.currentBeatRepresentation and rerender)

        console.log('options :');
        console.warn(options);
        this.model = options.model;

        // this is the html element into which this class should render its template
        this.measureBeatHolder = options.parentElHolder;
        this.el = options.singleBeat;
        this.parent = options.parent;
      } else {
        console.error('should not be in here!');
        this.model = new BeatModel;
      }
      this.render();
    },

    //We use css classes to control the color of the beat.
    //A beat is essentially an empty div.
    render: function(toggle){
      // the current state of the beat (is it ON or OFF?)
      var state = this.getSelectionBooleanCSS();

      // if render is being called from the toggle function, we may want to do something different
      if (toggle) {
        $('#beat'+toggle).toggleClass("ON");
        $('#beat'+toggle).toggleClass("OFF");
      } else {
        // this is reached during the initial rendering of the page

        // compile the template for this beat (respect the current representation)
        var compiledTemplate = _.template(this.representations[this.currentBeatRepresentation], {beat: this.model, beatAngle: this.beatAngle, state: state});
        // append the compiled template to the measureBeatHolder
        $(this.measureBeatHolder).append( compiledTemplate );
        // add click handler to this beat
        $("#beat"+this.model.cid).click($.proxy(this.toggle, this));
        // $(this.parentEl).append(compiledTemplate);
        return this;
      }
    },

    getSelectionBooleanCSS: function(){
      if (this.model.get("selected")) {
        return "ON";
      } else {
        return "OFF";
      }
    },

    /*
      This is called when a beat is clicked.
      It does a number of things:
      1. toggles the model's selected field.
      2. re-renders the beat.
      3. prints a console message.
      4. tells log to send a log of the click event.
      5. triggers a beatClicked event.
    */
    toggle: function(){
      //switch the selected boolean value on the model
      this.model.set('selected', !this.model.get('selected'));
      //re-render it, passing the clicked beat to render()
      this.render(this.model.cid);
      // log.sendLog([[1, "beat" + this.model.cid + " toggled: "+!bool]]);
      dispatch.trigger('beatClicked.event');
    }
  });
});

Upvotes: 0

Alex Mcp
Alex Mcp

Reputation: 19315

Backbone takes your events object and delegates all of those event types and selectors to the view's el. Any HTML that you want events to register on needs to be inserted inside the view's el, and the el needs to be inserted onto the page.

Typically I set up my views like this:

var myView = Backbone.View.extend({

  id: 'myView',

  events: {
    'click li' : 'myEventCallback'
  },

  initialize: function() {

    $('body').append(this.el); //el on the page now
    this.render(); //fills up el with useful markup
  },

  render: function() {
    //fills el with useful markup from a template
    this.el.html( JST['myTemplate' ]() ); 
  },

  myEventCallback: function() {
    //code for handling click events on the li's inside the el of this view
  }
});

Upvotes: 3

Related Questions