Learn how to make even better animations in AngularJS 1.1.5
AngularJS Animations hit the web last month in version 1.1.4 of AngularJS. While being a advanced feature, animations in AngularJS were a bit too basic for what they were. Now, with version 1.1.5, animations are even easier to use and 1.1.5 comes packaged with more great features for you to play with. With support for CSS Keyframe animations, multiple property durations + delays and a whole lot of fixes. AngularJS animations are better than ever.
Want to learn how to make use of these new kick ass animation features with AngularJS? Shift your focus and follow along on another jam-packed yearofmoo article :)
The previous yearofmoo AngularJS animation article, Animation in AngularJS has been updated to work with the new improvements in 1.1.5. Please read that article if you haven't already. The code repo for that article has also been upgraded to use 1.1.5 as well.
to topIf you're using a newer version of AngularJS (1.2.0), please refer to this article which explains how to perform animations using the new ngAnimate API.
to topJust like the previous article, this one comes with a code repository which showcases some of the new animation features + fixes present in version 1.1.5. Please view, clone, fork and play with the code in the repo as much as it you want! You're here to learn aren't you??!!
to topAnimations in 1.1.5 have been tweaked around from what they were in 1.1.4. The reasoning for this is to make them easier to use with other libraries and plugins. So what's been changed? CSS class naming suffixes have been changed, multiple delays and durations, as well as the ins and outs of overlapping animations in ngAnimate. Lets examine this in more detail...
Back in 1.1.4 there was a missing feature for supporting delay values as well as multiple delay/duration values for CSS transitions. This is now fully supported in 1.1.5. The following code below will be fully understood by ngAnimate:
.animation-enter {
/* delay for 2 seconds and then animate for 1 second */
transition:1s linear all 2s;
animation:animation_name 1s 2s;
/* animate for 10 seconds */
transition-duration: 1s, 10s, 4s;
animation-duration: 1s, 10s, 4s;
/* delay for 4 seconds before animating for 1 second */
transition-delay: 4s;
transition-duration: 1s;
animation-delay: 4s;
animation-duration: 1s;
/* delay for 10 seconds before starting */
transition-delay: 4s, 10s, 1s;
animation-delay: 4s, 10s, 1s;
}
ngAnimate will now properly calculate the total time for the max durations and delays together and complete the animation once the last delay timeout has been passed.
AngularJS still provides setup and a start class values for the element that is being animated, but now the suffix values of those CSS class values have changed a bit:
ngAnimate="..." | Setup Class | Start Class |
---|---|---|
{enter:'enter-animation'} | enter-animation | enter-animation-active |
{leave:'leave-animation'} | leave-animation | leave-animation-active |
{move:'move-animation'} | move-animation | move-animation-active |
{show:'show-animation'} | show-animation | show-animation-active |
{hide:'hide-animation'} | hide-animation | hide-animation-active |
{custom:'custom-animation'} | custom-animation | custom-animation-active |
As you can see, ngAnimate no longer attaches a -setup suffix to the setup class name. Instead, the setup class will be attached to the class attribute of the element as the exact value provided in animation definition. If you provide a general value for ngAnimate then each of the CSS class values will be expanded to the format of [name]-[event] and [name]-[event]-active.
So an element that contains an animation that is in progress will look like so:
<div ng-directive
ng-animate="{enter: something}"
class="enter-something enter-something-active">
...
</div>
Why these changes to ngAnimate? Well they're here so that ngAnimate can properly support CSS Keyframe animations without the need to cross define animations inside both CSS classes. Only the setup class is required when using CSS Keyframe animations (these are explained shortly).
If you happen to trigger another animation while ngAnimate is still animating then ngAnimate will automatically cancel the former animation and run any required DOM operations right away before the new animation takes place. It will also remove the CSS class values from the element being animated and then add the new CSS class names for the next animation to the same element.
If you're using CSS-enabled animations then you don't need to do anything--ngAnimate will add handle the cancellation for you-- but if you're using JavaScript-enabled animations then you have the option to handle the cancellation event via a cancel callback in your animation definition. Here's an example:
ngModule.animation('js-animation', function() {
return {
setup : function(element) {
setupAnimation(element);
},
start : function(element, done, memo) {
//animation code to run the animation
runAnimation(element);
done();
},
cancel : function(element, done) {
/*
perform any operations upon cancellation and
then call the done() method. Keep in mind
that when something is cancelled then any
subsequent calls to done() (like inside of the start
function) will be ignored by ngAnimate. So you don't
have to worry about your start function.
*/
cancelAnimation(element);
done();
},
};
});
Whenever you're doing any animations in your AngularJS application you should always use ngAnimate to parse your animation properties. Why should you do this? Animations are properly run, completed, and cancelled now in ngAnimate so you don't have to waste precious keystrokes by implementing your own animation stop and go code. Also, by exclusively using animations with ngAnimate, testing your AngularJS code is much easier since you can fully isolate your animation code from your directive or controller code. And custom animations are now also now detected and handled as well. These two will be explained later in the article.
In the earlier release of ngAnimate, it was difficult to control the very first animation that is triggered for a directive. This is because the directive itself has to be compiled first, and then, animated just right after. This can cause your animation to be visible or invisible before an animation starts off, causing a flicker. This has been fixed. All animations are quickly executed upon bootstrap and your browser won't have a to deal with a barrage of animations triggered at once when the page is rendered.
to topCSS3 Keyframe Animations are now fully supported with ngAnimate. To make them work, first define your CSS animation code by placing the animation timing styles inside of the setup class and make them use a CSS animation keyframe which is defined just as a normal CSS keyframe animation.
.custom-in {
-webkit-animation:custom_in 0.5s;
animation:custom_in 0.5s;
}
@keyframes custom_in {
from {
transform:rotate(-30deg);
opacity:0;
}
to {
transform:rotate(0deg);
opacity:1;
}
}
/* repeat the code above for the vendor-prefix stuff too... */
@-webkit-keyframes my_animation {...}
There is no need to define anything inside the active CSS class (in this case is would be custom-in-active), only the setup class is required (in this case just custom-in).
CSS Keyframe animations are much more powerful than CSS Transitions and there exist pure CSS3 animation frameworks which you can easily hook into ngAnimate with zero lines of JavaScript code. But what about Browser support? Well, as of now, all browsers that support CSS3 transitions also support CSS animations (keyframe animations that is). So it's up to you whether to use transitions, keyframe animations or javascript animations in your AngularJS code. Damn you're spoiled!
to topSyncing animations requires a bit of a different approach than what you would think when it comes to handling animations. You really just want your CSS/JS animations to setup a delay so that the previous animation can do it's thing before the new animation comes along. So for directives ngView, ngSwitch and ngInclude, the elements that will be animating are the elements that are injected into the parent (where the directive attribute is attached to). For any other directives, such as ngShow, ngHide and ngIf, they can only perform one animation at a time (since there is only one element being animated). Therefore, there is no need to perform any animation synchronization for the directives just mentioned.
Once the directive being animated removes the previous content and injects the new content then a leave and an enter animation will be triggered. This happens at the same time so, but both animations are still fired (because they're on different elements), so if you want the leave animation to do it's thing before the enter animation comes around then just include a transition-delay or animation-delay CSS property in your CSS animation definition code. If you're using JS-enabled animations, then just delay your animation code, using the injected $window object's setTimeout() function, by the duration of the former animation.
.leave-animation {
transition:1s linear all;
opacity:1;
}
.leave-animation.leave-animation-active {
opacity:0;
}
.enter-animation {
/* the trailing 1s here is the delay */
transition:1s linear all 1s;
opacity:0;
}
.enter-animation.enter-animation-active {
opacity:1;
}
For JavaScript-enabled animations, delay your animations by using a $window.setTimeout() call:
ngModule.animation('leave-animation', function($window) {
return {
start : function(element, done) {
$window.setTimeout(1000, function() {
//run the animation
//call done when the animation is complete
done();
});
}
};
});
The best, pure CSS framework out there for animations is known as animate.css and has been crafted together by a talented developer known as Dan Eden This crazy CSS framework can easily be used with ngAnimate in version 1.1.5. There are plans to have a plugin/library for AngularJS to work with this tool with less code, but for now you need to add one extra animation name property inside of the ngAnimate attribute.
To make this work, download animate.css and include it among your other CSS files for your web application and link it in the HTML. Then for each animation linked to ngAnimate just prefix your animation value with animated (plus a space) before your animation name. It's important that you download animate.css and keep it in the same domain as your website because most browsers restrict access to examining CSS properties for stylesheets that are fetched from outside of the browser origin. As you can imagine, this is essential for ngAnimate to properly calculate the total duration time from the animation/transition styles for the animation to properly animate.
<!-- the animated class name is required by animate.css -->
<div ng-directive
ng-animate="'animated fadeIn'">
...
</div>
<div ng-directive
ng-animate="{enter:'animated fadeIn', leave:'animated fadeOut'}">
...
</div>
Keep in mind that, despite their being a huge range of animations, it's unlikely that you would use any outward animation (like fadeOut, slideOut, rollOut) for an enter, show or move animation. So the easy trick here is that anything with an Out suffix should be used with leave or hide animations, while anything with an In suffix should be used for enter, move or show animations. You can decide for you own if you're using custom animations.
That's all that is required. Visit the animate.css homepage to get a full listing on the animations that are offered. The link below contains a small page that makes use of ngAnimate with animate.css and the demo website featured in this article also makes a great use of animate.css for animations.
to topGreensock is one of the most advanced and feature-rich animation libraries out there and it has been built from the group up to be provide performance driven JavaScript/CSS animations for Web Applications / Websites. With a powerful API, Greensock is easy to integrate together with ngAnimate.
Greensock provides a variety of animation wrappers designed for performing varying ranges of animation effects. For the purposes of this article and ngAnimate, lets shift our focus onto TweenMax (which provides more than we need to create a sleek animation in our application). TweenMax.to works similar to the JQuery $(element).animate() method, however, conveniently, it also handles CSS3 vendor prefixes for special animation properties, delays and provides various API callbacks.
The three TweenMax methods that we will be paying attention to are:
Method | What it does | JQuery Equivalent |
---|---|---|
TweenMax.to(element, duration, properties) | Animates the styles present in the properties object to the element or elements stretched over a total time specified by the duration | $(element).animate(styles) |
TweenMax.set(element, styles) | Applies the CSS styles present in the styles object to the element | $(element).css(styles) |
TweenMax.staggerTo(elements, duration, properties, step, onAllCompleteFn) | Animates all elements, but starts the animation on each element after a certain delay which results in a step-like animation. | See code example below... |
var index = 0, step = 100;
forEach(elements, function(element) {
setTimeout(step * (index++) + duration, function() {
$(element).animate(properties);
});
});
setTimeout(index * step + duration, onAllCompleteFn);
TweenMax also conveniently manages the state for the animation any element being animated therefore if you cancel or start a new animation then TweenMax will automatically handle the transition for you (which works perfectly with ngAnimate).
GreenSock plays well with AngularJS since TweenMax can be easily placed inside of your JavaScript-enabled animations code. There is no need to hook it up into a directive or factory, just call the to(), set() or staggerTo() methods when you need to.
angular.module('AppAnimations', [])
.animation('list-out', ['$window',function($window) {
return {
start : function(element, done) {
TweenMax.set(element, {position:'relative'});
var duration = 1;
//we can use onComplete:done with TweenMax, but lets use
//a delay value for testing purposes
TweenMax.to(element, 1, {opacity:0, width:0});
$window.setTimeout(done, duration * 1000);
}
}
}])
.animation('list-in', ['$window',function($window) {
return {
setup: function(element) {
TweenMax.set(element, {opacity:0, width:0});
},
start : function(element, done) {
var duration = 1;
//we can use onComplete:done with TweenMax, but lets use
//a delay value for testing purposes
TweenMax.to(element, duration, {opacity:1, width:210});
$window.setTimeout(done, duration * 1000);
}
}
}])
.animation('list-move', ['$window',function($window) {
return {
start : function(element, done) {
var duration = 1;
//we can use onComplete:done with TweenMax, but lets use
//a delay value for testing purposes
TweenMax.to(element, duration, {opacity:1, width:210});
$window.setTimeout(done, duration * 1000);
}
}
}])
<!-- repeated items -->
<div id="id-{{ result.id }}"
ng-repeat="result in results track by result.id"
class="result"
ng-animate="{leave:'list-out', enter:'list-in', move:'list-move'}">
<a href="" app-focus="result" class="inner">
<h5>{{ result.name }}</h5>
<img ng-src="http://builtwith.angularjs.org/projects/{{ result.thumb }}" />
</a>
</div>
There's a lot more you can do with TweenMax and GreenSock. The demo application that is apart of this article contains a working application that makes use of GreenSock for animations.
to topMany of the usecases for JavaScript-animations may require getting access to the $scope variable. While the $scope variable isn't passed in to the setup, start or cancel callbacks, it can easily be fetched using the angular.element(element).scope() function.
Lets see this in detail:
ngModule.animation('my-animation', function() {
var getScope = function(e) {
return angular.element(e).scope();
};
return {
setup : function(element) {
var $scope = getScope(element);
},
start : function(element, done, memo) {
var $scope = getScope(element);
}
cancel : function(element, done) {
var $scope = getScope(element);
}
};
});
One thing to keep in mind, however, is that each directive will either create it's own scope, use the scope of the parent or create a subscope for each child that it creates within itself (I'm looking at you ngRepeat). This means that the scope value to you get from our getScope(element) method may not be what you expect it to be. The table below describes what the scope will be for the given element inside of the JavaScript-enabled animation.
Directive | getScope(element) | Parent Scope |
---|---|---|
ngView, ngSwitch, ngIf, ngInclude | Returns the scope of the element wrapped around the template that will be injected into the directive | getScope(element).$parent |
ngRepeat | Returns the scope of the repeated element | getScope(element).$parent |
ngShow, ngHide | Returns the scope of the ngShow/ngHide element | getScope(element) |
Custom Animation | The scope of the element given for animator.animate(event, element); | getScope(element) or getScope(element).$parent |
So this means that with a directive such as ngRepeat, the scope that you get from the element will be the scope of the repeated element (the element that has the ng-repeat attribute one it). So any data assigned to the $scope object, containing the ngRepeat element, will not be accessible from that same animation acting on another element (you would need to read/write to $scope.$parent to make that happen).
to topYou can now attach custom animations to the ngAnimate attribute. What does this mean? Well if you make use of the $animator service inside of your own directives then you can perform custom animations (any animations other than the standard enter, leave, move, show and hide) using the animate(event, element) method of your animation object.
Why is this really necessary? Well if you plan on using animations in your own directives then you can fully rely on the internals of ngAnimate to detect, run and cancel your animations for you. Anything outside of ngAnimate would require the proper detection of the duration of the animation and ngAnimate does all this for you. Still not convinced? Well you can also very easily abstract out any animation code for your directives since ngAnimate operates outside of the code of your directive.
Here's an example:
ngModule.directive('someDirective', function($animator) {
return function($scope, element, attrs) {
var animator = $animator($scope, attrs);
element.bind('click', function() {
animator.animate('special', element);
if(!$scope.$$phase) $scope.$apply();
});
};
});
<div some-directive ng-animate="{special:'some-animation'}"></div>
You can now attach this animation with CSS using the value which you assigned in your ngAnimate attribute or using JavaScript-enabled animations.
to topTesting animations has not been covered yet anywhere on the web and there's nothing really to it other than using the ngAnimate attribute and the $animator service in your tests or just testing directives that use ngAnimate natively.
To test JavaScript animations, we just grab access to the $animator service and manually call the animation events for any of your JS animations. Then we check to see if the timeout values and style properties are correct or set in the right way. This is super easy, but the real challenge is keeping your tests running in sync rather than in async. This means that your tests won't fire off any timers or animation events that are going on as your application is being tested. How can this be achieved? Well by injecting $window rather than using window by itself, we can mock the setTimeout event and prevent any delay from occurring. Then we can test that delay value that was given to the timeout and see if it is what we expected. If you can't get around the animation timer (lets say your JavaScript animation library is closed in) then your best option is to stick to using async tests. So to make this simple, here's some test code written with MochaJS that will test out both async and sync animations.
The animation test code below tests the greensock animations code featured in an earlier section in the article.
//you need to include angular-mocks.js into karma to make this work
describe('Testing Sync Animations', function() {
beforeEach(module('AppAnimations'));
var w;
beforeEach(module(function($provide) {
w = window;
var setTimeout = window.setTimeout,
fnQueue = [];
w.setTimeout = function(fn, delay) {
fnQueue.push({
delay : delay,
process : fn
});
};
w.setTimeout.expect = function(delay) {
var first = fnQueue.shift();
expect(first.delay).to.equal(delay);
return first;
};
$provide.value('$window', w);
}));
it("should synchronously test the animation",
inject(function($animator, $document, $rootScope) {
var body = angular.element($document[0].body);
var element = angular.element('<div>hello</div>');
var animator = $animator($rootScope, {
ngAnimate: '{enter:\'list-in\', leave:\'list-out\'}'
});
animator.enter(element, body);
expect(element.hasClass('list-in')).to.equal(true);
window.setTimeout.expect(1).process(); //the setup function
expect(element.hasClass('list-in-active')).to.equal(true);
window.setTimeout.expect(1000).process(); //the start function
//now that the animation is over (timeout is gone)
expect(element.hasClass('list-in')).to.equal(false);
expect(element.hasClass('list-in-active')).to.equal(false);
}));
});
//you need to include angular-mocks.js into karma to make this work
describe('Testing Async Animations', function() {
beforeEach(module('AppAnimations'));
var $animator, $document, $window, $rootScope;
beforeEach(inject(function(_$animator_, _$document_, _$window_, _$rootScope_) {
$animator = _$animator_;
$document = _$document_;
$window = _$window_;
$rootScope = _$rootScope_;
}));
it("should asynchronously test the animation", function(done) {
var body = angular.element($document[0].body);
var element = angular.element('<div>hello</div>');
var animator = $animator($rootScope, {
ngAnimate: '{enter:\'list-in\', leave:\'list-out\'}'
});
element.css('opacity', 0);
animator.enter(element, body);
//lets add 100ms just to be safe
var timeout = 1100;
$window.setTimeout(function() {
expect(element.css('opacity')).to.equal("1");
done();
}, timeout);
});
});
Testing CSS-enabled animations is close to the same as the first sync test that was created for the JavaScript-enabled animation spec. What this means is that you're simply checking to see that the CSS class properties are assigned onto your element properly. Unfortunately, there is no way to test if a CSS style is applied to an element without using Async testing since the animation needs to kick in to update those values. What you could do, however, is provide an inline style attribute which overrides the timeout value of the animation and then check to see the style properties on your element once the 2nd CSS class is attached onto the element. This is a bit of an overkill, since anything CSS shouldn't really be tested with Mocha or Jasmine, but it is possible.
to topVersion 1.1.5 comes with a crazy wave of fixes and features for ngAnimate and things are getting better with each new Pull Request that hits the AngularJS Github Repository. There are new features in the works, such as providing support for ngAnimate to easily hook itself into other frameworks with custom CSS naming conventions. Other features will animation spacing delays so that you can natively stagger your animations one tick at a time (think about how nice this will be for ngRepeat).
If there is anything else that comes to mind, then please email me or post an issue on the AngularJS Github Repo. We're all in this together so lets make animations the best that it can possibly be! Thank you!
to top