Wednesday, January 29, 2014

Watching Polymer Attributes


I grow obsessed.

Double binding of AngularJS and Polymer attributes remains unsatisfying. Binding attributes works just when Angular is pushing changes to Polymer (there is even a nifty screencast). But if Polymer updates the same attribute, AngularJS will ignore it.

I am binding my <x-pizza> Polymer with the usual curly braces:
<pre ng-bind="pizzaState"></pre>
<p>
  <x-pizza polymer-directive state="{{pizzaState}}"></x-pizza>
</p>
Since Angular double-binding does not work out of the box with Polymer elements like <x-pizza>, I have resorted to writing that custom polymer-directive in Angular. I stayed up late last night and, instead of writing Patterns in Polymer, I came up with this version of the directive which works pretty darn well:
directive('polymerDirective', function($timeout) {
  return {
    restrict: 'A',
    link: function(scope, element) {
      // Always get updated Polymer element
      function polymer() {
        return element.parent().children()[0];
      }

      // Helper to wait for Polymer to expose the observe property
      function onPolymerReady(fn) {
        polymer().observe ?
          fn() :
          $timeout(function(){onPolymerReady(fn);}, 10);
      }

      // When Polymer is ready, establish the bound variable watch
      onPolymerReady(function(){
        // When Polymer sees a change to the bound variable,
        // $apply / $digest the changes here in Angular
        polymer().observe['state'] = function(){scope.$apply()};
        scope.$watch(
          function() {return element.attr('state');},
          function(value) {
            scope.pizzaState = value;
          }
        );
      });
    }
  };
});
There are three pieces in there that make this work. First, I need direct access to the Polymer element, not the Angular wrapped version of the element. Second, the Polymer is not ready for this directive until it has the observe property. The onPolymerReady() function polls for that property every 10 milliseconds until it exists, then calls the supplied callback. Lastly, the actual directive link goes inside that callback—when the Polymer is ready, I watch for changes on the state attribute, updating the bound variable in the current scope when it changes.

And that works fairly well. Within no more than 10 milliseconds of the Polymer being decorated with the usual methods and properties, Angular is now able to see changes and bind to changes in the Polymer:



And, if I add an ng-model, I find that Angular can still push changes down to the Polymer:



Two-way binding of a Polymer with minimal fuss. There is just one teeny, tiny problem. The observe property is probably not ideal for general usage:
polymer().observe['state'] = function(){scope.$apply()};
The observe property in Polymer is a cheap way to establish change listeners on properties in Polymer. The drawback to it is that (currently), it prevents named change listeners from firing inside the Polymer. That is, if my Polymer relied on a change listener for the state attribute:
Polymer('x-pizza', {
  // ...
  stateChanged: function(oldValue, newValue) {
     // Something very important to the internal workings of the Polymer goes here...
  },
  // ..
});
Then the very act of preventing this merely by setting observe['state'] would prevent that very important update from occurring. Not cool.

Instead of doing that, I think it best to use a bit of the Polymer platform. In this case, I use the MutationObserver polyfill:
          var observer = new MutationObserver(function() {scope.$apply()});
          observer.observe(polymer(), {attributes: true});
Mutation observers are useful for watching for any kind of change on an Element. It is possible to filter the watch for changes on an element, its children, or as I have done here, its attributes. Whenever any kind of change or changes is observed, the MutationObserver's callback is invoked.

And that does the trick nicely. Now I am safe in the knowledge that I am not interfering with any inner workings of my Polymer, but am still seeing the attribute change when it is supposed to. It would be nice if Angular and Polymer understood each other sufficiently to do this on their own, but until that happens, I am happy to have a seemingly stable solution to the problem.


Day #1,011

2 comments:

  1. I think I heard somewhere, that the Angular team was looking at integrating Polymer into the Angular framework. Hopefully this is true, and if it is, then the problem you're having won't exist in the future.

    ReplyDelete
    Replies
    1. There has definitely been discussion: https://groups.google.com/d/msg/polymer-dev/eAA0SeVu-8E/cG2XZyF7HZcJ. Until then, I'm pretty OK with this solution. It is similar-ish to the Dart solution (https://github.com/dart-lang/angular_node_bind) -- it just needs to be generalized a tad. Maybe something for tomorrow night :)

      Delete