Update (12 Jan): Since writing this article we have updated the doAnim method described below such that it no longer depends on an Angular value. It also supports an optional callback. Find its full definition as a Gist here.
CSS animations are brilliant – they allow us to quickly perform complex manipulations within the DOM by stating (at least) the property to animate and for how long, saving time that would have been spent writing the logic in JavaScript. For example, if we wanted to animate opacity on an element we might create our stylesheet to look like the following. Then we can add our fadeIn class to #myElement and watch it fade into view over 0.25 seconds.
#myElement { opacity: 0; transition: opacity 0.25s ease; } .fadeIn { opacity: 1; }
In order to invoke an animation we have to add this class to the element manually in JavaScript. Why would we need to do this? Here are some use-cases:
- Each time some user-action is completed, such as a mouse click on a specific element. This is likely to be the most common use-case.
- After some promise is resolved, for example in view updates across the many MVC front-end frameworks, specifically resolvers in Angular’s $routeProvider.
- A callback is fired after some computation has completed. This use-case is where I decided that a function like that which will follow (doAnim) would be useful.
And here is the simplest way we could add the animation class to our element:
document.querySelector('#myElement').className += ' fadeIn'; // Notice the single space after our string literal but before our class name
However, when it comes to repeating an animation once, twice, or more times, our JavaScript becomes convoluted and loses readability. First we must remove the class from the element and then re-add it, and continue to do this on each occasion we want the animation to happen. The vanilla JS code might end up looking something like this:
var el = document.querySelector('#myElement'); el.className += ' animationClassname'; // Initial animation // Later, when we want to repeat the animation: el.className = el.className.replace('fadeIn', ''); el.className += ' fadeIn'; // Second animation, and so on...
So how to combat this? If you are familiar with AngularJS you will be good friends with`angular.element`. With this function we can produce a JQLite object instance by passing it a regular JS DOM object, obtained via `document.querySelector`, or any of the other similar methods on the `document` object. For example:
var el = document.querySelector('#myElement'); // A regular JS DOM object angular.element(el); // A JQLite object which wraps `el`
From here we can take advantage of the many JQLite convenience methods, two of which are particularly useful to us in this instance: `addClass` and `removeClass`. So let’s try re-writing the above code to use these methods:
var el = angular.element(document.querySelector('#myElement')); el.addClass('fadeIn'); // Later, when we want to repeat the animation: el.removeClass('fadeIn'); el.addClass('fadeIn');
More readable! We are getting somewhere… but this still takes four lines of code. Wouldn’t it be nice if we could call a function on our JQLite object which did all of this for us? Well, we can extend the `angular.element` prototype and achieve this ourselves. After we have configured our Angular app by calling `.config` we can do some extra set-up stuff inside a call to `.run`. The Angular run method is executed when the application is in a ‘ready’ state – all configuration and initialisation has completed – and allows normal dependency injection so that we have access to things like $timeout. So first of all we want to create a new method on the `angular.element` prototype chain. Let’s call it doAnim. Like so:
angular.module('ourApp') // .config(...), etc. .run(function() { angular.element.prototype.doAnim = function() { }; });
Perfect. So now every call to `angular.element` will produce a normal JQLite instance with all its own methods plus our new doAnim function. But it doesn’t do anything yet. Let’s fill it out:
angular.module('ourApp') // .config(...), etc. .value('animDurations', { fadeIn: 0.25 // 0.25 seconds as defined in our stylesheet }) .run(function(animDurations, $timeout) { angular.element.prototype.doAnim = function(animationClassname) { if(this.hasClass(animationClassname)) { return; } this.addClass(animationClassname); $timeout(function() { this.removeClass(animationClassname); }.bind(this), animDurations[animationClassname] * 1000); }; });
There are a few things to note in the above code:
- We also created an Angular value, prior to our call to run, which lets us access any JavaScript datatype as a dependency across our module, in this case a key-value object of our animation class names and their corresponding durations. This will need to be maintained by us, the developer, such that any new animations created which we want to use with doAnim have their durations added to this object.
- The anonymous function passed to our $timeout call is bound to the local scope by appending `.bind(this)` after its declaration (closing curly brace). This saves us writing a reference to the local scope to a variable and therefore also saves us a line of code.
- The duration of our timeout is multiplied by 1000. This is because I chose to define all CSS animations in seconds, but the duration passed to $timeout must be in milliseconds. Be conscious of this if you are defining your animations using a different unit of time in your stylesheets. If defining them in milliseconds, for example, then this multiplication can be omitted completely.
Woohoo! We can now do what took four lines of code in two! And after that, so long as we keep the angular.element instance close, firing the animation can be done in one call to doAnim. Like so:
var el = angular.element(document.querySelector('#myElement')); el.doAnim('fadeIn');
– Sam
[Abi wrote this bit] We are always looking for new developers so if you’ve read this blog, found it helpful and are interested to find out more about what we do, get in touch, we are always up for a pint and a chat in the pub team@outlandish.com