Monday, February 24, 2014

Child Polymers Should Not Access Parent Polymers


I am nearly done with my internationalization strategy for Polymer. But something about my approach is bothering me. And it is really bothering me.

Polymer and the Shadow DOM make it so hard to access the containing element and my current solution is to just have the parent element hand itself over. Specifically, my <hello-you> Polymer is handing a copy of itself to the <polymer-translate> element that it holds in its shadow DOM:
Polymer('hello-you', {
  // ...
  ready: function() {
    this.i18n = this.shadowRoot.querySelector('polymer-translate');
    this.i18n.el = this;
  },
  // ...
});
The advantage of this approach is, of course, that the <polymer-translate> child element can easily update the label field in the <hello-you> parent whenever a change is needed:
Polymer('polymer-translate', {
  // ...
  translate: function() {
    if (this.el === null) return;

    var el = this.el;
    el.hello = i18n.translate('app.hello');
    el.colorize = i18n.translate('app.colorize');
    // ...
  },
  // ...
});
Instead of passing the full object, I think I would like to try binding a variable. I am already binding the locale attribute of <hello-you> to the locale attribute of <polymer-translate>:
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="polymer-translate.html">
<polymer-element name="hello-you" attributes="locale">
  <template>
    <!-- ... -->
    <polymer-translate locale={{locale}}></polymer-translate>
  </template>
  <script src="hello_you.js"></script>
</polymer-element>
So I create the labels binding in the <hello-you> template definition:
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="polymer-translate.html">
<polymer-element name="hello-you" attributes="locale">
  <template>
    <!-- ... -->
    <polymer-translate locale={{locale}} labels="{{labels}}"></polymer-translate>
  </template>
  <script src="hello_you.js"></script>
</polymer-element>
After removing the ready() method that had passed the parent to the child, I modify the child <polymer-translate> template to accept the labels attribute:
<polymer-element name="polymer-translate" attributes="locale labels">
  <script type="text/javascript" src="../bower_components/i18next/i18next.js"></script>
  <script src="polymer_translate.js"></script>
</polymer-element>
Then I modify <polymer-translate>'s translate() method to update the bound labels variable instead of the parent element:
Polymer('polymer-translate', {
  labels: {},
  translate: function() {
    this.labels = {
      hello: i18n.translate('app.hello'),
      colorize: i18n.translate('app.colorize'),
      how_many: i18n.translate('app.how_many'),
      instructions: i18n.translate('app.instructions'),
      language: i18n.translate('app.language')
    };
  },
  // ...
});
With that, I can work back into the parent <hello-you> template to update the various labels. Instead of being direct properties of <hello-you>, the labels are now part of the labels property. So, {{colorize}} becomes {{labels.colorize}}:
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="polymer-translate.html">
<polymer-element name="hello-you" attributes="locale">
  <template>
    <div>
      <h2><span>{{labels.hello}}</span> {{your_name}}</h2>
    </div>
    <p>
      <input value="{{your_name}}">
      <input type=submit value="{{labels.colorize}}!" on-click="{{feelingLucky}}">
    </p>
    <p class=help-block>
      {{labels.instructions}}
    </p>
    <!-- ... -->
    <polymer-translate locale={{locale}} labels="{{labels}}"></polymer-translate>
  </template>
  <script src="hello_you.js"></script>
</polymer-element>
That actually solves another problem that I had with the original approach—the labels had not been visually distinct from “real” properties. Now it is quite obvious which bound variables are labels and which are direct properties.

With that, I have my translated Polymer working properly again:



This is not a wholesale success however. The second text <input> above previously bound a pluralization message to describe the number of red balloons that the user has. In other words, the <polymer-translate> child element needs access to data from the <hello-you> parent. And I am bit stumped at how best to do this.

For tonight, I bind the input field to a “private looking” property of labels:
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="polymer-translate.html">
<polymer-element name="hello-you" attributes="locale">
  <template>
    <!-- ... -->
    <p>
      {{labels.how_many}}
      <input value="{{labels.__count__}}">
    </p>
    <p>
      {{labels.count_message}} <!-- [l10n] pluralization -->
    </p>
    <!-- ... -->
    <polymer-translate locale={{locale}} labels="{{labels}}"></polymer-translate>
  </template>
  <script src="hello_you.js"></script>
</polymer-element>
Then, in the <polymer-translate> child, I have to do a bit of a dance to ensure that __count__ is not lost when assigning labels:
Polymer('polymer-translate', {
  labels: {},
  translate: function() {
    var count = this.labels.__count__;
    this.labels = {
      // ...
      __count__: count,
      count_message: i18n.translate(
        "balloon",
        { count: count ? parseInt(count, 10) : 0 }
      )
    };
  },
  // ...
});
That could probably be a little cleaner (perhaps updating the existing labels instead of assigning a new object literal each time?). Still, it works. After making that change, I can again greet Fred in French and congratulate him on his 99 ballons rouges:



This could probably use a little more thought, but I feel much better now that I am no longer allowing the child direct access to the parent. It also helps that the labels are visually distinct with this approach. So this seems like a winner.

Day #1,036

No comments:

Post a Comment