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 theseconst 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.
function 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.
function 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.
function createDots() { // ... slides.forEach(slide => { const dot = document.createElement('button') dot.classList.add('carousel__dot') })}Then, we append each dot into dotsContainer.
function 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.
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:
- Use the index. If index is 0, we know it’s the first dot.
- Check if the slide contains
is-selected. Ifslidecontains theis-selectedclass, 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).
function 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) })}
Finally, we return dotsContainer from createDots. This lets us use dotsContainer anywhere we need to.
function 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 variableconst slides = [...carousel.querySelectorAll('.carousel__slide')]
// Declare the createDots functionfunction createDots() { /* ... */}
// Declare the dotsContainer variable using createDotsconst 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 DOMcarousel.appendChild(dotsContainer)
// Listen to dotsContainerdotsContainer.addEventListener('click', event => { // ...})Cleaning up
If you followed the steps so far, you’ll have a section of code that looks like this:
// Declaring variablesconst 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 functionfunction createDots () { ... }
// Declaring more variablesconst 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 functionfunction createDots () { ... }
// Declaring all variablesconst 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 variablesconst dotsContainer = createDots()const dots = [...dotsContainer.children]
// Declaring other functionsThere’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) => { /* ... */}function getCurrentSlideIndex() { /* ... */}
nextButton.addEventListener('click', event => { const currentSlideIndex = getCurrentSlideIndex() const nextSlideIndex = currentSlideIndex + 1
switchSlide(currentSlideIndex, nextSlideIndex) // ...})- We created
nextButtonbefore usingaddEventListener. - We created
getCurrentSlideIndexbefore using in the event listener. - We created
currentSlideIndexbefore we foundnextSlideIndex. - We created
switchSlide,currentSlideIndex, andnextSlideIndexbefore 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.
function 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()- We created
createDotsfirst. createDotsrequireslides.- But
slidescannot be found withincreateDots. It cannot be found abovecreateDotseither. - We created
slidesaftercreateDots.
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.
function 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.
function 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.
function 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 functionsfunction 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! 😃).
Welcome! Unfortunately, you don’t have access to this lesson. To get access, please purchase the course or enroll in Magical Dev School.
Unlock this lesson