Angular nested animation issues

I'll start off by saying that particularly for data heavy sites, Angular is great. The dependency injection, separation of concerns and data binding all promote well written code, and save a load of time. But, and its a big hairy butt… The animation system as of 1.2 leaves developers that are trying to do anything but the simplest animations, rather wanting.

The system intends to provide hooks for CSS and JavaScript animation, with the latter being vital, as IE9 and below don't support transitions. Also, for doing anything more than fairly simple animations, JavaScript animation libraries such as Greensock are a must.

So, on to how it works, and the issues…

CSS Animations
The easiest way to illustrate this is with a route that has an animation specified on ng-view, and sub animation within the transitioned in view that is triggered by a directive on that page using $animate.addClass().

The way to do this would be to specify your animations in a CSS file:

.anim-page-transition.ng-enter,
.anim-page-transition.ng-leave {
  -webkit-transition: all 1s ease-in-out;
  -moz-transition: all 1s ease-in-out;
  -ms-transition: all 1s ease-in-out;
  -o-transition: all 1s ease-in-out;
  position: absolute;
}

.anim-page-transition.ng-enter,
.anim-page-transition.ng-leave.ng-leave-active {
  opacity: 0;
}

.anim-page-transition.ng-leave,
.anim-page-transition.ng-enter.ng-enter-active {
  opacity: 1;
}

.anim-slide{
  -webkit-transition: all 1s ease-in-out;
  -moz-transition: all 1s ease-in-out;
  -ms-transition: all 1s ease-in-out;
  -o-transition: all 1s ease-in-out;
  margin-left: 200px;
}

Make sure to add the animation reference to the ng-view element

<div ng-view class="anim-page-transition"></div>  

Then trigger the animation to run from within the directive's link function

.directive('myDirective',
function(){

  return {
    restrict: 'A',
    link: function(scope, element, attrs){

      $animate.addClass(element, 'anim-slide', function(){
        console.log('animation complete!');
      });

    }
  }
})

See it all together in a the following Plunker

This works fine when the pages first loads, but on any subsequent navigations, the inner animation executes without a transition due to the Angular framework causing all child animations to execute immediately. There is a dirty and classic work around though… fooling it with a timeout…

function($animate, $timeout){  
  return {
    restrict: 'A',
    link: function(scope, element, attrs){
      $timeout(function(){
        $animate.addClass(element, 'anim-slide', function(){
          console.log('animation complete!');
        });
      });
    }
  }
}

Again, here's the a Plunker to show it running.

Great you think, problem solved, but unfortunately it isn't entirely, because done handler of $animate.addClass fires immediately, preventing you from executing any code when the child animation finishes… something that is fairly fundamental to single page apps, particularly in the graphically demanding world of advertising production. Currently there doesn't seem to be any kind of fix for this apart from either removing the outer animation, which works but isn't viable (see Plunker) or setting off a separate timer, which as you can imagine would create all kind of other issues, and be rather untidy way of doing things.

So, back to the drawing board… maybe we can do it with JavaScript…

JavaScript Animations
Start by specifying your animations:

.animation('.anim-page-transition-js',
  function() {
    return {
      enter: function (element, done) {
        $(element).css({
          position: 'absolute',
          opacity: 0
        });
        $(element).animate({opacity:1}, 1000, done);
      },
      leave: function (element, done) {
        $(element).css({
          position: 'absolute',
          opacity: 1
        });
        $(element).animate({opacity:0}, 1000, function(){
        $(element).remove();
        done();
      });
    }
  }
)

.animation('.anim-slide-js',
  function() {
    return {

      addClass: function (element, className, done) {
        $(element).animate({marginLeft: 200}, 1000, done);
      },

      removeClass: function (element, className, done) {
        $(element).animate({marginLeft: 0}, 1000, done);
      }

    }
  }
)

Then add the reference to the page transition animation your ng-view node:

<div ng-view class="anim-page-transition-js"></div>  

Then trigger it off as before in the directive:

function($animate, $timeout){  
  return {
    restrict: 'A',
    link: function(scope, element, attrs){

      $animate.addClass(element, 'anim-slide-js', function(){
        console.log('animation complete!');
      });
    }
  }
}

See it all together in a Plunker.

The result of this, which should, according to the documentation work, is the the outer animation working fine, but the inner animation only working when the app first loads up. So perhaps adding a short timeout, as we did before will help… Nope, unfortunately the inner animation only runs the first time, unless you add a timeout that is longer than the page transition animation plus around 20ms (See and Plunker), but this requires the directive having knowledge about components outside of it which isn't really ideal, and only works about 90% of the time. As before, the outer animation is killing all the the child animations.

A partly working hack is to remove the outer animation and add a 10ms delay on the inner animation:

.directive('myDirective',
  ['$animate', '$timeout',
    function($animate, $timeout){

      return {
        restrict: 'A',
        link: function(scope, element, attrs){
          $timeout(function(){
            console.log('firing animate in directive');
            $animate.addClass(element, 'anim-slide-js');
          }, 10);
        }
      }
    }
  ]
)

And the Plunker

But this this still results in the animation failing about 50% of the time if route changes are triggered before the animation on the exiting page is complete and doesn't give us the overall effect we need.

Conclusions
Hmmm... If you're not supporting IE9 and CSS transitions are adequate for the effects you're trying to achieve, you'll be fine, as long as you don't need listen for when inner transitions complete. As far as JavaScript goes, its kind of a no go for anything but the simplest apps where everything is contained within one page and none of your animations are nested. Which is a real shame because many complex apps, particularly in the advertising world, need to use rich sequencing libraries such as Greensock to achieve the results demanded by clients and designers.

Solutions
Hmmm... again, I don't think there really are any at the moment, apart from to trigger off your animations directly from within directives. This makes my developer spider sense feel not very nice, as it goes against the “Angular Way” and could potentially create all kinds of issues if multiple developers are working on a project, and a mixture of direct triggering and by the book animation techniques are being used. But projects need animating, and if the framework doesn't make the grade, then hacks and work arounds need to be applied.

Hopefully this will be fixed in future versions, but in the meantime its time to find some creative solutions to the problem… Watch out for another blog post soon...