Robert Mosolgo

To my knowledge, batman.js is not maintained. For that reason, I don't suggest that you use it for a new project!

Computed Properties: Batman.js and Ember.js

Batman.js is a front-end MVC framework with an unrivaled implementation of key-value observing. I will explore computed properties in batman.js by contrasting them with Ember.js’s computed properties.

First, disclaimers!

  • I didn’t write any of the Batman.Property code that makes this feature possible. I’m only a fanboy!
  • I don’t know Ember.js. I’ve just gathered examples from the Ember Guides.

To explore computed properties, let’s take the canonical fullName example. It:

  • depends on two other properties, firstName and lastName
  • returns a string that joins firstName and lastName with a space
  • can be set
  • sets firstName and lastName by splitting on whitespace

We’ll also explore an aggregrated roster property which:

  • depends on fullName for each person
  • joins fullName with ,

fullName in Ember.js

(This is yanked wholesale from the Computed Properties Guide.)

A couple of things to notice:

  • fullName is defined as one function which handles get and set operations.
  • fullName must be told what properties it depends on.
App.Person = Ember.Object.extend({
  firstName: null, // These aren't necessary, they're
  lastName: null,  // just for clarity.

  fullName: function(key, value, previousValue) {
    // setter
    if (arguments.length > 1) {
      var nameParts = value.split(/\s+/);
      this.set('firstName', nameParts[0]);
      this.set('lastName',  nameParts[1]);
    }

    // getter, also the return value is cached
    return this.get('firstName') + ' ' + this.get('lastName');
  }.property('firstName', 'lastName')
});

Usage is pretty standard: use get and set to access properties.

var captainAmerica = App.Person.create();
captainAmerica.set('fullName', "William Burnside");
captainAmerica.get('firstName'); // William
captainAmerica.get('lastName');  // Burnside

fullName in Batman.js

Two things to notice:

  • get and set operations are defined separately.
  • fullName doesn’t have to be told what its dependencies are.
class App.Person extends Batman.Object
  @accessor 'firstName' # not necessary,
  @accessor 'lastName'  # just here for clarity

  @accessor 'fullName',
    get: (key) -> "#{@get('firstName')} #{@get('lastName')}"
    set: (key, value) ->
      nameParts = value.split(/\s+/)
      @set('firstName', nameParts[0])
      @set('lastName', nameParts[1])
      return value # should return newly-set value, although the `get` function will be used for caching.

The usage is almost identical:

captainAmerica = new App.Person
captainAmerica.set('fullName', 'William Burnside')
captainAmerica.get('firstName') # William
captainAmerica.get('lastName')  # Burnside

roster in Ember.js

(This was adapted from the Computed Properties and Aggregate Data Guide.)

Some things stood out to me:

  • roster’s properties are declared with a DSL. Array dependencies are limited to one layer deep (ie, you can’t use @each twice).
  • mapBy is provided by Ember.Enumerable to handle arrays of objects. Nice!
App.PeopleController = Ember.Controller.extend({
  people: [
    App.Person.create({firstName: "Tom", lastName: "Dale"}),
    App.Person.create({firstName: "Yehuda", lastName: "Katz"})
  ],

  roster: function() {
    var people = this.get('people');
    return people.mapBy('fullName').join(', ');
  }.property('people.@each.fullName')
});

roster in Batman.js

Here’s the analogous construction in batman.js:

class App.PeopleController extends Batman.Controller
  @accessor 'people', ->
    new Batman.Set([ # this is future-code: constructor will take an array in v0.17.0
      new App.Person(firstName: "Tom", lastName: "Dale")
      new App.Person(firstName: "Yehuda", lastName: "Katz")
    ])

  @accessor 'roster', ->
    @get('people').mapToProperty('fullName').join(', ')

One thing is the same:

  • mapToProperty works like mapBy

You might notice two big differences:

  • people is a Batman.Set instead of a native Array.
  • roster didn’t have to be told what its dependencies are

By using batman.js data structures inside @accessor functions, we benefit from batman.js’s automatic source tracking. It looks like automatic source tracking was considered by the Ember core team, but deemed impossible or prohibitively expensive.

I recently saw a quote in a React.js talk:

Intellectuals solve probelms. Geniuses prevent them. - Albert Einstein

I think that’s just what the Shopify team did when they implemented Batman.Observable! The API is very simple and it Just WorksTM.

My Opinion

Pros of batman.js:

  • Elegant @accessor API for getters and setters: define get and set separately instead of testing for arguments.
  • Automatic dependency tracking: batman.js knows what objects & properties were accessed during computation and observes accordingly.
  • There’s no limit to the depth of enumerable dependencies. Any property of a Batman.Object that’s accessed will be tracked, no matter where it exists in the app.

In fact, @accessor is the heart and soul of a batman.js app. You’re basically declaring a system of computed properties, then updating that system from user input. Batman.js propagates information to wherever it needs to be.

Cons of batman.js:

  • “It’s just not Ember.” You miss out on huge user base, corporate support, and everything that goes with that.
  • Beyond that, batman.js resources are sparse. The new guides, cookbook and API docs are improving every week, but for advanced usage you still have to sourcedive sometimes.
  • There is a performance hit for global observability. The only place I’ve noticed it is with complex iteration views (batmanjs/batman#1086). I’m hoping to tackle this soon since it’s becoming an issue in PCO Check-ins.

I’m not aware of any features missing from batman.js, but I do miss the “googleability” of a well-traveled path. Batman.js also lacks some of the dev tools like a decent Chrome extension and a command-line client.

I always want to know how things works, so getting in the source is actually a benefit for me.

Six of one, half-dozen of the other:

  • Dependency DSL vs Batman.{DataStructure}
  • Calling super: this._super vs. @wrapAccessor
  • External API with get and set
  • Cached values in computed properties
  • In batman.js, you can opt out of tracking with Batman.Property.withoutTracking. It’s obscure, but I think it’s ok because batman.js always covers the more common case.

One thing that I found in neither framework was rate-limited properties, a la Knockout. I’d love to have a built-in option for this in batman.js.