A few weeks ago, I made this nice little side-bar that allowed a user to click an element and then perform actions on that element. They could also add new elements.
The idea of showing a quick animation for when a new element is animated using Angular’s animation framework sounded easy. However, it turned out to be problematic.
The approach of adding an animation based on elements within an ng-repeat isn’t straight forward. Let’s look at the structure of a typical CSS-based Angular animation directive, or at least the one I’m interested in.
var newItem = function ($timeout) { var animation = { enter: function (element, done) { element.addClass('new-item'); $timeout(function () { element.removeClass('new-item'); done(); }, 2000); } }; return animation; }; newItem.$inject = ['$timeout']; angular.module('myApp.services') .animation('.new-list-item', newItem);
The premise is that I want to add a CSS class when an item is created and then have it removed two seconds later. It’s a nice visual clue to user for their new element.
Animations have a few different callback methods that can be used when someone enters, leaves, etc a DOM element. The available behaviors and callbacks vary based on the element to which an animation is attached. The directive depicted above is called “newItem” and we’re using the enter callback. As I mentioned, this will cause an animation to be applied when an item is created in the DOM. However the animation is applied on initial render of views that aren’t part of initial app run. It makes sense, but I had hoped it would be easy to conditionally apply the animation. The animation gets attached by the “.animation” call that specifies the class that Angular will look for. Further reading is available at the Angular website.
Here’s a fiddle illustrating the problem (click ‘Result’ to re-run it). Notice that all items are “animated” in red initially, which we don’t want.
The fix, or at least the most straight forward one, is to turn off animations temporarily. This can be accomplished with the $animate provider.
// Turn off $animate.enabled(false); // Turn on $animate.enabled(true);
One issue with this approach is that Angular doesn’t provide a built-in eventing mechanism for determining when an ng-repeat has completed rendering. So, how do we know when to turn animation off and then back on? Using an arbitrary delay time with setTimeout(..) or $timeout is something to be avoided. Fortunately, Angular does have some hooks into ng-repeat that we can utilize within a new directive. One such hook is an iterator that ng-repeat attaches to its scope called $last. The approach illustrated below utilizes this to $emit a message which our controller can listen for and turn animations back on once received.
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);
To take advantage of the directive, we need only apply it to the repeated element, turn off animations initially in our controller, and then turn them back on once the message is received. The controller code would look something like this:
$animate.enabled(false); $scope.$on('ngRepeatFinished', function () { $animate.enabled(true); });
Finally, here’s a fiddle with it all wrapped up.