Detecting every time NgRepeat is Redrawn

Home / Detecting every time NgRepeat is Redrawn

Quite a while back, I wrote a blog post that detailed how it’s possible to use a directive to determine when an Angular repeater is finished. My solution, like nearly every other solution I looked at has one major flaw. It’s only triggered on first render.


That blog post suggested taking advantage of a directive that inspects ng-repeat’s $last object. The directive is below since it’s pretty small.

var onRepeatFinish = function ($timeout) {
    var directive = {
        restrict: 'A',
        link: function (scope, element, attr) {
            if (scope.$last === true) {
                $timeout(function () {
                    scope.$emit('ngRepeatFinished');
                });
            }
        }
    };
    return directive;
};
 
onRepeatFinish.$inject = ['$timeout'];
angular.module("myApp.directives")
    .directive('onRepeatFinish', onRepeatFinish);

But, as I mentioned, it fires only on initial render. Subsequent updates to the underlying bound array generally do not re-trigger the directive. This makes some sense since all linking and compiling has already occurred. For my purposes, as outlined in that other blog post, this was actually fine and was my intended use. I was using it, effectively, as a one-time toggle.

However, in a current project, I’m working with tables that have “sticky” or fixed position headers. I need to resize the headers any time the underlying data changes. This is due to the fact that the tables are auto-sized and the tbody column widths change based on bound data. This left me in a bit of a conundrum since the previous “onRepeatFinish,” which I thought would solve my problem, did not.

After a bit of digging, I discovered that what I need can be accomplished with triggered watchers. The tables that I’m rendering are rendered via a custom table directive. As such, I already have a collection watcher to know when the underlying bound array changes. Tapping into this, I can trigger a watch, as needed, of the DOM to determine when the ng-repeat used in the table directive completes. Once complete, the triggered watcher can be unwatched.

Another mechanism that makes this possible is Angular’s $evalAsync. This is similar to using a $timeout, but it ensures that the method that’s being evaluated executes after all templates are rendered, including all ng-repeat nodes, due to the way it works with the digest cycle. Here’s a snippet from the Angular docs:

$evalAsync() is a new function which was first introduced in AngularJS 1.2.X, and for me it’s the “smarter” brother of $timeout().
Before $evalAsync() was introduced, Officially answered by the Angular team, when you have issues with cycles and want to reflect changes from outside the “Angular world”, use $timeout().

After Angular has evolved and more users have experienced this known issue, the Angular team has created the $evalAsync(). This function will evaluate the expression during the current cycle or the next. The Angular docs state this:

The $evalAsync makes no guarantees as to when the expression will be executed, only that:

  • it will execute after the function that scheduled the evaluation (preferably before DOM rendering).
  • at least one $digest cycle will be performed after expression execution.

Any exceptions from the execution of the expression are forwarded to the $exceptionHandler service.

Note: if this function is called outside of a $digest cycle, a new $digest cycle will be scheduled. However, it is encouraged to always call code that changes the model from within an $apply call. That includes code evaluated via $evalAsync.

With that in mind, using this bit of code within my array watcher/directive, I can trigger when to redraw the table headers reliably:

$scope.$watchCollection('records', function () {
    // Watch for our DOM element's children to change
    var watch = $scope.$watch(function () {
        return $scope.element.children().length;
    }, function () {
        // Once change is detected, use $evalAsync to wait for
        // directives to finish
        $scope.$evalAsync(function () {
            tblCtrl.repeatFinish();

            // Unwatch the DOM element's children
            watch();
        });
    });
});

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.