Build a Sweet AngularJS Photo Slider Pt 2 with ngTouch

slider

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 Code

Adding 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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<!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.

1
angular.module('website', ['ngAnimate', 'ngTouch'])

Step 3: Swipe

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<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.

1
2
3
.nonDraggableImage{
    -webkit-user-drag: none;
}

From there we add nonDraggableImage to our img tag and we are good to go!

1
2
3
4
5
6
7
<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.

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
angular.module('website', ['ngAnimate', 'ngTouch'])
    .controller('MainCtrl', function ($scope) {
        /* Code omitted */
       
        $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;
        };
    })

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().

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
.animation('.slide-animation', function () {
    return {
        addClass: function (element, className, done) {
            var scope = element.scope();

            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();
            }
        }
    };
});

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().

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
34
35
36
.animation('.slide-animation', function () {
    return {
        addClass: function (element, className, done) {
            var scope = element.scope();

            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();
            }
        }
    };
});

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
Build a Sweet AngularJS Photo Slider Pt 2 with ngTouch

17 Responses

  1. 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

    Chris October 10, 2013 at 2:05 am #
  2. w00t! Nevermind … I got it! :-) … your reference to jquery.min.js is missing the https:// part of the url. That seemed to fix it.

    Chris October 10, 2013 at 2:10 am #
  3. Awesome! I will take a look at the repo and make sure everything is in order.

    simpulton October 10, 2013 at 4:03 pm #
  4. 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.

    Dmitry December 29, 2013 at 9:35 am #
  5. 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.

    Dmitry December 29, 2013 at 11:48 am #
  6. Thanks for the heads up Dmitry… I will have a look. #highFive

    simpulton December 30, 2013 at 12:59 am #
  7. 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 January 22, 2014 at 6:33 am #
  8. Dmitry — you are awesome :D

    simpulton January 22, 2014 at 3:22 pm #
  9. 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 :)

    Dmitry January 24, 2014 at 9:24 am #
  10. Any ideas about auto rotating the slider based on a timer?

    Matt February 19, 2014 at 12:46 pm #
  11. Easy… use $timeout to increment or decrement through the array of pictures.

    simpulton February 19, 2014 at 6:15 pm #
  12. “Easy‚Ķ use $timeout to increment or decrement through the array of pictures.”
    Can you please elaborate? :)

    Tomer February 26, 2014 at 8:01 am #
  13. 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!

    simpulton February 27, 2014 at 5:34 pm #
  14. 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.

    Eshwaran April 22, 2014 at 9:49 am #
  15. The same exact way I approached the photo slider :D The concepts are the same but the UI is different.

    simpulton April 22, 2014 at 2:10 pm #
  16. Why so slow on mobile device?

    Aft May 11, 2014 at 8:38 am #
  17. I haven’t specifically optimized for a mobile device so the images are much larger than they should be.

    simpulton May 13, 2014 at 12:23 pm #

Leave a Reply