9 Comments

Dynamic Binding in Ember.js Using Observers

knot_binding

I’m currently working on an Ember.js web application that is a basic CRUD app with a twist — we don’t know all of the form fields or model attributes ahead of time, but rely on a JSON document to specify the names and types of attributes for which we need to render form controls. This presents some interesting challenges, the most significant being that our input elements don’t know what to bind their value to until they are instantiated.

After trying a few different approaches, we settled on using a pair of observers to accomplish what a more direct binding could not in this scenario. One observer watches the input’s value property and updates the remote property value, and the other observer watches the remote property and updates the input’s value.

Our Solution

Read on for a few more details. This example uses Ember.js 1.0.0-RC.1, and we are using the same general solution in 1.0.0-RC.3.

Say we have the following definition for a set of fields:

{"fields":  [
  { "name": "First Name", "key": "first_name" },
  { "name": "Last Name", "key": "last_name" },
  { "name": "Email", "key": "email" }
]}

We want to display a text field for each element in the array and bind its value to a property on our model defined by the key. This example uses a CollectionView to render a subclass of Ember.TextField for each element. The DynamicBoundTextField here has the pair of observers.

App.DynamicBoundTextField = Ember.TextField.extend
  placeholderBinding: 'content.name'
  
  # Update the remote property's value when the value of
  # our text field changes
  _setRemoteValue:(->
    val = @get('value')
    @set("controller.data.#{@get('content.key')}", val) if val?
  ).observes('value')
  
  # Since we don't know the name of the property to
  # observe ahead of time, create the observer when
  # inserted into the DOM (we'll have the key then).
  didInsertElement: ->
    updateCurrentValue = =>
      currentValue = @get("controller.data.#{@get('content.key')}")
      console.log currentValue
      @set('value', currentValue) if currentValue?
    updateCurrentValue()
    @addObserver "controller.data.#{@get('content.key')}", updateCurrentValue

Our implementation uses an intermediate object to perform the observation because it requires specialized knowledge about the context that the binding is being performed in, and we felt it was odd having that information on the input itself. But it works the same way.

Try it out in this JS Bin: Runtime binding using observers JS Bin.

Things that Didn’t Work

TextField with a valueBinding

The primary constraint we’re dealing with is not knowing what the TextField should bind to until it’s created. This means we can’t simply place a valueBinding on our text input in our class definition like most Ember examples show. It needs another level of indirection to work.

TextField and an Intermediate Object with Bindings

So we tried creating a valueBinding to a known property, inputValue, of an intermediate view. The intermediate view then bound its inputValue to the target property in init. This caused interesting but unwanted behavior, like the target property getting set to undefined instead of the input’s value getting set to the existing value of the target property. It just didn’t work out very well.

Our current, working implementation does have an intermediate object, but it uses the observer pair pattern instead of a binding to the target property to avoid the bad behavior described above.