Animate Your Angular Application

(Brought to you by the Angular Escape Plan. Try a few Angular lessons for free.)

When an Angular app refreshes the DOM, the default experience is jarring because Angular dumps the elements into the view with no transition.

The good news is Angular comes with great support for animations. The bad news is, it might not be exactly what you expected. Angular doesn't do animations for you, instead it provides events you can hook into with your own, custom animations.

Understanding $animate and the ngAnimate module

In a non-Angular Javascript app, you write the code that updates the DOM, so while you're writing that code you sprinkle in some custom animations, NBD. But in an Angular app you're often using built-in directives and not mucking around in the DOM directly.

So what's a developer to do?

How would animations work in a web application if you weren't using Angular?

You would:

And you're either doing this with Javascript or CSS.

When you add animations to your Angular application, you follow this pattern, but in a very Angular-y way that completely decouples your animation code from your directive code.

This is a good thing.

Angular's built-in directives are pre-wired for animations. What that means is you get access to animation "events" which you can hook into with either CSS classes or Javascript code. These events correspond with adding or removing elements or classes — basically anything that might make sense to animate.

That might sound a bit weird to you right now, but the upside is you can create your own custom directives and then let end users of those directives define their own animations.

Code reuse FTW.

This is exactly how Angular designed its own directives. This way developers and designers get to choose animations because they aren't predefined by Angular. And you can create the animations any way you like, with CSS transitions/animations or a Javascript library.

Build your own directive

It's easier to understand how all the pieces fit together if you build and animate a simple custom directive you write yourself. Then you can circle back and understand exactly what's happening in the built-in directives.

Here is a simple directive for hiding an element with no support for animations:

app.controller("example", function($scope){
    $scope.awesome = false;
});
  
app.directive("myHide", function(){
    return {
        restrict: 'A',
        link: function(scope, elem, attrs){
            scope.$watch(attrs.myHide, function(value){
                if (value) {
                    elem.addClass("hide");
                } else {
                    elem.removeClass("hide");
                }
            });
        }
    };
});
  

The myHide directive watches an expression, (in this case the value of 'awesome'), and when the expression evaluates as truthy, adds a class to the element and removes it when it's false. The class sets display to none, so the myHide element is hidden when the expression is truthy.

  <div class="myHideExample" ng-controller="example">
    <div class="message">
      <p my-hide="awesome">Hide this text if awesome</p>
    </div>
    <button class="button" ng-click="awesome = !awesome">Toggle awesomeness</button>
  </div>  
  

Hide this text if awesome

This works, but there is no transition, it just pops in and out of existence.

Add an animation to this directive without $animate

Since Angular animations work by adding CSS classes to elements only during key events, (or triggering Javascript callbacks, we'll cover that in a bit), let's add a simple fade animation to our directive with the constraint that the Javascript can only add or remove CSS classes. The Javascript will not know what the animation does, and if the CSS classes are not defined, the directive will still work but with no animations.

This should provide some illumination into how $animate works.

The animation for myHide will fade the opacity of the element from 1 to 0, (and back when the state is toggled). At the end of the animation, display should be set to none.

And this presents an interesting problem, because you can't set display to none until the end of the animation — otherwise the whole animation is running while the element isn't visible. Oops. So you will need one CSS class to represent the transition/animation and another CSS class to set display to none after everything is done.

The CSS so far would look like ...

//the final state
.hide {
    display: none;
}
//the animation
.hide-add-start {
    transition: opacity 1s;
    opacity: 0;
}
  

And then a few lines of Javascript in the directive to add the classes at the right time

// add this first to start the animation
elem.addClass("hide-add-start");
setTimeout(function() {
    // add the hide class after animation is finished
    elem.addClass("hide");
    // clean up
    elem.removeClass("hide-add-start");
}, 1000);
  

So the .hide-add-start class adds the transition and the final value, and then after the transition is done the .hide class is added which sets display to none.

The CSS for removing and animating the hide class

.hide-remove {
    transition: opacity 1s;
    opacity: 0;
}
.hide-remove-active {
    opacity: 1;
}
  

To animate when the .hide class is removed, the first step is to set the opacity to 0. What would happen if you removed the hide class without setting the opacity to 0? The element would just pop in with no animation at all.

In order to create a transition from an opacity of 0 to 1, you need another class to define the end state. So one class will define the start state plus transition/animation and another will define the end state to animate to.

The Javascript is almost exactly the same as adding the .hide class, but there are now two classes.

elem.addClass("hide-remove");
elem.removeClass("hide");
  // cause a reflow
elem[0].offsetHeight;
elem.addClass("hide-remove-active");
setTimeout(function(){
    elem.removeClass("hide-remove");
    elem.removeClass("hide-remove-active");
}, 1000);

The line between removing .hide and adding .hide-remove-active causes a reflow. Without it, the browser doesn't apply the transition and the element just pops in.

And the final result ...

Hide this text if awesome

And now you know how $animate and ngAnimate module work

Adding animations to your directive is not as simple as adding and removing a class. You need to know when the animation starts, when it ends, what the start and end states are, and the Javascript needs to coordinate all of it. And that's exactly what $animate does.

The $animate service has methods for adding/removing classes and elements. When you use these methods in your directive, Angular automatically adds and removes classes to the element you are animating.

It adds and removes classes at correct times so you can define start and end states. Not only that, it reads the times from your CSS, so you define the timing in one place.

Rewrite the directive to use $animate

The $animate service has several methods for adding, removing, moving elements or adding or removing classes. The idea is to use those methods instead of directly manipulating the DOM and let Angular trigger Javascript animations or add/remove the extra CSS classes you need.

You can inject the $animate service without loading ngAnimate and everything will still work, it just won't trigger your animations. This is great because you can now create custom directives that will work even if animations aren't defined or used.

If you do want animations to be activated, you have to download the ng-animate module Javascript (however you want) and include the ng-animate module in your app, like so:

var app = angular.module('animations', ['ngAnimate']);
  

With $animate, the new version of the myHide directive now looks like this.

app.directive("myHide", function($animate){
    return {
        restrict: 'A',
        link: function(scope, elem, attrs){
            scope.$watch(attrs.myHide, function(value){
                if (value) {
                    $animate.addClass(elem, "hide");
                } else {
                    $animate.removeClass(elem, "hide");
                }
            });
        }
    };
});

The CSS is going to be slightly different. In addition to the actual class you are adding to your element, addClass and removeClass add two additional classes: one for the animation and the starting styles and one more for the end styles. Both of these extra classes are removed at the end.

The CSS classes added follow a naming convention. So the class you are adding in this case is "hide" so $animate will add a "hide-add" class where you should define the animation and the starting styles and then a "hide-add-active" class which will contain any end styles.

Here is a screenshot of the docs which explains exactly what extra classes are created, the naming convention, and when each of the classes is added.

With that in mind, the CSS looks like:

.hide-add {
    display: block;
    transition: opacity 1s;
    opacity: 1;
}
.hide-add-active {
    opacity: 0;
}
.hide-remove {
    transition: opacity 1s;
    display: block;
    opacity: 0;
}
.hide-remove-active {
    opacity: 1;
}  
  

The "hide-add" class sets display to "block" because the "hide" class is added at the same time and it sets the display to "none". Not what we want.

The end result using ngAnimate

Hide this text if awesome

Even though this directive is only adding a class, $animate also supports methods for other operations on the DOM which will also add/remove CSS classes for you so you can animate basically anything in your Angular app.

Most of the built-in directives use $animate to do DOM manipulation, so that means you can animate those as well. The list of built-in directives that use $animate and what they support is here.

ngAnimate and Javascript animations

You can use Javascript animations instead of CSS animations/transitions. The following examples use TweenMax, but you can use whatever library you want.

In addition to adding CSS classes, the $animate service will also trigger any Javascript animations you define in your app.

app.directive("myHideJs", function($animate){
    return {
        restrict: 'A',
        link: function(scope, elem, attrs){
            scope.$watch(attrs.myHideJs, function(value){
                if (value) {
                    $animate.addClass(elem, "hide-js");
                } else {
                    $animate.removeClass(elem, "hide-js");
                }
            });
        }
    };
});


app.animation('.hide-js-animated', function(){
    return {
        addClass: function(element, className){
            TweenMax.to(element, 1, {
                'opacity': 0
                });
        },
        removeClass: function(element, className){
            TweenMax.to(element, 1, {
                'opacity': 1
            });
        }
    }
});

You can see, in the directive you use the $animate service the same way you use it for CSS animations. There's no difference.

Below the directive is the animation. The animation is named using a simple, single CSS class selector. Any element that uses this animation must have this class, otherwise it won't be animated.

The object returned by the animation call defines two properties, addClass and removeClass. These are defined because the directive is using addClass and removeClass. If the directive was removing or adding elements instead, you would define the 'leave' or 'enter' properties.

The full list of events is here, in the "JavaScript-defined Animations" section.

Here is the Angular template using the Javascript animation. Note the class on the element that will eventually be animated matches the name of the animation defined by the Angular app.

<div class="myHideExample" ng-controller="example">
  <div class="message">
    <p my-hide-js="awesome" class="hide-js-animated">Hide this text if awesome</p>
  </div>
  <button class="button" ng-click="awesome = !awesome">Toggle awesomeness</button>
</div>

Hide this text if awesome

Animating built-in directives

Most of the built-in directives use $animate just like the myHide directive. Just look at the code for ngHide:

var ngHideDirective = ['$animate', function($animate) {
  return {
    restrict: 'A',
    multiElement: true,
    link: function(scope, element, attr) {
      scope.$watch(attr.ngHide, function ngHideWatchAction(value) {
        // The comment inside of the ngShowDirective explains why we add and
        // remove a temporary class for the show/hide animation
        $animate[value ? 'addClass' : 'removeClass'](element,NG_HIDE_CLASS, {
          tempClasses: NG_HIDE_IN_PROGRESS_CLASS
        });
      });
    }
  };
}];

Look familiar? That's because it's almost exactly the same as the myHide directive you've been looking at this whole time. There are a couple differences, mainly ngHide uses a ternary operator instead of an if/else to determine calling addClass or removeClass.

If you look at the other built-in directives, you'll see calls to $animate. The documentation for each directive records which events you can hook into an animate. Then it's just a matter of either creating CSS animations or Javascript animations and matching all of your names to the naming convention.

Sick of all the 'magic' in Angular?

The Angular learning curve is a doozy. It's worth it in the end, but Angular is filled with a more than a few strange, new concepts and the end result can sometimes look downright magical.

All frameworks are opinionated, Angular is no exception. The problem is with Angular you can start creating apps that work easily, but before you know it you're running into problems that are difficult to troubleshoot and debug.

When you don't do things the "Angular way", you are swimming against the current. But how do you figure out the right way to do something in Angular?

Drop your email in the form below and I'll send you a few sample lessons on key Angular concepts like Dependency Injection and Directives. The lessons are all from the Angular Escape Plan, Angular training for busy, experienced web developers like you.

Follow @sfioritto Tweet