Code


<!DOCTYPE html> <html> <head> <script src="angular.js"></script> <script src="angular-animate.js"></script> <script src="script.js"></script> <link rel="stylesheet" href="style.css"> </head> <body ng-app="mainModule" ng-controller="mainController"> <h3>1. Complete animation events example</h3> <button type="button" ng-click="onStartAnimationClick()">Start the animation</button><br /> <button type="button" ng-click="onCancelAnimationStepClick()">Cancel current animation step</button><br /> <br /> <strong>Animation state:</strong> {{animationState}}<br /> <br /> <div id="animatedBoxParent"></div> <br /> <h3>2. Enter and Leave example</h3> <label>Show the square: <input type="checkbox" ng-model="enterLeaveSwitch" /></label><br /> <br /> <div class="box custom-js-animation" ng-if="enterLeaveSwitch"></div> </div> </body> </html>
angular.module("mainModule", ["ngAnimate"]) .animation(".custom-js-animation", function ($timeout) { return { animate: function(element, className, from, to, done) { // NO ANIMATION // I don't want to animate this event so I call the "done()" function immediately done(); }, enter: function (element, done) { // FADE IN var i = 0; var animateFunc = function () { element.css({opacity: (i / 100)}); i++; if (i <= 100) { $timeout(animateFunc, 10); } else { done(); } }; // Start the animation animateFunc(); return function (cancelled) { // Animation complete or cancelled if (cancelled) { // Make the "animateFunc" set the opacity to "1" and then end immediately i = 100; } }; }, leave: function (element, done) { // SHRINK AND FADE OUT var targetScale = 0.1; var animationSteps = 100; var i = animationSteps; var currScale = 1.0; var scaleDecPerStep = (currScale - targetScale) / animationSteps; var animateFunc = function () { element.css( { opacity: (i / animationSteps), transform: "scale(" + currScale + ")" } ); currScale -= scaleDecPerStep; i--; if (i >= 0) { $timeout(animateFunc, 10); } else { done(); } }; // Start the animation animateFunc(); return function (cancelled) { // Animation complete or cancelled if (cancelled) { // NOTE: there's a "short-circuit" in the current version of AngularJS (1.3.8) // that makes this handler function be called always with cancelled == true // so there's no way for me to know if a leave animation completes normally // or has been cancelled (it always looks cancelled). // Make the "animateFunc" end immediately i = 0; } }; }, move: function (element, done) { // NO ANIMATION // I don't want to animate this event so I call the "done()" function immediately done(); }, // Animation that can be triggered before the class is added beforeAddClass: function (element, className, done) { if (className == "box-border") { // GROW var i = 0; var targetScale = 1.1; var animationSteps = 100; var currScale = 1.0; var scaleIncPerStep = (targetScale - currScale) / animationSteps; var animateFunc = function () { element.css({transform: "scale(" + currScale + ")"}); currScale += scaleIncPerStep; i++; if (i <= animationSteps) { $timeout(animateFunc, 10); } else { // Make sure the target scale is set correctly to avoid // possible rounding errors. element.css({transform: "scale(" + targetScale + ")"}); done(); } }; // Start the animation animateFunc(); } else { done(); } return function (cancelled) { // Animation complete or cancelled if (cancelled) { // Make the "animateFunc" set the scale to targetScale and then end immediately i = animationSteps; } }; }, // Animation that can be triggered after the class is added addClass: function (element, className, done) { if (className == "box-border") { // SHRINK var i = 0; var targetScale = 1.0; var animationSteps = 100; var currScale = 1.1; var scaleDecPerStep = (currScale - targetScale) / animationSteps; var animateFunc = function () { element.css({transform: "scale(" + currScale + ")"}); currScale -= scaleDecPerStep; i++; if (i <= animationSteps) { $timeout(animateFunc, 10); } else { // Make sure the target scale is set correctly to avoid // possible rounding errors. element.css({transform: "scale(" + targetScale + ")"}); done(); } }; // Start the animation animateFunc(); } else { done(); } return function (cancelled) { // Animation complete or cancelled if (cancelled) { // Make the "animateFunc" set the scale to targetScale and then end immediately i = animationSteps; } }; }, // Animation that can be triggered before the class is removed beforeRemoveClass: function (element, className, done) { if (className == "box-border") { // ADD GLOW var i = 0; var targetMultiplier = 1.0; var animationSteps = 100; var currMultiplier = 0.0; var multIncPerStep = (targetMultiplier - currMultiplier) / animationSteps; var animateFunc = function () { element.css( { "box-shadow": "0px 0px " + Math.round(15 * currMultiplier) + "px " + Math.round(5 * currMultiplier) + "px rgba(135, 206, 250, 0.75)" } ); currMultiplier += multIncPerStep; i++; if (i <= animationSteps) { $timeout(animateFunc, 10); } else { // Make sure the target box-shadow is set correctly to avoid // possible rounding errors. element.css({"box-shadow": "0px 0px 15px 5px rgba(135, 206, 250, 0.75)"}); done(); } }; // Start the animation animateFunc(); } else { done(); } return function (cancelled) { // Animation complete or cancelled if (cancelled) { // Make the "animateFunc" set the final box-shadow and then end immediately i = animationSteps; } }; }, // Animation that can be triggered after the class is removed removeClass: function (element, className, done) { if (className == "box-border") { // REMOVE GLOW var i = 0; var targetMultiplier = 0.0; var animationSteps = 100; var currMultiplier = 1.0; var multDecPerStep = (currMultiplier - targetMultiplier) / animationSteps; var animateFunc = function () { element.css( { "box-shadow": "0px 0px " + Math.round(15 * currMultiplier) + "px " + Math.round(5 * currMultiplier) + "px rgba(135, 206, 250, 0.75)" } ); currMultiplier -= multDecPerStep; i++; if (i <= animationSteps) { $timeout(animateFunc, 10); } else { // Completely remove the box-shadow element.css({"box-shadow": "none"}); done(); } }; // Start the animation animateFunc(); } else { done(); } return function (cancelled) { // Animation complete or cancelled if (cancelled) { // Make the "animateFunc" set the final box-shadow and then end immediately i = animationSteps; } }; } }; }) .controller("mainController", function ($scope, $animate, $timeout) { $scope.animationState = "OFF"; var currAnimationPromise = null; $scope.onStartAnimationClick = function () { var animatedBoxParent = document.querySelector("#animatedBoxParent"); if (animatedBoxParent !== null) { animatedBoxParent = angular.element(animatedBoxParent); var animatedBox = angular.element("<div id=\"animatedBox\" class=\"box custom-js-animation\"></div>"); // Enter var animationStep1 = function () { $scope.animationState = "In progress (step 1 - ENTER)..."; currAnimationPromise = $animate.enter(animatedBox, animatedBoxParent); currAnimationPromise.then( function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { // NOTE: the timeout here is important to make the ENTER animation // cleanup completely. This is not a problem with class-based // animations (ADD CLASS and REMOVE CLASS). $timeout(animationStep2, 20); }); } ); }; // Add Class ("box-border") var animationStep2 = function () { $scope.animationState = "In progress (step 2 - ADD CLASS)..."; currAnimationPromise = $animate.addClass(animatedBox, "box-border"); currAnimationPromise.then( function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { $timeout(animationStep3, 20); }); } ); }; // Remove Class ("box-border") var animationStep3 = function () { $scope.animationState = "In progress (step 3 - REMOVE CLASS)..."; currAnimationPromise = $animate.removeClass(animatedBox, "box-border"); currAnimationPromise.then( function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { $timeout(animationStep4, 20); }); } ); }; // Leave var animationStep4 = function () { $scope.animationState = "In progress (step 4 - LEAVE)..."; currAnimationPromise = $animate.leave(animatedBox); currAnimationPromise.then( function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { currAnimationPromise = null; $scope.animationState = "ENDED"; }); } ); }; // Start the animation animationStep1(); } }; $scope.onCancelAnimationStepClick = function () { if (currAnimationPromise !== null) { $animate.cancel(currAnimationPromise); currAnimationPromise = null; } }; });
* { box-sizing: border-box; } .box { width: 100px; height: 100px; background-color: lightskyblue; display: block; } .box-border { border: 1px solid #808080; }

Example


Description


In the previous examples we've seen how to animate the built-in AngularJS directives and how to use the $animate service to animate our own directives. Now it's time to see how we can define our own animation completely with JavaScript.

Let's start with the basic structure of a JavaScript animation

angular.module("moduleName", ["ngAnimate"])
  .animation(".custom-animation-name", function ()
  {
    return {
      // Animate event
      animate: function(element, className, from, to, done)
      {
        // Animation code

        ...

        return function (cancelled)
        {
          // Animation complete or cancelled

          ...

          if (cancelled)
          {
            ...
          }
        };
      },
      // Enter event
      enter: function (element, done)
      {
        // Animation code

        ...

        return function (cancelled)
        {
          // Animation complete or cancelled

          ...

          if (cancelled)
          {
            ...
          }
        };
      },
      // Leave event
      leave: function (element, done)
      {
        // Animation code

        ...

        return function (cancelled)
        {
          // Animation complete or cancelled

          ...

          if (cancelled)
          {
            ...
          }
        };
      },
      // Move event
      move: function (element, done)
      {
        // Animation code

        ...

        return function (cancelled)
        {
          // Animation complete or cancelled

          ...

          if (cancelled)
          {
            ...
          }
        };
      },
      // Animation that can be triggered before the class is added
      beforeAddClass: function (element, className, done)
      {
        // Animation code

        ...

        return function (cancelled)
        {
          // Animation complete or cancelled

          ...

          if (cancelled)
          {
            ...
          }
        };
      },
      // Animation that can be triggered after the class is added
      addClass: function (element, className, done)
      {
        // Animation code

        ...

        return function (cancelled)
        {
          // Animation complete or cancelled

          ...

          if (cancelled)
          {
            ...
          }
        };
      },
      // Animation that can be triggered before the class is removed
      beforeRemoveClass: function (element, className, done)
      {
        // Animation code

        ...

        return function (cancelled)
        {
          // Animation complete or cancelled

          ...

          if (cancelled)
          {
            ...
          }
        };
      },
      // Animation that can be triggered after the class is removed
      removeClass: function (element, className, done)
      {
        // Animation code

        ...

        return function (cancelled)
        {
          // Animation complete or cancelled

          ...

          if (cancelled)
          {
            ...
          }
        };
      }
    };
  });

We use the animation method available in any AngularJS module to define our custom JavaScript animation. That method accepts two arguments: the animation name (custom-animation-name in this case) and a function that returns a definition object with all our animation methods. You can notice that there's an equivalence with the methods offered by the $animate service that we've discussed in the previous example:

  • $animate.animate() uses the animate() method in our definition object;
  • $animate.enter() uses the enter() method in our definition object;
  • $animate.leave() uses the leave() method in our definition object;
  • $animate.move() uses the move() method in our definition object;
  • $animate.addClass() uses the beforeAddClass() and addClass() methods in our definition object;
  • $animate.removeClass() uses the beforeRemoveClass() and removeClass() methods in our definition object;
  • $animate.setClass() uses the beforeAddClass(), addClass(), beforeRemoveClass() and removeClass() methods in our definition object.

Each animation method in the definition object has some easy to understand parameters (for example, element is the HTML element that is the animation target) and a done parameter at the end. This last parameter is a function that must be called when the animation ends. Each animation method also lets us return a function with a cancelled parameter. This is a handler that is called when the animation ends and the cancelled parameter tells us if the animation terminated normally (cancelled == false) or if it has been cancelled (cancelled == true).

Point 1 of the example declares a new JavaScript animation named custom-js-animation and shows an animation made of multiple steps that use the animation methods that we've defined in the definition object. Here are the details of the definition object:

  • animate(): we don't want to animate this method so we call the done() function immediately;
  • enter(): we've defined a fade-in animation for this method;
  • leave(): we've defined a shrink and fade-out animation for this method;
  • move(): we don't want to animate this method so we call the done() function immediately;
  • beforeAddClass(): we've defined a grow animation for this method;
  • addClass(): we've defined a shrink animation for this method;
  • beforeRemoveClass(): we've defined an add glow animation for this method;
  • removeClass(): we've defined a remove glow animation for this method.

These are the four animation steps of the example:

  1. We call $animate.enter() to add a new div element with the custom-js-animation class associated to it. Since the custom-js-animation represents an animation, AngularJS knows that the enter() method of the animation definition object must be used to animate the HTML element.
  2. We call $animate.addClass() to add the box-border class to the div element and this triggers the beforeAddClass() and addClass() methods of the animation definition object. Both these methods perform an animation only in case the box-border class is added and not for other classes.
  3. We call $animate.removeClass() to remove the box-border class from the div element and this triggers the beforeRemoveClass() and removeClass() methods of the animation definition object. Both these methods perform an animation only in case the box-border class is removed and not for other classes.
  4. We call $animate.leave() to remove the div element and AngularJS knows that the leave() method of the animation definition object must be used to animate the HTML element.

Each method of the animation definition object handles the cancellation of the animation by making it reach its final state immediately (and the done() method is called immediately as well).

Now we must consider two important things in this example that might not be easily understandable by just looking at the source code:

  • There's a problem with the animation end handler function returned by the leave() method of the definition object. There's a "short-circuit" in the current version of AngularJS (1.3.8) that makes this handler function be called always with cancelled == true so there's no way for us to know if a leave animation completes normally or has been cancelled (it always looks cancelled).
  • You see that in each handler of an animation promise we use the $timeout service to set a 20 milliseconds timeout before the next animation step starts. In the enter animation, the timeout is important to make the animation cleanup completely before the next one starts. This is not a problem with class-based animations (add class and remove class). The number of milliseconds must be over 16.66 because it turns out that in the AngularJS source code that value is used as a timeout to wait for a reflow of the DOM when an animation that causes a structural change of the DOM (like enter and leave) is executed. The value 16.66 is simply 1000 / 60 = 16.66 (one second divided by 60).

Point 2 of the example shows that the enter and leave animations are executed automatically by AngularJS by simply adding the custom-js-animation class to a div. When the div is added to the DOM, AngularJS scans the defined animations, finds custom-js-animation and executes it. The same happens when the div is removed from the DOM.