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. Enter and Leave</h3> <button type="button" ng-click="onAddSquareClick(false)">Add the square as a child (Enter)</button><br /> <button type="button" ng-click="onAddSquareClick(true)">Add the square as a sibling (Enter)</button><br /> <button type="button" ng-click="onRemoveSquareClick()">Remove the square (Leave)</button><br /> <br /> <strong>Enter animation state:</strong> {{enterAnimationStateBox1}}<br /> <strong>Leave animation state:</strong> {{leaveAnimationStateBox1}}<br /> <br /> <div id="box1Parent" class="box"></div> <br /> <h3>2. Move</h3> <button type="button" ng-click="onMoveSquaresClick()">Move the squares</button><br /> <br /> <strong>Move animation state:</strong> {{moveAnimationState}}<br /> <br /> <span id="moveLeftmostElement"></span> <div id="blueBox" class="box animated-element" style="background-color: lightskyblue; display: inline-block; margin-left: 5px; margin-right: 5px;"></div> <div id="redBox" class="box animated-element" style="background-color: lightcoral; display: inline-block; margin-left: 5px; margin-right: 5px;"></div> <div id="greenBox" class="box animated-element" style="background-color: lightgreen; display: inline-block; margin-left: 5px; margin-right: 5px;"></div> <br /> <h3>3. Add Class, Remove Class, Set Class</h3> <button type="button" ng-click="onAddClassClick()">Add class</button><br /> <button type="button" ng-click="onRemoveClassClick()">Remove class</button><br /> <br /> <strong>Add class animation state:</strong> {{addClassAnimationStateBox2}}<br /> <strong>Remove class animation state:</strong> {{removeClassAnimationStateBox2}}<br /> <br /> <div id="box2" class="invisible-box animated-element"></div> <br /> <button type="button" ng-click="onSetClassClick()">Set class</button><br /> <br /> <strong>Set class animation state:</strong> {{setClassAnimationStateBox3}}<br /> <br /> <div id="box3" class="empty-box animated-element box-fill"></div> <br /> <h3>4. Animate and Cancel</h3> <br /> <strong>NOTE:</strong> this point doesn't work properly on Firefox due to this bug https://bugzilla.mozilla.org/show_bug.cgi?id=830056<br /> <br /> <button type="button" ng-click="onStartLongAnimationClick()">Start long animation</button><br /> <button type="button" ng-click="onCancelLongAnimationStepClick()">Cancel current long animation step</button><br /> <br /> <strong>Long animation state:</strong> {{longAnimationStateBox4}}<br /> <br /> <div id="box4" class="invisible-box"></div> <br /> <h3>5. Enable/Disable animations</h3> <label>Enable animations: <input id="enableAnimationsCheck" type="checkbox" ng-model="areAnimationsEnabled" ng-change="enableDisableAnimations(areAnimationsEnabled)" /> </label><br /> <br /> <h3>6. Custom animated directive</h3> <label>Enable glow: <input type="checkbox" ng-model="enableGlow" /></label><br /> <br /> <div class="box" element-glow="enableGlow"></div> </div> </body> </html>
angular.module("mainModule", ["ngAnimate"]) .directive("elementGlow", function ($animate) { // Initialization var unwatchProperty = null; // Definition object return { // Post-link function link: function (scope, element, attrs) { var watchedProperty = attrs.elementGlow; if (watchedProperty !== undefined && watchedProperty !== "") { unwatchProperty = scope.$watch(watchedProperty, function (newVal, oldVal, scope) { if (newVal !== oldVal) { if (newVal) { $animate.animate(element, { "box-shadow": "none" }, { "box-shadow": "0px 0px 15px 5px rgba(135, 206, 250, 0.75)" }, "element-glow-animation" ); } else { $animate.animate(element, { "box-shadow": "0px 0px 15px 5px rgba(135, 206, 250, 0.75)" }, { "box-shadow": "none" }, "element-glow-animation" ); } } }); element.one("$destroy", function () { // Directive cleanup if (unwatchProperty !== null) { unwatchProperty(); unwatchProperty = null; } }); } } }; }) .controller("mainController", function ($scope, $animate, $q) { $scope.enterAnimationStateBox1 = "OFF"; $scope.leaveAnimationStateBox1 = "OFF"; $scope.moveAnimationState = "OFF"; $scope.addClassAnimationStateBox2 = "OFF"; $scope.removeClassAnimationStateBox2 = "OFF"; $scope.setClassAnimationStateBox3 = "OFF"; $scope.longAnimationStateBox4 = "OFF"; $scope.areAnimationsEnabled = true; var moveSequence = 1; var setClassFlag = false; var currLongAnimationPromise = null; $scope.onAddSquareClick = function (addAsSibling) { var box1Parent = document.querySelector("#box1Parent"); if (box1Parent !== null) { box1Parent = angular.element(box1Parent); var box1 = angular.element("<div id=\"box1\" class=\"inner-box animated-element\"></div>"); if (addAsSibling) { box1.css({position: "relative"}); } else { box1.css({position: "absolute"}); } $scope.enterAnimationStateBox1 = "In progress..."; $animate.enter(box1, (addAsSibling ? null : box1Parent), (addAsSibling ? box1Parent : null)).then(function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { $scope.enterAnimationStateBox1 = "ENDED"; }); }); } }; $scope.onRemoveSquareClick = function () { var box1 = document.querySelector("#box1"); if (box1 !== null) { box1 = angular.element(box1); $scope.leaveAnimationStateBox1 = "In progress..."; $animate.leave(box1).then(function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { $scope.leaveAnimationStateBox1 = "ENDED"; }); }); } }; $scope.onMoveSquaresClick = function () { var moveLeftmostElement = document.querySelector("#moveLeftmostElement"); var blueBox = document.querySelector("#blueBox"); var redBox = document.querySelector("#redBox"); var greenBox = document.querySelector("#greenBox"); if (moveLeftmostElement !== null && blueBox !== null && redBox !== null && greenBox !== null) { moveLeftmostElement = angular.element(moveLeftmostElement); blueBox = angular.element(blueBox); redBox = angular.element(redBox); greenBox = angular.element(greenBox); $scope.moveAnimationState = "In progress..."; var promise; switch (moveSequence) { // From BLUE RED GREEN to GREEN BLUE RED case 1: promise = $q.all([ $animate.move(greenBox, null, moveLeftmostElement), $animate.move(blueBox, null, greenBox), $animate.move(redBox, null, blueBox) ]); break; // From GREEN BLUE RED to RED GREEN BLUE case 2: promise = $q.all([ $animate.move(redBox, null, moveLeftmostElement), $animate.move(greenBox, null, redBox), $animate.move(blueBox, null, greenBox) ]); break; // From RED GREEN BLUE to BLUE RED GREEN case 3: promise = $q.all([ $animate.move(blueBox, null, moveLeftmostElement), $animate.move(redBox, null, blueBox), $animate.move(greenBox, null, redBox) ]); break; } moveSequence++; if (moveSequence > 3) { moveSequence = 1; } promise.then(function () { $scope.moveAnimationState = "ENDED"; }); } }; $scope.onAddClassClick = function () { var box2 = document.querySelector("#box2"); if (box2 !== null) { box2 = angular.element(box2); $scope.addClassAnimationStateBox2 = "In progress..."; $animate.addClass(box2, "custom-class").then(function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { $scope.addClassAnimationStateBox2 = "ENDED"; }); }); } }; $scope.onRemoveClassClick = function () { var box2 = document.querySelector("#box2"); if (box2 !== null) { box2 = angular.element(box2); $scope.removeClassAnimationStateBox2 = "In progress..."; $animate.removeClass(box2, "custom-class").then(function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { $scope.removeClassAnimationStateBox2 = "ENDED"; }); }); } }; $scope.onSetClassClick = function () { var box3 = document.querySelector("#box3"); if (box3 !== null) { box3 = angular.element(box3); $scope.setClassAnimationStateBox3 = "In progress..."; setClassFlag = !setClassFlag; $animate.setClass(box3, (setClassFlag ? "box-border" : "box-fill"), (setClassFlag ? "box-fill" : "box-border")).then(function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { $scope.setClassAnimationStateBox3 = "ENDED"; }); }); } }; $scope.onStartLongAnimationClick = function () { var box4 = document.querySelector("#box4"); if (box4 !== null) { box4 = angular.element(box4); var animationStep1 = function () { $scope.longAnimationStateBox4 = "In progress (step 1)..."; currLongAnimationPromise = $animate.animate(box4, { opacity: 0 }, { opacity: 1 }, "custom-inline-animation-1", { // Add these classes to the element during the animation tempClasses: ["box-border"] } ); currLongAnimationPromise.then(function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { animationStep2(); }); }); }; var animationStep2 = function () { $scope.longAnimationStateBox4 = "In progress (step 2)..."; currLongAnimationPromise = $animate.animate(box4, null, null); currLongAnimationPromise.then(function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { animationStep3(); }); }); }; var animationStep3 = function () { $scope.longAnimationStateBox4 = "In progress (step 3)..."; currLongAnimationPromise = $animate.animate(box4, { opacity: 1 }, { opacity: 0 }, "custom-inline-animation-2" ); currLongAnimationPromise.then(function () { // NOTE: I must use $scope.$apply() inside this callback $scope.$apply(function () { currLongAnimationPromise = null; // Remove the inline styles set by animationStep1 and animationStep3 box4.removeAttr("style"); $scope.longAnimationStateBox4 = "ENDED"; }); }); }; // Start the animation animationStep1(); } }; $scope.onCancelLongAnimationStepClick = function () { if (currLongAnimationPromise !== null) { $animate.cancel(currLongAnimationPromise); currLongAnimationPromise = null; } }; $scope.enableDisableAnimations = function (enableAnimations) { $animate.enabled(enableAnimations); }; });
* { box-sizing: border-box; } .box { width: 100px; height: 100px; background-color: lightskyblue; display: block; } .inner-box { margin: 10px; width: 80px; height: 80px; background-color: lightcoral; display: block; } .invisible-box { width: 100px; height: 100px; background-color: lightcoral; display: block; opacity: 0; } .empty-box { width: 100px; height: 100px; border: 1px solid #ffffff; display: block; } .custom-class { opacity: 1; } .box-border { border: 1px solid #808080; } .box-fill { background: lightgreen; } .animated-element.ng-enter, .animated-element.ng-leave, .animated-element.ng-move, .animated-element.custom-class-add, .animated-element.custom-class-remove, .animated-element.box-border-add, .animated-element.box-border-remove, .animated-element.box-fill-add, .animated-element.box-fill-remove { -webkit-transition: 1.5s linear all; transition: 1.5s linear all; } .animated-element.ng-enter { opacity: 0; } .animated-element.ng-enter.ng-enter-active { opacity: 1; } .animated-element.ng-leave { opacity: 1; } .animated-element.ng-leave.ng-leave-active { opacity: 0; } .animated-element.ng-move { opacity: 0; } .animated-element.ng-move.ng-move-active { opacity: 1; } .animated-element.ng-move-stagger { /* Set a 400ms delay between each successive move animation */ -webkit-transition-delay: 0.4s; transition-delay: 0.4s; /* * In case the stagger doesn't work then these two values * must be set to 0 to avoid an accidental CSS inheritance. */ -webkit-transition-duration: 0s; transition-duration: 0s; } .animated-element.custom-class-add { opacity: 0; } .animated-element.custom-class-add.custom-class-add-active { opacity: 1; } .animated-element.custom-class-remove { opacity: 1; } .animated-element.custom-class-remove.custom-class-remove-active { opacity: 0; } .animated-element.box-border-add { border-color: #ffffff; } .animated-element.box-border-add.box-border-add-active { border-color: #808080; } .animated-element.box-border-remove { border-color: #808080; } .animated-element.box-border-remove.box-border-remove-active { border-color: #ffffff; } .animated-element.box-fill-add { background: none; } .animated-element.box-fill-add.box-fill-add-active { background: lightgreen; } .animated-element.box-fill-remove { background: lightgreen; } .animated-element.box-fill-remove.box-fill-remove-active { background: none; } @-webkit-keyframes blinkAndGrow { from { background-color: lightcoral; } 25%, 75% { transform: scale(1.1); background-color: red; } 50% { transform: scale(1.0); background-color: lightcoral; } to { background-color: lightcoral; } } @keyframes blinkAndGrow { from { background-color: lightcoral; } 25%, 75% { transform: scale(1.1); background-color: red; } 50% { transform: scale(1.0); background-color: lightcoral; } to { background-color: lightcoral; } } @-webkit-keyframes shrinkAndDisappear { from { opacity: 1; } 70% { transform: scale(0.8); opacity: 0.8; } to { transform: scale(0.0); opacity: 0; } } @keyframes shrinkAndDisappear { from { opacity: 1; } 70% { transform: scale(0.8); opacity: 0.8; } to { transform: scale(0.0); opacity: 0; } } .custom-inline-animation-1 { -webkit-transition: 1.0s linear all; transition: 1.0s linear all; } .ng-inline-animate { -webkit-animation: blinkAndGrow 1.5s linear; animation: blinkAndGrow 1.5s linear; } .custom-inline-animation-2 { -webkit-animation: shrinkAndDisappear 0.8s linear; animation: shrinkAndDisappear 0.8s linear; } .element-glow-animation { -webkit-transition: 0.6s linear all; transition: 0.6s linear all; }

Example


Description


In the previous example we've seen how to animate AngularJS built-in directives and now we're going to see which tools AngularJS offers us to animate our own directives or simply to animate some HTML elements. This can be done with the $animate service.

Before moving on, remember that if you want to see all the methods made available by the $animate service, you can check the official documentation. In the same page you'll see for each method the exact sequence of CSS classes that are applied to the target HTML element during the animation. Remember also that starting with AngularJS 1.3 all the methods return a promise to know when the animation is complete, while in previous AngularJS versions a callback was used for the same purpose. Here we're going to focus on version 1.3 so we're going to use promises taking advantage of all the flexibility they offer.

Let's start by taking a look at point 1 of the example. Here we see how to use the enter and leave methods of the $animate service. The enter method adds an element to the DOM and executes the corresponding enter animation. This means that we must define our own CSS transitions or keyframe animations for the ng-enter class like we've seen in our previous example about animated directives. In our HTML page we've got a div element with ID box1Parent and we use the enter method to add another div (with ID box1) inside box1Parent or as its sibling. When box1 is added we can see the enter animation. We use the promise returned by the enter method to know when the animation is complete and we update the enterAnimationStateBox1 variable to display the animation state during its progress. As you can see, inside the callback of the promise we must use $scope.$apply because the code inside the callback would be outside the AngularJS life-cycle otherwise. The use of the leave method is very similar. In this case the CSS transitions or keyframe animations must be defined for the ng-leave class and we use the method to remove the box1 we've previously added.

In point 2 of the example we see how to use the move method. Here we have three boxes and each time we invoke the move method we want them to shift by one position. moveLeftmostElement is just a placeholder because we need to tell move to move an element after another one so here's the description of the first move sequence:

  1. $animate.move(greenBox, null, moveLeftmostElement): move greenBox after moveLeftmostElement;
  2. $animate.move(blueBox, null, greenBox): move blueBox after greenBox;
  3. $animate.move(redBox, null, blueBox): move redBox after blueBox.

Since we've defined a staggering animation for the move event, each move animation starts a short time after the previous one thus giving the staggering effect. Remember that we've talked about the staggering animations in the example about animated directives. We use the $q service to wait for all the animation promises to end and notice that in this case we don't need to use $scope.$apply inside the then method callback for the promise returned by $q. That would be needed if we were using directly the promises returned by the move method like it happens with any other animation method of the $animate service.

In point 3 of the example we simply see how we can add and remove a class and animate the target HTML element. We use three methods of the $animate service: addClass, removeClass and setClass. There's nothing special in the first two methods, while the third one (setClass) allows us to add and remove CSS classes at the same time and as you can see, the animations for both the add and remove events run in parallel.

Point 4 shows the use of the animate method and how we can cancel a running animation with the cancel method. You should follow this description while looking also at the official documentation of the animate method. Here we have a long animation made of three successive steps:

  1. The animation is on the opacity property that we've specified directly inline in the animate method and we make it go from 0 to 1 (this means that at the beginning of the animation, the style attribute of the animated HTML element will contain the value opacity: 0; while at the end of the animation it will contain the value opacity: 1; and that inline style will stay there even after the animation ends, it will not be removed). The CSS transition is defined in the custom-inline-animation-1 class. During the animation, the box-border class is added to the animated element and is removed at the end of the animation. This last functionality is not documented and, as you've probably seen in the official documentation, for most of the animation methods there's an options parameter. In the version of AngularJS used for this example, that parameter is an object that accepts an array of class names in its tempClasses property with the classes that have to be added to the animated element while the animation is running. Since all of this is not documented and I've found it with a close inspection of AngularJS source code, you should not rely on it for the future, it has been presented here just to let you know what that mysterious options parameter is for.
  2. The animation is completely defined in the CSS. Since we call the animate method just with the target HTML element, we don't specify any inline style to animate and we use the default ng-inline-animate class of the method to specify our CSS animation.
  3. This step is a mix of step 1 and 2, so we've both specified inline styles to apply at the beginning and the end of the animation and a custom-inline-animation-2 class to define a CSS keyframe animation. In this case both the inline styles and the CSS animation will be applied and at the end of the animation the style attribute of the animated HTML element will contain the value opacity: 0;. Inside the promise callback for the end of the animation step 3 we remove the style attribute completely from the HTML element to restore its state as it was before animation step 1 started.

Each time an animation step starts, we keep track of the current animation promise in the currLongAnimationPromise variable. If we want to cancel a running animation, all we have to do is call the $animate.cancel method with the corresponding promise as argument. Each time you cancel an animation step in the example, the corresponding promise is immediately resolved and the corresponding callback is called as if the animation ended normally and the next animation step starts.

Point 5 of the example shows how we can easily completely disable animations or enable them again. To do this, we just need to call the $animate.enabled method. If you disable the animations, you can try any animation in the example and you'll see that they all reach their final state immediately instead of going through a progression.

In point 6 we finally make our own animated directive. We create the elementGlow directive that applies a glow effect to an element. This directive accepts as input the name of a scope variable that tells if the glow should be made visible or not. In this case, the enableGlow variable set by a checkbox makes the glow visible or invisible. Inside the directive we keep track of the value changes of the variable with the scope.$watch method, we use the $animate.animate method to animate the target HTML element and we've defined the CSS transition in the element-glow-animation class. It's very important to notice that we also handle the $destroy event for the HTML element to which the directive is applied because we need to unwatch the variable in case the element is removed from the DOM, otherwise the $watch handler would be fired every time the enableGlow changes even though the HTML element is not on the DOM anymore! We use the element.one method because the handler needs to be fired just once for the $destroy event and detached from the element immediately after its execution. Handling the $destroy event is a good habit for any of your custom directives in case you need to watch some variables defined outside the directives.

In the next example we'll see how AngularJS lets us define even JavaScript animations.

NOTE: due to a bug in Firefox, inline animations (that we have when we use the $animate.animate method) with CSS keyframes don't work and this is the case of point 4 in this example, so that point is not going to work properly on Firefox (see https://bugzilla.mozilla.org/show_bug.cgi?id=830056 for this bug).