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 CodeBuilding 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
Good demo, I like it.
Animations is something I want to play with in a future.
Is there anyway to make it so that the animation slides the proper direction based on which arrow was clicked? In the demo no matter which arrow I press the image always slides in from the right. I would expect it to slide in from the left if I clicked the left arrow.
Good question… and there is indeed a way to do that. I cover that in Part 2 of this blog post but! feel free to give it a try on your own. I would love to see how someone else would approach this problem.
Thanks, for this tutorial, great stuff.
But I have a problem. I put the slider in the middle of a one page website, and now all the time I push one of the arrows or dots it jumps to the beginning of the website. So how can I change the <a class="arrow prev" href="#" …?
Thanks in advance and cheers
Flo
@simpulton Please remove slide-animation class from index.html file. Same thing is happen with me.
Good tutorial and thanks. This helped me quite a lot. I had however a hard time using this code with $routeProvider. Following Code:
{{slide.description}}
will reload the Controller and therefore the index state of “currentIndex” is lost when clicking on a link. It took me ages until I figured out what actually causes this issue.
I have created buttons for this purpose:
{{slide.description}}
This prevents the Controller from re-initializing.
Ok, swallowing HTML elements. Sorry folks. Just use buttons with ng-click. No a href.
I couldnt get the exact connection between the methods setCurrentSlideIndex() and isCurrenntSlideIndex(). When setCurrentSlideIndex() is called in the ng-onClick why does angular call isCurrenntSlideIndex() ? Whats the connection? Can you please elaborate?
Thanks
Hi KC —
isCurrentSlideIndex gets called when $scope.currentIndex is modified so that the rest of the slider can update to the new index i.e. image. Essentially in this code here ng-hide=”!isCurrentSlideIndex($index)” the image is saying “Check and see if I am the current image and if not… then I need to hide”
Thanks. So basically when model (in this case scope.currentIndex) is changed, then angular will call all the functions where that scope variable is referenced.
Yep courtesy of the AngularJS digest cycle.
Demo does not work. I’m using Chrome.
Posted demo works fine for me. Check the repo for the latest. I need to update the post.
Perhaps $scope.prevSlide and $scope.prevSlide functions are flipped?
I am confused… 😀
No wonder that you are confused, sorry my bad. I wanted to say it seems that code in $scope.nextSlide and $scope.prevSlide functions are flipped. I am expecting :
$scope.prevSlide = function () {
$scope.currentIndex = ($scope.currentIndex > 0) ? –$scope.currentIndex : $scope.slides.length – 1;
};
$scope.nextSlide = function () {
$scope.currentIndex = ($scope.currentIndex < $scope.slides.length – 1) ? ++$scope.currentIndex : 0;
};
How to add also time to move slides in time interval ?
WOW! that was amazing….! Thanks 🙂
Hey Bro its a great tutorial but can you please help me out i am new to Angular JS i want to move the slider automatically but i am unable to get it , please help me out with this.
Hi,
I really these examples, they are amazing, however.
I am stuck in a newbie AngularJS question!
I have done the fork (off github), then clone cmd git and got the source local.
But when I try the “npm start”, in said directory, its always complaining about the ENENT about “package.json”
This might one obvious to most but to me its got me stuck!
Any help is greatly appreciated!
Note:
-I have already done the “https://docs.angularjs.org/tutorial” with no hiccups!
-I also have the seed app, which runs too.
-I have node.js v0.10.28!
Thanks in advance,
That is because it is not a node project. 😀 I recommend creating a project in WebStorm and then viewing in browser from the IDE.
little word of warning. if you are using routing then every anchor tag is going to trigger routing instead of slide show. remove the href tags all-together.
Nice..
Thank you for literally the best angularJS slider script ever!
Excepted a small problem here:
$scope.prevSlide = function () {
$scope.currentIndex = ($scope.currentIndex 0) ? –$scope.currentIndex : $scope.slides.length – 1;
};
nextSlide actually is prevSlide here 🙂
I can see you did this:
But what for?
Thanks Elyas 🙂 I may have in my haste gotten those two methods mixed up. I also used a slider off of the Apple site as my starting point and that was in there… probably not necessary. I DO accept pull requests 😀
Trying to use this for pages, can’t seem to get the index change to change the display
Hi Felix — can you post a plunk?
I found your awesome tuto on GitHub (https://github.com/simpulton/angular-photo-slider) but it was not working perfectly fine. The animation between two slides was not as smooth as yours and it was impossible to go back to the first slide when you reached the last one.
I finally found why. I think that there is a small mistake in the HTML. Indeed, the line 31 was :
“”
I changed it to :
“”
And then it is working perfectly fine and smooth just like in your demo. Is it normal?
My next steps will be to adapt your slide show in order to be responsive, to add a time function in order to change the slide automatically and to find if there is a way to control the slide show with the keyboard.
Best regards and thank you very much for your awesome slide show.
dude, great work and thanks in advance ..
but animation is not working , can you please give the description for ‘slide-animation’ class..looks like it is missing in the style page
Great tut…just implement this into my site. Is there an ‘autoPlay: true’ setting for this slider?
Thank you.
Thanks! The answer to that mystery is here
Demo doesn’t seem to work. Only 1 photo displays with no controls using mac/chrome.
John
Hi John — are you sure? It works fine for me. The controls are a little hard to see on the first slide.
How would you make it automatic. Sliding automatic including the buttons
Very nice, I’m a noob and its helped give a nice practical example to get me started.
Now for the kink: what if your photos are not all of the same size? I’ve tried it and the bounding box sizes for the first image and then remains the same for all others. How do you get it to resize for each image?
I would do it with a directive 😀 Grab the element parent dimensions and then resize proportionately. Ping me back if this doesn’t make sense.
I think what is happening is that my directive to resize the “container slider” based on the size of the currently active image breaks ng-repeat, or, at least, makes it inoperative. This means that the directive will show the container, but none of the images nor the nav (both relying on ng-repeat). So what do I have to do to my directive, so that ng-repeat works again?
I suppose if the code I posted has any value, you can add it into the slider and watch it break.
Good example
Great tutorial, but I think that you schould build a left / right slide.
My friend… come back for part two 😀 Build a Sweet AngularJS Photo Slider Pt 2 with ngTouch
Nice 1……
Very helpful for the beginners…
Awesome tutorial!, Thank you very much. 🙂
HI, This is really amazing tutorial. Thanks a ton.
The github code is not doing animation like demo is doing. I did replace animation part in github code (app.js file) as follow…
.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’);
TweenMax.set(element, { left: element.parent().width() });
TweenMax.to(element, 0.5, {left: 0, onComplete: done });
}
else {
done();
}
}
};
});
Ah yes, the regal Sir TweenMax of Greensock. I do know that fellow and think quite highly of him.
Hello. This a great slider!!! I have run into a problem and I am sure I am just doing something incorrectly, as I am new to Angular. I am using ngRoute to inject html pages into my index.html page. If I view the page that has the slider by itself (slider.html) it looks perfect. If I view the index.html page that is injecting the slider into the page (slider.html), the slider does not show. Everything else on slider.html shows but not the slider itself. Any ideas what I may be doing wrong? The site is not live so I do not have a link to show.
@Chris Does any of your other routes work? I suspect there is a problem with how routing is set up.
Hi.. I currently finished the fourth step, but the application so far is not allowing me to navigate between images as it should when I click on the dots or arrows. Any kind of help is highly appreciated.
Hi Naga – can you put your code in a plunk so I can see it? Are you throwing an error?
I couldn’t paste the HTML code in the comment box… Can you tell me how to do it?
Please put your code in a plunk at http://plnkr.co/ or post it on Github. Comment threads are a really poor place to troubleshoot code.
Here is my code I used so far
http://plnkr.co/edit/EkzrF9MjVZ9YQFDRKa03?p=preview
Here is a working example… http://plnkr.co/edit/u9kzj7zWTzcm0r0rCRnW?p=preview
Your next and previous code was not correct.
This should get you unstuck. You just need to add in the animations. As a troubleshooting mechanism, I recommend referencing the final code in the repository to see where your code differs and working from there.
That helps. Thank you so much for your help @Lukas
Just to let everyone know the problem was with the previous slide code and also it seems with the angularJS library that I used. When I used the one used in this article, it worked.
Hiya! Great tutorial, you did a good job explaining everything.
I’m running into a problem with the animation part though..
Whenever I press the next or prev button, I get this error: ‘angular.js:13920 TypeError: element.parent(…).width is not a function’.
It seems to be caused by the TweenMax code but I can’t figure out what’s wrong with it. Any tips?
Cheers,
Max
Hi Max — can you put it in a plunk so I can see what you are working with? Thanks!
Thanks for the quick reply,
It’s kind of hard to put the project into plunkr because of my structure but as I’m pretty sure it’s caused by the animation I hope this will do: https://plnkr.co/edit/aJizsWqqjg9QVxzNuj44?p=catalogue
The reason I say this is because whenever I have the template of the animation (with the //CODE GOES HERE comments still in place) the slider works fine but without animation, and as soon as I add the tweenmax code it crashes.
Thanks in advance!
Between 1.2 and 1.5 the API for querying the DOM has changed a bit. Here is a plunk updated to the latest https://plnkr.co/edit/wHecehQqM9Bxd6Nfkimo?p=preview. The main difference is that I had to wrap element in $(element) and tadah! everything is right with the world again.
Fixed it! Thanks again! 🙂