Introduction

Where we left our slider in the previous chapter is where we will now continue it from and give it an advanced level of touch interactivity.

The code examples from the previous chapter will more or less remain the same, with only certain differences at one or two places.

So why not move into actually discovering the journey to making our slider even more interactive than before!

Move with the swipe

The slider we constructed previously operated entirely on the touchstart and touchend events - only when we ended our swipe was when we saw any result from our touch interaction. Nothing else was there to indicate interactivity right away as we were performing a swipe across the screen.

The question is what can we possibly do to our slider, in particular give which event, so that it can signify interactivity right away on a swipe? Which event are we missing right now in the code?

An event to help

Well we simply need to let a touchmove event come into the play.

Good to know which event to use, but now arises another question: how to use the touchmove event? What do we need to do in this event's handler? Take a guess - what happens to your home page's thumbnails as you swipe across them.

The thumbnails move with your swipe i.e the speed at which you move your touch pointer is exactly the speed at which they move. And once you leave off the screen, they either navigate or return back to their original position (depending on the speed and amount of swipe).

In our case, ultimately, we need .slides-wrapper to move as the touchmove event occurs.

To accomplish this we simply need the event's object, the clientX property and a style's addition.

Analyse the code below and comment whether it correctly works or not in moving slidesWrapper with the swipe.

slidesWrapper.addEventListener("touchmove", function(e) {
    slidesWrapper.style.transform = "translateX(" + e.changedTouches[0].clientX + "px)";
});
  • Yes
  • No

The code shown here is incorrect, with two problems.

Firstly we have used e.changedTouches[0].clientX directly to translate .slides-wrapper. The problem in doing so is answered by considering a simple scenario:

Let's say we initiate touchstart at clientX = 100px and then move 1px to the right. This results in clientX = 101px. Now if we translate .slides-wrapper by this value it will be translated by 101px, where it needed to move by 1px only.

The turn over from this is that we need to move slidesWrapper according to the difference between clientX and startPos (the initial clientX) value.

Therefore what we can do to solve this is to include a new variable deltaPos in the touchmove handler which computes this difference, and then moves slidesWrapper by this amount.

slidesWrapper.addEventListener("touchmove", function(e) {
    deltaPos = e.changedTouches[0].clientX - startPos;
    slidesWrapper.style.transform = "translateX(" + deltaPos + "px)";
});

The second problem in the code above is that we aren't saving the amount of translation already performed. This is explained as follows:

(Suppose that the width of each slide is 800px.)

We swipe to go forward from slide 1 to slide 2, by deltaPos = -450px - and hence get the slider navigated (translateX becomes -800px). Everything works fine until the point when we repeat this procedure on slide 2, to go to slide 3.

Ideally we want deltaPos to be added to the previous translateX for example going forward 100px should result in translateX = -800 + 100px, but in our code we translate by deltaPos directly.

Due to this, no matter which ever slide we are on, once we swipe we come back to the slide 1 since deltaPos ⩽ slideWidth.

This explanation might throw you out of track to be able to comprehend the second problem - so it is recommended that you read it carefully and try to fully understand each word.

Thereby the way to solve this issue is to simply save the amount of movement already performed. And to do this we use a variable moved by assigning it a value in touchend:

var moved = 0; // declare move somewhere at the top of the script

slidesWrapper.addEventListener("touchend", function(e) {
    /* other code */
    if ((endTime - startTime) < benchmarkTime || deltaPos >= halfSlideWidth) {
       if (deltaPos === 0) return;
       else if (deltaPos < 0 && counter !== (len - 1)) counter++;
       else if (deltaPos > 0 && counter !== 0) counter--;
       moved = halfSlideWidth * -2 * counter;
       navigateSlider();
    }
});
Note that here we've taken the statement navigateSlider() out of the conditional block (from lines 5-10) to line 11. This is done so that if the swipe is slow and before the halfway position, .slides-wrapper can navigate back to its original position.

With moved calculated its only a matter of seconds to include it in touchmove:

slidesWrapper.addEventListener("touchmove", function(e) {
    deltaPos = e.changedTouches[0].clientX - startPos;
    slidesWrapper.style.transform = "translateX(" + (deltaPos + moved) + "px)";
});

And here we have a working slider. Check it out and see what problems you still notice in it.

Janky Touchmove Slider

Janky experience

If you tried out the example above and have returned here with the thought "Uh, that slider was so janky" then you aren't alone in this experience. Anyone who will try the example will make this same remark.

But not everyone will try to think on the cause of this issue. Accordingly we'll first let you think for the reason..

Choose the best reason for the laggy behaviour of the slider example above.

  • .slides-wrapper has a transition property set on it which causes every translation to take some time to complete.
  • We do a resource-intensive calculation inside our touchmove handler.
  • We use translateX instead of left to move our slider.

Well, it seems that the culprit for the sluggish sliding is the property transition on .slides-wrapper. As we get the element to translate on our swipe, every single pixel moved takes some time to do so. This time duration is the real cause of the jank.

To solve this we simply need to remove transition from .slides-wrapperso that it can move freely.

However this shall be the case only uptil the point we are swiping: at touchend, when we end the swipe, we need to reinforce the transition back so that the slider can again navigate smoothly.

Hence the boil down is: remove the transition at touchstart and reinforce it at touchend.

slidesWrapper.addEventListener("touchstart", function(e) {
    slidesWrapper.style.transition = "none"; // no transition
    /* other code */
});
slidesWrapper.addEventListener("touchend", function(e) {
    slidesWrapper.style.transition = "1s ease-in-out"; // add a transition
    /* other code */
});
/* other code */ means the rest of the code in the event's handler.

Now let's try out the example below and see if everything is working perfectly.

Rectified Touchmove Slider

Well we notice some jankiness even after removing transition from .slides-wrapper. What could possibly have caused this?

Choose the best reason for the laggy behaviour of the slider example above, apart from transition.

  • The touchmove handler doesn't call e.preventDefault() and hence doesn't remove the background work being done on every swipe.
  • We do a resource-intensive calculation inside our touchmove handler.
  • translateX is a janky property as it causes reflow of the page.

The culprit is the background work which gets performed once we start swiping. It causes our work in the touchmove event janky as, intuitively, the event is already occupied with some tasks.

The way to solve this problem comes straight from the event object - the method preventDefault() will remove the default behaviour happening on the touchmove event.

The touchmove handler likewise becomes:

slidesWrapper.addEventListener("touchmove", function(e) {
    e.preventDefault();
    deltaPos = e.changedTouches[0].clientX - startPos;
    slidesWrapper.style.transform = "translateX(" + deltaPos + "px)";
});

Now let's finally try out the slider with these added rectifications and see if anymore are required. Well, in effect, there isn't any problem left!

Smooth Touchmove Slider

And everything works amazingly!

Quick swiping

Go back to the example above and try swiping quickly across the slides. Were you able to notice some snappy navigations?

As you swipe across the slides very quickly there comes a point where they start to snap instead of smoothly sliding in. This is caused by the removal of transition on touchstart.

Here's the explanation of the issue:

We start at slide 1, and swipe quickly to go to the right. The event touchstart occurs, then touchmove and finally touchend where the swipe is detected to be fast; after which counter is incremented, transition is applied to .slides-wrapper, and consequently the second slide is initiated to be brought in. It will take some time to completely slide-in due to the time-duration of transition.

Now if we start swiping again, before this duration; as always touchstart will occur and as a consequence remove transition from .slides-wrapper. This removal will cause the previous navigation (which was in the way to be completed) to snap in i.e be applied instantly without any transition.

Therefore the turn out from this explanation is that we shouldn't remove transition on touchstart. But then where to remove it? Can you think of any other place?

After a bit of thinking into the solution, we arrive at the following conclusion:

The property transition needs to be inforced only when a navigation is about to be performed i.e navigateSlider() has to be invoked. This can be from a navigation or pagination button, a key event, a touchend event, or an autoplay.

So why not take this transition add/removal logic into the navigateSlider() function and remove it from all other places to get all cases dealt with rightaway.

No transition on .slides-wrapper initially; therefore no transitioning on touchstart and touchmove. However on touchend: 1) call navigateSlider() 2) apply transition 3) let the transition complete and then 4) remove it. Simple working!

Clever solution, isn't it ;)