11 December 2013

(Note: The code examples below use coffeescript instead of plain javascript. If you don’t know coffeescript here’s a quick cheat sheet: @foothis.foo and () -> stmtfunction () { stmt }. Additionally text in curly braces {{…}} is Ember’s templating language.)

Finnish road sign #122

Finnish road sign number 122, “Two-way traffic”. (Source: Wikimedia Commons)

While doing a retry on Ember for freezr user interface, I hit a problem I’d like to share with you. I didn’t find help on the internet on this so I hope if someone hits the same problem this post will help.

Anyway, I hit one major gotcha that had me scratching my head for a long time. I had used ember-time as a basis on how to implement a “since state change” time display. Converting the original code to coffeescript was straightforward (but see below for an update):

App.FromNowView = Ember.View.extend
  nextTick: null
  tagName: 'time'
  template: Ember.Handlebars.compile '{{view.output}}'
  output: (() -> (moment @get('value')).fromNow(true)).property('value')
  tick: () ->
    @nextTick = Ember.run.later this, (() ->
      @notifyPropertyChange('value')
      @tick()), 1000
  willDestroyElement: () ->
    Ember.run.cancel @nextTick
  didInsertElement: () -> @tick()

and it was used like this:

{{view "App.FromNowView" valueBinding="stateUpdated"}}

Which worked great when the page was first loaded but it failed to update the time view after updates. I was really really confused. The state value was itself updated in the rendered view correctly immediately after Project.reload() finished, but text derived from stateUpdated field was not. WTF?? This is what was happening in the browser:

How it looked

Top row is what happened in the UI and the bottom ones showing what the server actually sent to the client on state change from running to freezing to frozen states. Why is it stuck on “for 4 hours”?

Time to debug. So,

  • I checked the JSON response. Yep, it had the correct, updated value.

  • I wondered whether the name was somehow conflicting (it was originally stateChanged), so I renamed the JSON field and model field. No effect.

  • I put tons of log output statements in Ember end Ember Data code. This was a great learning experience in itself, as now I have a lot better understanding how Ember propagates value changes. Nice stuff, I think. However digging deeper and deeper I kept seeing that the updated value was being passed correctly along, yet still refusing to show up in the actual web page.

  • I wondered whether the date attribute type was doing something fishy and switched to string instead. No effect, the “bad” value persisted.

  • I searched the net high and low to no avail.

I started to do voodoo coding. Poking at things and hoping the problem is mysteriously fixed.

Finally I added logging to DS.attr’s use of Ember.computed and …

… all was made clear to me.

All of the other fields were getting the value from @_data element (which contained the updated values set by DS.Model.setupData) exceptexcept for stateUpdated which got its value from @_attributes!

At this moment I remembered what I earlier read about Ember bindings. And that there was a difference between normal bindings and one-way bindings. And that the valueBinding="stateUpdated" did a binding on App.FromNowView.value to Project.stateUpdated. And that this was a normal e.g. two-way binding meaning that updates on Project.stateUpdated are propagated to App.FromNowView.value and vice versa.

I was not getting the updated value from JSON response because I had already overwritten it myself.

This is the offending line:

@notifyPropertyChange('value')

This doesn’t actually change the value of value, but Ember doesn’t know that so it propagates the event to the bound field of Project.stateUpdated, which eventually results in Project.set('stateUpdated', «value») where the new value was actually the old value. I’ll try to put this into a picture.

In the figure below I’ve used green for events initiated by Ember Data and red for those initiated by App.FromNowView and the gray arrows show bindings between different Ember-controlled values. I refer to objects by their class names, so Project.stateUpdated below is not a class field but a field in an instance of Project class.

How it worked incorrectly

In the template the statement valueBinding="stateUpdated" creates the two-way fat gray arrow binding (top row). The binding from App.FromNowView.value to App.FromNowView.output is a one-way binding and comes from the use of property('value') on the output function (right column). Finally the App.FromNowView.output binding to {{view.output}} comes from somewhere deep inside the templating system (bottom row).

The initial value is loaded by Ember Data and is propagated from top left corner by the green arrows. First, Project.stateUpdated is changed, which then propagates to App.FromNowView.value, which in turn causes the value of App.FromNowView.output to change, which finally causes the {{view.output}} template to be (re-)rendered. This will in turn cause the get chain to propagate back in the chain, finally resulting in the nicely formatted time delta value to be written into the HTML page for user to see.

This is where the call to tick messes things up. It will be called every second, and it will call notifyPropertyChange('value') which in turn causes two propagations to occur — one back to the original Project.stateUpdated value thus overwriting it, and the other to propagate to the output template. This meant that the output value was correctly updated as time passed, but any change in the actual stateUpdated value as reported by the backend was not reflected in the human-readable output.

(I’m not sure, but I think Ember’s idea is that since I’ve overwritten the values myself it will keep them around until I call either save or rollback. I’m not sure whether it is sensible to call reload at all when you have uncommited changes in the model.)

Now that I had understood the true problem the solution came immediately. In the application I just wanted to ensure that updates on the bound value are propagated to App.FromNowView.output, which was already automatically updating when the bound value was changed. It also has to be refreshed as time progresses (“a few seconds” → “a minute”) which does not need to refresh the bound value, just the output value. The correct update sequence where display updates do not affect the actual state update time value is shown in the picture below:

How it works correctly

Now tick will only cause the rendered value to be updated while all changes in the original model are also honored. The change is trivially simple with changing the property change event fired on the output element:

tick: () ->
  @nextTick = Ember.run.later this, (() ->
    @notifyPropertyChange('output')
    @tick()), 1000

With this simple change everything was finally made good!

So what’s the lesson learned? When using Ember, you need to understand how a value is bound, to where, and what type of binding makes sense for any particular situation. Also don’t use @notifyPropertyChange indiscriminantly on values that are bound from outside the caller’s control.

Update: Ember-time itself has since been fixed. You’ll need to look at bf3383c6 or earlier commit to see the original version.




blog comments powered by Disqus