Wednesday, April 9, 2014

Working (with Hacks) Polymer.dart Js-Interop


All right, Polymer.dart what gives with your js-interop data binding?

I know that it works (a least partially) because my Dart <x-pizza> element can set the button label in the JavaScript <x-pizza-toppings> element:
<link rel="import" href="vulcanized.html"><!-- JS x-pizza-toppings -->
<polymer-element name="x-pizza">
  <template>
    <!-- ... -->
    <x-pizza-toppings id="firstHalfToppings"
                      name="First Half Toppings"
                      ingredients="{{ingredients}}"></x-pizza-toppings>
    <!-- ... -->
  </template>
  <script type="application/dart;component=1" src="x_pizza.dart"></script>
</polymer-element>
The name attribute in the JavaScript <x-pizza-toppings> element then becomes the button label on the pizza toppings chooser and is working just fine:



The problem is the ingredients binding. In the Dart element, this is set to a list of strings that describe toppings that a prospective pizza eater might want:
@CustomTag('x-pizza')
class XPizza extends PolymerElement {
  final List<String> ingredients = [
    'pepperoni',
    'sausage',
    'green peppers'
  ];
  // ...
}
This Dart list in <x-pizza> should be bound to the ingredients attribute in the JavaScript <x-pizza-toppings> element by virtue of the mustache binding in <x-pizza>'s template:
    <x-pizza-toppings id="firstHalfToppings"
                      name="First Half Toppings"
                      ingredients="{{ingredients}}"></x-pizza-toppings>
This is Polymer's way of telling <x-pizza-toppings> that it should replace whatever it defaults to for its own ingredients attribute/property with the value from the containing <x-pizza>. And in fact, this works swimmingly when both <x-pizza> and <x-pizza-toppings> are Dart or are both JavaScript. But somehow it is not working when the former is Dart and the latter is JavaScript.

I eventually track this down in the JavaScript console:
> dart_el = document.querySelector('x-pizza');
i {impl: x-pizza, parentNode_: undefined, firstChild_: null, lastChild_: null, nextSibling_: undefined…}
> js_el = dart_el.shadowRoot.querySelector('x-pizza-toppings')
i {impl: x-pizza-toppings#firstHalfToppings, parentNode_: b, firstChild_: null, lastChild_: null, nextSibling_: c…}
js_el.ingredients
"[pepperoni, sausage, green peppers]"
Do you see the problem? It took me much longer than I care to admit to notice.

The ingredients property is, in fact set. It even contains the correct data. But it is a single string, not a list of strings. Instead of "[pepperoni, sausage, green peppers]" I should be seeing something like ["pepperoni", "sausage", "green peppers"].

To resolve this little problem, I try many things. Many things. Among the ones that I would hope to work are using hinting in the JavaScript element:
Polymer('x-pizza-toppings', {
  // ...
  ready: function() {
    this.model = [];
    this.ingredients = [];
  },
  // ...
}
That has no effect—the value still comes through as a string instead of a list.

I also try to explicitly bind a JavaScript array from Dart by binding a new jsIngredients getter in the <x-pizza> template:
    <x-pizza-toppings id="firstHalfToppings"
                      name="First Half Toppings"
                      ingredients="{{jsIngredients}}"></x-pizza-toppings>
And then “jsifying” the ingredients in that getting:
@CustomTag('x-pizza')
class XPizza extends PolymerElement {
  final List<String> ingredients = [
    'pepperoni',
    'sausage',
    'green peppers'
  ];

  get jsIngredients => new JsObject.jsify(ingredients);
  // ...
}
But I still get a string instead of a list inside JavaScript.

As is often the case with these things, I try something silly when I am just about to give up. And it works. Instead of binding a list, I find that I can bind JSON version of that list. I convert the jsIngredients getter to encode as JSON:
  get jsIngredients => JSON.encode(ingredients);
With that, I am binding a JSON version of my list in the template:
<x-pizza-toppings id="firstHalfToppings"
                      name="First Half Toppings"
                      ingredients="{{jsIngredients}}"></x-pizza-toppings>
And somehow that works. The options list in my JavaScript element are now populated. I can select values from them. Events are propagated back up to the main <x-pizza> element and the pizza state is updated properly:



In other words, that JSON binding, which I tried on a complete lark, solved the last of my problems. I have a complete Polymer js-interop solution working. Yay!

It may be working, but there are some definite kinks that need to be worked out. As I noted last night, I cannot use this at all in Dartium, but instead have to compile the whole thing to JavaScript. This is not horrible, but there should be no need. And this JSON version of the list is wrong. If it works OK in pure Dart or pure JavaScript, then I ought to be able to bind a list to JavaScript attribute.

Ah well, it's not a prerelease for nothing. I will try to submit some bug reports tomorrow. For now, this is a fine point to stop blogging and start writing some Patterns in Polymer!



Day #29

3 comments:

  1. Glad it at least works, after a fashion.

    ReplyDelete
  2. I may be mistaken, but shouldn't that be: """final List ingredients = toObservable([ ... ]);"""?

    ReplyDelete
    Replies
    1. It looks like I was a little confused on this post.

      There is no need to mark a final list as observable -- it is final and won't be changing. But there is also no reason to have difficulty binding it in a Polymer template.

      I think I mean to JSON.encode something other than that List. And yes, you are correct, whatever that something else was would have to be observable.

      Sorry for the confusion :-\

      Delete