Sunday, January 26, 2014

Holy Cow: Binding Angular to Polymer Take 2


I really hoped to finish writing the SVG chapter in Patterns in Polymer last night. As it turns out, AngularJS had other ideas. Like not working at all like I expected.

Last night's question was how can I bind an AngularJS application so that it has access to data inside a Polymer? I have already done this in the Dart versions of both libraries, so I thought this would be an easy question to answer in the JavaScript equivalents. Thanks to angular_node_bind in Dart, I could use square brackets to establish two-way binding between Polymer and Angular:
<h3>Oooh! Let's make beautiful pizza together</h3>
<pre>{{ pizza }}</pre>
<p>
  <x-pizza pizzaState="[[ pizza ]]"></x-pizza>
</p>
That did not work in JavaScript. I note that there was a long discussion on the subject some time ago—and the syntax for Dart's angular_node_bind seems to have been at least partially inspired from that post. But, if there a JavaScript equivalent out there, my Google-fu fails me.

I next tried to write my own directive to accomplish this. Even if it is not a generalized solution (and I still suspect there is already one out there somewhere), this should not be too difficult, right? I write the JavaScript version as:
<h3>Oooh! Let's make beautiful pizza together</h3>
<pre ng-bind="state"></pre>
<p>
  <x-pizza polymer-directive state="state"></x-pizza>
</p>
The state variable in both cases can be two-way bound if that polymer-directive scopes the attribute with =:
var pizzaStoreApp = angular.module('pizzaStoreApp', [ /* Angular modules here... */ ]).
directive('polymerDirective', function($interval) {
  return {
    restrict: 'A',
    scope: { state: '=' },
    // ...
  };
});
That should make an attribute directive (“A”) that binds the value of state in the local scope with the value of the same name in the parent scope. From there, I can establish a simple watch on the state attribute from the <x-pizza> Polymer:
directive('polymerDirective', function($interval) {
  return {
    restrict: 'A',
    scope: { state: '=' },
    link : function(scope, element, attrs) {
      scope.$watch(
        function() { // The value being watched..
          return element.attr('state');
        },
        function(value) { // What to do when the value changes ...
          scope.state = value;
        }
      );
    }
  };
});
But darn it, that does not work. The <x-pizza> Polymer is correctly updating the value of the state attribute, but Angular is not acknowledging the change:



It turns out that Angular is not calling the $digest event loop, and so the $watch is never being evaluated. If I add an <input ng-model>—even one that is not tied to a model used anywhere else—then typing in that input field will invoke $digest, which, in turn, calls my $watch:



So my ultimate solution is to set an interval in my digest. By default, $interval will evaluate the digest loop, so the interval function does not need to do anything:
directive('polymerDirective', function($interval) {
  return {
    restrict: 'A',
    scope: { state: '=' },
    link : function(scope, element, attrs) {
      scope.$watch(
        function() {
          return element.attr('state');
        },
        function(value) {
          scope.state = value;
        }
      );

      $interval(function(){},500);
    }
  };
});
And that seems to do the trick:



But is that the best way to do this?


Day #1,008

3 comments:

  1. Don't know if you've thought of it, but another way Polymer and Angular can talk to each other is through events. You probably have though, and if it's like D-Bus you want to reduce your use of events otherwise you get a sluggish app.

    ReplyDelete
    Replies
    1. I thought about that, and I'm pretty sure it would work, but... it just doesn't feel like the Angular way. I suppose that doesn't make that much of a difference for a book named Patterns in Polymer, but still :-\

      Delete
    2. Yeah I can understand that. Also, after posting my last comment, I actually realised, in Dart at least, I think Observable uses events anyway.

      Delete