Using Angular $compile to Escape Watcher Hell

This is a blog on how to handle repeating directive elements without using the ng-repeat directive when using AngularJS. We’ll keep this blog high level and assume the reader knows Angular development.

Sample:

<div ng-repeat='spell in spells'>
  <harry-potter doing='spell.name'>
    {{spell.incantation}}
  </harry-potter>
</div>

This is a blog on watcher performance and so lets take the sample above and see what happens. If the sample array spells has 7 items, ngRepeat creates (7 * 2) + 1 = 15 $watchers. This is because Angular creates watchers in its built in ng-repeat directive to support the scope which in this case is the harry-potter directive. But wait 15 watchers is fine! No need to worry!

Yes, while this is fine for small applications, it quickly digresses into an unmanageable overhead in larger apps. This is especially true when ngRepeating on directives who use ngRepeat inside themselves. It is a good rule of thumb to keep your watchers below 2,000. (as sourced from here, here, and here). Once you start getting into displaying large amounts of data such as fintech tables, commercial customers, or complex transactions; you start hitting that 2,000 watcher mark awfully quickly if you don’t manage your ng-repeats.

The problems with ng-repeat:

For problem 1, the common thing said is to avoid filtering the ng-repeat as this causes a re-paint and re-digest of the content each time. More info on Watch Digest here. Another key thing to do is to use track by which was introduced in Angular 1.2 which improves rendering performance.

track by:

<div ng-repeat='spell in spells track by spell.id'>
  <harry-potter doing='spell.name'>
    {{spell.incantation}}
  </harry-potter>
</div>

For problem 2, the root fo the problem is two-way binding, Angular then creates watchers for each scope item it uses. If you could imagine the directive harry-potter to have another ng-repeat inside of it, you would exponentially grow the number of watchers you have. There are some factors to limit the growth of the watchers (which can arise from ngIfs, any expression in your template, or even the plain old $scope.$watch) by using bind once. Angular 1.3 allows you to specify which scope items are only needed to be binded once (using the double colon :: marker). Items which are binded once means that it won’t be two way binded. If you are using an Angular version before 1.3, there is a very famous project by Pasvaz to handle this called bindonce.

bindonce:

<div ng-repeat='spell in ::spells track by spell.id'>
  <harry-potter doing='::spell.name'>
    {{::spell.incantation}}
  </harry-potter>
</div>

For problem 3, comes the hard part of the story what do you do when you only wish to programatically add new directives in your ng-repeat without hitting the digest loop for all the other elements that are already there. The trick here is to use $compile coupled with angular.element. Basically we are manually creating the new directives as if we were doing the ng-repeat ourselves similar to dynamic children in React.

$compile and angular.element:

function link(scope, iElem, attrs) {
  scope.spells.forEach((spell) => {
    // Create a Harry Potter directive in our view
    const hp = angular.element('<harry-potter doing="spell.name">{{spell.incantation}}</harry-potter>');

    // Compile view into a function
    const compiledElement = $compile(hp);

    // Bind view to scope
    const bindedElement = compiledElement(scope);

    // Attach to ng-Repeat
    iElem.append(bindedElement);
  });
}

There you have it, a hack away from making ng-repeat magical again. Angular really provides all the hooks albeit not all the documentation to support all uses cases of any application. This blog was based off a real world example but have been changed to preserve privacy.

 
116
Kudos
 
116
Kudos

Now read this

ES6 Reflect API

This is the part of a series of blogs on the new features on the upcoming ECMAScript 6 (ES6) specification which JavaScript implements. In this blog we focus on the Reflect API introduced in ES6. Reflect Object # What is the Reflect API?... Continue →