Thursday, February 27, 2014

Double Binding Observables in Polymer Dart


Tonight, I hope to apply the lessons learned with my JavaScript approach to internationalization of Polymer back in Dart.

Over the course of the past week or so, this exercise has morphed from an exploration of i18n into an exploration of using strategies in Polymer and, last night, into an exploration on how communication should work between Polymers. Last night's solution eliminates all message passing (in the Smalltalk sense, at least). Instead, I was able to rework my translation strategy such that it relied entirely on bound variables:
<polymer-element name="hello-you" attributes="locale">
  <template>
    <!-- ... -->
    <polymer-translate-custom locale={{locale}} labels="{{labels}}"></polymer-translate-custom>
  </template>
</polymer-element>
Both <hello-you>, which is defined by this template, and the <polymer-translate-custom> Polymer, which is used by <hello-you>, have a locale attribute. The template binds <hello-you>'s locale to <polymer-translate-custom>'s locale attribute. The end result is that when <hello-you> changes its locale, then <polymer-translate-custom> immediately knows about it. And, when <polymer-translate-custom> changes its locale, <hello-you> immediately knows about it. The same goes for labels.

The thing about these two attributes is that they are only changed in one place each. The locale attribute only changes inside <hello-you>, when the user changes the value in the “Language” drop-down list:



Similarly, the labels are only changed inside of <polymer-translate-custom> (when a change in locale is noted).

So it is almost like the bound variables are unidirectional mechanisms for communicating states. Actually, that is exactly what they are in the case. Since this is the first time I have noticed this, I have no idea if this is a useful pattern (i.e. the type that ought to make it into Patterns in Polymer). But the idea is tantalizing. All the more so because it would seem to transcend language and work in Polymer.dart just as easily as in PolymerJS.

To put that theory to the test, I convert the Dart implementation of <hello-you> to use this approach (previously <hello-you> would send <polymer-translate-custom> the translate() message). That goes very smoothly except for two minor things.

First, in the <hello-you> template, I have to use proper Map indexing (with square brackets and strings):
    <-- ... -->
    <p class=help-block>
      {{labels['instructions']}}
    </p>
    <p>
      {{labels['how_many']}}
      <input value="{{count}}">
    </p>
    <p>
      {{labels['count_message']}}
    </p>
    <-- ... -->
Not too horrible in the grand scheme of things, but not quite as nice as JavaScript's dot notation for object literals:
    <!-- ... -->
    <p class=help-block>
      {{labels.instructions}}
    </p>
    <p>
      {{labels.how_many}}
      <input value="{{balloon_count}}">
    </p>
    <p>
      {{labels.count_message}}
    </p>
    <!-- ... -->
The other problem is that I cannot call the translate() method “translate” in <polymer-translate-custom>. Apparently there is a translate attribute. Grr....

Well, since the method is effectively private anyway, I suppose it does not matter too much. So I fall back to the more generic “update”:
@CustomTag('polymer-translate')
class PolymerTranslate extends PolymerElement {
  @published String locale = 'en';
  @published Map labels = {};
  Map translations = {};
  // ...
  update() {
    if (!translations.containsKey(locale)) return;

    var l10n = translations[locale];
    labels['hello'] = l10n['hello'];
    labels['colorize'] = l10n['colorize'];
    labels['instructions'] = l10n['instructions'];
    labels['how_many'] = l10n['how_many'];
    labels['language'] = l10n['language'];
  }
}
Those two issues aside, the conversion goes very smoothly. I also realize the same decrease in the size, the scope, and the complexity of <hello-you> now that it no longer needs to worry about telling <polymer-translate-custom> when to translate—or even be aware of the existence of <polymer-translate-custom> at all.

Except…

I finally realize that I have a tiny problem. The communication is not quite unidirectional. I have been updating the number of balloons—needed for the pluralized message—as “__count__” in the labels. The idea is that the surrounding double underscored indicate that this attribute is exceptional. Unfortunately, this change does not seem to make it to <polymer-translate-custom>. That change registers in <hello-you>:
@CustomTag('hello-you')
class HelloYou extends PolymerElement {
  // ...
  @observable Map labels = toObservable({
    'hello': 'Hello',
    'colorize': 'Colorize'
  });
  // ...
  ready() {
    super.ready();

    changes.listen((list){
      list.
        where((r)=> r.name == #count).
        forEach((_){
          labels['__count__'] = count;
          print(labels); // This runs and shows the updated count
        });
    });
  }
  // ...
}
But the change to labels is not seen in <polymer-translate-custom>:
@CustomTag('polymer-translate-custom')
class PolymerTranslateCustom extends PolymerElement {
  @published Map labels = toObservable({});
  // ...
  ready() {
    super.ready();
    changes.listen((list){
      print(list); // Never hit here...
    });
  }
}
And this has me stumped.

Update: Figure it out. Kind of.

If I add a simple Timer.run() (i.e. defer execution for one event loop), then it works as desired:
@CustomTag('polymer-translate-custom')
class PolymerTranslateCustom extends PolymerElement {
  @published Map labels;
  // ...
  ready() {
    super.ready();
    Timer.run((){
      labels.changes.listen((list) {
        print('[polymer_translate] $list');
        update();
      });
    });
  }
  // ...
}
I believe what is happening here is that, by virtue of the assignment-by-data-binding in <hello-you>'s template, <polymer-translate-custom>'s labels property is replaced completely. By waiting for the event loop, I give that assignment a chance to occur after which I can listen for changes.

I could be wrong about the explanation, but the result is that, with Timer.run() the changes are seen and without, the changes are not seen.


Day #1,039

2 comments:

  1. Hi to all ...

    I trust all is well, overall. On this topic: after following a few of these examinations and a multitude or my own trials, with three or four stackoverflow queries ...

    I pretty much perceive the {{current}} DART implementation of an "on-change" property and event management to be kind of like Russian diplomacy in the Chrimea. Good spin; no traction; lots of force.

    ReplyDelete
    Replies
    1. My error ... any solution that need a timer is not an event based solution. Mea culpa, I figured we were past 1940-s computer science by now. Signed, very disappointed.

      Delete