This tutorial will present a way of implementing a wrapper directive for the popular animate.css animations file.

Introduction

I recently discovered this interesting css animations file called animate.css. While playing with it (which you can do here), I decided to use it for improving our company’s website and further test its features.

As github user daneden describes it: “animate.css is a bunch of cool, fun, and cross-browser animations for you to use in your projects. Great for emphasis, home pages, sliders, and general just-add-water-awesomeness”.

The animation classes come with some default parameters but you can easily change the duration of your animations, add a delay or change the number of times that it plays, by overwriting some of the css properties. This way you can make your animations look and behave exactly as you wish.

However, if you want to animate a website page you may need a little more than just css. For example, some animations may need be triggered only when the linked page element is in the browser viewport. Also, if you want to add a lot of animations with custom properties and you are manually overwriting each property, things will get quite messy.

For those reasons I decided to try and implement a wrapper directive that will internally set animation names, trigger events and timing properties on each animation.

Strategy

Let’s start by thinking it through. I figured out that this kind of directive can be implemented in two ways:

The standard approach, which consists of having a single directive as attribute on each animated DOM element (e.g. animation), with specific related attributes describing the animation name and properties that will be overwritten. Or following the ngMessages + ngMessage directives example from Angular ngMessages module. There can be two directives (e.g. animations and animation) grouped as follows: one or more animation directives individually attached to a DOM element and contained inside a animations directive.

Both strategies have advantages and disadvantages.

After analysing a bit the context in which this directive will most probably be used I decided to pick the strategy which uses pair directives.

Argumentation:

There may be many individual elements with the same animation and the same custom properties. The animations directive can be used to keep the general properties and then animation directive will only mark the elements which should be animated. This will skip a lot of duplicate code.

directive can be used to keep the general properties and then directive will only mark the elements which should be animated. This will skip a lot of duplicate code. There may be many individual elements with many common properties and just a few differences. The animations directive can be used to keep the general properties and then use animation directive will provide the specific ones for each element. This will also keep the code in the shortest and comfortable version possible.

directive can be used to keep the general properties and then use directive will provide the specific ones for each element. This will also keep the code in the shortest and comfortable version possible. The animation directive could also overwrite any property set on the animations directive, in case of need.

Customisable properties

In the most basic way, an animation from this css file is applied to an element by assigning the animated class, combined with one of the available animation names (e.g. pulse, flash, shake, slideIn, etc.). Besides this, we need to take care other aspects like:

Setting some of the customisable properties (by overwriting css):

animation effect delay (how much time to wait until running the animation)

animation effect duration (how long it will last)

animation effect iterations (how many times it will consecutively repeat)

Adding some more behaviour:

set the animation on an element only when the element is inside the viewport (it is visible on the screen).

when the element is inside the viewport (it is visible on the screen). set the animation only the first time when the element appears in the viewport.

the first time when the element appears in the viewport. disable an animation on small devices.

After a short research on how to accomplish the behaviour enhancement part, I discovered Waypoints.

Waypoints is a simple library that makes it easy to execute a function whenever you scroll to an element. The executed function can be used to assign classes to the subject element, so it is perfect for setting the animation classes to a DOM element when we scroll to it. It is true that Waypoints has other more specialised “children” but we are not going to use them here.

Animations directive

This directive will be use as a wrapper for the animated elements.

We want to create this directive in the form of a HTML tag and use it to store the general properties for the inner animation directives.

Here is the preview for it:

Animations directive angular .module('app') .directive('animations', animationsFunc); function animationsFunc() { return { restrict: 'E', scope: { duration: '@', // effect duration delay: '@', // effect delay iterations: '@', // effect repeat times name: '@', // animation name trigger: '@', // animation triggering event triggerOnce: '@', // how many times to apply an effect when triggering event is fired triggerOffset: '@', // trigger event when element is visible in a certain height position on the screen disableOnSmallDevices: '@' // disable child elements animation on small devices } controller: ... } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 angular . module ( 'app' ) . directive ( 'animations' , animationsFunc ) ; function animationsFunc ( ) { return { restrict : 'E' , scope : { duration : '@' , // effect duration delay : '@' , // effect delay iterations : '@' , // effect repeat times name : '@' , // animation name trigger : '@' , // animation triggering event triggerOnce : '@' , // how many times to apply an effect when triggering event is fired triggerOffset : '@' , // trigger event when element is visible in a certain height position on the screen disableOnSmallDevices : '@' // disable child elements animation on small devices } controller : . . . }

Scope attributes are bound with ‘@’ which means there will be only one-way data binding.

Now add a controller to this directive. The purpose of the controller is to merge the specified properties with a set of predefined ones and return the result. Any child directive will have access to the general properties through this controller.

Animations directive controller function controller: function($scope) { var defaultProperties = { duration: '2s', delay: '0s', iterations: '1', animation: '', trigger: '', triggerOnce: '', triggersCnt: 0, triggerOffset: '100%', disableOnSmallDevices: 'false' }; this.getProperties = function () { var foundProperties = { duration: $scope.duration, delay: $scope.delay, iterations: $scope.iterations, animation: $scope.name, trigger: $scope.trigger, triggerOnce: $scope.triggerOnce, triggerOffset: $scope.triggerOffset, disableOnSmallDevices: $scope.disableOnSmallDevices }; return _.merge({}, foundProperties, defaultProperties, function(foundProp) { if (foundProp) { return foundProp; } }); } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 controller : function ( $ scope ) { var defaultProperties = { duration : '2s' , delay : '0s' , iterations : '1' , animation : '' , trigger : '' , triggerOnce : '' , triggersCnt : 0 , triggerOffset : '100%' , disableOnSmallDevices : 'false' } ; this . getProperties = function ( ) { var foundProperties = { duration : $ scope . duration , delay : $ scope . delay , iterations : $ scope . iterations , animation : $ scope . name , trigger : $ scope . trigger , triggerOnce : $ scope . triggerOnce , triggerOffset : $ scope . triggerOffset , disableOnSmallDevices : $ scope . disableOnSmallDevices } ; return _ . merge ( { } , foundProperties , defaultProperties , function ( foundProp ) { if ( foundProp ) { return foundProp ; } } ) ; } }

For the merging part, I am using lodash 1.3.1 because it is already used in other parts of the website, but you can use whatever you wish, or implement the merge by yourself (which I don’t recommend).

Animation directive

This directive will take care of the hard work if there is any at all.

We want the animation directive to be present in form of an attribute on the DOM element we wish to animate. For this to happen we will restrict the directive to ‘A’.

We said that the animations controller will return the general properties to each animation directive through its controller. We get our hands on the controller by requesting it using require: ‘^animations‘. The ‘^‘ sign tells angular to search for the controller between the parents of the element.

This directive will also have a controller, which we will use to to expose a small API into the link function.

Animation directive angular .module('app') .directive('animation', animationFunc); function animationFunc($timeout) { return { restrict: 'A', require: '^animations', controller: function ($scope) { ... } link: function (scope, element, attrs, animationsCtrl) { ... } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 angular . module ( 'app' ) . directive ( 'animation' , animationFunc ) ; function animationFunc ( $ timeout ) { return { restrict : 'A' , require : '^animations' , controller : function ( $ scope ) { . . . } link : function ( scope , element , attrs , animationsCtrl ) { . . . } } }

We also want to be able to overwrite any property set on the animations directive. To be able to do this, we could similarly create an isolated scope (like the one for animations) and add the property names there. This sounds like a good idea, but since the directive is an element, there are many chances that it will be placed on a DOM element which already has an isolated scope. In this case an error message will be thrown with a text similar to “Multiple directives asking for new/isolated scope on…”.

Having this case in mind, I decided to make a predefined list of attributes and check then using attrs[propertyName]. In order to get the final animation properties for the current element, I wrote down the following function into the controller:

function getFinalProperties(attrs, animationsCtrl) { var selfProps = {}; var wantedProps = [ 'animation', 'delay', 'duration', 'iterations', 'trigger', 'triggerOnce', 'triggerOffset', 'disableOnSmallDevices' ]; wantedProps.map(function(prop) { if (attrs.hasOwnProperty(prop) && attrs[prop]) { selfProps[prop] = attrs[prop]; } return prop; }); return _.merge(animationsCtrl.getProperties(), selfProps); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function getFinalProperties ( attrs , animationsCtrl ) { var selfProps = { } ; var wantedProps = [ 'animation' , 'delay' , 'duration' , 'iterations' , 'trigger' , 'triggerOnce' , 'triggerOffset' , 'disableOnSmallDevices' ] ; wantedProps . map ( function ( prop ) { if ( attrs . hasOwnProperty ( prop ) & amp ; & amp ; attrs [ prop ] ) { selfProps [ prop ] = attrs [ prop ] ; } return prop ; } ) ; return _ . merge ( animationsCtrl . getProperties ( ) , selfProps ) ; }

Now we can have the final properties to be applied on each animated element by calling getFinalProperties() in the top of the link function.

Having the final properties, we can set the animation to the corresponding element together with its duration, delay and iterations properties (if specified). You can find instructions on how to manually do this with CSS here. We will be applying the same thing, but let the directive do it for us.

In order to do this, I wrote another function into the controller.

function applyAnimation(element, props) { var duration, iterations, delay; if (props.duration) { duration = { '-webkit-animation-duration': props.duration, '-moz-animation-duration': props.duration, '-o-animation-duration': props.duration, '-ms-animation-duration': props.duration } } if (props.delay) { delay = { '-webkit-animation-delay': props.delay, '-moz-animation-delay': props.delay, '-o-animation-delay': props.delay, '-ms-animation-delay': props.delay } } if (props.iterations) { iterations = { '-webkit-animation-iteration-count': props.iterations, '-moz-animation-iteration-count': props.iterations, '-o-animation-iteration-count': props.iterations, '-ms-animation-iteration-count': props.iterations } } if (props.disableOnSmallDevices === 'true') { element.addClass('disable-animated'); } element.addClass('animated ' + props.animation); element.css(_.merge(duration, delay, iterations)); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 function applyAnimation ( element , props ) { var duration , iterations , delay ; if ( props . duration ) { duration = { '-webkit-animation-duration' : props . duration , '-moz-animation-duration' : props . duration , '-o-animation-duration' : props . duration , '-ms-animation-duration' : props . duration } } if ( props . delay ) { delay = { '-webkit-animation-delay' : props . delay , '-moz-animation-delay' : props . delay , '-o-animation-delay' : props . delay , '-ms-animation-delay' : props . delay } } if ( props . iterations ) { iterations = { '-webkit-animation-iteration-count' : props . iterations , '-moz-animation-iteration-count' : props . iterations , '-o-animation-iteration-count' : props . iterations , '-ms-animation-iteration-count' : props . iterations } } if ( props . disableOnSmallDevices === 'true' ) { element . addClass ( 'disable-animated' ) ; } element . addClass ( 'animated ' + props . animation ) ; element . css ( _ . merge ( duration , delay , iterations ) ) ; }

When disableOnSmallDevices is true, we add an extra class called disable-animated (or whatever you like). This class is responsible for not letting the animations run on small devices. It is up to your decision what you consider a small device. My disable-animated class looks like this:

.disable-animated { @media only screen and (max-width : 990px) { /*CSS transitions*/ -o-transition-property: none !important; -moz-transition-property: none !important; -ms-transition-property: none !important; -webkit-transition-property: none !important; transition-property: none !important; /*CSS transforms*/ -o-transform: none !important; -moz-transform: none !important; -ms-transform: none !important; -webkit-transform: none !important; transform: none !important; /*CSS animations*/ -webkit-animation: none !important; -moz-animation: none !important; -o-animation: none !important; -ms-animation: none !important; animation: none !important; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 .disable-animated { @media only screen and (max-width : 990px) { /*CSS transitions*/ -o-transition-property : none !important ; -moz-transition-property : none !important ; -ms-transition-property : none !important ; -webkit-transition-property : none !important ; transition-property : none !important ; /*CSS transforms*/ -o-transform : none !important ; -moz-transform : none !important ; -ms-transform : none !important ; -webkit-transform : none !important ; transform : none !important ; /*CSS animations*/ -webkit-animation : none !important ; -moz-animation : none !important ; -o-animation : none !important ; -ms-animation : none !important ; animation : none !important ; } }

Similar to addAnimations we create the removeAnimation function:

function removeAnimation(element, props) { if (props.disableOnSmallDevices === 'true') { element.removeClass('disable-animated'); } element.removeClass('animated ' + props.animation); } 1 2 3 4 5 6 function removeAnimation ( element , props ) { if ( props . disableOnSmallDevices === 'true' ) { element . removeClass ( 'disable-animated' ) ; } element . removeClass ( 'animated ' + props . animation ) ; }

Now we want to implement a function that will assign the animation and trigger it only when the element is inside the viewport. For this we are using Waypoints. You can check out their API here but I will also describe what I used from it.

Here is the function responsible for adding a waypoint to the given element:

function addWaypoint(element, props, expectedDirection) { var domElement = element.get(0); return $timeout(function() { return new Waypoint({ element: domElement, handler: function (scrollDirection) { if ( props.triggerOnce === 'true' && props.triggersCnt >= 1 ) { return; } if (scrollDirection === expectedDirection) { applyAnimation(element, props); props.triggersCnt++; } else { removeAnimation(element, props); } }, offset: props.triggerOffset }); }); } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 function addWaypoint ( element , props , expectedDirection ) { var domElement = element . get ( 0 ) ; return $ timeout ( function ( ) { return new Waypoint ( { element : domElement , handler : function ( scrollDirection ) { if ( props . triggerOnce === 'true' && props . triggersCnt >= 1 ) { return ; } if ( scrollDirection === expectedDirection ) { applyAnimation ( element , props ) ; props . triggersCnt ++ ; } else { removeAnimation ( element , props ) ; } } , offset : props . triggerOffset } ) ; } ) ; }

All this function does is calling the Waypoint constructor with a JSON object containing the following:

element – the DOM element on which the waypoint will be set.

element on which the waypoint will be set. handler – the callback function that will be executed when the subject element is in the viewport. When handler is called it is also called with a parameter telling the direction of the scroll (e.g. if you scroll down to the element then the handler will be called like this: handler(‘down’)).

offset – this joins the action when the element is visible inside the viewport. You can think about this property as an imaginary horizontal line crossing the screen at a specific height given in percent(%).

Notice this function is using $timeout so you need to inject it into the directive function to make it available. Why did I use $timeout there? Answer is: because in the initialising steps of the application it looks like in some cases elements are temporary located upper in the page than their usual position. This happens for a fraction of a second (I believe during the first digest cycle of Angular), then they quickly move back to their places. Though I don’t know for sure, I think this may be strongly related to the css styles used to describe the page structure and Angular’s data binding step priority. If waypoints are set right at the beginning, they detect that some elements are in the viewport and immediately trigger the animation (by mistake) instead of applying the same behaviour only after the element is at its correct position in the page. In order to not risk the animation trigger earlier than it should, I decided to use $timeout and delay every addWaypoint effect to the next digest cycle (when the template should already be instantiated correctly).

With that being said we have all the utility functions we need to write down the link function. Here is the final animation controller structure. The details of each function were already shown and discussed above.

controller: function($scope) { $scope.utility = { applyAnimation: applyAnimation, removeAnimation: removeAnimation, addWaypoint: addWaypoint, getFinalProperties: getFinalProperties }; function getFinalProperties(attrs, animationsCtrl) {...} function applyAnimation(element, props) {...} function removeAnimation(element, props) {...} function addWaypoint(element, props, expectedDirection) {...} } 1 2 3 4 5 6 7 8 9 10 11 12 13 controller : function ( $ scope ) { $ scope . utility = { applyAnimation : applyAnimation , removeAnimation : removeAnimation , addWaypoint : addWaypoint , getFinalProperties : getFinalProperties } ; function getFinalProperties ( attrs , animationsCtrl ) { . . . } function applyAnimation ( element , props ) { . . . } function removeAnimation ( element , props ) { . . . } function addWaypoint ( element , props , expectedDirection ) { . . . } }

And here is the animation link function code:

link: function (scope, element, attrs, animationsCtrl) { var finalProps = scope.utility.getFinalProperties(attrs, animationsCtrl); if (finalProps.trigger === 'scroll-down-to-element') { scope.utility.addWaypoint(element, finalProps, 'down'); } else if (finalProps.trigger === 'scroll-up-to-element') { scope.utility.addWaypoint(element, finalProps, 'up'); } else { scope.utility.applyAnimation(element, finalProps); } } 1 2 3 4 5 6 7 8 9 10 11 link : function ( scope , element , attrs , animationsCtrl ) { var finalProps = scope . utility . getFinalProperties ( attrs , animationsCtrl ) ; if ( finalProps . trigger === 'scroll-down-to-element' ) { scope . utility . addWaypoint ( element , finalProps , 'down' ) ; } else if ( finalProps . trigger === 'scroll-up-to-element' ) { scope . utility . addWaypoint ( element , finalProps , 'up' ) ; } else { scope . utility . applyAnimation ( element , finalProps ) ; } }

Notice that any action resumes to calling the API we just built in the controller:

First we get the properties needed to be assigned. If there is a trigger event name specified we create and add a waypoint to the element. If there is no trigger event name then we simply add the animation class.

You can see those directives working here.

Summary

In this tutorial I presented a method of implementing a wrapper directive for the animate.css styles. This method consists of creating a pair of two directives called animations and animation (check them in action here). The first one has the purpose of wrapping common animation properties and the latter for marking the elements that need to be animated and specifying the individual properties that need to be set. Working with those directives would reduce code duplication in the template and would also enhance animate.css with the possibility of triggering animations only when the user scrolls to the animated page elements. Furthermore, they eliminate the need of manually writing additional css to customise the animations as needed.

I am aware that there may be other possible ways of implementing this in a better way. I am open to any relevant suggestions of improvement and willing to analyse and learn from them, so make sure you drop me a comment.

Here at Algotech Solutions, we love writing about Javascript and Angular, but also about a variety of other subjects: good coding techniques, Git tips and tricks, book recommendations and cloud services. Let us know what your favourite topics are and stay in touch!

Free email updates! Get the latest content first. No spam. Just occasional emails with great engineering posts. Send me great engineering posts