šŸ›  Carousel: Creating dots with JavaScript

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

šŸ›  Carousel: Creating dots with JavaScript

When we created dots for the carousel, we wrote them manually in the HTML. Hereā€™s what we wrote:

<div class="carousel__dots">
  <button class="carousel__dot is-selected"></button>
  <button class="carousel__dot"></button>
  <button class="carousel__dot"></button>
</div>

Since the number of dots is the same as the number of slides, we can create dots automatically with JavaScript. This lets us keep the number of dots and slides consistent without manual effort.

Removing old code

First, weā€™ll remove dots from the HTML and JavaScript.

<!-- Remove these -->
<div class="carousel__dots">
  <button class="carousel__dot is-selected"></button>
  <button class="carousel__dot"></button>
  <button class="carousel__dot"></button>
</div>
// And remove these
const dotsContainer = carousel.querySelector('.carousel__dots')
const dots = [...carousel.querySelectorAll('.carousel__dot')]

You may need to comment out parts of the code that require dotsContainer and dots before we create the dots with JavaScript. (Iā€™ll leave you to comment out the necessary code on your own).

Creating the dots

First, weā€™ll make a function called createDots.

const createDots = _ => {
  // ...
}

This function should create the following HTML.

<div class="carousel__dots">
  <button class="carousel__dot is-selected"></button>
  <button class="carousel__dot"></button>
  <button class="carousel__dot"></button>
</div>

To make this HTML, we need to use createElement to create the wrapping <div>. We also use classList.add to add the carousel__dots class.

const createDots = _ => {
  const dotsContainer = document.createElement('div')
  dotsContainer.classList.add('carousel__dots')
}

To make the three <button> elements, we loop through slides. This lets us create one dot for each slide.

const createDots = _ =>  {
  // ...
  slides.forEach(slide => {
    const dot = document.createElement('button')
    dot.classList.add('carousel__dot')
  })
}

Then, we append each dot into dotsContainer.

const createDots = _ =>  {
  // ...
  slides.forEach(slide => {
    const dot = document.createElement('button')
    dot.classList.add('carousel__dot')
    dotsContainer.appendChild(dot)
  })
}

If you log dotsContainer, you should see three <button> elements.

HTML Structure created with JavaScript.

Adding the is-selected class

The first slide has the is-selected class, so the first dot should also have the is-selected class.

We can add the is-selected class in two ways:

  1. Use the index. If index is 0, we know itā€™s the first dot.
  2. Check if the slide contains is-selected. If slide contains the is-selected class, we know itā€™s the selected slide.

Both methods work, but the second one is more robust. It allows you to start the carousel on the second slide. (Iā€™ll leave you to figure this one out if youā€™re interested. šŸ˜‰ Hint: You need to write code to position .carousel__contents on the starting slide).

const createDots = _ =>  {
  // ...
  slides.forEach(slide => {
    const dot = document.createElement('button')
    dot.classList.add('carousel__dot')

    if (slide.classList.contains('is-selected')) {
      dot.classList.add('is-selected')
    }

    dotsContainer.appendChild(dot)
  })
}
Created HTML. First dot contains is-selected class.

Finally, we return dotsContainer from createDots. This lets us use dotsContainer anywhere we need to.

const createDots = _ =>  {
  // ...
  return dotsContainer
}

Using createDots

We need to use createDots to create the dots.

const dotsContainer = createDots()

But remember, createDots used the slides variable. We need to make sure slides is available before we call createDots. This means we need to use createDots like this:

// Declare the slides variable
const slides = [...carousel.querySelectorAll('.carousel__slide')]

// Declare the createDots function
const createDots = _ => { /* ... */}

// Declare the dotsContainer variable using createDots
const dotsContainer = createDots()

After creating dotsContainer, we need to create the dots variable for use in other parts of our code. We also need to append dotsContainer into the DOM.

const dotsContainer = createDots()
const dots = [...dotsContainer.children]

// Adds dots into the DOM
carousel.appendChild(dotsContainer)

// Listen to dotsContainer
dotsContainer.addEventListener('click', event => {
  // ...
})

Cleaning up

If you followed the steps so far, youā€™ll have a section of code that looks like this:

// Declaring variables
const carousel = document.querySelector('.carousel')
const previousButton = carousel.querySelector('.previous-button')
const nextButton = carousel.querySelector('.next-button')
const contents = carousel.querySelector('.carousel__contents')
const slides = [...carousel.querySelectorAll('.carousel__slide')]

// Declaring a function
const createDots = _ => { ... }

// Declaring more variables
const dotsContainer = createDots()
const dots = [...dotsContainer.children]

// Declaring more functions
// ...

This variable -> function -> variable -> function order is not pretty. We get confused and slightly overwhelmed because there are too many things to keep track of.

Ideally, we want to put all variables in a block and all functions in another block to keep things simple:

// Variables
// ...

// Functions
// ...

We cannot pull createDots down into other functions because dotsContainer needs to use createDots. What we can do is bring createDots above the other variables:

// Declaring a function
const createDots = _ => { ... }

// Declaring all variables
const carousel = document.querySelector('.carousel')
const previousButton = carousel.querySelector('.previous-button')
const nextButton = carousel.querySelector('.next-button')
const contents = carousel.querySelector('.carousel__contents')
const slides = [...carousel.querySelectorAll('.carousel__slide')]

// Declaring more variables
const dotsContainer = createDots()
const dots = [...dotsContainer.children]

// Declaring other functions

Thereā€™s a slight problem if we do this.

External variables and lexical scope

For most of our functions, we declared variables before using them. Hereā€™s an example:

const nextButton = carousel.querySelector('.next-button')

const switchSlide = (currentSlideIndex, targetSlideIndex) => {/* ... */}
const getCurrentSlideIndex = _ => {/* ... */}


nextButton.addEventListener('click', event => {
  const currentSlideIndex = getCurrentSlideIndex()
  const nextSlideIndex = currentSlideIndex + 1

  switchSlide(currentSlideIndex, nextSlideIndex)
  // ...
})
  1. We created nextButton before using addEventListener.
  2. We created getCurrentSlideIndex before using in the event listener.
  3. We created currentSlideIndex before we found nextSlideIndex.
  4. We created switchSlide, currentSlideIndex, and nextSlideIndex before using it.

Our code flows in a top-down format. Everything is declared before we use it.

But for createDots, this is not the case.

const createDots = _ => {
  const dotsContainer = document.createElement('div')
  dotsContainer.classList.add('carousel__dots')

  slides.forEach(slide => {
    const dot = document.createElement('button')
    dot.classList.add('carousel__dot')

    if (slide.classList.contains('is-selected')) {
      dot.classList.add('is-selected')
    }

    dotsContainer.appendChild(dot)
  })
}

const slides = [...carousel.querySelectorAll('.carousel__slide')]
const dotsContainer = createDots()
  1. We created createDots first.
  2. createDots require slides.
  3. But slides cannot be found within createDots. It cannot be found above createDots either.
  4. We created slides after createDots.

In this case, createDots still work because slides was declared before we used createDots. We say slides is in the lexical scope.

If we flipped the slides and dotsContainer code around, createDots wonā€™t work anymore.

// This wouldn't work.
const dotsContainer = createDots()
const slides = [...carousel.querySelectorAll('.carousel__slide')]

Code can be quite fragile (especially in functions) if you use external variables. To fix this ā€œflipping of orderā€ thing, we can pass slides into createDots.

const createDots = slides => {/* ... */}

const slides = [...carousel.querySelectorAll('.carousel__slide')]
const dotsContainer = createDots(slides)

This way, we know dotsContainer must come after slides. Thereā€™s no mistake to order we declare these two variables.

Weā€™ve been relying on external variables

Weā€™ve been relying onĀ external variables (and hence lexical scope) for every function we created for the carousel.

If you pay attention, you can see slides and dots variables used in functions. Hereā€™s an example. In switchSlide, we used contents even though contents is not declared within switchSlide.

const switchSlide = (currentSlideIndex, targetSlideIndex) => {
  const currentSlide = slides[currentSlideIndex]
  const targetSlide = slides[targetSlideIndex]
  const destination = getComputedStyle(targetSlide).left

  contents.style.transform = `translateX(-${destination})`
  currentSlide.classList.remove('is-selected')
  targetSlide.classList.add('is-selected')
}

Hereā€™s another example. In highlightDot, we used dots even though dots is not declared in highlightDot.

const highlightDot = (currentSlideIndex, targetSlideIndex) => {
  const currentDot = dots[currentSlideIndex]
  const targetDot = dots[targetSlideIndex]
  currentDot.classList.remove('is-selected')
  targetDot.classList.add('is-selected')
}

What weā€™ve done is ok! We donā€™t need to worry about these external variables because we know they are declared upfront. We wonā€™t mistake the order we use the variables.

Hoisting

Since we brought createDots to the top of our JavaScript file, you can say we hoisted createDots manually.

We can hoist createDots automatically if we use a normal function instead of an arrow function. If we do this, we can keep all variables in one place and all functions in one place.

// Declaring all variables
// ...
const slides = [...carousel.querySelectorAll('.carousel__slide')]
const dotsContainer = createDots()
const dots = [...dotsContainer.children]

// Declaring all functions
function createDots(slides) {/* ... */}
// ...

Whether you do this is up to you.

Thatā€™s it!

(Bonus: You can also create previousButton and nextButton with JavaScript too. Try it! šŸ˜ƒ).