Guide: Animations

AngularJS provides animation hooks for common directives such as ngRepeat, ngSwitch, and ngView, as well as custom directives via the $animate service. These animation hooks are set in place to trigger animations during the life cycle of various directives and when triggered, will attempt to perform a CSS Transition, CSS Keyframe Animation or a JavaScript callback Animation (depending on if an animation is placed on the given directive). Animations can be placed using vanilla CSS by following the naming conventions set in place by AngularJS or with JavaScript code when it's defined as a factory.

Animations are not available unless you include the ngAnimate module as a dependency within your application.

Below is a quick example of animations being enabled for ngShow and ngHide:

Installation

See the API docs for ngAnimate for instructions on installing the module.

You may also want to setup a separate CSS file for defining CSS-based animations.

How they work

Animations in AngularJS are completely based on CSS classes. As long as you have a CSS class attached to a HTML element within your website, you can apply animations to it. Lets say for example that we have an HTML template with a repeater in it like so:

<div ng-repeat="item in items" class="repeated-item">
  {{ item.id }}
</div>

As you can see, the .repeated-item class is present on the element that will be repeated and this class will be used as a reference within our application's CSS and/or JavaScript animation code to tell AngularJS to perform an animation.

As ngRepeat does its thing, each time a new item is added into the list, ngRepeat will add a ng-enter class name to the element that is being added. When removed it will apply a ng-leave class name and when moved around it will apply a ng-move class name.

Taking a look at the following CSS code, we can see some transition and keyframe animation code set for each of those events that occur when ngRepeat triggers them:

/*
  We're using CSS transitions for when
  the enter and move events are triggered
  for the element that has the .repeated-item
  class
*/
.repeated-item.ng-enter, .repeated-item.ng-move {
  -webkit-transition:0.5s linear all;
  -moz-transition:0.5s linear all;
  -o-transition:0.5s linear all;
  transition:0.5s linear all;
  opacity:0;
}

/*
 The ng-enter-active and ng-move-active
 are where the transition destination properties
 are set so that the animation knows what to
 animate.
*/
.repeated-item.ng-enter.ng-enter-active,
.repeated-item.ng-move.ng-move-active {
  opacity:1;
}

/*
  We're using CSS keyframe animations for when
  the leave event is triggered for the element
  that has the .repeated-item class
*/
.repeated-item.ng-leave {
  -webkit-animation:0.5s my_animation;
  -moz-animation:0.5s my_animation;
  -o-animation:0.5s my_animation;
  animation:0.5s my_animation;
}

@keyframes my_animation {
  from { opacity:1; }
  to { opacity:0; }
}

/*
  Unfortunately each browser vendor requires
  its own definition of keyframe animation code...
*/
@-webkit-keyframes my_animation {
  from { opacity:1; }
  to { opacity:0; }
}

@-moz-keyframes my_animation {
  from { opacity:1; }
  to { opacity:0; }
}

@-o-keyframes my_animation {
  from { opacity:1; }
  to { opacity:0; }
}

The same approach to animation can be used using JavaScript code (jQuery is used within to perform animations):

myModule.animation('.repeated-item', function() {
  return {
    enter : function(element, done) {
      element.css('opacity',0);
      jQuery(element).animate({
        opacity: 1
      }, done);

      // optional onDone or onCancel callback
      // function to handle any post-animation
      // cleanup operations
      return function(isCancelled) {
        if(isCancelled) {
          jQuery(element).stop();
        }
      }
    },
    leave : function(element, done) {
      element.css('opacity', 1);
      jQuery(element).animate({
        opacity: 0
      }, done);

      // optional onDone or onCancel callback
      // function to handle any post-animation
      // cleanup operations
      return function(isCancelled) {
        if(isCancelled) {
          jQuery(element).stop();
        }
      }
    },
    move : function(element, done) {
      element.css('opacity', 0);
      jQuery(element).animate({
        opacity: 1
      }, done);

      // optional onDone or onCancel callback
      // function to handle any post-animation
      // cleanup operations
      return function(isCancelled) {
        if(isCancelled) {
          jQuery(element).stop();
        }
      }
    },

    // you can also capture these animation events
    addClass : function(element, className, done) {},
    removeClass : function(element, className, done) {}
  }
});

With these generated CSS class names present on the element at the time, AngularJS automatically figures out whether to perform a CSS and/or JavaScript animation. If both CSS and JavaScript animation code is present, and match the CSS class name on the element, then AngularJS will run both animations at the same time.

Class and ngClass animation hooks

AngularJS also pays attention to CSS class changes on elements by triggering the add and remove hooks. This means that if a CSS class is added to or removed from an element then an animation can be executed in between, before the CSS class addition or removal is finalized. (Keep in mind that AngularJS will only be able to capture class changes if an expression or the ng-class directive is used on the element.)

The example below shows how to perform animations during class changes:

Although the CSS is a little different than what we saw before, the idea is the same.

Which directives support animations?

A handful of common AngularJS directives support and trigger animation hooks whenever any major event occurs during its life cycle. The table below explains in detail which animation events are triggered

Directive Supported Animations
ngRepeat enter, leave, and move
ngView enter and leave
ngInclude enter and leave
ngSwitch enter and leave
ngIf enter and leave
ngClass or {{class}} add and remove
ngShow & ngHide add and remove (the ng-hide class value)

For a full breakdown of the steps involved during each animation event, refer to the API docs.

How do I use animations in my own directives?

Animations within custom directives can also be established by injecting $animate directly into your directive and making calls to it when needed.

myModule.directive('my-directive', ['$animate', function($animate) {
  return function(scope, element, attrs) {
    element.on('click', function() {
      if(element.hasClass('clicked')) {
        $animate.removeClass(element, 'clicked');
      } else {
        $animate.addClass(element, 'clicked');
      }
    });
  };
}]);

Animations on app bootstrap / page load

By default, animations are disabled when the Angular app bootstraps. If you are using the ngApp directive, this happens in the DOMContentLoaded event, so immediately after the page has been loaded. Animations are disabled, so that UI and content are instantly visible. Otherwise, with many animations on the page, the loading process may become too visually overwhelming, and the performance may suffer.

Internally, ngAnimate waits until all template downloads that are started right after bootstrap have finished. Then, it waits for the currently running $rootScope.Scope and the one after that to finish. This ensures that the whole app has been compiled fully before animations are attempted.

If you do want your animations to play when the app bootstraps, you can enable animations globally in your main module's run function:

myModule.run(function($animate) {
  $animate.enabled(true);
});

How to (selectively) enable, disable and skip animations

There are three different ways to disable animations, both globally and for specific animations. Disabling specific animations can help to speed up the render performance, for example for large ngRepeat lists that don't actually have animations. Because ngAnimate checks at runtime if animations are present, performance will take a hit even if an element has no animation.

This function can be called in the config phase of an app. It takes a regex as the only argument, which will then be matched against the classes of any element that is about to be animated. The regex allows a lot of flexibility - you can either allow animations only for specific classes (useful when you are working with 3rd party animations), or exclude specific classes from getting animated.

app.config(function($animateProvider) {
  $animateProvider.classNameFilter(/animate-/);
});
/* prefixed with animate- */
.animate-fade-add.animate-fade-add-active {
  transition:1s linear all;
  opacity:0;
}

The classNameFilter approach generally applies the biggest speed boost, because the matching is done before any other animation disabling strategies are checked. However, that also means it is not possible to override class name matching with the two following strategies. It's of course still possible to enable / disable animations by changing an element's class name at runtime.

This function can be used to enable / disable animations in two different ways:

With a single boolean argument, it enables / disables animations globally: $animate.enabled(false) disables all animations in your app.

When the second argument is a native DOM or jQuery element, the function enables / disables animations on this element and all its children: $animate.enabled(false, myElement). This is the most flexible way to change the animation state. For example, even if you have used it to disable animations on a parent element, you can still re-enable it for a child element. And compared to the classNameFilter, you can change the animation status at runtime instead of during the config phase.

Note however that the $animate.enabled() state for individual elements does not overwrite disabling rules that have been set in the classNameFilter.

Via CSS styles: overwriting styles in the ng-animate CSS class

Whenever an animation is started, ngAnimate applies the ng-animate class to the element for the whole duration of the animation. By applying CSS transition / animation styling to the class, you can skip an animation:

.my-class{
  transition: transform 2s;
}

.my-class:hover {
  transform: translateX(50px);
}

my-class.ng-animate {
  transition: 0s;
}

By setting transition: 0s, ngAnimate will ignore the existing transition styles, and not try to animate them (Javascript animations will still execute, though). This can be used to prevent issues with existing animations interfering with ngAnimate.

Preventing flicker before an animation starts

When nesting elements with structural animations such as ngIf into elements that have class-based animations such as ngClass, it sometimes happens that before the actual animation starts, there is a brief flicker or flash of content where the animated element is briefly visible.

To prevent this, you can apply styles to the ng-[event]-prepare class, which is added as soon as an animation is initialized, but removed before the actual animation starts (after waiting for a $digest). This class is only added for structural animations (enter, move, and leave).

Here's an example where you might see flickering:

<div ng-class="{red: myProp}">
  <div ng-class="{blue: myProp}">
    <div class="message" ng-if="myProp"></div>
  </div>
</div>

It is possible that during the enter event, the .message div will be briefly visible before it starts animating. In that case, you can add styles to the CSS that make sure the element stays hidden before the animation starts:

.message.ng-enter-prepare {
  opacity: 0;
}

/* Other animation styles ... */

Preventing Collisions with Existing Animations and Third Party Libraries

By default, any ngAnimate enabled directives will assume any transition / animation styles on the element are part of an ngAnimate animation. This can lead to problems when the styles are actually for animations that are independent of ngAnimate.

For example, an element acts as a loading spinner. It has an inifinite css animation on it, and also an ngIf directive, for which no animations are defined:

@keyframes rotating {
    from { transform: rotate(0deg); }
    to { transform: rotate(360deg); }
}

.spinner {
    animation: rotating 2s linear infinite;
}

Now, when the ngIf changes, ngAnimate will see the spinner animation and use it to animate the enter/leave event, which doesn't work because the animation is infinite. The element will still be added / removed after a timeout, but there will be a noticable delay.

This might also happen because some third-party frameworks place animation duration defaults across many element or className selectors in order to make their code small and reuseable.

You can prevent this unwanted behavior by adding CSS to the .ng-animate class that is added for the whole duration of an animation. Simply overwrite the transition / animation duration. In the case of the spinner, this would be:

.spinner.ng-animate {
    transition: 0s none;
    animation: 0s none;
}

If you do have CSS transitions / animations defined for the animation events, make sure they have higher priority than any styles that are independent from ngAnimate.

You can also use one of the two other strategies to disable animations.

More about animations

For a full breakdown of each method available on $animate, see the API documentation.

To see a complete demo, see the animation step within the AngularJS phonecat tutorial.

doc_AngularJS
2016-03-29 16:11:09
Comments
Leave a Comment

Please login to continue.