Effortless Abstraction with Ember.js

Ember.js has gotten a lot of attention recently for being confusing or hard to get started with. I’ll definitely admit that getting started with Ember took more than its fair share of code-reading, as the guides and documentation didn’t provide enough detail for building real-world apps, but for the most part, I’ve been amazed at how easy it is to build sophisticated applications with Ember. This post explores one critical feature of Ember that enables this: pervasive data binding.

UI-based applications involve a lot of state, and most of an implementation involves responding to and propagating changes to that state. Desktop applications are structured as trees of components, each with their own individual state, and each of which emits events in response to user interactions. Classic web apps build a snapshot of the current state as an HTML page, then wait for HTTP requests to change the state or request an updated or alternate view. Whether you’re building an MVC/MVP desktop application or server- or client-side web application, most of the programming chores in modern frameworks deal with propagating change and translating between layers of abstraction in state.

Ember provides the most complete and general solution to this problem of any system I’ve used. Ember supports data binding at the object level, taking care of change propagation for you.

Three Pillars of Ember State Management

Data Binding

The key part of Ember’s solution to dealing with change is data binding. Data binding lets you declaratively establish a relationship between two names. Both refer to the same value, and when either changes the other changes too, like two pointers to the same address. In Ember, this is trivial:

  var person = Ember.Object.create({
    firstName: "Elvis",
    lastName: "Einstein"
  });
 
  var contactController = Ember.Object.create({
    person: person,
    firstNameBinding: "person.firstName",
    lastNameBinding: "person.lastNameBinding",
  });

Here, the firstName and lastName properties of our contactController object are data bound to the corresponding properties of the contactController’s person, here “Elvis” and “Einstein.” If the person’s name changes, so will the data-bound properties of the controller. Further, if the controller’s person is swapped out to another, the controller will automatically reflect the new person’s information. Without writing a single event handler, contactController responds to change in which person it represents and change in that person’s properties.

Computed Properties

While there are many cases where properties need to remain perfectly in sync, it is perhaps even more common that one property is derived from another. Ember makes this easy too by providing computed properties. Computed properties are values that are derived from others, and when any of the dependencies change, the computed property is updated accordingly. For example, we might want to say the text fields of a contact form are enabled only when a contact is currently being edited, for example:

  var contactController = Ember.Object.create({
    person: person,
 
    // ...
 
    areFieldsEnabled: function() {
      return person != null;
    }.property("person");
  });

Now, whenever the contactController has a person, fields could be enabled in our form (by data binding the areFieldsEnabled property to the enabled state of our form elements). They would then disable automatically if the contactController’s person is set to null. Since we pass the string “person” into the property method, Ember knows that areFieldsEnabled should be updated whenever that property changes.

Observers

Sometimes you need a little more flexibility than data binding and computed properties give you. In these cases, you can usually do what you need with observers. Observers are event handlers that get invoked whenever a value changes. I recently used an observer when implementing a search control. Users would type text into a field that was data-bound to a searchText property. This text would be compiled into a regular expression, and the interface would update to reflect which values matched the regular expression.

This sounds like a perfect use case for computed properties, however I didn’t want to run a search on each keystroke, but instead only when the user stopped typing. Underscore.js has a function called debounce which does just this — it takes a function and a duration in milliseconds, and invokes the function after the function hasn’t been called in that much time. In this case, the object that represented my search interface looked something like this:

  var SearchController = Ember.Controller.extend({
    searchText: "",
    regExp: null,
 
    searchTextChanged: _.debounce(function() {
      this.set 'regExp', compileSearchRegExp(this.get("searchText"))
    }, 500).observes("searchText")
  });

Our search field is data bound to the searchText of a SearchController. Whenever the user types into the text field, searchText is updated, and our searchTextChanged observer is invoked. searchTextChanged sets the regExp on the controller 500 milliseconds after the user stops typing.

Effortless Abstraction

These features are game changers, and not just because it’s easy to keep the UI in sync with the model. For me the real win is that introducing new layers of abstraction is made as easy as possible. In most other frameworks, propagating change between layers of abstraction is a real burden. It’s often easier to hack in a little extra logic to an object or method than to abstract it out into a new object, which often requires overhead of forwarding or translating events. With data binding and computed properties, change is propagated through intermediate layers for you automatically.

Not only are these abstractions easy to use, they’re also surprisingly efficient. By default, the value of computed properties are cached until a dependency change, memoising computations that could otherwise be very expensive. Even better, bindings and computed properties are integrated with Ember’s run loop. Instead of updating immediately upon change, some changes are deferred until the current execution context finishes, allowing updates to happen once instead of many times. Updates to the DOM, in particular, happen just once and only to those elements whose values have actually changed (instead of just updating the dom when an event happens, even if the new state is the same as the old).

The other great thing about these concepts is that they lend themselves to defining state in a natural way. We have a pretty complex form with a checkbox tree, whose representation is a computed property derived from our domain. Each node in the tree is represented by an object with an isChecked state. These statements paraphrase data bindings, computed properties, and observers from our code base, with code that is the logical JavaScript equivalent of the usually longer English:

  • A node is checked if it is a leaf which is checked, or it is a branch which has checked children.
  • When you set the isChecked property of a branch, it sets the isChecked property of all of its children to the same value. A leaf just saves the value.
  • A node is indeterminate if it is a branch with both unchecked and checked children.
  • The node’s indeterminate state is data bound to the same property of the browser checkbox.
  • We observe the isChecked property of a node and add the node’s value to a set of selectedValues if it changes to true; we remove it from selectedValues when it changes to false.
  • All of our form elements are dependencies of a params object, which represents query parameters for a search.
  • Whenever the params object changes, we perform an AJAX request to the server to get new results. This is done with a debounced observer, so we only query the server when changes to the form have stopped for a short duration.

This set of features and their impact on the design of the rest of Ember make it the most productive and enjoyable application framework I’ve used. It enabled me to write code the way I think of it in a testable way, without requiring a ton of boilerplate. It may not be easy yet, but it will be. And when it is, it makes easy things which are hard with anything else.