Event Driven Behavior in Backbone.js

Sometimes, you need Backbone.js to do something more interesting then appending view after view to your DOM.  Sometimes you need changes in one view to cause changes in another (e.g. only allow one view to be in an editing state at a time), but giving views knowledge of other views has a bad smell – and that sort of tight coupling defeats a lot of what Backbone.js brings to the table.

Sadly, there aren’t a lot of full examples out there on how to solve this.  I did find this one, but I wanted more detail, so, I decided to create one.

It’s a simple app that presents two separate collections with three views each.  Clicking on any view will turn it red.  Our special constraint here is that we only want one view red at a time.  It will look like this:

jinx

First, let’s start with getting things going without the special constraint working.  We will start with the model – it has two attributes – some text and whether it should be blushing

JinxModel: Backbone.Model.extend( {
   defaults: function() {
      return {
         'blushing' : false,
         'text' : ''
      };
   }
})

The view for this model listens for clicks and updates the blushing property on its model

JinxView: Backbone.View.extend( {
   model: Jinx.JinxModel,

   className: 'jinx-view',

   initialize: function( options ) {
       this.listenTo( this.model, 'change', this.render );
   },

   events: {
       'click' : 'onClick'
    },

   onClick: function( event ) {
       event.preventDefault();
       if ( this.model.get( 'blushing' ) == false ) {
         this.model.set( { 'blushing': true } );
       } else {
          this.model.set( { 'blushing': false } );
       }
    },

   render: function() {
      var template = _.template( '<p <%if (blushing) { %>class="blushing"<% } %> > <%= text %></p>' );
      this.$el.html( template( this.model.toJSON() ) );
      return this;
   }
})

A collection and collection view…

JinxCollection: Backbone.Collection.extend( {
   model: Jinx.JinxModel
}),

JinxCollectionView: Backbone.View.extend( {
   collection: Jinx.JinxCollection,

   className: 'jinx-collection-view',

   initialize: function( ) {
      this.listenTo( this.collection, 'add', this.addOne );
      this.listenTo( this.collection, 'reset', this.addAll );
   },

   render: function() {
      this.collection.forEach( this.addOne, this );
      return this;
   },

   addOne: function( jinxmodel ) {
      var jinxView = new Jinx.JinxView( { model: jinxmodel} );
      this.$el.append( jinxView.render().el );
   },

   addAll: function() {
      this.collection.forEach( this.addOne, this );
   }
})

And then the app itself:

JinxApp: Backbone.Router.extend( {
   initialize: function() {

      this.jinxCollectionA = new Jinx.JinxCollection();

      this.jinxCollectionA.add( new Jinx.JinxModel( { checked: false, text: 'A1' } ) );
      this.jinxCollectionA.add( new Jinx.JinxModel( { checked: false, text: 'A2' } ) );
      this.jinxCollectionA.add( new Jinx.JinxModel( { checked: false, text: 'A3' } ) );
      this.jinxCollectionViewA = new Jinx.JinxCollectionView( { collection: this.jinxCollectionA } );
      this.jinxCollectionViewA.render();

      this.jinxCollectionB = new Jinx.JinxCollection();
      this.jinxCollectionB.add( new Jinx.JinxModel( { checked: false, text: 'B1' } ) );
      this.jinxCollectionB.add( new Jinx.JinxModel( { checked: false, text: 'B2' } ) );
      this.jinxCollectionB.add( new Jinx.JinxModel( { checked: false, text: 'B3' } ) );
      this.jinxCollectionViewB = new Jinx.JinxCollectionView( { collection: this.jinxCollectionB } );
      this.jinxCollectionViewB.render();

      $( '#content' ).html( this.jinxCollectionViewA.el );
      $( '#content' ).append( this.jinxCollectionViewB.el );
   }
})

That’s all well and good, but we now need to take care of our special constraint – only one view can be blushing at a time.  To do this, we are going to use an instance of Backbone.Events:

Change the app to instantiate Backbone.Events, pass it to each CollectionView, and to listen for a custom event “jinxapp:unblushall”:

initialize: function() {

this.vent = _.extend({}, Backbone.Events);

this.jinxCollectionA = new Jinx.JinxCollection();
this.jinxCollectionA.add( new Jinx.JinxModel( { checked: false, text: "A1" } ) );
this.jinxCollectionA.add( new Jinx.JinxModel( { checked: false, text: "A2" } ) );
this.jinxCollectionA.add( new Jinx.JinxModel( { checked: false, text: "A3" } ) );
this.jinxCollectionViewA = new Jinx.JinxCollectionView( { collection: this.jinxCollectionA, vent: this.vent } );
this.jinxCollectionViewA.render();

this.jinxCollectionB = new Jinx.JinxCollection();
this.jinxCollectionB.add( new Jinx.JinxModel( { checked: false, text: "B1" } ) );
this.jinxCollectionB.add( new Jinx.JinxModel( { checked: false, text: "B2" } ) );
this.jinxCollectionB.add( new Jinx.JinxModel( { checked: false, text: "B3" } ) );
this.jinxCollectionViewB = new Jinx.JinxCollectionView( { collection: this.jinxCollectionB, vent: this.vent } );
this.jinxCollectionViewB.render();

$( '#content' ).html( this.jinxCollectionViewA.el );
$( '#content' ).append( this.jinxCollectionViewB.el );

this.vent.bind( "jinxapp:unblushall", this.unblushAll, this );
},

Add an event handler to the app that will send an event to subscribed views to unblush themselves.

unblushAll: function( event ) {
   this.vent.trigger( 'jinxview:unblush', false ); // instead of false, we could pass this, but the view doesn't really need it</strong>
}

Modify the collection view to accept the vent from the app and pass it into views as they are created

JinxCollectionView: Backbone.View.extend( {
...
initialize: function( options ) {
   this.listenTo( this.collection, "add", this.addOne );
   this.listenTo( this.collection, "reset", this.addAll );
   this.vent = options.vent;
},
...
addOne: function( jinxmodel ) {
   var jinxView = new Jinx.JinxView( { model: jinxmodel, vent: this.vent } );
   this.$el.append( jinxView.render().el );
},
...

And then lastly, have the views subscribe to the event from the app and unblush when the event is received:

JinxView: Backbone.View.extend( {
...
initialize: function( options ) {
   this.listenTo( this.model, 'change', this.render );
   this.vent = options.vent;
   this.vent.bind( "jinxview:unblush", this.unBlush, this );
},
...
onClick: function( event ) {
   event.preventDefault();
   if ( this.model.get( 'blushing' ) == false ) {
      // If we are currently unblushed, ask app to clear anyone else before we blush ourselves</strong>
      this.vent.trigger( 'jinxapp:unblush', false ); // we could pass a reference to this.model instead of false if we wanted to
      this.model.set( { 'blushing': true } );
   } else {
      this.model.set( { 'blushing': false } );
   }
},

unBlush: function( event ) {
   if ( this.model.get( 'blushing' ) ) {
      this.model.set( { 'blushing': false } );
   }
},
...

That’s all there is to it – now when a view is about to blush, it sends a message to the app, which sends an unblush to all subscribing views – they all unblush if needed and then our view can blush.

In a way, this example is overkill though – I wanted to really show view – app – view communication, but there really isn’t any reason why the view could just send jinxview:unblush itself – and leave the app out of the transaction – that would work too!

The complete source is available at https://github.com/allendav/jinx

Comments, questions? Fire away!

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s