The Setup
This is a continuation of my previous post Build a Sweet Photo Slider with AngularJS Animate where I am going show how easy it is to add touch capabilities to the photo slider.
As an added bonus, I am going to show you how to dynamically set the direction of the animation so that the overall user experience is better.
Check out the demo and grab the repository off of Github and let’s get started!
Part One Demo CodeAdding Swipe Functionality
The first part of this post is going show you how to add and utilize ngTouch in the application.
Step 1a: HTML
In the index.html file we need to add the angular-touch.min.js file since ngTouch is not part of the AngularJS core.
<!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 CODE OMITTED --> <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="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.0-rc.2/angular-touch.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>
Step 1b: JavaScript
And in the js/app.js file we need to add the ngTouch submodule so it is available for the application.
angular.module('website', ['ngAnimate', 'ngTouch'])
Step 3: Swipe
<div class="container slider"> <img ng-repeat="slide in slides" class="slide slide-animation nonDraggableImage" ng-swipe-right="nextSlide()" ng-swipe-left="prevSlide()" 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>
The awesome thing about adding in swipe functionality to the slider is that the methods to handle the events already exist. We are going to simply hook into nextSlide() and prevSlide(). On the img tag we are going to call these functions with ng-swipe-right and ng-swipe-left respectively.
Step 3: Swipe For Real This Time!
If we were not dealing with images this would conclude this section of the tutorial. But! Since we ARE dealing with images, we need to do one more thing to make this work. Swipe events in ngTouch work on desktop browsers via dragging. In Webkits browsers when you drag across an image, the image will actually drag with your mouse which overrides the ngTouch events.
It is easy to disable this behavior with the following CSS.
.nonDraggableImage{ -webkit-user-drag: none; }
From there we add nonDraggableImage to our img tag and we are good to go!
<div class="container slider"> <img ng-repeat="slide in slides" class="slide slide-animation nonDraggableImage" ng-swipe-right="nextSlide()" ng-swipe-left="prevSlide()" ng-hide="!isCurrentSlideIndex($index)" ng-src="{{slide.image}}"> <!-- Code omitted --> </div>
Updating the Animations
Now that we can swipe left and right to move through the images, the fact that the animations only happen in one direction is a bit odd.
In the next part of this tutorial I am going to show you how to dynamically control the direction of the animation depending on the interaction. This is where I think JavaScript animations really shine because it allows you to apply logic on the fly.
Step 4: Left or Right?
We need to be able to set a flag keeps track of what direction we want the animation to go. The first step is to create a direction property on $scope and we are going to set it to left.
angular.module('website', ['ngAnimate', 'ngTouch']) .controller('MainCtrl', function ($scope) { /* Code omitted */ <pre><code> $scope.direction = 'left'; $scope.currentIndex = 0; $scope.setCurrentSlideIndex = function (index) { $scope.direction = (index > $scope.currentIndex) ? 'left' : 'right'; $scope.currentIndex = index; }; $scope.isCurrentSlideIndex = function (index) { return $scope.currentIndex === index; }; $scope.prevSlide = function () { $scope.direction = 'left'; $scope.currentIndex = ($scope.currentIndex < $scope.slides.length - 1) ? ++$scope.currentIndex : 0; }; $scope.nextSlide = function () { $scope.direction = 'right'; $scope.currentIndex = ($scope.currentIndex > 0) ? --$scope.currentIndex : $scope.slides.length - 1; }; }) </code></pre>
The most obvious places to set $scope.direction is in the prevSlide and nextSlide methods. When prevSlide is called, we set $scope.direction to left and when nextSlide is called, we set $scope.direction to right.
We can take this one step further by setting $scope.direction in the $scope.setCurrentSlideIndex as well. Using a ternary operator, we are setting $scope.direction to left if the new index is greater than $scope.currentIndex and right if it is not.
Step 5: Get That Scope!
We now have a property on MainCtrl that we are using to track the direction we want to the animation to go, but how do we access that property in the animation? Well it so happens that we have access to the element scope that the animation is acting upon by calling element.scope().
.animation('.slide-animation', function () { return { addClass: function (element, className, done) { var scope = element.scope(); <pre><code> if (className == 'ng-hide') { /* Code omitted */ } else { done(); } }, removeClass: function (element, className, done) { var scope = element.scope(); if (className == 'ng-hide') { element.removeClass('ng-hide'); /* Code omitted */ } else { done(); } } }; </code></pre> });
Being that scope is not going to change on the element, I store a reference so that I only have to call element.scope() once per event handler.
Step 6: And Now Some Math
Now that we can access direction on scope, it is a matter of dialing in the animation object for TweenMax.
I am going to spare you the commentary on the few iterations I did to get the opposing animations working, but it essentially came down to using the negative or positive value of element.parent().width().
.animation('.slide-animation', function () { return { addClass: function (element, className, done) { var scope = element.scope(); <pre><code> if (className == 'ng-hide') { var finishPoint = element.parent().width(); if(scope.direction !== 'right') { finishPoint = -finishPoint; } TweenMax.to(element, 0.5, {left: finishPoint, onComplete: done }); } else { done(); } }, removeClass: function (element, className, done) { var scope = element.scope(); if (className == 'ng-hide') { element.removeClass('ng-hide'); var startPoint = element.parent().width(); if(scope.direction === 'right') { startPoint = -startPoint; } TweenMax.set(element, { left: startPoint }); TweenMax.to(element, 0.5, {left: 0, onComplete: done }); } else { done(); } } }; </code></pre> });
In the addClass event handler, I am setting finishPoint to element.parent().width() and if the direction IS NOT right then I am setting it to its negative value.
In the removeClass event handler, I am doing pretty much the opposite. I am setting startPoint to element.parent().width() and if the direction IS right then I am setting it to its negative value.
Conclusion
And that concludes this edition of “Pimp My Photo Slider”! I was able to actually implement these changes in approximately 15 minutes which is a super small investment for such a nice return on the user experience.
Big thanks to my friend Johan Steenkamp aka @johanstn for the great feedback and suggestions on the app. A hard requirement for being a great developer is knowing great developers. Thanks bro!
Resources
Remastered Animation in AngularJS 1.2
Getting Started with the JavaScript Version of the GreenSock Animation Platform (GSAP)
Part One Demo Code
Hi Lukas,
I really like your demo and I’ve been studying the code. I also read up on the other site references you’ve made in this 2 part blog post. For me the demo runs great and I think I understand the code. I’ve been working in Angularjs for awhile now. But, for some strange reason, I can’t get your GitHub code to work 100%. I see the photos and things look right, but, it does not transition correctly. That is the photos just appear instead of gliding in like they do in your demo. Also, clicking the left chevron does not make the photos rotate. I compared the code in the download to your blog post and things seem right. I don’t know what small little thing is breaking the transitions. Any chance you could double check your GitHub code or possibly upload the code used in the demo? Thanks for making the effort to talk about Angularjs animations. There is not a lot currently out there.
Best regards,
Chris
w00t! Nevermind … I got it! 🙂 … your reference to jquery.min.js is missing the https:// part of the url. That seemed to fix it.
Awesome! I will take a look at the repo and make sure everything is in order.
Seems to work properly only on 1.2.0-rc2 as in the example. On angular(-animate) 1.2.1 the original slide that should be animated out is just hidden instantly without any transition.
Actually tested it with google-hosted versions and it works up to 1.2.0rc3 and does not since 1.2.0
Probably something happened to the way animations are handled.
Thanks for the heads up Dmitry… I will have a look. #highFive
There is one more issue I noticed with that. I’m not sure whether it’s angular’s fault as your code is pretty straightforward but here it goes:
On mobile devices the slides won’t change properly with touch swipes unless you use buttons at least once.
So on first load there’s a significant delay (calculations?) after you swipe and before the next/previous slide is shown and no animations are triggered (glitch?)
On the other hand triggering slides with arrows (next/prev) or small dots (setSlideIndex) works fine with animations. If you use these controls to change the slide at least once then touch swipes start working and animations work as well.
The issue goes away if you don’t do second ng-repeat loop for the dots. I tested that on Iphone and IPad in both Chrome and Safari. Both with your demo an my application. With latest angular 1.2.8 the issue is still there.
Dmitry — you are awesome 😀
Hey, thanks. You are as well!
Weirdest thing – it seems the actual order in which your ng-repeat loops are going does affect the behaviour in response to touch swipes. After I moved the second repeat loop for the dots to go before(!) the loop for the slides everything magically worked! I have no idea why that is happening but it is. Perhaps something with the $index is messed up…
Anyway thanks again for such a nice touch slider. It is by far the best and simplest thing around 🙂
Any ideas about auto rotating the slider based on a timer?
Easy… use $timeout to increment or decrement through the array of pictures.
“Easy… use $timeout to increment or decrement through the array of pictures.”
Can you please elaborate? 🙂
Hi Tomer –
Sure… so we are using nextSlide and prevSlide to increment or decrement through $scope.currentIndex. I would simply use $timeout to call nextSlide or prevSlide at some predefined interval ie
$scope.nextSlideOnTimer = function() { $scope.nextSlide(); $timeout($scope.nextSlideOnTimer, SOME_INTERVAL); }
From there you can extend the idea and add in the ability to cancel timers if you want to go the other way etc.
Check out Ben Nadel’s post on how to do that here http://www.bennadel.com/blog/2548-Don-t-Forget-To-Cancel-timeout-Timers-In-Your-destroy-Events-In-AngularJS.htm
Hope this helps!
Hi Lukas –
I was just wondering how you would approach building a dynamically generated menu drawer with features similar to this use case such as gesture sliding pagination etc.
The same exact way I approached the photo slider 😀 The concepts are the same but the UI is different.
Why so slow on mobile device?
I haven’t specifically optimized for a mobile device so the images are much larger than they should be.
Thx For sharing..
but, how if I would show more image (two image slide)
Hi – thank you for sharing.
I re-implement your controller using controllerAs syntax, this makes the element.scope().direction in the animation directive not available.
Is there a tip to make this work with controllerAs syntax?
/Dennis
Whatever you defined your controller as ie MainCtrl as main will put a main property on $scope. You should be able to put reach it via element.scope().main.direction. Drop it in a plunk and we can check it out.
Hi – Of course that makes perfect sense. Thought i tried it.
Thanks.
/Dennis
How to let it auto slide?
Using $timeout – check out this post Build a Full Page AngularJS Slideshow Plus Magic Tricks! for the details.
Hey could show an example of using an external JSON feed with the slider ? So how I could my photo list is json instead
A simple $http call would do the trick. There are tons of examples in the blog on how to do that.
No need for jQuery. Simply replace element.parent().width(); with
angular.element(document.querySelectorAll(“.slider”))[0].getBoundingClientRect().width;
Little more verbose 😀 but a great point!
Love you man. Clear explanation and it works!
Thanks Evan! Felt weird to load whole jQuery library just to get the width() function. Thanks to the author and the people commenting on this post, I’ve learned a lot!
Thanks! 😀
Missing The Http ( affect the transaction effect ) 😉
Pardon?
Hi! i can’t receive the value of slide.description!
And i don’t see result of this:
{{slide.description}}
Not any description(.
Can you please explain this moment?