Friday, February 28, 2014

I Should Really Know By Now When Polymer Data Binding Assigns Values


Tonight, I hope to solve a minor mystery with data binding in the Dart flavor of Polymer. I have a workaround, but the problem surprised me enough that it seems worth investigating.

Things went wonky for me when I tried binding the labels property of the <hello-you> Polymer to the labels attribute of the <polymer-translate> Polymer:
<polymer-element name="hello-you" attributes="locale">
  <template>
    <polymer-translate locale={{locale}} labels="{{labels}}"></polymer-translate>
  </template>
</polymer-element>
The idea of doing this is to provide a vehicle through which the <polymer-translate> Polymer can communicate labels to apply to <hello-you> for the current locale:



And it works. To a point.

What does not work—at least not without a workaround—is watching the labels attribute for changes inside <polymer-translate>:
@CustomTag('polymer-translate-custom')
class PolymerTranslate extends PolymerElement {
  @published Map labels = toObservable({});
  // ...
  ready() {
    super.ready();
    labels.changes.listen((list){
        print('[polymer_translate] $list');
      // Never hit here :(
    });
  }
}
Since <polymer-translate> is the thing changing the labels (e.g. when the locale changes), it might seem a little strange that I want to watch for changes in there as well. In all honesty, my reasons in this case are not as pure as they should be.

It feels like changes to bound variables in Polymer really want to be unidirectional (e.g. <polymer-translate> pushing changes to <hello-you>). Whether that is right is open for debate, but I am already violating this rule by pushing a change in the opposite direction. I am pushing a single attribute change from <hello-you> to <polymer-translate>—a number that will affect a pluralization translation. When that occurs I want <polymer-tranlate> to see the change, and react by updating the other labels attributes accordingly.

But <polymer-translate> never sees the change. Unless…

Unless I wait a single browser event loop before establishing that listener:
@CustomTag('polymer-translate-custom')
class PolymerTranslate extends PolymerElement {
  @published Map labels = toObservable({});
  // ...
  ready() {
    super.ready();
    Timer.run((){
      labels.changes.listen((list) {
        print('[polymer_translate] $list');
        // Now we hit here :)
        update();
      });
    });
  }
  // ...
}
When I observe the same thing in the JavaScript version of this code, it works. Of course, the JavaScript version has observe blocks:
Polymer('polymer-translate-custom', {
  labels: {},
  // ...
  observe: {
    'labels.__count__': 'translate'
  },
  // ...
});
Wait a second… JavaScript. I remember something about object literals and shared state in the JavaScript flavor of Polymer… Dang it. I'm an idiot.

I should expect this kind of behavior when binding variables—in either Dart or JavaScript. When I bind <hello-you>'s locale property to <polymer-translate>'s locale attribute, I am replacing the initial value in <polymer-translate> with a reference to the object in <hello-you>.

When <polymer-translate> is ready(), it still thinks locale is the original value. And then <hello-you> immediately proceeds to replace it with what it thinks locale should be. And I wind up listening for changes on nothing. By waiting for one browser event loop, I give <hello-you> a chance to make that assignment, and thus my code works.

Now that I understand this, I realize that, instead of doing this when <polymer-translate> is ready(), I should do it when <polymer-translate> has enteredView() (or it is attached now?):
@CustomTag('polymer-translate')
class PolymerTranslate extends PolymerElement {
  @published Map labels;
  // ...
  enteredView() {
    super.enteredView();
    labels.changes.listen((list) {
      print('[polymer_translate] $list');
      update();
    });
  }
  // ...
}
It still seems to be called enteredView() in the version (0.9.5) that I have, but I really need to upgrade Dart as well as Polymer. Regardless, this works now. Once <polymer-translate> has entered the view—of <hello-you>—the appropriate labels are set and everything works just fine without the need to wait.

Now that I think about it, this makes complete sense. I have a hard time even recalling why I would have thought otherwise. When I assign an attribute—be it in a template or directly—I replace the original value with the newly assigned value. Not only is the value different, but any associated properties or event listeners are also different.

Sometimes it just helps to take a step back to figure these things out. Other times, it helps to make the mistake in full, public view after struggling with it for a day. Hopefully the latter results in actual learning and remembering!


Day #1,040

3 comments:

  1. It's still not very clear to me. Can you please explan this last part better? Will your book include a visual schema of this?

    ReplyDelete
    Replies
    1. Apologies. The explanations I do here are definitely practice explanations. I will certainly do better in the book. And yes, I may have to resort to some awesome crayon drawings to illustrate :)

      I'm not sure what the "last part" is, but I'll try this...

      The trick is to _not_ establish the changes listener when polymer-translate is _ready_. Rather, I wait until it has “entered the view” of hello-you.

      At the split second that polymer-translate enters the view of hello-you, hello-you replaces polymer-translate's original labels instance variable. Both hello-you and polymer-translate point to the same, exact variable (the labels variable from hello-you) by virtue of hello-you's data binding (labels={{labels}}). Thus, by establishing the changes listener in enteredView(), I am assured that nothing will replace labels with another object and my changes listener is listening to the object that will be updated.

      When I attached to labels in ready(), I attached to polymer-translate's labels, which was then completely overwritten by hello-you's labels. The listener was remained attached to the original polymer-translate labels, but both hello-you and polymer-translate were updating hello-you's labels.

      Yah, I could use a bit more practice explaining that :)

      If you are still unclear, let me know. But I'll get it right in the book.

      Delete
  2. It is kind of amusing how 'old' patterns kreep in new projects. The mantra 'add your listeners when your component enter the document' (this is directly taken from Google's Closure library docs) seem to remain valid:)

    It also seems (and this is unfortunate) that one has to read both the JS and Dart documentation for polymer to be able to create something. In this case the 'problem' you ad is very clearly stated in the documentation on polymer's web site (and has been there for a long time btw), and the logic in it is pretty clear in JS world (prototype is a shared object and thus putting object on it leads to shared states, while replacing objects on it leads to missing references), in Dart world thus should have been taken care of since there are no prototypes in Dart.....

    Good job catching this one;)

    ReplyDelete