Knockout Style Data-Bind Attributes For Backbone

Hot on the heels of my Backbone.ModelBinding plugin release, I pushed a significant update out to the github repository. Included in this release is the rest of the form input types, including radio button groups, select boxes, etc (be sure to read the documentation in the readme for a more complete list of Backbone.ModelBinding supports now). More importantly, though, in order to get the rest of the form element conventions built, I had to create a pluggable architecture for my plugin.

That’s right, folks, my plugin allows plugins. :)

And with the ability to plug your own model binding conventions into Backbone.ModelBinding, I had the crazy idea of putting together some basic ‘data-bind’ attribute binding for any and all html elements and attributes, ala Knockout.

Re-Rendering On Model Update

Let’s take a look at the example I keep using for this plugin, again:

Screen Shot 2011 07 29 at 12 44 29 PM

We all know that Backbone.ModelBinding will handle the binding between the form inputs and the model that is bound to the add/edit view. However, the big box that represents our medication in the “Current Medications” list was not updated automatically. In order to get this to update, we had to re-render the view with the updated model.

Here’s what that code looks like (using an event aggregator in this case):

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

AddEditView = Backbone.View.extend({
  events: {
    "click #save": "save"
  }

  initialize: function(options){
    this.vent = options.vent;
  },

  save: function(){
    this.model.save();
    this.vent.trigger("modelUpdated");
  }
});

MedicationView = Backbone.View.extend({
  initialize: function(options){
    _.bindAll(this, "render");
    options.vent.bind("modelUpdated", this.render);
  },

  render: function(){
    // ... jQuery template stuff here
  }
});

Not bad – pretty standard backbone code, really. My medication view re-renders itself when the model has been saved. Except I don’t think we should have to re-render the entire view just to update a few fields on the form.

Data-Bind Your Way To Happiness

Let’s try this again with my new data bind convention in place.

AddEditView = Backbone.View.extend({
  render: function(){
    // ... jQuery template stuff here
    Backbone.ModelBinding.call(this);
  }
});

MedicationView = Backbone.View.extend({
  render: function(){
    // ... jQuery template stuff here
    Backbone.ModelBinding.call(this);
  }
});

from 27 lines of code down to 13? Now that’s more like it!

But more important than the number of lines of code, you’ll notice that there’s no view re-rendering happening for the medication view. I’ve also removed the use of the event aggregator from this scenario. That’s because we don’t need to re-render the entire view when the model is updated, and we don’t need to wait for the event aggregator to tell us that the model was updated. We can let our data-bind conventions work their magic and update the medication view as changes are being made!

So, then… how do we get this magic to work? First off, we need to build the data-bind convention:

var DataBindConvention = {
  selector: "*[data-bind]",
  handler: {
    bind: function(selector, view, model){
      view.$(selector).each(function(index){
        var element = view.$(this);
        var databind = element.attr("data-bind").split(" ");
        var elementAttr = databind[0];
        var modelAttr = databind[1];

        model.bind("change:" + modelAttr, function(changedModel, val){
          switch(elementAttr){
            case "html":
              element.html(val);
              break;
            case "text":
              element.text(val);
              break;
            default:
              element.attr(elementAttr, val);
          }
        });

      });
    }
  }
};

Backbone.ModelBinding.Conventions.dataBind = DataBindConvention;

Drop this code into your project, somewhere. Be sure it is executed after the Backbone.ModelBinding.js file has been loaded so that the very last line can correctly attach the new DataBindConvention to the convention list (also checkout the readme on the github repo for more information on this convention’s structure).

Once we have that in place, we just need to add some data-bind attributes to our html. Here’s an approximation of what the html for the above medication view looks like:

<fieldset>
  <legend data-bind="text trade_name">Some drugs</legend>
  <ul>
    <li class="medication-data">
      <div class="label">Dosage</div>
      <div class="data" data-bind="text dosage">5000mg</div>
    </li>
    <li class="medication-data">
      <div class="label">Route</div>
      <div class="data" data-bind="text route">something</div>
    </li>
    ...
  </ul>
</fieldset>

The key here is the ‘data-bind’ attribute that I’ve added to the divs that are displaying the model’s data. The data-bind convention that we set up looks for this attribute and then parses the information in it to set up the databinding. The first word of the attribute’s value tells our convention how to modify the element. The second word tells the convention which model attribute to use for the element’s value. In this case, we’re setting the text attribute of the divs to the model attribute that represents the data we want: ‘trade_name’ (the name of the drug), ‘dosage’ and ‘route’.

Now when we update our form by typing into the text boxes and then causing the text box to lose focus (so that the ‘change’ event can fire), our medication view updates instantly!

Screen Shot 2011 07 29 at 12 47 43 PM

As I tabbed out of the “Trade name” field and into the “Dosage” field, the medication view updated and showed the change in the medication’s name!

A Pale Reflection, A Glimpse Of What’s To Come

I realize that what I’ve built is only a pale reflection of what Knockout offers for it’s data-binding capabilties. However, what I’ve shown is already helping me simplifiy my applications, reduce the amount of code that I’m writing, and improve the user experience.

There’s a lot of work left to do with this convention before I bake it in, permanently. There are some additional behaviors to add to this convention and there are some tweaks to be made to the existing code. For example – better handle drop lists which right now, always end up displaying the “value” of the seelction instead of the text of the selection. I also want to explore what Brandon Satrom is building for Knockout, to remove the use of the data-bind attributes. I may be able to leverage what he’s building to help improve what I’m building.

I need to talk about how to build a proper cancel operation for this form, as well.. After all, we don’t want the medication view to continue displaying the modified information if we hit the “close” link instead of the “Save” button… but that’s another blog post (coming soon).

For now, you can grab the source for this data-binding convention from the gist and start playing with data-bind for backbone it in your own application!


Post Footer automatically generated by Add Post Footer Plugin for wordpress.

About Derick Bailey

Derick Bailey is an entrepreneur, problem solver (and creator? :P ), software developer, screecaster, writer, blogger, speaker and technology leader in central Texas (north of Austin). He runs SignalLeaf.com - the amazingly awesome podcast audio hosting service that everyone should be using, and WatchMeCode.net where he throws down the JavaScript gauntlets to get you up to speed. He has been a professional software developer since the late 90's, and has been writing code since the late 80's. Find me on twitter: @derickbailey, @mutedsolutions, @backbonejsclass Find me on the web: SignalLeaf, WatchMeCode, Kendo UI blog, MarionetteJS, My Github profile, On Google+.
This entry was posted in Backbone, Backbone.ModelBinding, Javascript, Model-View-Controller. Bookmark the permalink. Follow any comments here with the RSS feed for this post.
  • Anonymous

    Great Stuff Dude!
    btw, any reason why you shouldn’t replace that code-

    model.bind(“change:” + modelAttr, function(changedModel, val){
    switch(elementAttr){
    case “html”:
    element.html(val);
    break;
    case “text”:
    element.text(val);
    break;
    default:
    element.attr(elementAttr, val);
    }
    });

    with the following: (?)

    model.bind(“change:” + modelAttr, function(changedModel, val){
    if (element[elementAttr]) {
    element[elementAttr](val);
    }
    else {
    element['attr'](elementAttr, val);
    }
    });

    Just curious…

    • http://mutedsolutions.com Derick Bailey

      without having run that code, i don’t think it would work. ‘element’ is a jQuery selector object, so i don’t think that code would work. the call to ‘element[elementAttr]‘ would try to look for an element within the selector, at index elementAttr, which wouldn’t be valid.

      • Anonymous

        Hmm, a quick test on the firebug console showed that it works, but not sure about the context..?

        • http://mutedsolutions.com Derick Bailey

          well… i stand corrected! :) 

          i’ll have to try it out and see what happens

  • http://twitter.com/farkashon farkashon

    Very nice indeed!

  • http://vikbhatti.com Vik Bhatti

    Epic work! 

  • http://www.easyate.com/ Viceroy

    I searched for something completely different, but found your website! And have to say thanks. Nice read. Will come back.