Lesson 3: AngularJS Animations

In this Lesson, we will cover the following recipes:

  • Creating a simple fade in/out animation
  • Replicating jQuery's slideUp() and slideDown() methods
  • Creating enter animations with ngIf
  • Creating leave and concurrent animations with ngView
  • Creating move animations with ngRepeat
  • Creating addClass animations with ngShow
  • Creating removeClass animations with ngClass
  • Staggering batched animations

Introduction

AngularJS incorporates its animation infrastructure as a separate module, ngAnimate. With this, you are able to tackle animating your application in several different ways, which are as follows:

  • CSS3 transitions
  • CSS3 animations
  • JavaScript animations

Using any one of these three, you are able to fully animate your application in an extremely clean and modular fashion. In many cases, you will find that it is possible to add robust animations to your existing application using only the AngularJS class event progression and CSS definitions—no extra HTML or JS code is needed.

This Lesson assumes that you are at least broadly familiar with the major topics involved in browser animations. We will focus more on how to integrate these animations into an AngularJS application without having to rely on jQuery or other animation libraries. As you will see in this Lesson, there are a multitude of reasons why utilizing AngularJS/CSS animations is preferred to their respective counterparts in libraries such as jQuery.

Note

For the sake of brevity, the recipes in this Lesson will not include any vendor prefixes in the CSS class or animation definitions. Production applications should obviously include them for cross-browser compatibility, but in the context of this Lesson, they are merely a distraction as AngularJS is unconcerned with the content of CSS definitions.

The ngAnimate module comes separately packaged in angular-animate.js. This file must be included alongside angular.js for the recipes in this Lesson to work.

Creating a simple fade in/out animation

AngularJS animations work by integrating CSS animations into a directive class-based finite state machine. In other words, elements in AngularJS that serve to manipulate the DOM have defined class states that can be used to take full advantage of CSS animations, and the system moves between these states on well-defined events. This recipe will demonstrate how to make use of the directive finite state machine in order to create a simple fade in/out animation.

Note

A finite state machine (FSM) is a computational system model defined by the states and transition conditions between them. The system can only exist in one state at any given time, and the system changes state when triggered by certain events. In the context of AngularJS animations, states are represented by the presence of CSS classes associated with the progress of a certain animation, and the events that trigger the state transformations are controlled by data binding and the directives controlling the classes.

Getting ready

As of AngularJS 1.2, animation comes as a completely separate module in AngularJS—ngAnimate. Your initial files should appear as follows:

(style.css)

.animated-container {
  padding: 20px;
  border: 5px solid black;
}

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <label>
      <button ng-click="boxHidden=!boxHidden">
        Toggle Visibility
      </button>
    </label>
    <div class="animated-container" ng-hide="boxHidden">
      Awesome text!
    </div>
  </div>
</div>

(app.js)

angular.module('myApp', ['ngAnimate'])
.controller('Ctrl', function($scope) {
  $scope.boxHidden = true;
});

You can see that the given code simply provides a button that instantly toggles the visibility of the styled <div> element.

How to do it…

There are several ways to accomplish a fade in/out animation, but the simplest is to use CSS transitions as they integrate very nicely into the AngularJS animation class state machine.

The animation CSS classes need to cover both cases, where the element is hidden and needs to fade in, and where the element is shown and needs to fade out. As is the case with CSS transitions, you need to define the initial state, the final state, and the transition parameters. This can be done as follows:

(style.css)

.animated-container {
  padding: 20px;
  border: 5px solid black;
}
.animated-container.ng-hide-add, 
.animated-container.ng-hide-remove {
  transition: all linear 1s;
}
.animated-container.ng-hide-remove,
.animated-container.ng-hide-add.ng-hide-add-active {
  opacity: 0;
}
.animated-container.ng-hide-add,
.animated-container.ng-hide-remove.ng-hide-remove-active {
  opacity: 1;
}

These CSS classes cover the bi-directional transition to fade between opacity: 0 and opacity: 1 in 1 second. Clicking on the <button> element to toggle the visibility will work to trigger the fade in and fade out of the styled <div> element.

How it works…

Since CSS transitions are triggered by the change of relevant CSS classes, using the AngularJS class state machine allows you to animate when a directive manipulates the DOM. The show/hide state machine is cyclical and operates as shown in the following table (this is a simplified version of the full ng-show/ng-hide state machine, which is provided in detail in the Creating addClass animations with ngShow recipe):

Event

Directive state

Styled element classes

Element state

Initial state

ng-hide=true

animated-container

ng-hide

display:none

boxHidden=false

ng-hide=false

animated-container

ng-animate

ng-hide-remove

opacity:0

Time quanta elapses

ng-hide=false

animated-container

ng-animate

ng-hide-remove

ng-hide-remove-active

The animation is triggered; transition to opacity:1 occurs

Animation completes

ng-hide=false

animated-container

display:block

boxHidden=true

ng-hide=true

animated-container

ng-animate

ng-hide

ng-hide-add

opacity:1

Time quanta elapses

ng-hide=true

animated-container

ng-animate

ng-hide

ng-hide-add

ng-hide-add-active

The animation is triggered; transition to opacity:0 occurs

Animation completes

ng-hide=true

animated-container

ng-hide

display:none

Note

The state machine shown in the preceding table is a simplified version of the actual animation state machine.

You can now see how the CSS classes utilize the animation class state machine to trigger the animation. When the directive state changes (in this case, the Boolean is negated), AngularJS applies sequential CSS classes to the element, intending them to be used as anchors for a CSS animation. Here, Time quanta elapses refers to the separate addition of ng-hide-add or ng-hide-remove followed by the ng-hide-add-active or ng-hide-remove-active classes. These classes are added sequentially and separately (this appears to be instantaneous, you will be unable to see the separation when watching the classes in a browser inspector), but the nature of the offset addition causes the CSS transition to be triggered properly.

In the case of moving from hidden to visible, the CSS styling defines a transition between the .animated-container.ng-hide-add selector and the .animated-container.ng-hide-add.ng-hide-add-active selector, with the transition definition attached under the .animated-container.ng-hide-remove selector.

In the case of moving from visible to hidden, the styling defines the opposite transition between the .animated-container.ng-hide-add selector and the .animated-container.ng-hide-add.ng-hide-add-active selector, with the transition definition attached under the .animated-container.ng-hide-add selector.

There's more…

As the class state machine is controlled entirely by the ng-hide directive, if you want to invert the animation (initially start as shown and then make the transition to hidden), all that is needed is the use of ng-show on the HTML element instead of ng-hide. These opposing directives will implement the class state machine appropriately for their definition, but will always use the ng-hide class as the default reference. In other words, using the ng-show directive will not utilize ng-show-add or ng-show-remove or anything of the sort; it will still be ng-hide, ng-hide-add or ng-hide-remove, and ng-hide-add-active or ng-hide-remove-active.

Keeping things clean

Since the animation starts as hidden, and you are loading the JS files at the bottom of the body, this is the perfect opportunity to utilize ng-cloak in order to prevent the styled div element from flashing before compilation. Modify your CSS and HTML as follows:

(style.css)

[ng:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak {
  display: none !important;
}

(index.html)
...
<div class="animated-container" ng-show="boxHidden" ng-cloak>
  Awesome text!
</div>

No more boilerplate animation styling

Formerly, when animating ng-hide or ng-show, the display property needed to incorporate display:block!important during the animation states. As of AngularJS 1.3, this is no longer necessary; the ngAnimate module will handle this for you.

See also

  • The Creating addClass animations with ngShow and Creating removeClass animations with ngClass recipes go into further depth with the state machines that drive the directive animations

Replicating jQuery's slideUp() and slideDown() methods

jQuery provides a very useful pair of animation methods, slideUp() and slideDown(), which use JavaScript in order to accomplish the desired results. With the animation hooks provided for you by AngularJS, these animations can be accomplished with CSS.

Getting ready

Suppose that you want to slide a <div> element up and down in the following setup:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="displayToggle=!displayToggle">
      Toggle Visibility
    </button>
    <div>Slide me up and down!</div>
  </div>   
</div>

(app.js)
angular.module('myApp', ['ngAnimate'])
.controller('Ctrl', function($scope) {
  $scope.displayToggle = true;
});

How to do it…

A sliding animation requires truncation of the overflowing element and a transition involving the height of the element. The following implementation utilizes ng-class:

(style.css)

.container {
  overflow: hidden;
}
.slide-tile {
  transition: all 0.5s ease-in-out;
  width: 300px;
  line-height: 300px;
  text-align: center;
  border: 1px solid black;
  transform: translateY(0);
}
.slide-up {
  transform: translateY(-100%);
}

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="displayToggle=!displayToggle">
      Toggle Visibility
    </button>
    <div class="container">
      <div class="slide-tile" 
         ng-class="{'slide-up': !displayToggle}">
        Slide me up and down!
      </div>
    </div>
  </div>
</div>

A slightly more lightweight implementation is to tie the class definitions into the ng-show state machine:

(style.css)

.container {
  overflow: hidden;
}
.slide-tile {
  transition: all 0.5s ease-in-out;
  width: 300px;
  line-height: 300px;
  text-align: center;
  border: 1px solid black;
  transform: translateY(0);
}
.slide-tile.ng-hide {
  transform: translateY(-100%);
}

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="displayToggle=!displayToggle">
      Toggle Visibility
    </button>
    <div class="container">
      <div class="slide-tile" ng-show="displayToggle">
        Slide me up and down!
      </div>
    </div>
  </div>   
</div>

How it works…

CSS transitions afford the convenience of a bi-directional animation as long as the endpoints and transitions are defined. For both of these implementations, the translateY CSS property is used to implement the sliding, and the hidden state (slide up for the ng-class implementation, and ng-hide for the ng-show implementation) is used as the concealed transition state endpoint.

See also

  • The Creating addClass animations with ngShow and Creating removeClass animations with ngClass recipes go into further depth with the state machines that drive the directive animations

Creating enter animations with ngIf

AngularJS provides hooks to define a custom animation when a directive fires an enter event. The following directives will generate enter events:

  • ngIf: This fires the enter event just after the ngIf contents change, and a new DOM element is created and injected into the ngIf container
  • ngInclude: This fires the enter event when new content needs to be brought into the browser
  • ngRepeat: This fires the enter event when a new item is added to the list or when an item is revealed after a filter
  • ngSwitch: This fires the enter event after the ngSwitch contents change, and the matched child element is placed inside the container
  • ngView: This fires the enter event when new content needs to be brought into the browser
  • ngMessage: This fires the enter event when an inner message is attached

Getting ready

Suppose that you want to attach a fade-in animation to a piece of the DOM that has a ng-if directive attached to it. When the ng-if expression evaluates to true, the enter animation will trigger, as the template is brought into the page.

Note

The ngIf directive also has a complementary set of leave animation hooks, but those are not needed in this recipe and can be safely ignored if they are not being used.

The initial setup, before animation is implemented, can be structured as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="visible=!visible">Toggle</button>
    <span class="target" ng-if="visible">Bring me in!</span>
  </div>   
</div>

(app.js)

angular.module('myApp', ['ngAnimate'])
.controller('Ctrl', function($scope) {
  $scope.visible = true;
});

Note

The example in this recipe only uses ngIf, but it could have just as easily been performed with ngInclude, ngRepeat, ngSwitch, or ngView. All of the enter events fired for these directives involve content being introduced to the DOM in some way, so the animation hooks and procedures surrounding the animation definition can be handled in a more or less identical fashion.

How to do it…

When the button is clicked, this code instantaneously brings the <div> element with a ngIf expression attached to it into view as soon as the expression evaluates to true. However, with the inclusion of the ngAnimate module, AngularJS will add in animation hooks, upon which you can define an animation when the <div> element enters the page.

An animation can be defined by a CSS transition, CSS animation, or by JavaScript. The animation definition can be constructed in different ways. CSS transitions and CSS animations will use the ng-enter CSS class hooks to define the animation, whereas JavaScript animations will use the ngAnimate module's enter() method.

CSS3 transition

To animate with transitions, only the beginning and end state class styles need to be defined. This is shown here:

(style.css)

.target.ng-enter
{
  transition: all linear 1s;
  opacity: 0;
}
.target.ng-enter.ng-enter-active {
  opacity: 1;
}

CSS3 animation

Similar to CSS3 transition, it is relatively simple to accomplish the same animation with CSS keyframes. Since the animation is defined entirely within the keyframes, only a single class reference is needed in order to trigger the animation. This can be done as follows:

(style.css)

.target.ng-enter {
  animation: 1s target-enter;
}
@keyframes target-enter {
  from {
    opacity: 0;
  }
  to {
    opacity: 1;
  }
}

JavaScript animation

Animating with JavaScript requires that you manually add and remove the relevant CSS classes, as well as explicitly call the animations. Since AngularJS and jqLite objects don't have an animation method, you will need to use the jQuery object's animate() method:

(app.js)

angular.module('myApp', ['ngAnimate'])
.controller('Ctrl', function ($scope) {
  $scope.visible = false;
})
.animation('.target', function () {
  return {
    enter: function (element, done) {
      $(element)
      .css({
        'opacity': 0
      });
      $(element)
      .animate({
        'opacity': 1
      },
      1000,
      done);
    }
  };
});

How it works…

The enter animation behaves as a state machine. It cannot assume that either CSS transitions/animations or JavaScript animations are defined upon the <div> DOM element, and it must be able to apply all of them without creating conflicts. As a result, AngularJS will trigger the JavaScript animations and immediately begin the progression of the animation class sequence, which will trigger any CSS transitions/animations that might be defined upon them. In this way, both JavaScript and CSS animations can be used on the same DOM element simultaneously.

AngularJS uses a standard class naming convention for different states, which allows you to uniquely define each set of animations for the component being animated. The following set of tables define how the enter animation state machine operates.

The initial state of the animation components is defined as follows:

element

[<span class="target" ng-if="visible">
  Bring me in!
</span>,
<!-- end ngIf: visible -->]

parentElement

[<div>
  ...
</div>]

afterElement

[<!-- ngIf: visible -->]

The following table represents a full enter animation transition:

Event

DOM

The $animate.enter() method is called after the directive detects that ng-if evaluates to true

<div>
  <!-- ngIf: visible -->
</div>

The element is inserted into parentElement or beside afterElement

<div>
  <!-- ngIf: visible -->
  <span class="target" 
        ng-if="visible">
    Bring me in!
  </span>
  <!-- end ngIf: visible -->
</div>

The $animate service waits for a new digest cycle to begin animating; the ng-animate class is added

<div>
  <!-- ngIf: visible -->
  <span class="target ng-animate" 
        ng-if="visible">
    Bring me in!
  </span>
  <!-- end ngIf: visible -->
</div>

The $animate service runs the JavaScript-defined animations detected on the element

No change in DOM

The ng-enter class is added to the element

<div>
  <!-- ngIf: visible -->
  <span class="target ng-animate ng-enter" 
        ng-if="visible">
    Bring me in!
  </span>
  <!-- end ngIf: visible -->
</div>

The $animate service reads the element styles in order to get the CSS transition or CSS animation definition

No change in DOM

The $animate service blocks CSS transitions involving the element in order to ensure the ng-enter class styling is correctly applied without interference

No change in DOM

The $animate service waits for a single animation frame, which performs a reflow

No change in DOM

The $animate service removes the CSS transition block placed on the element

No change in DOM

The ng-enter-active class is added; CSS transitions or CSS animations are triggered

<div>
  <!-- ngIf: visible -->
  <span class="target ng-animate ng-enter ng-enter-active" 
        ng-if="visible">
    Bring me in!
  </span>
  <!-- end ngIf: visible -->
</div>

The $animate service waits for the animation to complete

No change in DOM

Animation completes; animation classes are stripped from the element

<div>
  <!-- ngIf: visible -->
  <span class="target" 
        ng-if="visible">
    Bring me in!
  </span>
  <!-- end ngIf: visible -->
</div>

The doneCallback() method is fired (if provided)

No change in DOM

Note

Since it does not affect animation proceedings, this recipe intentionally ignores the presence of the ng-scope class, which in reality would be present on the DOM elements.

There's more…

JavaScript and CSS transitions/animations are executed in a parallel. Since they are defined independently, they can be run independently even though they can modify the same DOM element(s) entering the page.

See also

  • The Creating leave and concurrent animations with ngView recipe provides the details of the complementary leave event

Creating leave and concurrent animations with ngView

AngularJS provides hooks used to define a custom animation when a directive fires a leave event. The following directives will generate leave events:

  • ngIf: This fires the leave event just before the ngIf contents are removed from the DOM
  • ngInclude: This fires the leave event when the existing included content needs to be animated away
  • ngRepeat: This fires the leave event when an item is removed from the list or when an item is filtered out
  • ngSwitch: This fires the leave event just after the ngSwitch contents change and just before the former contents are removed from the DOM
  • ngView: This fires the leave event when the existing ngView content needs to be animated away
  • ngMessage: This fires the leave event when an inner message is removed

Getting ready

Suppose that you want to attach a slide-in or slide-out animation to a piece of the DOM that exists inside the ng-view directive. Route changes that cause the content of ng-view to be altered will trigger an enter animation for the content about to be brought into the page, as well as trigger a leave animation for the content about to leave the page.

The initial setup, before animation is implemented, can be structured as follows:

(style.css)

.link-container {
  position: absolute;
  top: 320px;
}
.animate-container {
  position: absolute;
}
.animate-container div {
  width: 300px;
  text-align: center;
  line-height: 300px;
  border: 1px solid black;
}

(index.html)

<div ng-app="myApp">
  <ng-view class="animate-container"></ng-view>
  <div class="link-container"> 
    <a href="#/foo">Foo</a>
    <a href="#/bar">Bar</a>
  </div>
  
  <script type="text/ng-template" id="foo.html">
    <div>
      <span>Foo</span>
    </div>
  </script>
  <script type="text/ng-template" id="bar.html">
    <div>
      <span>Bar</span>
    </div>
  </script>
</div>

(app.js)

angular.module('myApp', ['ngAnimate', 'ngRoute'])
.config(function ($routeProvider) {
  $routeProvider
  .when('/bar', {
    templateUrl: 'bar.html'
  })
  .otherwise({
    templateUrl: 'foo.html'
  });
});

Note

The example in this recipe only uses ngView, but it could have just as easily been performed with ngInclude, ngRepeat, ngSwitch, or ngIf. All the leave events fired for these directives involve content being removed from the DOM in some way, so the animation's hooks and procedures surrounding the animation definition can be handled in a more or less identical fashion. However, not all of these directives trigger enter and leave events concurrently.

How to do it…

When the route changes, AngularJS instantaneously injects the appropriate template into the ng-view directive. However, with the inclusion of the ngAnimate module, AngularJS will add in animation hooks, upon which you can define animations for how the templates will enter and leave the page.

An animation can be defined by a CSS transition, CSS animation, or by JavaScript. The animation definition can be constructed in different ways. CSS transitions and CSS animations will use the ng-leave CSS class hooks to define the animation, whereas JavaScript animations will use the ngAnimate directive's leave() method.

It is important to note here that ng-view triggers the leave and enter animations simultaneously. Therefore, your animation definitions must take this into account in order to prevent animation conflicts.

CSS3 transition

To animate with transitions, only the beginning and end state class styles need to be defined. Remember that the enter and leave animations begin at the same instant, so you must either define an animation that gracefully accounts for any overlap that might occur, or introduce a delay in animations in order to serialize them.

CSS transitions accept a transition-delay value, so serializing the animations is the easiest way to accomplish the desired animation here. Adding the following to the style sheet is all that is needed in order to define the slide-in or slide-out animation:

(style.css)

.animate-container.ng-enter {
  /* final value is the transition delay */
  transition: all 0.5s 0.5s;
}
.animate-container.ng-leave {
  transition: all 0.5s;
}
.animate-container.ng-enter, 
.animate-container.ng-leave.ng-leave-active {
  top: -300px;
}
.animate-container.ng-leave, 
.animate-container.ng-enter.ng-enter-active {
  top: 0px;
}

CSS3 animation

Building this animation with CSS keyframes is also easy to accomplish. Keyframe percentages allow you to effectively delay the enter animation by a set length of time until the leave animation finishes. This can be done as follows:

(style.css)

.animate-container.ng-enter {
  animation: 1s view-enter;
}
.animate-container.ng-leave {
  animation: 0.5s view-leave;
}
@keyframes view-enter {
  0%, 50% {
    top: -300px;
  }
  100% {
    top: 0px;
  }
}
@keyframes view-leave {
  0% {
    top: 0px;
  }
  100% {
    top: -300px;
  }
}

JavaScript animation

Animating with JavaScript requires that you manually add and remove the relevant CSS classes, as well as explicitly call the animations. Since AngularJS and jqLite objects don't have an animation method, you will need to use the jQuery object's animate() method. The delay between the serialized animations can be accomplished with the jQuery delay() method. The animation can be defined as follows:

(app.js)

angular.module('myApp', ['ngAnimate', 'ngRoute'])
.config(function ($routeProvider) {
  $routeProvider
  .when('/bar', {
    templateUrl: 'bar.html'
  })
  .otherwise({
    templateUrl: 'foo.html'
  });
})
.animation('.animate-container', function() {
  return {
    enter: function(element, done) {
      $(element)
      .css({
        'top': '-300px'
      });
      $(element)
      .delay(500)
      .animate({
        'top': '0px'
      }, 500, done);
    },
    leave: function(element, done) {
      $(element)
      .css({
        'top': '0px'
      });
      $(element)
      .animate({
        'top': '-300px'
      }, 500, done);
    }
  };
});

How it works…

The leave animation state machine has a good deal of parity with the enter animation. State machine class progressions work in a very similar way; sequentially adding the beginning and final animation hook classes in order to match the element coming in and out of existence. AngularJS uses the same standard class naming convention used by the enter animation for the different animation states. The following set of tables define how the leave animation state machine operates.

The initial state of the animation components is defined as follows:

element

[<ng-view class="animate-container">
  <div>
    <span>Bar</span>
  </div>
</ng-view>]

The following table represents a full leave animation transition:

Event

DOM

The $animate.leave() method is called when a new view needs to be introduced

<ng-view class="animate-container">
  <div>
    <span>Bar</span>
  </div>
</ng-view>

The $animate service runs the JavaScript-defined animations detected on the element; the ng-animate class is added

<ng-view class="animate-container ng-animate">
  <div>
    <span>Bar</span>
  </div>
</ng-view>

The $animate service waits for a new digest cycle to begin animating

No change in DOM

The ng-leave class is added to the element

<ng-view class="animate-container ng-animate ng-leave">
  <div>
    <span>Bar</span>
  </div>
</ng-view>

The $animate service reads the element styles in order to get the CSS transition or CSS animation definition

No change in DOM

The $animate service blocks CSS transitions that involve the element in order to ensure that the ng-leave class styling is correctly applied without interference

No change in DOM

The $animate service waits for a single animation frame, which performs a reflow

No change in DOM

The $animate service removes the CSS transition block placed on the element

No change in DOM

The ng-leave-active class is added; CSS transitions or CSS animations are triggered

<ng-view class="animate-container ng-animate ng-leave ng-leave-active">
  <div>
    <span>Bar</span>
  </div>
</ng-view>

The $animate service waits for the animation to get completed

No change in DOM

The animation is complete; animation classes are stripped from the element

<ng-view class="animate-container">
  <div>
    <span>Bar</span>
  </div>
</ng-view>

The element is removed from DOM

<ng-view class="animate-container">
</ng-view>

The doneCallback() method is fired (if provided)

No change in DOM

Note

Since it does not affect the animation proceedings, this recipe intentionally ignores the presence of the ng-scope class, which in reality would be present in the DOM elements.

See also

  • The Creating enter animations with ngIf recipe provides the details of the complementary enter event

Creating move animations with ngRepeat

AngularJS provides hooks to define a custom animation when a directive fires a move event. The only AngularJS directive that fires a move event by default is ngRepeat; it fires a move event when an adjacent item is filtered out causing a reorder or when the item contents are reordered.

Getting ready

Suppose that you want to attach a slide-in or slide-out animation to a piece of the DOM that exists inside the ng-view directive. Route changes that cause the content of ng-view to be altered will trigger an enter animation for the content about to be brought into the page, as well as trigger a leave animation for the content about to leave the page.

Suppose that you want to animate individual pieces of a list when they are initially added, moved, or removed. Additions and removals should slide in and out from the left-hand side, and move events should slide up and down.

The initial setup, before animation is implemented, can be structured as follows:

(style.css)

.animate-container {
  position: relative;
  margin-bottom: -1px;
  width: 300px;
  text-align: center;
  border: 1px solid black;
  line-height: 40px;
}
.repeat-container {
  position: absolute;
}

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <div style="repeat-container">
      <input ng-model="search.val" />
      <button ng-click="shuffle()">Shuffle</button>
      <div ng-repeat="el in arr | filter:search.val" 
           class="animate-container">
        <span>{{ el }}</span>
      </div>
    </div>    
  </div>
</div>

(app.js)

angular.module('myApp', ['ngAnimate'])
.controller('Ctrl', function($scope) {
  $scope.arr = [10,15,25,40,45];
  
  // implementation of Knuth in-place shuffle
  function knuthShuffle(a) {
    for(var i = a.length, j, k; i; 
      j = Math.floor(Math.random() * i),
      k = a[--i], 
      a[i] = a[j], 
      a[j] = k);    
    return a;
  }
  
  $scope.shuffle = function() {
    $scope.arr = knuthShuffle($scope.arr);
  };
});

Note

In this recipe, the ng-repeat search filter is implemented merely to provide the ability to add and remove elements from the list. As search filtering does not reorder the elements as defined by AngularJS (this will be explored later in this recipe), it will never generate move events.

How to do it…

When the order of the displayed iterable collection changes, AngularJS injects the appropriate template into the corresponding location in the list, and sibling elements whose indices have changed will instantaneously shift. However, with the inclusion of the ngAnimate module, AngularJS will add in animation hooks, upon which you can define animations for how the templates will move within the list.

The animation can be defined by a CSS transition, CSS animation, or by JavaScript. The animation definition can be constructed in different ways. CSS transitions and CSS animations will use the ng-move CSS class hooks in order to define the animation, whereas JavaScript animations will use the ngAnimate module's move() method.

It is important to note here that ng-repeat triggers enter, leave, and move animations simultaneously. Therefore, your animation definitions must take this into account to prevent animation conflicts.

CSS3 transition

To animate with transitions, you can utilize the animation hook class states to define the set of endpoints for each type of animation. Animations on each individual element in the collection will begin simultaneously, so you must define animations that gracefully account for any overlap that might occur.

Adding the following to the style sheet is all that is needed in order to define the slide-in or slide-out animation for the enter and leave events and a fade in for the move event:

(style.css)

.animate-container.ng-move {
  transition: all 1s;
  opacity: 0;
  max-height: 0;
}
.animate-container.ng-move-active {
  opacity: 1;
  max-height: 40px;
}
.animate-container.ng-enter {
  transition: left 0.5s, max-height 1s;
  left: -300px;
  max-height: 0;
}
.animate-container.ng-enter-active {
  left: 0px;
  max-height: 40px;
}
.animate-container.ng-leave {
  transition: left 0.5s, max-height 1s;
  left: 0px;
  max-height: 40px;
}
.animate-container.ng-leave-active {
  left: -300px;
  max-height: 0;
}

CSS3 animation

Building this animation with CSS keyframes allows you to have the advantage of being able to explicitly define the offset between animation segments, which allows you a cleaner enter/leave animation without tiles sweeping over each other. The enter and leave animations can take advantage of this by animating to full height before sliding into view. Add the following to the style sheet in order to define the desired animations:

(style.css)

.animate-container.ng-enter {
  animation: 0.5s item-enter;
}
.animate-container.ng-leave {
  animation: 0.5s item-leave;
}
.animate-container.ng-move {
  animation: 0.5s item-move;
}
@keyframes item-enter {
  0% {
    max-height: 0;
    left: -300px;
  }
  50% {
    max-height: 40px;
    left: -300px;
  }
  100% {
    max-height: 40px;
    left: 0px;
  }
}
@keyframes item-leave {
  0% {
    left: 0px;
    max-height: 40px;
  }
  50% {
    left: -300px;
    max-height: 40px;
  }
  100% {
    left: -300px;
    max-height: 0;
  }
}
@keyframes item-move {
  0% {
    opacity: 0;
    max-height: 0px;
  }
  100% {
    opacity: 1;
    max-height: 40px;
  }
}

JavaScript animation

JavaScript animations are also relatively easy to define here, even though the desired effect has both serialized and parallel animation effects. This can be done as follows:

(app.js)

angular.module('myApp', ['ngAnimate'])
.controller('Ctrl', function($scope) {
  ...
})
.animation('.animate-container', function() {
  return {
    enter: function(element, done) {
      $(element)
      .css({
        'left': '-300px',
        'max-height': '0'
      });
      $(element)
      .animate({
        'max-height': '40px'
      }, 250)
      .animate({
        'left': '0px'
      }, 250, done);
    },
    leave: function(element, done) {
      $(element)
      .css({
        'left': '0px',
        'max-height': '40px'
      });
      $(element)
      .animate({
        'left': '-300px'
      }, 250)
      .animate({
        'max-height': '0'
      }, 250, done);
    },
    move: function(element, done) {
      $(element)
      .css({
        'opacity': '0',
        'max-height': '0'
      });
      $(element)
      .animate({
        'opacity': '1',
        'max-height': '40px'
      }, 500, done);
    }
  };
});

How it works…

The move animation state machine is very similar to the enter animation. State machine class progressions sequentially add the beginning and final animation hook classes in order to match the element that is being reintroduced into the list at its new index. AngularJS uses the same standard class naming convention used by the enter animation for different animation states.

Note

For the purpose of simplification, the following modifications and assumptions affect the content of the following state machine:

  • The ng-repeat directive is assumed to be passed an array of [1,2]. The move event is triggered by the array's order being reversed to [2,1].
  • The ng-repeat filter has been removed; a search filter cannot fire move events.
  • The ng-scope and ng-binding directive classes have been removed from where they would normally occur, as they do not affect the state machine.

The following set of tables define how the move animation state machine operates.

The initial state of the animation components is defined as follows:

element

[<div ng-repeat="el in arr" 
      class="animate-container">
  <span>1</span>
</div>,
<!-- end ngRepeat: el in arr -->]

parentElement

null

afterElement

[<!-- ngRepeat: el in arr -->]

The following table represents a full move animation transition:

Event

DOM

The $animate.move() method is invoked

<!-- ngRepeat: el in arr -->
<div ng-repeat="el in arr" 
     class="animate-container">
  <span>1</span>
</div>
<!-- end ngRepeat: el in arr -->
<div ng-repeat="el in arr " 
     class="animate-container">
  <span>2</span>
</div>
<!-- end ngRepeat: el in arr -->

The element is moved into parentElement or beside afterElement

<!-- ngRepeat: el in arr -->
<div ng-repeat="el in arr" 
     class="animate-container">
  <span>2</span>
</div>
<!-- end ngRepeat: el in arr -->
<div ng-repeat="el in arr " 
     class="animate-container">
  <span>1</span>
</div>
<!-- end ngRepeat: el in arr -->

The $animate service waits for a new digest cycle to begin animation; ng-animate is added

<!-- ngRepeat: el in arr -->
<div ng-repeat="el in arr " 
     class="animate-container ng-animate">
  <span>2</span>
</div>
<!-- end ngRepeat: el in arr -->
<div ng-repeat="el in arr " 
     class="animate-container">
  <span>1</span>
</div>
<!-- end ngRepeat: el in arr -->

The $animate service runs the JavaScript-defined animations detected in the element

No change in DOM

The ng-move directive is added to the element's classes

<!-- ngRepeat: el in arr -->
<div ng-repeat="el in arr" 
     class="animate-container ng-animate ng-move">
  <span>2</span>
</div>
<!-- end ngRepeat: el in arr -->
<div ng-repeat="el in arr " 
     class="animate-container">
  <span>1</span>
</div>
<!-- end ngRepeat: el in arr -->

The $animate service reads the element styles in order to get the CSS transition or CSS animation definition

No change in DOM

The $animate service blocks CSS transitions that involve the element to ensure that the ng-move class styling is correctly applied without interference

No change in DOM

The $animate service waits for a single animation frame, which performs a reflow

No change in DOM

The $animate service removes the CSS transition block placed on the element

No change in DOM

The ng-move-active directive is added; CSS transitions or CSS animations are triggered

<!-- ngRepeat: el in arr -->
<div ng-repeat="el in arr" 
     class="animate-container ng-animate ng-move ng-move-active">
  <span>2</span>
</div>
<!-- end ngRepeat: el in arr -->
<div ng-repeat="el in arr " 
     class="animate-container">
  <span>1</span>
</div>
<!-- end ngRepeat: el in arr -->

The $animate service waits for the animation to get completed

No change in DOM

Animation is complete; animation classes are stripped from the element

<!-- ngRepeat: el in arr -->
<div ng-repeat="el in arr" 
     class="animate-container">
  <span>2</span>
</div>
<!-- end ngRepeat: el in arr -->
<div ng-repeat="el in arr " 
     class="animate-container">
  <span>1</span>
</div>
<!-- end ngRepeat: el in arr -->

The doneCallback() method is fired (if provided)

No change in DOM

There's more…

The move animation's name can be a bit confusing as move implies a starting and ending location. A better way to think of it is as a secondary entrance animation used in order to demonstrate when new content is not being added to the list. You will notice that the move animation is triggered simultaneously for all the elements whose relative order in the list has changed, and that the animation triggers when it is in its new position.

Also note that even though the index of both elements changed, only one move animation was triggered. This is due to the way the movement within an enumerable collection is defined. AngularJS preserves the old ordering of the collection and compares its values in order to the entire new ordering, and all mismatches will fire move events. For example, if the old order is 1, 2, 3, 4, 5 and the new order is 5, 4, 2, 1, 3, then the comparison strategy works as follows:

Comparison

Evaluation

old[0] == new[0]

False, fire the move event

old[0] == new[1]

False, fire the move event

old[0] == new[2]

False, fire the move event

old[0] == new[3]

True, increment the old order comparison index until an element, which was not yet seen, is reached (2 was already seen in the new order; skip to 3)

old[2] == new[4]

True

Note

Astute developers will note that, with this order comparison implementation, a simple order shuffling will never mark the last element as "moved".

See also

  • The Staggering batched animations recipe demonstrates how to introduce an animation delay between batched events in an ngRepeat context

Creating addClass animations with ngShow

AngularJS provides hooks used to define a custom animation when a directive fires an addClass event. The following directives will generate addClass events:

  • ngShow: This fires the addClass event after the ngShow expression evaluates to a truthy value, and just before the contents are set to visible
  • ngHide: This fires the addClass event after the ngHide expression evaluates to a non-truthy value, and just before the contents are set to visible
  • ngClass: This fires the addClass event just before the class is applied to the element
  • ngForm: This fires the addClass event to add validation classes
  • ngModel: This fires the addClass event to add validation classes
  • ngMessages: This is fired to add the ng-active class when one or more messages are visible, or to add the ng-inactive class when there are no messages

Getting ready

Suppose that you want to attach a fade-out animation to a piece of the DOM that has an ng-show directive. Remember that ng-show does not add or remove anything from the DOM; it merely toggles the CSS display property to set the visibility.

The initial setup, before animation is implemented, can be structured as follows:

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="displayToggle=!displayToggle">
      Toggle Visibility
    </button>
    <div class="animate-container" ng-show="displayToggle">
      Fade me out!
    </div>
  </div>
</div>

(app.js)

angular.module('myApp', ['ngAnimate'])
.controller('Ctrl',function($scope) {
  $scope.displayToggle = true;
});

How to do it…

When the ng-show expression evaluates to false, the DOM element is immediately hidden. However, with the inclusion of the ngAnimate module, AngularJS will add in animation hooks, upon which you can define animations for how the element will be removed from the page.

The animation can be defined by a CSS transition, CSS animation, or by JavaScript. The animation definition can be constructed in different ways. CSS transitions and CSS animations will use the addClass CSS class hooks to define the animation, whereas JavaScript animations will use the ngAnimate directive's addClass() method.

CSS transitions

Animating a fade-in effect with CSS transitions simply requires attaching opposite opacity values when the ng-hide class is added. Remember that ng-show and ng-hide are merely toggling the presence of this ng-hide class through the use of the addClass and removeClass animation events. This can be done as follows:

(style.css)

.animate-container.ng-hide-add {
  transition: all linear 1s;
  opacity: 1;
}
.animate-container.ng-hide-add.ng-hide-add-active {
  opacity: 0;
}

CSS animation

Animating with a CSS animation is just as simple as CSS transitions, as follows:

(style.css)

.animate-container.ng-hide-add {
  animation: 1s fade-out;
}
@keyframes fade-out {
  0% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

JavaScript animation

Animating with JavaScript requires that you manually add and remove the relevant CSS classes, as well as explicitly call the animations. Since AngularJS and jqLite objects don't have an animation method, you will need to use the jQuery object's animate() method. This can be done as follows:

(app.js)

angular.module('myApp', ['ngAnimate'])
.controller('Ctrl', function($scope) {
  $scope.displayToggle = true;
})
.animation('.animate-container', function() {
  return {
    addClass: function(element, className, done) {
      if (className==='ng-hide') {
        $(element)
        .removeClass('ng-hide')
        .css('opacity', 1)
        .animate(
          {'opacity': 0}, 
          1000, 
          function() {
            $(element)
            .addClass('ng-hide')
            .css('opacity', 1);
            done();
          }
        );
      } else {
        done();
      }
    }
  };
});

Note

JSFiddle: http://jsfiddle.net/msfrisbie/4taoda1e/

Note that here, the opacity value is used for the animation, but is not the active class that hides the element. After its use in the animation, it must be reset to 1 in order to not interfere with the subsequent display toggling.

How it works…

Independent of what is defined in the actual class that is being added, ngAnimate provides animation hooks for the class that is being added to define animations. In the context of the ng-show directive, the ng-hide CSS class is defined implicitly within AngularJS, but the animation hooks are completely decoupled from the original class in order to provide a fresh animation definition interface. The following set of tables defines how the addClass animation state machine operates.

The initial state of the animation components is defined as follows:

element

<div class="animate-container" 
     ng-show="displayToggle">
  Fade me out!
</div>

className

'ng-hide'

The following table represents a full addClass animation transition:

Event

DOM

The $animate.addClass(element, 'ng-hide') method is called

<div class="animate-container" 
     ng-show="displayToggle">
  Fade me out!
</div>

The $animate service runs the JavaScript-defined animations detected on the element; ng-animate is added

<div class="animate-container ng-animate" 
     ng-show="displayToggle">
  Fade me out!
</div>

The .ng-hide-add class is added to the element

<div class="animate-container ng-animate ng-hide-add" 
     ng-show="displayToggle">
  Fade me out!
</div>

The $animate service waits for a single animation frame (this performs a reflow)

No change in DOM

The .ng-hide and .ng-hide-add-active classes are added (this triggers the CSS transition/animation)

<div class="animate-container ng-animate ng-hide ng-hide-add ng-hide-add-active" 
     ng-show="displayToggle">
  Fade me out!
</div>

The $animate service scans the element styles to get the CSS transition/animation duration and delay

No change in DOM

The $animate service waits for the animation to get completed (via events and timeout)

No change in DOM

The animation ends and all the generated CSS classes are removed from the element

<div class="animate-container ng-hide" 
     ng-show="displayToggle">
  Fade me out!
</div>

The ng-hide class is kept on the element

No change in DOM

The doneCallback() callback is fired (if provided)

No change in DOM

See also

  • The Creating removeClass animations with ngClass recipe provides the details of the complementary removeClass event

Creating removeClass animations with ngClass

AngularJS provides hooks that can be used to define a custom animation when a directive fires a removeClass event. The following directives will generate removeClass events:

  • ngShow: This fires the removeClass event after the ngShow expression evaluates to a non-truthy value, and just before the contents are set to hidden
  • ngHide: This fires the removeClass event after the ngHide expression evaluates to a truthy value, and just before the contents are set to hidden
  • ngClass: This fires the removeClass event just before the class is removed from the element
  • ngForm: This fires the removeClass event to remove validation classes
  • ngModel: This fires the removeClass event to remove validation classes
  • ngMessages: This fires the removeClass event to remove the ng-active class when there are no messages, or to remove the ng-inactive class when one or more messages are visible

Getting ready

Suppose that you want to have a div element slide out of the view when a class is removed. Remember that ng-class does not add or remove any elements from the DOM; it merely adds or removes the classes defined within the directive expression.

The initial setup, before animation is implemented, can be structured as follows:

(style.css)

.container {
  background-color: black;
  width: 200px;
  height: 200px;
  overflow: hidden;
}
.prompt {
  position: absolute;
  margin: 10px;
  font-family: courier;
  color: lime;
}
.cover {
  position: relative;
  width: 200px;
  height: 200px;
  left: 200px;
  background-color: black;
}
.blackout {
  left: 0;
}

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <button ng-click="displayToggle=!displayToggle">
      Toggle Visibility
    </button>
    <div class="container">
      <span class="prompt">Wake up, Neo...</span>
      <div class="cover" 
         ng-class="{blackout: displayToggle}">
      </div>
    </div>
  </div>
</div>

(app.js)

angular.module('myApp', ['ngAnimate'])
.controller('Ctrl', function($scope) {
  $scope.displayToggle = true;
});

How to do it…

When the ng-class value for blackout evaluates to false, it will immediately be stripped out. However, with the inclusion of the ngAnimate module, AngularJS will add in animation hooks, upon which you can define animations for how the class will be removed.

The animation can be defined by a CSS transition, CSS animation, or by JavaScript. The animation definition can be constructed in different ways. CSS transitions and CSS animations will use the removeClass CSS class hooks to define the animation, whereas JavaScript animations will use the ngAnimate directive's removeClass() method.

CSS transitions

Animating a slide-out effect with CSS transitions simply requires a transition that defines the left positioning distance. Remember that ng-class is merely toggling the presence of the blackout class through the use of the addClass and removeClass animation events. This can be done as follows:

(style.css)

.blackout-remove {
  left: 0;
}
.blackout-remove {
  transition: all 3s;
}
.blackout-remove-active {
  left: 200px;
}

CSS animation

Animating with a CSS animation is just as simple as CSS transitions, as follows:

(style.css)

.blackout-remove {
  animation: 1s slide-out;
}
@keyframes slide-out {
  0% {
    left: 0;
  }
  100% {
    left: 200px;
  }
}

JavaScript animation

Animating with JavaScript requires that you manually add and remove the relevant CSS classes, as well as explicitly call the animations. Since AngularJS and jqLite objects don't have an animation method, you will need to use the jQuery object's animate() method. This can be done as follows:

(app.js)

angular.module('myApp', ['ngAnimate'])
.controller('Ctrl', function($scope) {
  $scope.displayToggle = true;
})
.animation('.blackout', function() {
  return {
    removeClass: function(element, className, done){
      if (className==='blackout') {
        $(element)
        .removeClass('blackout')
        .css('left', 0)
        .animate(
          {'left': '200px'}, 
          3000, 
          function() {
            $(element).css('left','');
            done();
          } 
        );
      } else {
        done();
      }
    }
  };
});

How it works…

The ngAnimate directive provides animation hooks for the class that is being removed in order to define animations independent of the actual class. In the context of this ng-class directive implementation, the blackout CSS class is defined explicitly, and the animation hooks build on top of this class name. The following set of tables defines how the removeClass animation state machine operates.

The animation components are defined as follows:

element

<div class="cover blackout" 
     ng-class="{blackout: displayToggle}">
</div>

className

'blackout'

The following table represents a full removeClass animation transition:

Event

DOM

The $animate.removeClass(element, 'blackout') method is called

<div class="cover blackout" 
     ng-class="{blackout: displayToggle}">
</div>

The $animate service runs the JavaScript-defined animations detected in the element; ng-animate is added

<div class="cover blackout ng-animate" 
     ng-class="{blackout: displayToggle}">
</div>

The .blackout-remove class is added to the element

<div class="cover blackout ng-animate blackout-remove"
     ng-class="{blackout: displayToggle}">
</div>

The $animate service waits for a single animation frame (this performs a reflow)

No change in DOM

The .blackout-remove-active class is added and .blackout is removed (this triggers the CSS transition/animation)

<div class="cover ng-animate 
  blackout-remove blackout-remove-active" 
     ng-class="{blackout: displayToggle}">
</div>

The $animate service scans the element styles to get the CSS transition/animation duration and delay

No change in DOM

The $animate service waits for the animation to get completed (via events and timeout)

No change in DOM

The animation ends and all the generated CSS classes are removed from the element

<div class="cover" 
     ng-class="{blackout: displayToggle}">
</div>

The doneCallback() callback is fired (if provided)

No change in DOM

See also

  • The Creating addClass animations with ngShow recipe provides the details of the complementary addClass event

Your Coding Challenge

In the previous recipes we looked at creating different types of animation in varying scenarios, let's try to create animation on the page scroll. You might have seen this a number of times, whenever you scroll down an application, elements should animate. The duration for animation should be 2 seconds.

This is very similar to what we have done in one of the preceding sections. Here, we will look at the page load factor. This is a bit tricky; but once this challenge is done, you will feel more confident about your animation skills.

Let us know your output @PacktPub!

Staggering batched animations

AngularJS incorporates native support for staggering animations that happen as a batch. This will almost exclusively occur in the context of ng-repeat.

Getting ready

Suppose that you have an animated ng-repeat implementation, as follows:

(style.css)

.container {
  line-height: 30px;
}
.container.ng-enter,
.container.ng-leave,
.container.ng-move {
  transition: all linear 0.2s;
  
}
.container.ng-enter,
.container.ng-leave.ng-leave-active,
.container.ng-move {
  opacity: 0;
  max-height: 0;
}
.container.ng-enter.ng-enter-active,
.container.ng-leave,
.container.ng-move.ng-move-active {
  opacity: 1;
  max-height: 30px;
}

(index.html)

<div ng-app="myApp">
  <div ng-controller="Ctrl">
    <input ng-model="search" />
    <div ng-repeat="name in names | filter:search"
         class="container">
      {{ name }}
    </div>
  </div>
</div>

(app.js)

angular.module('myApp', ['ngAnimate'])
.controller('Ctrl', function($scope) {
  $scope.names = [
    'Jake',
    'Henry',
    'Roger',
    'Joe',
    'Robert',
    'John'
  ];
});

How to do it…

Since the animation is accomplished through the use of CSS transitions, you can tap into the CSS class staggering that is afforded to you by adding the following to the style sheet:

(style.css)

.container.ng-enter-stagger,
.container.ng-leave-stagger,
.container.ng-move-stagger {
  transition-delay: 0.2s;
  transition-duration: 0;
}

How it works…

For the example dataset, filtering with J will cause multiple elements to be removed, as well as multiple elements to change their index. All of these changes correspond to an animation event. Since these animations occur simultaneously, AngularJS can take advantage of the fact that animations are queued up and executed in batches within a single reflow to compensate for the fact that reflows are computationally expensive.

The -stagger classes essentially act as shims for successive animations. Instead of running all the animations in parallel, they are run serially, delimited by the additional stagger transition.

There's more…

It is also possible to stagger animations using keyframes. This can be accomplished as follows:

(style.css)

.container.ng-enter-stagger,
.container.ng-leave-stagger,
.container.ng-move-stagger {
  animation-delay: 0.2s;
  animation-duration: 0;
}
.container.ng-leave {
  animation: 0.5s repeat-leave;
}
.container.ng-enter {
  animation: 0.5s repeat-enter;
}
.container.ng-move {
  animation: 0.5s repeat-move;
}
@keyframes repeat-enter {
  from {
    opacity: 0;
    max-height: 0;
  }
  to {
    opacity: 1;
    max-height: 30px;
  }
}
@keyframes repeat-leave {
  from {
    opacity: 1;
    max-height: 30px;
  }
  to {
    opacity: 0;
    max-height: 0;
  }
}
@keyframes repeat-move {
  from {
    opacity: 0;
    max-height: 0;
  }
  to {
    opacity: 1;
    max-height: 30px;
  }
}

See also

  • The Creating move animations with ngRepeat recipe goes through all the intricacies of animating an ngRepeat directive's events
See also
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.19.75.133