A Simple Slider

Chapter 3 23 mins

Learning outcomes:

  1. Making the slider interactive
  2. What is Repeat-the-cycle
  3. What is Break-the-flow

Introduction

In the previous Slider Basics chapter, we constructed the HTML as well as the CSS for our slider from scratch and it is now that we will dig deeper into its last requirement i.e. JavaScript

Likewise, before you start on with this chapter make sure you are familiar with the previous one. Not only this but to be able to fully comprehend this chapter, you ought to know JavaScript really well.

If you feel that you're not fluent in JavaScript, please consider taking our JavaScript course.

Making the buttons interactive

Recall that the HTML code in the previous chapter had two navigation buttons for moving across the slider — one for going left/backwards and the other one for going right/forwards.

Now we shall make both these buttons interactive.

So first thing's first, let's start by selecting the buttons and then giving each a click event listener:

var navElements = document.getElementsByClassName("slider_nav");

navElements[0].addEventListener('click', function() {});
navElements[1].addEventListener('click', function() {});

The buttons are selected via their class name 'slider_nav' (in line 1) and then each one is assigned a click listener separately.

We use addEventListener() instead of the direct onclick property because it makes sure that no previous handlers setup on the element node (via onclick) are overridden.

Note the naming we've used here for the variable: the term sliderNav (sliderNavElements) follows from the class name of the elements we wish to select i.e. .slider_nav, while the term Elements (sliderNavElements) follows from the fact that the variable represents a collection of HTML elements, and not just any random collection objects.

We could've just called the variable sliderNav, however that would not convey the fact that it holds a collection of HTML element nodes.

Remember: naming is very important!

If you don't know what are event handlers, or the whole concept of events in JavaScript, then you can consider going over our JavaScript Events unit.

With the buttons selected, let's think about their actions.

Now what do you think shall happen when the ← Previous button is pressed? Similarly, what should happen when the Next → button is pressed?

Well, the actions are pretty basic:

  1. The ← Previous button shall hide the current slide and show the previous one.
  2. The Next → button shall hide the current slide and show the next one.

Let's take an example.

Suppose we have a slider with 3 slides. The page loads and the first slide is shown. The Next → button is pressed and so the second slide is to be shown, while the first one is to be hidden.

Now this can be easily done by setting display: none on the first slide and setting display: block on the second slide. In the end, the second slide is visible to the user.

Now suppose that the Next → button is pressed again. This time the second slide has to be hidden and the third slide has to be shown. The same property assignments would occur as in the previous case, but this time on the second and third slides, respectively.

In short, whenever the Next → button is pressed, the current slide is hidden and the next slide is shown.

The question is how to know which slide is the current slide?

How to know which slide is the current slide?

One way to do this is to:

Create a global variable that holds the index of the current slide. Initially, it's set to 0 to indicate that the first slide is currently shown (the first slide has index 0). On the click of the Next → button, it's incremented by 1.

For example, when the Next → button is pressed while the first slide is currently active, this variable gets incremeted to 1. This means that the second slide is now to be shown (remember that the second slide has index 1).

Another way is to:

Create a global variable that stores a reference to the current slide. Initially this variable points to the first slide. On the click of the Next → button, it is set to the nextElementSibling of the current slide.

OK so now what should we use?

Both methods are equally good if the slider only offers left-right navigation. However, if there is a pagination feature on the slider (which we shall explore in the next chapter) where the user could go to any slide he/she desires, then the former way would work a lot better.

Here's how...

Imagine we have a slider containing 10 slides. The page loads and, as always, the first slide is shown. Now the user clicks the 10th pagination circle to navigate to the 10th slide directly.

If we go with the first choice, i.e. a global variable holds the index of the current slide, we can simply set the variable to 10.

However, if we go with the second choice, i.e. a global variable holds a reference to the current slide, then we would need to get the reference to the 10th slide by first obtaining a list of all slides and then accessing the 10th element from it.

As is clear, the first choice is better — it gives us direct access to the 10th slide in the example illustrate.

Hence, we'd go with the first choice.

Essentially two things are required in this approach:

  1. A global variable to hold the index of the current slide.
  2. Another global variable to hold a list of all the slides.

We'll call the former currentIndex and the latter slideElements.

The name slideElements implies the fact that the variable holds a collection of elements that are slides of the sliders. Simple.

Consider the code below which defines both these new variables:

var currentIndex = 0;

var slideElements = document.getElementsByClassName('slider_slide');
var navElements = document.getElementsByClassName("slider_nav");

navElements[0].addEventListener('click', function() {});
navElements[1].addEventListener('click', function() {});

Let's now lay out the code for the click handlers of the ← Previous and Next → buttons.

To recap it, and this time in an elaborate way, when the ← Previous button is clicked, the current slide is hidden and the previous slide shown. The currentIndex is also decremented at the same time.

The same goes for the Next → button; except for that the next slide is shown and currentIndex is incremented.

Here's the code so far:

var currentIndex = 0;

var slideElements = document.getElementsByClassName('slider_slide');
var navElements = document.getElementsByClassName("slider_nav");

navElements[0].addEventListener('click', function() {
   slideElements[currentIndex].style.display = "none";
   currentIndex--;
   slideElements[currentIndex].style.display = "block";
});

navElements[1].addEventListener('click', function() {
   slideElements[currentIndex].style.display = "none";
   currentIndex++;
   slideElements[currentIndex].style.display = "block";
});

In the handler function for navElements[0] (from line 6 - 10), we start by hiding the current slide using display: none. Then we decrement currentIndex to point to the previous slide. Finally we show the previous slide using display: block.

The same goes for the navElements[1] handler, apart from that it increments currentIndex.

With all this, we have a working slider, but not without some errors.

Simple Interactive Slider

Our next aim is to make this basic slider error-free.

Making the click listeners DRY

If you notice in the code above, we are merely repeating statements that show and hide the slides, inside the click listeners.

We've highlighted the concerned statements below:

navElements[0].addEventListener('click', function() {
   slideElements[currentIndex].style.display = "none";
   currentIndex--;
   slideElements[currentIndex].style.display = "block";
});

navElements[1].addEventListener('click', function() {
   slideElements[currentIndex].style.display = "none";
   currentIndex++;
   slideElements[currentIndex].style.display = "block";
});

This is not desirable.

Whenever developing programs, the DRY principle shall be in mind. DRY stands for Don't Repeat Yourself and simply says that we should keep from repeating code in programs, as much as we can.

Instead of repeating stuff, we should create functions, put the repeating code inside them and then use these functions. This makes our programs extensible and much easier to maintain.

So going back to our slider's script, we'll now create a function and put the repeating statements within it. But before that, we need a name for the function.

What name do you think suits here?

First let's try to think about what exactly does the set of statements that we'll ultimately put inside the function do.

Well, they seem to navigate the slider, either to the left or the right.

Following from this idea, we feel that navigateSlider() does a pretty good job of conveying the meaning of the underlying code i.e. the function navigates the slider. Hence, we'll go with it.

Let's first clear away the statements from within the click handlers and put a call to navigateSlider() there:

navElements[0].addEventListener('click', function() {
navigateSlider(); }); navElements[1].addEventListener('click', function() {
navigateSlider(); });

Now, let's define navigateSlider() and put the set of repeating statements inside it:

function navigateSlider() {
   slideElements[currentIndex].style.display = "none";
   currentIndex++;
   slideElements[currentIndex].style.display = "block";
}

Even before we reload the browser tab to put this function into action in the code, we can notice a problem in it that we haven't yet rectified.

Can you spot it?

The problem lies exactly in line 3 above. Recall that we can move our slider either to the left or to the right. In the event of going left, the currentIndex variable has to be decremented to correctly point to the previous slide. Similalry, in the event of going right, it has to be incremented to correctly point to the next slide.

However, in line 3, we are only incrementing the variable — there is no decrementing. The function just doesn't know where to go. No matter whichever button we press, it takes us to the next slide.

The solution to this is fairly simple, but not one-liner. We have to put in a little bit of thought.

Here's the approach we take...

The problem is that navigateSlider() doesn't know where to go i.e. to the previous slide or to the next slide. We just ought to find some way to tell it where to go.

Let's create a new global variable called newIndex that holds the index of the slide where we ought to navigate next. Inside navigateSlider(), the slide with index newIndex is given the style display: block while the slide with index currentIndex is given the style display: none.

Said in other words, the new slide is shown while the current slide (that's already there visible on the screen) is hidden.

Finally, since at the end the new slide becomes the current slide, the currentIndex variable is updated to newIndex.

And that's it.

Time to bring all this discussion to the glyphs of code:

var currentIndex = 0;
var newIndex = null; var slideElements = document.getElementsByClassName('slider_slide'); var navElements = document.getElementsByClassName("slider_nav"); function navigateSlider() { slideElements[newIndex].style.display = "block"; slideElements[currentIndex].style.display = "none"; currentIndex = newIndex; } navElements[0].addEventListener('click', function() {
newIndex--; navigateSlider(); }); navElements[1].addEventListener('click', function() {
newIndex++; navigateSlider(); });

Within each click handler, newIndex is changed based on the purpose of the corresponding button i.e. for the ← Previous button, newIndex is decremented, whereas for Next →, newIndex is incremented.

Following from this, navigateSlider() is smart enough to take care of the rest of the job.

Note, however, that even as of yet, the errors in the slider haven't been rectified. We've only made the code much more flexible than before.

Live Example

Dealing with end slides

In the code above, we haven't dealt with cases where the end slides, i.e. the first and last slides, are reached. In these cases, we at least don't want to increment or decrement newIndex because doing so can throw errors.

How?

At the start, we have newIndex = 0. Therefore, pressing the ← Previous button decrements it to -1. Consequently, the statement slideElements[newIndex].style.display = "block" throws an exception as there's no such index as -1!

Similarly, at the last slide, we have newIndex = 2. Therefore, pressing the Next → button increments it to 3. Consequently, the statement slides[newIndex].style.display = "block" once again throws an exception as there's no such index as 3 (only 0, 1 and 2)!

How to solve this?

What we can simply do it to use conditional statements to check for the end positions of the slider and then either repeat the cycle or break the flow of slides.

Let's see what both these mean.

Repeat the cycle

Suppose that we are on the last slide of a slider, and at this point press the Next → button.

In this repeat-the-cycle behavior, what we do is that we take him to the first slide again, and thus repeat the navigation cycle. In this way, the slider keeps on navigating with the Next → button, without any breakpoint.

The same also applies to the ← Previous button when we are on the first slide — in this case, we would repeat the cycle by showing the last slide, and hiding the first one.

But how do we check for these end cases in code?

As a hint, the checks involve the newIndex variable.

Let's see the solution.

After the ← Previous button is pressed while we are on the first slide, newIndex becomes -1.

Hence, the left-end case can be checked by newIndex === -1.

On the same lines,

After the Next → button is pressed while we are on the last slide, newIndex becomes equal to the total number of slides.

Hence, the right-end case can be checked by.... Well, we need one thing for it — the total number of slides.

In the code below, we introduce another variable called slidesLength that holds the total number of slides in the slider. This value is obtained via slideElements.length:

var currentIndex = 0;
var newIndex = null;

var slideElements = document.getElementsByClassName('slider_slide');
var slidesLength = slideElements.length; var navElements = document.getElementsByClassName("slider_nav"); /* ... */

Now with slidesLength in place, let's go back to the check for the right-end case.

The check is simply newIndex === slidesLength.

So far, so good.

With both the checks figured out, it's time to go into the navigateSlider() function and deal with these special cases a little differently.

That is, if newIndex === -1, we reset it back to slidesLength - 1 to point to the last slide, and if newIndex === slidesLength, we reset it back to 0 to point to the first slide.

Here's the complete code:

var currentIndex = 0;
var newIndex = 0;

var slideElements = document.getElementsByClassName('slider_slide');
var slidesLength = slideElements.length;
var navElements = document.getElementsByClassName("slider_nav");

function navigateSlider() {
   if (newIndex === -1) {
      newIndex = slidesLength - 1;
   }
   else if (newIndex === slidesLength) {
      newIndex = 0;
   }

   slideElements[newIndex].style.display = "block";
   slideElements[currentIndex].style.display = "none";
   currentIndex = newIndex;
}

navElements[0].addEventListener('click', function() {
   newIndex--;
   navigateSlider();
});

navElements[1].addEventListener('click', function() {
   newIndex++;
   navigateSlider();
});

Voila! Now our slider works perfectly without any errors.

Repeat-the-cycle slider

Break the flow

The second way to deal with the end slides is to break the flow.

As the name implies, it will simply stop navigation to the left from the first slide, or to the right from the last slide, unlike the first choice where we can navigate infinitely.

Visually, we represent this idea by disabling the buttons, which is done by setting the disabled attribute on them and then styling the :disabled psuedo state with a very low opacity.

But before that, we need to determine when exactly do we need to disable either of the buttons.

If we are on the first slide, the ← Previous button ought to be disabled, or else it ought to be enabled.

Similarly,

If we are on the last slide, the Next → button ought to be disabled, or else it ought to be enabled.
Pretty basic, isn't this?

In the code below, we rewrite the navigateSlider() function to implement this break-the-flow behavior:

function navigateSlider() {
   if (newIndex === 0) {
      navElements[0].disabled = true;
   }
   else {
      navElements[0].disabled = false;
   }
   
   if (newIndex === slidesLength - 1) {
      navElements[1].disabled = true;
   }
   else {
      navElements[1].disabled = false;
   }

   slideElements[newIndex].style.display = "block";
   slideElements[currentIndex].style.display = "none";
   currentIndex = newIndex;
}

Now although the code works perfectly, it seems quite repetitive. As before, it's time to DRY it out.

Notice the conditionals that are disabling or enabling navElements[0] and navElements[1] above. Each set of conditionals merely repeats the statements setting the disabled attribute on the respective button node.

The snippet below highlights the repeating statements:

function navigateSlider() {
   if (newIndex === 0) {
navElements[0].disabled = true; } else {
navElements[0].disabled = false; } if (newIndex === slidesLength - 1) {
navElements[1].disabled = true; } else {
navElements[1].disabled = false; } slideElements[newIndex].style.display = "block"; slideElements[currentIndex].style.display = "none"; currentIndex = newIndex; }

Fortunately, a better approach is very intuitive to think of — in fact, it's already there in the code!

That is, instead of performing the check for each button and then disabling or enabling it, we just set the disabled attribute on it to the respective expression — newIndex === 0 for the ← Previous button whereas newIndex === slidesLength - 1 for the Next → button.

If either of these expressions evaluates to true, the corresponding button's disabled attribute is automatically set to true. The same goes for evaluation to false.

Here's the improved code:

function navigateSlider() {
navElements[0].disabled = (newIndex === 0);
navElements[1].disabled = (newIndex === slidesLength - 1); slideElements[newIndex].style.display = "block"; slideElements[currentIndex].style.display = "none"; currentIndex = newIndex; }

Isn't this much better than the previous snippet?

Let's see the slider in action.

Live Example

It works flawlessly with just one thing missing.

When the page loads, the first slide is shown to us. Likewise, the ← Previous button should ideally be disabled. But in the example above, it isn't.

Why?

Simply because the logic for disabling/enabling the button is within navigateSlider() which isn't called until and unless a button is clicked.

So what's the solution?

Just call navigateSlider() manually at the end of the script.

In code below, we perform this very thing:

var currentIndex = 0;
var newIndex = 0;

var slideElements = document.getElementsByClassName('slider_slide');
var slidesLength = slideElements.length;
var navElements = document.getElementsByClassName("slider_nav");

function navigateSlider() { /* ... */ }

navElements[0].addEventListener('click', function() { /* ... */ });

navElements[1].addEventListener('click', function() { /* ... */ });

navigateSlider();

Let's see whether it works now.

Live Example

Strangely, there is just no slide shown with the page load!

And that's because of the wrong order of two statements in the function navigateSlider().

Can you spot them?

Well, in navigateSlider(), we first show the new slide and then hide the current one. And this is the exact problem. When we call navigateSlider() manually in the code above, newIndex = 0 and currentIndex = 0. Hence, to begin with, the first slide (the new slide) is shown and then the first slide (the current slide) is hidden. The latter statement causes the first slide to be hidden as the page is done loading and likewise we see no slide at all.

The solution is once again very simple — just shift the order of statements inside navigateSlider().

This is done as follows:

function navigateSlider() {
   navElements[0].disabled = (newIndex === 0);
   navElements[1].disabled = (newIndex === slidesLength - 1);

slideElements[currentIndex].style.display = "none";
slideElements[newIndex].style.display = "block"; currentIndex = newIndex; }

Now let's check out the slider.

Break-the-flow slider

Flawless.

Moving on

Despite the fact that the slider we were able to create so far was a very simple one, it had a lot of details to deal with, quite near overwhelming. Didn't it?

So you can surely relate to the fact that whatever is coming up next will be much difficult and detailed than this simple slider's very elementary ideas!

In the coming chapters we will move on to add numerous features to our slider including the legacy swiping interaction which would be literally 10 times as much information on this page, and even 10 times more fun!

To keep things in order, let's now focus on adding a simple feature to our slider which is that of pagination.