Saturday, May 3, 2014

Refactoring Angular Directives that Use Custom Elements


I have my hard-earned AngularJS and Polymer test. Now it's time to make use of it by refactoring the code it tests.

First, I ought to remark that the test was not that hard to write. I started out about 2 weeks ago to refactor the code in angular-bind-polymer. Most of the effort since has been for fun—or more accurately to satisfy various bits of curiosity along the way. I am currently between books, which affords me a little more time for such activity, but all good things must end.

So tonight, I see if, as I surmised two weeks ago, I can remove code from the angular-bind-polymer directive that polls for Polymer decorations on the target Polymer. What's especially crazy is that I suspect the things that made the test so hard to write these last two nights can go away without the polling for Polymer decorations, but I get ahead of myself.

The angular-bind-polymer library is an Angular directive that enables Angular to bind scope variables to custom element attributes. So if my Angular code has an answered variable in the current scope, I can bind it to a Polymer custom element by injecting the angular-bind-polymer library and assigning the bind-polymer attribute to the custom element:
<x-double bind-polymer in="2" out="{{answer}}"></x-double>
<pre ng-bind="answer"></pre>
Last night's test does pretty much the same thing and is conceptually fairly simple:
describe('Double binding', function(){
  // Build in setup, check expectations in tests
  var ngElement;

  // Load the angular-bind-polymer directive
  beforeEach(module('eee-c.angularBindPolymer'));

  beforeEach(inject(function($compile, $rootScope, $timeout) {
    // Setup the angular element, the polymer element, 
    // and do a lot of manual manipulation of
    // angular internals here....
  }));

  // The actual test
  it('sees values from polymer', function(){
    expect(ngElement.innerHTML).toEqual('4');
  });
});
I load the directive, do a bunch of setup and test that my angular element sees the correct result from the Polymer element. Conceptually, simple, but as I found last night, difficult in practice. This is mostly due to some difficult to understand angular manipulation. And when I say difficult to understand, I don't mean that I am an angular god and would find it difficult to explain to beginners—I mean I don't understand it. Mostly the last bit in the test setup:
  beforeEach(inject(function($compile, $rootScope, $timeout) {
    // Container to hold angular and polymer elements
    var container = document.createElement('div');
    container.innerHTML =
      '<pre ng-bind="answer"></pre>' +
      '<x-double bind-polymer in="2" out="{{answer}}"></x-double>';
    document.body.appendChild(container);

    // The angular element is the first child (the <pre> tag)
    ngElement = container.children[0];

    // Compile the document as an angular view
    $compile(document.body)($rootScope);
    $rootScope.$digest();

    // Must wait one event loop for ??? to do its thing
    var done = false;
    setTimeout(function(){ done = true; }, 0);
    waitsFor(function(){ return done; });

    // ??? has done its thing, so flush the current timeout
    runs(function(){ $timeout.flush(); });
  }));
As I mentioned from the outset, I think this strange need for Jasmine timeout/waits/runs is due to my polling for Polymer. So I remove it from the directive code:
angular.module('eee-c.angularBindPolymer', []).
directive('bindPolymer', function($q, $timeout) {
  return {
    restrict: 'A',
    link: function(scope, element, attrs) {
      // Initialization code, and...
      // The onPolymerReady function to poll for polymer
      // decorations on the custom element

      // When Polymer is ready, establish the bound variable watch
      // onPolymerReady().
      //   then(function(){
          // When Polymer sees a change to the bound variable,
          // $apply / $digest the changes here in Angular
          var observer = new MutationObserver(function() {
            scope.$apply();
          });
          observer.observe(polymer(), {attributes: true});

          for (var _attr in attrMap) {
            scope.$watch(
              function() {return element.attr(_attr);},
              function(value) {
                scope[attrMap[_attr]] = value;
              }
            );
          }
        // });
    }
  };
});
With that, my test immediately fails with:
        Error: No deferred tasks to be flushed
            at Function.self.defer.flush (/home/chris/repos/angular-bind-polymer/bower_components/angular-mocks/angular-mocks.js:119:15)
            at Function.$delegate.flush (/home/chris/repos/angular-bind-polymer/bower_components/angular-mocks/angular-mocks.js:1642:20)
            at null. (/home/chris/repos/angular-bind-polymer/test/BindingSpec.js:52:31)
But this is simply because I have that bothersome $timeout flushing in my test. Now that I can (hopefully) remove the polling for Polymer attributes, I can remove the $timeout flushing as well.

Unfortunately, I still require a Jasmine waitsFor(), but this is for my example Polymer element to update itself, not for any obscure Angular / Polymer interaction. So the entire test becomes:
describe('Double binding', function(){
  // Build in setup, check expectations in tests
  var ngElement;

  // Load the angular-bind-polymer directive
  beforeEach(module('eee-c.angularBindPolymer'));

  beforeEach(inject(function($compile, $rootScope) {
    // Container to hold angular and polymer elements
    var container = document.createElement('div');
    container.innerHTML =
      '<pre ng-bind="answer"></pre>' +
      '<x-double bind-polymer in="2" out="{{answer}}"></x-double>';
    document.body.appendChild(container);

    // The angular element is the first child (the <pre> tag)
    ngElement = container.children[0];

    // Compile the document as an angular view
    $compile(document.body)($rootScope);
    $rootScope.$digest();

    // Must wait one event loop for Polymer to do its thing
    var done = false;
    setTimeout(function(){ done = true; }, 0);
    waitsFor(function(){ return done; });
  }));

  // The actual test
  it('sees values from polymer', function(){
    expect(ngElement.innerHTML).toEqual('4');
  });
});
I think I can live with that. It still requires a fair amount of setup, but it is typical Angular test setup—building the HTML, compiling it, and processing it. The only nod to Polymer or anything else out of the ordinary is the zero second timeout, which simply waits one browser event loop so that Polymer can update the custom element. Since this is a common Polymer testing practice (at least in my experience), I am quite happy to live with this.

And it seems that I can release a new version of angular-bind-polymer with a little less code, which is always nice.



Day #53

No comments:

Post a Comment