Select Page

The Setup

AngularJS animations took another shot of performance enhancing drugs with a new API rewrite for AngularJS 1.2. Matias Niemela, aka Year of Moo has been doing an incredible job on the project and I wanted to showcase how easy it is to work with the new API by building a slider in about 50 lines of JavaScript and even fewer lines of HTML.

For an extra punch we are going to do this JavaScript style using the Greensock animation library which was a staple in the Flash world and has been ported over to JavaScript. I cannot say enough great things about the work that Jack Doyle has done on this project. I have been using it for years and I love it!

Check out the demo and grab the repository off of Github and let’s get started!

Demo Code

Building the Slider

We are going to assemble the photo slider in two phases. The first phase is building out the photo slider from a functional standpoint and the second phase involves wiring up the animations. DISCLAIMER: I am not going to be digging into the CSS that I used to assemble the slider because I want to focus on the AngularJS parts specifically but the layout is fairly straightforward.

Step 1a: Starting HTML

Here is the HTML in the index.html file that we are going to be starting with. In AngularJS 1.2, animations are no longer part of the core and that is why we are including angular-animate.min.js in conjunction with angular.min.js. We are also including TweenMax which is the Greensock animation library that is going to be doing all the heavy lifting for us.

<!DOCTYPE html>
<html ng-app="website">
<head>
    <meta charset="utf-8">
    <title>AngularJS Animate Slider</title>
    <link href="css/bootstrap.css" rel="stylesheet">
    <link rel="stylesheet" href="css/styles.css">
</head>

<body ng-controller="MainCtrl">

<!-- SLIDER GOES HERE -->

<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular-animate.min.js"></script>
<script src="http://cdnjs.cloudflare.com/ajax/libs/gsap/1.10.3/TweenMax.min.js"></script>
<script src="js/app.js"></script>
</body>
</html>

We are also bootstrapping the application with ng-app=”website” on the html tag and defining the MainCtrl on the body tag.

Step 1b: Starting JavaScript

And in the js/app.js file you will see the bare minimum JavaScript that we need to make a working AngularJS application.

angular.module('website', ['ngAnimate'])
    .controller('MainCtrl', function ($scope) { });

The most important thing to point out is that we are injecting ngAnimate as a sub-module into the website module. Once we have done this, animations will be available for the entire module.

Step 2: Set Up the Slides

We are going to create a collection of image objects that we can use to bind to in the HTML to display the photos and also use to assemble the navigation.

angular.module('website', ['ngAnimate'])
    .controller('MainCtrl', function ($scope) {
        $scope.slides = [
            {image: 'images/img00.jpg', description: 'Image 00'},
            {image: 'images/img01.jpg', description: 'Image 01'},
            {image: 'images/img02.jpg', description: 'Image 02'},
            {image: 'images/img03.jpg', description: 'Image 03'},
            {image: 'images/img04.jpg', description: 'Image 04'}
        ];
    });

We have defined a slides array on $scope that we are going use in the next step.

Step 3: Display the Slides

To display the slides we are going to loop over the slides array using ng-repeat and attach an img element for each slide in the slides array.

<body ng-controller="MainCtrl">

<div class="container slider">
    <img ng-repeat="slide in slides" class="slide" ng-src="{{slide.image}}">
</div>
<!-- CODE OMITTED -->

</body>

Using ng-repeat=”slide in slides” we gain access to the slide.image property which we bind to ng-src with double curly braces.

Step 4a: Display a Single Slide

Now that we are displaying the images on the page it is time to add the functionality to keep track of what slide is being displayed.

angular.module('website', ['ngAnimate'])
    .controller('MainCtrl', function ($scope) {
        $scope.slides = [
            {image: 'images/img00.jpg', description: 'Image 00'},
            {image: 'images/img01.jpg', description: 'Image 01'},
            {image: 'images/img02.jpg', description: 'Image 02'},
            {image: 'images/img03.jpg', description: 'Image 03'},
            {image: 'images/img04.jpg', description: 'Image 04'}
        ];

<pre><code>    $scope.currentIndex = 0;

    $scope.setCurrentSlideIndex = function (index) {
        $scope.currentIndex = index;
    };

    $scope.isCurrentSlideIndex = function (index) {
        return $scope.currentIndex === index;
    };
});
</code></pre>

We are going to create a currentIndex property and initialize it to 0 since we want to start with the first slide. From there, we add setCurrentSlideIndex which allows us to modify currentIndex by passing in a new index value.

The final piece is to add a simple method called isCurrentSlideIndex which allows us to get a boolean value if the index we send in is the current slide index we are on.

Step 4b: Display a Single Slide

Why would we want a function that tells if an index is the current slide index? Good question! One of my favorite things about AngularJS is that not only can we bind to primitive values but functions as well. This allows us to toggle functionality in the layout based on the value of a function.


<div class="container slider">
    <img ng-repeat="slide in slides" class="slide"
         ng-hide="!isCurrentSlideIndex($index)" ng-src="{{slide.image}}">
</div>

Case and point, we want to hide the image if it is not the current slide. The addition of ng-hide=”!isCurrentSlideIndex($index)” is just the golden ticket we need to do this. This is a fairly concise piece of code that is actually doing quite a bit and so let us break it down into pieces. ng-hide will hide an element with display:none if its expression evaluates to true. ng-repeat generates a handy variable called $index that allows you to reference the index of the item that is currently being processed. This is perfect since we can use this to coordinate currentIndex in the MainCtrl. From there, we are simply saying that if the $index of that image is NOT the currentIndex then hide the image.

Step 4c: Display a Single Slide

And so now that we are showing and hiding images based on currentIndex we are going to create a navigation mechanism that will allow us to manipulate currentIndex.


<div class="container slider">
    <img ng-repeat="slide in slides" class="slide"
         ng-hide="!isCurrentSlideIndex($index)" ng-src="{{slide.image}}">

<nav class="nav">
<div class="wrapper">
<ul class="dots">
<li class="dot" ng-repeat="slide in slides">
                    <a href="#" ng-class="{'active':isCurrentSlideIndex($index)}"
                       ng-click="setCurrentSlideIndex($index);">{{slide.description}}</a></li>
</ul></div>
</nav>
</div>

You are now going to see the power of having a single source of truth that is abstracted away from your DOM. Notice the code on the li tag and its child elements. Did you notice there some recurring themes from the code used to generate the images?

We are going to use ng-repeat again to generate a navigation element for each item in the slides array. We are also going to use ng-click to call setCurrentSlideIndex and set currentIndex to the $index of the element that is clicked. The additional twist to all of this is that we are dynamically attaching an active class using ng-class and the value returned by isCurrentSlideIndex($index). If the value returned by isCurrentSlideIndex is true then the active class is applied.

At this point, you should be able click a navigation item and see the corresponding image. Minimum viable product baby!

Step 5a: Next and Previous Slide

We can also navigate through the slides in a linear fashion by incrementing and decrementing currentIndex.

angular.module('website', ['ngAnimate'])
    .controller('MainCtrl', function ($scope) {
        $scope.slides = [ /* CODE OMITTED */ ];

<pre><code>    $scope.currentIndex = 0;

    $scope.setCurrentSlideIndex = function (index) {
        $scope.currentIndex = index;
    };

    $scope.isCurrentSlideIndex = function (index) {
        return $scope.currentIndex === index;
    };

    $scope.prevSlide = function () {
        $scope.currentIndex = ($scope.currentIndex < $scope.slides.length - 1) ? ++$scope.currentIndex : 0;
    };

    $scope.nextSlide = function () {
        $scope.currentIndex = ($scope.currentIndex > 0) ? --$scope.currentIndex : $scope.slides.length - 1;
    };
});
</code></pre>

To accomplish this wee need to add prevSlide and nextSlide methods which decrement and increment currentIndex respectively. The ternary operators in these methods are in place to allow for looping. If you are at the first slide and hit the previous button, currentIndex jumps to the last item in the slides array. If you are at the last slide and hit the next button, then you jump to the first slide. That’s right! You spin me right round!

Step 5b: Next and Previous Slide

And how do we invoke the awesome powers of nextSlide and prevSlide? ng-click! bam! done! next!


<div class="container slider">
    <img ng-repeat="slide in slides" class="slide"
         ng-hide="!isCurrentSlideIndex($index)" ng-src="{{slide.image}}">

    <a class="arrow prev" href="#" ng-click="nextSlide()"></a>
    <a class="arrow next" href="#" ng-click="prevSlide()"></a>

<nav class="nav">
<div class="wrapper">
<ul class="dots">
<li class="dot" ng-repeat="slide in slides">
                    <a href="#" ng-class="{'active':isCurrentSlideIndex($index)}"
                       ng-click="setCurrentSlideIndex($index);">{{slide.description}}</a></li>
</ul></div>
</nav>
</div>

And that concludes the first phase of our photo slider project. We have literally created a fully functional slider in like 30 lines of JavaScript and some HTML. AngularJS still impresses me at how much you can get done with so little.

Animating the Slider

And now on the fun part! Heralding trumpets in the background Behold! AngularJS animations accompanied the regal Sir TweenMax of Greensock.

Step 6: Laying the Foundation

Animations within AngularJS are based on specific events that trigger the JavaScript or CSS animations. There are some AngularJS directives that come with their own special animation events as well as some generic events such as addClass and removeClass which get triggered when a class is added or removed.

angular.module('website', ['ngAnimate'])
    .controller('MainCtrl', function ($scope) {
        // CODE OMITTED
    })
    .animation('.slide-animation', function () {
        return {
            addClass: function (element, className, done) {
                if (className == 'ng-hide') {
                    // ANIMATION CODE GOES HERE
                }
                else {
                    done();
                }
            },
            removeClass: function (element, className, done) {
                if (className == 'ng-hide') {
                    // ANIMATION CODE GOES HERE
                }
                else {
                    done();
                }
            }
        };
    });

JavaScript animations are defined using the animation service and follows a similar convention as everything else within the AngularJS domain by taking a name and function parameter. It is important to note that the name is defined as a CSS class since all animations in AngularJS follow a class based naming convention.

We are going to call our animation .slide-animation and within the factory function, we are defining handlers for the addClass and removeClass events. These events get fired when any class is dynamically added to the element the animation is defined on in this case we only want to act on ng-hide activity. Once the event has been triggered and the appropriate handler has been called, AngularJS pretty much delegates the entire animation to whatever you have chosen to handle that. The only responsibility you have is to call done when the animation is complete so that AngularJS knows the animation is finished.

Step 7: Animating the Intro

Now that the AngularJS part is set up, it is time to add in the actual animations via TweenMax. We are going to start with the ‘intro’ and define the animation that happens when ng-hide is added.

NOTE: By ‘intro’ I mean the animation that happens when we introduce the ng-hide class. This could be confusing because the result of this animation is that an element actually leaves which in THAT context would be considered an ‘outro’. Work with me here!

angular.module('website', ['ngAnimate'])
    .controller('MainCtrl', function ($scope) {
        // CODE OMITTED
    })
    .animation('.slide-animation', function () {
        return {
            addClass: function (element, className, done) {
                if (className == 'ng-hide') {
                    TweenMax.to(element, 0.5, {left: -element.parent().width(), onComplete: done });
                }
                else {
                    done();
                }
            },
            removeClass: function (element, className, done) {
                if (className == 'ng-hide') {
                    // ANIMATION CODE GOES HERE
                }
                else {
                    done();
                }
            }
        };
    });

TweenMax is really robust and I recommend you check out Getting Started with the JavaScript Version of the GreenSock Animation Platform (GSAP) on how to get up and running. For the sake of this tutorial, I am going to simplify TweenMax by saying that it basically takes the element you want to animate, the duration and a parameter object defining what and how you want the animation to work.

The element we want to animate is the element parameter that gets injected into our event handler. Convenient! We are going to animate for half a second and we are saying that when ng-hide is added we want to move the element left the entire width of its parent element so that it is no longer visible. We are also calling done by passing it is as the callback for the onComplete handler.

Step 8: Animating the Outro

We are actually attaching .slide-animation to multiple elements via ng-repeat and so when one element is being hidden, another element is being shown. This means you have two opposite events being fired at the same time which together make for a really neat transition from one state to another. The ‘outro’ side of this animation is defining how we want the slide to appear when we remove the ng-hide class.

angular.module('website', ['ngAnimate'])
    .controller('MainCtrl', function ($scope) {
        // CODE OMITTED
    })
    .animation('.slide-animation', function () {
        return {
            addClass: function (element, className, done) {
                if (className == 'ng-hide') {
                    TweenMax.to(element, 0.5, {left: -element.parent().width(), onComplete: done });
                }
                else {
                    done();
                }
            },
            removeClass: function (element, className, done) {
                if (className == 'ng-hide') {
                    element.removeClass('ng-hide');

<pre><code>                TweenMax.set(element, { left: element.parent().width() });
                TweenMax.to(element, 0.5, {left: 0, onComplete: done });
            }
            else {
                done();
            }
        }
    };
});
</code></pre>

Small detour… because we cannot set !important via JavaScript, we are simply removing ng-hide immediately and taking it from there. This will hopefully be resolved in the final release of AngularJS 1.2. For further clarify check out the Year of Moo blog post. Translation: I just took Matias’s word for it!

Now! Back on task! The first thing we are going to do is set the element immediately to the right of the slider by setting the left property to the width of the element’s parent. And then we simply animate on the left property to 0 so that the image slides in from right to left.

Step 9: Wiring It Up

Our .slide-animation is all dressed up but so far nowhere to go. Let’s change that.

Now gather around for a demonstration on why I think AngularJS animations are so clever. This is our code without the animation.


<div class="container slider">
    <img ng-repeat="slide in slides" class="slide"
         ng-hide="!isCurrentSlideIndex($index)" ng-src="{{slide.image}}">
         <!-- CODE OMITTED -->
</div>

This is our code with the animation. Did you blink? Did you see it?!


<div class="container slider">
    <img ng-repeat="slide in slides" class="slide slide-animation"
         ng-hide="!isCurrentSlideIndex($index)" ng-src="{{slide.image}}">
         <!-- CODE OMITTED -->
</div>

That is exactly right! We are essentially wrapping up complex animation functionality in a class based directive. Goodness! I seriously love this stuff!

Conclusion

AngularJS animations are super powerful and super easy to spin up once you understand the events and conventions around them. I personally LOVE that they decided to move in the class based direction because it makes it really easy and non-intrusive to add animations your application and it is makes integration with other libraries really easy.

To Matias and the AngularJS team… thank you for building something so incredibly awesome that is so fun and powerful to work with.

To Jack Doyle… thank you for making what I consider to be the greatest animation library EVER!

Resources

Remastered Animation in AngularJS 1.2

Getting Started with the JavaScript Version of the GreenSock Animation Platform (GSAP)

Demo Code