Monday, January 27, 2014

Better Angular $apply and Polymer


OK. One more crack at it. Just one more.

I continue to struggle getting AngularJS to double bind variables from Polymer. I am working on an <x-pizza> Polymer that builds an SVG representation of a pizza:



Additionally, I have it publishing the current state of the pizza in an state attribute so that the element ends up looking something like:
<x-pizza state="pepperoni,green peppers
sausage
sausage"><x-pizza>
In the Angular application, I have a directive that tries to bind that state so that it can be reflected elsewhere in the application:
<pre ng-bind="pizzaState"></pre>
<p>
  <x-pizza polymer-directive state="{{pizzaState}}"></x-pizza>
</p>
But Angular refuses to bind that variable—not without some manual intervention.

In my custom polymer-directive, I would expect that I can $observe() or $watch() for changes on attrs.state, but that does me no good. However Angular is compiling the attributes, it is not able to see subsequent updates from Polymer.

Actually, it can read the updated attributes via element.attr('state'), but it cannot listen for changes from Polymer. So I exploited that last night by setting a regular Angular $interval to $apply() the usual Angular event loop—the same event loop that a change in a normal element would trigger automatically. And that worked, save for either the delay in the $interval() or higher than normal load as the $interval() reran $apply().

So tonight, I try for something a little less broad. Instead of re-rerunning $apply(), I only do so when there is an event in the Polymer that contains my directive:
var pizzaStoreApp = angular.module('pizzaStoreApp', [ /* ... */ ]).
directive('polymerDirective', function($interval, $timeout) {
  return {
    restrict: 'A',
    link : function(scope, element, attrs) {
      scope.$watch(
        function() {
          return element.attr('state');
        },
        function(value) {
          scope.pizzaState = value;
        }
      );

      element.on('click keyup', function(){
        scope.$apply();
      });
      // $interval(function(){},500);
    }
  };
});
But that does not work. What is interesting about that not working is that the element—my <x-pizza> Polymer—does not appear to be generating events that Angular's element can see. But it is generating events that the rest of the page sees. And it is not as if Angular is looking at the wrong element—after all, I can get the current attribute value through element. Somehow, Polymer seems to be confusing part of Angular's element and attr. I hate to put out a solution in Patterns in Polymer that involves such flakiness, but it is starting to seem like I am going to have to accept some level of oddness.

Anyhow, even though I cannot listen for events on element, I can listen for events on element.parent():
      // ...
      element.parent().on('click keyup', function(){
        scope.$apply();
      });
      // ...
That works fairly well. Whenever there is a click or keyup event in my Polymer, the Angular directive runs through the $apply() / $digest() loop, which includes the watcher for changes to element.attr('state').

I do find that this works best if I yield the browser event loop for one cycle, giving the Polymer a chance to fully update its internal state as well as its attribute. The easiest way to accomplish this in Angular is with a simple $timeout for zero seconds—it doesn't even need to do anything, just wait:
      // ...
      element.parent().on('click keyup', function(){
        $interval(function(){},500);
      });
      // ...
With that, I have my Angular application more or less updating only when changes might reasonably occur in my Polymer:



About the only thing that does not work in this scenario is if the Polymer were to generate some kind of update in response to something aside from a user action. I do not see anyway around this other than listening for some kind of custom change event or by accepting some delay in notification to the Angular application when using an $interval. This is pretty darn close, but… I may actually take one more day to try to solve this.

Day #1,009

No comments:

Post a Comment