AngularJS with Animation and Callback

What is the problem

When we use AngularJS, manipulating the DOM element is something we often want to do. I encountered a problem recently that I have an object bound to a bunch of fields in a container, then what I need to do is to bind a new object, but with an fade in and fade out animation. So it is pretty much like this:

  1. Fade out the container with old object
  2. Bind the new object
  3. Fade in the container with new object

How did I solve it

I first went to Google (of course this is what you guys would do as well), and found this article AngularJS - Animating the Angular Way by EggHead. It is a really good tutorial talks about how to define a directive to watch for the attribute and then use the $animate service to do the animation. However, this article did not address how to solve the problem of updating the scope after the animation. So here I just want to quickly give you the way of how to do it.

Actually it is pretty simple, all you need is to add a call back function, and some tricks.

Here is an example, or you can try to open the demo here.

<!DOCTYPE html>
<html>
<head>
    <title>AngularJS Animate And Callback Demo :: XYZHOU.com</title>
</head>
<body>

<div id="ng-app" ng-app="myApp" ng-controller="myController" ng-init="init()">
    <div fade-me="isFade" ng-click="flip()">
        Click this: {{myValue}}
    </div>
</div>

<script src="//code.jquery.com/jquery-2.1.0.min.js"></script>
<script src="//code.angularjs.org/1.2.14/angular.min.js"></script>
<script src="//code.angularjs.org/1.2.14/angular-animate.min.js"></script>

<script>
    var myApp = angular.module('myApp', ['ngAnimate']);

    myApp.controller('myController', ['$scope', function($scope) {

        $scope.init = function() {

            // init values
            $scope.isFade = false;
            $scope.myValue = "Foo";

        };

        $scope.flip = function() {

            // flip the value only when the container is faded out
            if ($scope.isFade)
                $scope.myValue = ($scope.myValue === "Foo") ? "Bar" : "Foo";

            // flip the fade value
            $scope.isFade = !$scope.isFade;

        }

    }]);

    // define the directive
    myApp.directive("fadeMe", ['$animate', function($animate) {
        return function(scope, element, attrs) {

            // watch the fade-me directive
            scope.$watch(attrs.fadeMe, function(newVal) {
                if (newVal) {

                    $animate.addClass(element, "fade", function() {

                        // this its the call back, wrap it in $apply to update the angular models
                        scope.$apply(scope.flip());

                    });

                } else {
                    $animate.removeClass(element, "fade");
                }
            });
        }
    }]);

    // define the animate service
    myApp.animation(".fade", function() {
        return {
            addClass: function(element, className, callback) {
                element.animate({ opacity: 0 }, callback);
            },
            removeClass: function(element, className, callback) {
                element.animate({ opacity: 1 }, callback);
            }
        }
    });

</script>

</body>
</html>

Wrap it up

  1. Import necessary scripts (jQuery for animate(), angularjs, angularjs-animate).
  2. Create the directive and define the animate service.
  3. Watch the fade value in the directive and provide a callback with $apply to update the angular model.

The key idea here is to use scope.$apply to update the angular models, because I think the callback is just passed to jQuery as a normal callback function, and has nothing to do with angular. It is about the same issue when you want to use a jQuery plugin.