Practical Abstraction in Ember.js

Embers

Ember provides a LOT of powerful abstractions. With all that power, it can be difficult to know what tools will best add meaningful functionality to your app while gaining leverage for the future. The Ember docs will walk you through the roles of templates, controllers, models, and all the other pieces you’d expect in a modern JavaScript app framework, but what they don’t cover are tactics for using some of the deeper concepts of Ember (and functional JavaScript programming) to help you write less, but better, code.

Here are a few tools we’re using on my current project, a non-trivial Ember.js application, to do just that.

Ember.computed Macros

Let’s start simple. The best code is the code you don’t have to write yourself. To that end, Ember provides a set of computed properties that solve common cases. See Tony’s post about Ember.computed macros for more info.

Inheritance

Still pretty simple. A comfortable, familiar friend. Inheritance is still a simple and effective tool. Inheritance in Ember works well when:

  • you need to slightly alter the behavior of an existing class by overriding a few functions or computed properties
  • the properties and body of the functions can be fully defined ahead of time (no dynamic names, etc.)
  • the pattern you’re building applies exclusively to objects of the same type

By far, our biggest use of inheritance is to modify the behavior of built-in Ember classes like Ember.Select and Ember.TextField.

It may not be the right choice if:

  • there is already inheritance in play, and the new functions don’t fit well with the existing responsibilities of the superclass
  • the same patterns need to be applied to multiple properties in the same object
  • functions and properties from more than one source are needed

As an example, we subclassed a few of Ember’s controls and overrode some of the particulars (in CoffeeScript):

Web.ChosenSelectOption = Ember.SelectOption.extend
  template: Ember.Handlebars.compile('{{unbound view.label}}')
 
Web.ChosenSelect = Ember.Select.extend
  chosenOptions: {}
  attributeBindings: ['data-placeholder']
  template: Ember.Handlebars.compile(
    '...omitted for brevity'
  )
  didInsertElement: ->
    @_super()
    @$().chosen(@chosenOptions)
   ...

Injecting other Objects

Ember has a built-in dependency injection container. It is ubiquitous and easy to use, so use it. Be sure to check out the ability to inject objects into an entire category of other objects.

# To register your own object
Web.Api = Ember.Object.extend
  getJSON: (url) ->
    ...
Web.register 'service:api', Web.Api
 
# Or, as a non-singleton object
Web.register 'service:api', Web.Api, {singleton: false}
 
# To inject objects you depend on
Web.UserRepository = Web.Repository.extend
  getForId: (id) ->
    @api.getJSON("/users/#{id}")
Web.register 'repository:user', Web.UserRepository
Web.inject 'repository:user', 'api', 'service:api'
 
# To inject an object into a category of other objects. All objects
# registered as "repository:<name>" will get the api object injected.
Web.inject 'repository', 'api', 'service:api'

Mixins

A Mixin in Ember is a set of properties and functions that can be included in another class, similar to a Ruby module. Mixins are a great way to share functions that address cross-cutting concerns (e.g., logging, authorization) or capabilities that are likely to be needed in places where inheritance isn’t an option.

We built a Mixin to add AJAX request methods to a number of our model objects. In retrospect, injecting an object that encapsulated our JSON API’s request methods might have been a better choice, but the Mixin did get the job done.

Web.Requestable = Ember.Mixin.create
  postJSON: (opts) ->
    opts.includeAuthToken ?= true
    $.ajax
      type: 'POST'
      url: "#{Web.apiURL}#{opts.path}"
      data: JSON.stringify(opts.data)
      contentType: 'application/json'
      processData: false
      success: opts.success
      error: opts.error

Most of our current use of Mixins involve Mixin generators — keep reading!

Computed Property Generators

Sounds a bit more complicated, but the concept is simple: build a function that builds a computed property. We identified a few patterns in our application that applied to a number of properties spread across many of our model objects.

Property generators fit well when:

  • The property names that the pattern will be applied to vary
  • Not all objects of a particular type need the property, or the types are heterogeneous

For a simple example, this builds a property that reverses the value of a different string property:

Web.Model.reversedString = (propertyToReverse) ->
  (->
    @get(propertyToReverse).split("").reverse().join("")
  ).property(propertyToReverse)
 
Web.User = Ember.ObjectProxy.extend
  backwardsName: Web.Model.reversedString("name")

The same thinking can be applied to Observers and Bindings as well.

Mixin Generators

Building on the example we built above, a natural next step is a Mixin generator. Mixin generation is handy when:

  • You need more than one property or function to be built. We had cases where we needed a pair — one computed property and one observer — to mediate between two representations of an array.
  • Like the computed property generation, when property names need to vary across uses of the Mixin.

For example, we needed to mediate between an abbreviated representation and a full representation of an object. The abbreviated representation is a single property/value pair from a model object, and the full representation is an Ember.ObjectProxy-based model class.

###
Builds a mapping from an existing abbreviated represenation of a property 
within  the `content` of an ObjectProxy to a full representation using a
given repository.
@param {string} propertyName - The name of the property
@param {string} attributeName - The name of the attribute to keep in the
    abbreviated representation
@param {string} repoName - The name of the repository object registered in
    the DI container (under "repository:<name>")
###
Web.Model.mappedArrayProperty = (propertyName, attributeName, repoName) ->
  mixinProperties = {}
  mixinProperties[propertyName] = ((key, value) ->
    if arguments.length == 1
      repository = @container.lookup "repository:#{repoName}"
      ids = _.map(@get("content.#{propertyName}"), (obj) ->
        obj[propertyName]
      repository.find ids
    else
      assignedObjects = @get(propertyName)
      assignedObjects.splice 0, assignedObjects.length, value...
      assignedObjects
  ).property("content.#{propertyName}.@each")
 
  mixinProperties["_update_#{propertyName}"] = ((sender,key,value) ->
    assignedValues = _.map @get(propertyName), (obj) =>
      obj.getProperties attributeName
    if @get('content')?
      @set "content.#{propertyName}", assignedValues
  ).observes("#{propertyName}.@each", "content")
 
  Ember.Mixin.create(mixinProperties)
 
friendsMap = Web.Model.mappedArrayProperty('friends', 'id', 'user')
Web.UserModel = Ember.ObjectProxy.extend friendsMap,
  # The content will contain a property 'friends' that contains
  # an array of objects like this: {id: '23AD'}
  content: {}

It’s possible that the body of the observer above should be invoked via Ember.run.once(…) according to the Ember.js docs.

Further Reading

I highly recommend reading Drew’s “Effortless Abstraction with Ember.js” post for some really great insights into building on the powerful abstractions Ember provides.

The Ember docs are increasingly full of great details as well. In particular, consider reading about: