Two-Way Data Bindings in Backbone

by Andrew on January 8, 2013

Lately, I’ve been doing a lot of work with Backbone.js. As client-side MV*/MVWs (Model-View-Whatever) continue to grow in popularity, there’s an ongoing debate over which one is best.

I like Backbone. I’m comfortable using it. However, a coworker recently pointed me to Ember.js for their stellar implementation of two-way data binding. I haven’t used Ember myself, but reading over their documentation and API, it looks like an exciting framework.

At some point, I’m hoping to try Ember. Until then, I find myself wanting several of the features Ember offers available as part of my Backbone project.

Let’s make it happen.

Along with two-way bindings, Ember offers computed properties of model attributes which is very cool. Backbone Mutators is an awesome extension for Backbone that provides this capability. Using Backbone Mutators, I’ll show you a pattern for creating “Ember-like” two-way bindings within your Backbone project.

I’ll be recreating the example provided in the Ember.js docs. I’ve changed some of the variable names for the sake of clarity.

// Constructors
// ==============================================
var Wife = Backbone.Model.extend({
  mutators: {
    householdIncome: { // Model attribute name
      set: function (key, value, options, set) {
        set(key, value, options); // Set wife's householdIncome.
        var husband = this.get("husband");
        if( !_.isUndefined(husband) && !_.isEqual(husband.attributes.householdIncome, value) ) {
          // If the wife has a husband and
          // his income does not already match hers, set it.
          husband.set("householdIncome", value);
        }
      }
    }
  }
});
var Husband = Backbone.Model.extend({
  mutators: {
    householdIncome: { // Model attribute name
      set: function (key, value, options, set) {
        set(key, value, options); // Set husband's householdIncome.
        var wife = this.get("wife");
        if( !_.isUndefined(wife) && !_.isEqual(wife.attributes.householdIncome, value) ) {
          // If the husband has a wife and
          // her income does not already match his, set it.
          wife.set("householdIncome", value);
        }
      },
      get: function () {
        var wife = this.get("wife");
        if( !_.isUndefined(wife) ) {
          // If he has a wife, get her income.
          return wife.get("householdIncome");
        }
      }
    }
  }
});

The preceding code may appear confusing at first, but after a few passes it will hopefully start to make sense.

Using Mutators, we’re defining our computed property, “householdIncome”. Mutators allows you to overtake both the get and set methods for a property, so we’re using these methods to establish our two-way binding.

The if statement within each set function is also avoiding an infinite loop by not setting the partner’s property when it already matches. If it does match, that means the property change originated with the other, so there is no need to set it again.

// Objects
// ==============================================
var Sarah = new Wife();
var Jim = new Husband();

// Define Two-Way Binding Relationship
// ==============================================
Sarah.set({
  husband: Jim
});
Jim.set({
  wife: Sarah
});

Here, we’re creating a husband and wife and establishing the relationship between the two. This will be necessary in order for the the get and set methods within the constructors to do their magic.

That’s it. You’re done!

Let’s now test the code to make sure it works similarly to the Ember example.

Sarah.set("householdIncome", 80000)
Jim.get('householdIncome'); // 80000

// Someone gets raise.
Jim.set('householdIncome', 90000);
Sarah.get('householdIncome'); // 90000

Sweet.

Conclusion


Is it as lean as the Ember example? Not nearly.

It’s important however to remember that Backbone is meant to be a light-handed library, only giving you what you need to get started. Ember is a framework, a different beast with its own set of concerns.

While the Ember team has gone through the trouble of abstracting away the set up, this pattern offers you the potential for more control over your binding relationships.

At the very least, I hope this approach will settle some of that “Ember envy” you may be having on your Backbone project :)

{ 7 comments }