Slider Effect Throttling

Chapter 10 19 mins

Learning outcomes:

  1. The problem of quick navigations
  2. Implementing the solution


So far in this unit, we've covered a total of three effects in our slider: fade-out-fade-in, fade-out, and slide.

There can obviously be many more effects such as squeeze, 3D flip, rotate, crossfade, and so on, but the point is not how many effects we have, but rather how well-programmed and error-free are they.

If we take a closer look on the effects created thus far, we see one very obvious peculiarity that ought to be rectified before we can move on to address other concerns of our slider. The problem is brought about by quick navigations.

The quick navigation problem

When we navigate to a new slide, a couple of CSS transitions/animations come to play their part in the navigation. As we know, these transitions take a certain amount of time to complete formally referred to as their duration.

Now, if another navigation is made at a point when the previous one is still on its way to completion, what happens is that we get a slight jankiness in navigation.

Remember that this is not a device-level problem. It's just the way our effects work.

For example, taking the fade-out-slide-in effect, when a navigation is made, it fades out the current slide and at the same time slides in the new one using a CSS animation. If we navigate while a navigation is under way to its completion, what would happen is that the sliding-in slide would snap, and slowly fade out after which the new slide would slide-in from the right.

The most important and concerning thing over here is the snap — it is what creates the jankiness effect when we quickly navigate the slider.

In some effects, such as fade-out-fade-in, or slide, this jankiness isn't really noticeable; but it's obviously still there. However, if we increase the transition duration of these effects, then the jankiness would become even more pronounced in them as well.

In short, there is one problem in our slider that might not break its interactivity, but surely make it look poorly developed. Well-developed things are free of problems!

In the next section, we come up with a clever solution to this problem. That is, we don't perform any navigations until and unless we know that there is currently no pending navigation.

Let's start the discussion...

No direct navigations

When we navigate our slider by clicking on a pagination button or a navigation button, the navigateSlider() function is called, which performs the navigation of the slider with a desired effect. Now, if we perform another navigation right at this point, navigateSlider() is called once again, but this time a navigation is already pending.

As we stated before, these quick navigations can cause jankiness in the transition of the slider, which is the reason why we want to avoid them.

We want to prevent quick navigations from happening in our slider.

How to accomplish this?

Well, the first step is to check for any pending navigation in navigateSlider() and proceed only if there are none. But this has a prerequisite: we need some way to specify that a navigation is pending.

How to do this?

Well, when navigateSlider() is called, a navigation is begun, which completes only when the transition of the respective effect completes. From the point of calling navigateSlider() to the point at which the transition completes, the navigation is under work. That is, it is still pending.

Likewise, we can do one thing — set a Boolean variable navigationPending to true, when a navigation begins and reset it back to false when the navigation completes.

Then on subsequent calls to navigateSlider(), we can check for navigationPending and proceed with the navigation logic only if it is false.

That is, we proceed only if there is no pending navigation. Otherwise, we do nothing.

This means that when we consecutively call navigateSlider() in a short span of time (like by pressing the right navigation button very quickly), a navigation is only made after the first one completes.

Before its completion, all calls to navigateSlider() end without any effect. That is, all these quick pagination, or left/right button clicks become uninteractive.

Implementing the solution

Coming to the implementation of this whole idea, there are essentially three things we need to accomplish:

  1. Set navigationPending to true.
  2. Set navigationPending to false.
  3. Check navigationPending before making a navigation.

The first thing has to be done at the start of navigateSlider()'s body, as it's the place where the navigation begins.

The second thing has to be done when the whole navigation completes i.e when the transition/animation comes to an end.

Now there are two ways to execute a piece of code at the end of a transition:

  1. Handle the transitionend event on the given element.
  2. Set a timeout that completes exactly when the transition ends.

Let's talk on both these...

Suppose we have a slider with the fade-out effect whose transition duration is 1s. To execute a piece of code at the end of a navigation of this slider, we could listen to the transitionend event on the current slide. In the event's handler function, we set navigationPending to false and voila — on subsequent calls to navigateSlider(), the function would realise that navigationPending is false, and therefore execute its navigation logic ultimately performing a navigation.

On the outskirts, the idea of using an event is really simple, but deeper there are challenges.

In the fade-out effect, the current slide is the only one that's transitioned, and so it becomes the obvious target of the transitionend event.

However, if we take the fade-out-fade-in effect, there we have the new slide transitioned as well. In fact, the effect ends at the transition of the new slide. This means that in the fade-out-fade-in effect, we need to listen for the transitionend on the new slide; NOT on the current slide.

And it doesn't end here! If we take the slide effect, it has its own requirements.

In the slide effect, transitionend would need to be listened on the .slider_slides-cont element; NOT on the new slide, NOT even on the current slide.

These differences can obviously be addressed if we just stick to one effect in our slider and construct its code around that effect. But in our case, since we are developing a generalised slider library that could be customised by its users, we need to take another way to solve this inconsistency in the targets of transitionend.

And that way is to simply not use transitionend at all — end of story.

Rather, we'll go with setting a timeout, that will execute exactly at the end of the navigation.

The idea is that we invoke setTimeout() with its second delay argument set to the total time, in milliseconds, it takes for a navigation to complete.

Then, in the timeout's callback, we set navigationPending back to false to signal that no navigation remains pending.

One important thing to note over here is that we'll have to figure out the delay for the timeout ourself.

As we discussed above, the transitionend method doesn't require us to calculate any time delays — it essentially takes care of this itself.

However, going with setTimeout(), we have to figure out the total time it takes for a navigation to complete in milliseconds, and then pass this time as the second argument to the function.

This surely does require us to get into work and do a bit of math in figuring out the duration of the navigation, but at the same time it eases us from a lot of coding.

So for example, if a slider has a fade-out effect, whose .slider_slide--faded class has a transition duration of 5s, then the delay argument we would need to supply to setTimeout() would be 5000 (5s = 5000ms).

Similarly, if the slider has a fade-out-fade-in effect whose .slider_slide--faded-in class has a transition duration of 2s, and .slider_slide--faded-out class has a transition duration of 1s, then the second argument of setTimeout() would become 3000 (2s + 1s = 3s = 3000ms).

To sum up, here's what we have to do in rectifying our quick navigation problem:

  1. Figure out the time duration of each navigation, in milliseconds.
  2. Initialize a global Boolean variable navigationPending to false with the page load, to indicate that initially there are no pending navigations.
  3. Inside navigateSlider(), first check for navigationPending and proceed only if it's false.
  4. When a navigation is begun, set navigationPending to true and also set a timeout to fire after the delay calculated in step 1). In the timeout's callback, set navigationPending back to false.

And this is all that we need to do. Isn't this superbly simple?

Let's now accomplish all these things step by step in our slider's script.

We'll suppose that our slider has the fade-out effect, whose transition duration is 3s.

First we'll initialise our navigationPending Boolean to false, along with the rest of the global variables of our script:

var currentIndex = 0;
var newIndex = 0;
var navigationPending = false; /* ... */

Next, we'll redefine navigateSlider() to check navigationPending before proceeding any further:

function navigateSlider() {
   // If no navigation is pending, perform the navigation.
   if (!navigationPending) {
      /* ... */

We ought to proceed only if there is no navigation pending i.e. when navigationPending is equal to false.

Moving on, when we execute the if body shown above, we set navigationPending to true, since a navigation has begun and in the process of completion.

This is accomplished below:

function navigateSlider() {
   // If no navigation is pending, perform the navigation.
   if (!navigationPending) {
      navigationPending = true;
      /* ... */

The last thing left to be done is to reset navigationPending back to false at the end of the navigation. For this we'll set a timeout in the same if body shown above.

Recall that we supposed our slider has the fade-out effect with a transition duration of 500ms. Likewise, the second argument of setTimeout() would become 500:

function navigateSlider() {
   if (!navigationPending) {
      navigationPending = true;

      setTimeout(function() {
         navigationPending = false;
      }, 500);

      /* ... */

Now, since it's very specific to come at this setTimeout() statement in order to change its second delay argument, we'll create a global variable navigationDuration to hold it.

In this way, we'll just need to change the value of this global variable at the start of our script in order to change the second argument of setTimeout() — no need to come up to the definition of the timer.

Consider the following code snippets:

var currentIndex = 0;
var newIndex = 0;
var navigationPending = false;
var navigationDuration = 500; /* ... */

Here we define the navigationDuration variable.

And then below we replace the argument 500 passed in to setTimeout() with navigationDuration:

function navigateSlider() {
   if (!navigationPending) {
      navigationPending = true;

      setTimeout(function() {
         navigationPending = false;
      }, navigationDuration);

      /* ... */

Let's now try out this slider.

Live Example

Uh oh. There is still some problem remaining.

New index changing

When we quickly navigate the slider shown above, we still notice some weirdness in it. Let's try to understand its origins...

We'll suppose we have a slider with a fade-out effect whose transition duration is 3s. So here's what happens.

After the page loads, we navigate the slider to the right using the right navigation button. newIndex becomes 1 and then navigateSlider() is called.

In navigateSlider(), navigationPending is checked. Since, it evaluates to false, the if block executes, as a result navigating the slider and setting up the timeout to set navigationPending back to false.

In the meantime, while this navigation is happening, we again press the right button. newIndex becomes 2 and then navigateSlider() is invoked.

Once again, navigationPending is checked in the function. This time, since it's true, no navigation is made. Keep in mind that newIndex is 2 while currentIndex is 1.

We press the right button once again. newIndex becomes 3, while currentIndex is still 1. The same old navigation hasn't completed yet and so no further navigation is registered. However, the problem has already begun to show up.

Did you notice it?

It's the increment of newIndex that's the problem.

No navigations are being made, but regardless newIndex is constantly being incremented by each click of the right navigation button.

If we could somehow solve this, we would free our slider from literally any sort of issues. The question is how to solve this?

Think for a moment... the solution is just one-liner!

Let's review the problem. The problem is that newIndex gets incremented on each right button's click, even if no navigation is made afterwards. We have to stop this increment.

Now one option is to layout an additional check for navigationPending is the right button's click handler, and increment newIndex only if the Boolean is false. But if we do so then we would need to do the same thing in every place where we change newIndex and call navigateSlider().

This would be cleanly inefficient and too much work!

Any other solutions? Can we use currentIndex to our advantage?

Let's go back to the same scenario we built above and see where exactly does the problem begin?

When we navigate the slider to the right for the first time, newIndex becomes 1 and navigateSlider() gets called. navigationPending is false and consequently a navigation is made. Meanwhile, the right navigation button is pressed again. newIndex becomes 2, and navigateSlider() is called. However, no navigation is performed this time.

This is where the problem begins.

On this second click of the right navigation button, newIndex becomes 2 pointing to the third slide. However, the second slide is still on its way to showing up. There is no point of incrementing newIndex, given that the previous navigation has not ended yet.

Now what we can do to prevent this is as follows.

If navigationPending is false, we execute the navigation logic, as we have been doing so far.

However, if it's true, we reset newIndex back to the value of currentIndex. In other words, what we mean by this is that we aren't accepting any changes to newIndex right now, as the slider is still under way of its navigation. If any change is made to newIndex, we reset it back to currentIndex.

This second case can be executed simply by giving an else statement to the if statement in the navigateSlider() function.

Consider the following code:

function navigateSlider() {
   if (!navigationPending) {
      /* ... */

   // A navigation is pending, hence reset newIndex back to currentIndex.
else {
newIndex = currentIndex;
} }

Time to try out the slider:

Live Example

Perfect. This simple resetting tactic has finally cleared up all the weirdness from our slider. Now we can navigate it without any problems.

"I created Codeguage to save you from falling into the same learning conundrums that I fell into."

— Bilal Adnan, Founder of Codeguage