Want more updates, tutorials, and awesomeness in general? Sign up!

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

17 comments… add one

Leave a Comment