DEV Community

Cover image for Responsive JavaScript Carousel for API Hourly Data
Urooba
Urooba

Posted on

Responsive JavaScript Carousel for API Hourly Data

I almost mistook an incomplete solution for a finished one and moved on to work on other parts of my weather app! While working on the carousel that was supposed to show 12 hours of weather, I wanted to add the feature which would help in fetching the next day’s hours in case the current day was finished. However, instead of transitioning to the next day, the carousel kept looping back to the beginning of the current day's hours, and I mistakenly thought the task was complete. Yikes!

Initial Challenges

I thought about two ‘for loops’ but I don’t think that ‘j’ printing all its elements for the length of the entire ‘i’ was correct. I found a lot of blogs online about the use of modulus operator for "circular array" But I did not know how that would help my case. I needed to loop through the current day's hours and then switch to the next day once the hours reset to zero. A lot was happening and I needed to make it more concise and place everything in one function. Tough!

Recognizing Incomplete Solutions and Mistakes

I found something really cool online though, it may solve a big problem for me. It helped me understand how modulus operator works for circular arrays. Here is the example on the website:

const daysOfWeek = [
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
  "Saturday",
  "Sunday",
];
// Function to cycle through the days of the week
function cycleDays(index) {
  return daysOfWeek[index % daysOfWeek.length];
}
// Let's cycle through the days:
for (let i = 0; i < 10; i++) {
  console.log(`Day ${i + 1}: ${cycleDays(i)}`);
}
Enter fullscreen mode Exit fullscreen mode

The result is like:
Day 1: Monday
Day 2: Tuesday
...

What I wanted was, instead of going back to the daysOfWeek array, and start from ‘Monday’, it should go to a completely different array. So, I took the code to the code editor and changed it a bit. First, I made a variable called ‘currentIndex’ and stored the modulus operation in it. Then I logged it to the console. It reset after 6 and became zero again.

Though, I was logging the wrong variable to the console. Because, if I wrote the if condition like this: if(currentIndex === 0), it would actually move toward a new array right at the beginning of the loop. So, now I logged the "index" instead, and finally I found the answer! In order to test the new code, I made a new array for ‘months’ and then tried to make the switch. But I made another mistake—let me show you:

const daysOfWeek = [
  "Monday",
  "Tuesday",
  "Wednesday",
  "Thursday",
  "Friday",
  "Saturday",
  "Sunday",
];
const months = [
  'Jan',
  'Feb',
  'March'
];
// Function to cycle through the days of the week
function cycleDays(index) {
  let currentIndex = index % daysOfWeek.length
  console.log(index)
 if(index === 7){
   return months[currentIndex]
 } else {
     return daysOfWeek[currentIndex];
 }
}
// Let's cycle through the days:
for (let i = 0; i < 10; i++) {
  console.log(`Day ${i + 1}: ${cycleDays(i)}`);
}
Enter fullscreen mode Exit fullscreen mode

After logging "Jan", it switched back to the original array. The mistake was strict equality check, I should have used ‘greater than or equal to’ instead. When I plugged that in, it successfully switched to the new array!

Now, I wanted the loop to start from the current hour and continue without stopping, with a marker in place to switch between the arrays. That marker will be the modulus operator instead of the length of the array. I could also use the length of the array, which in this case is 24, but I’m sticking to the hard-coded value of 24 for now.

currentIndex = (currentIndex + 1) % 9

This line allows me to switch from day one to day two during the loop without stopping it. Here's another trial (I updated the arrays to resemble API results):

const dayOne = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];
const dayTwo = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
let hours = dayOne;
let currentHour = 5;
function cycleDays(currentHour) {
  let currentIndex = currentHour
  for (let i = 0; i < 10; i++) {
    console.log(`index is ${currentIndex} and dayOne is ${hours[currentIndex]}`)
    if(currentIndex === 0){
      hours = dayTwo
    console.log(`index is ${currentIndex} and dayTwo is ${hours[currentIndex]}`)
    } 
  currentIndex = (currentIndex + 1) % 9

} 
}
cycleDays(currentHour)

Enter fullscreen mode Exit fullscreen mode

Notice something interesting in the results:

index is 5 and monday is six and i is 0
index is 6 and monday is seven and i is 1
index is 7 and monday is eight and i is 2
index is 8 and monday is nine and i is 3
index is 9 and monday is ten and i is 4
index is 0 and monday is one and i is 5
index is 0 and tuesday is 11
index is 1 and monday is 12 and i is 6
index is 2 and monday is 13 and i is 7
index is 3 and monday is 14 and i is 8
index is 4 and monday is ¬15 and i is 9

The issue here is that the loop runs once from the start, and when it reaches the condition (if(currentIndex === 0)), it switches the array. However, when currentIndex = 0 (i.e., 10 % 10 = 0), the hours[currentIndex] is accessed before the if condition is executed. That’s why you see values from dayOne (like "one") even after the switch.

To fix this, the if condition needs to be checked right after currentIndex becomes 0, so that the array switch happens before logging:

console.log(index is ${currentIndex} and monday is ${hours[currentIndex]} and i is ${i})...

By changing the position of the condition, it can be ensured that the switch occurs at the correct time without first accessing the wrong array.

const monday = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];
const tuesday = [11, 12, 13, 14, 15, 16, 17, 18, 19, 20];
let hours = monday;
let currentHour = 5;
function cycleDays(currentHour) {
  let currentIndex = currentHour
  for (let i = 0; i < 10; i++) {
    console.log(`index is ${currentIndex} and monday is ${hours[currentIndex]} and i is ${i}`)
    if(currentIndex === 0){
      hours = tuesday
    console.log(`index is ${currentIndex} and tuesday is ${hours[currentIndex]}`)
    } 
  currentIndex = (currentIndex + 1) % 10
} //for 
}
cycleDays(currentHour)

Enter fullscreen mode Exit fullscreen mode

My code is almost there. Here, the only mistake I am making is logging ‘Monday’ instead of ‘Tuesday’. The values are from the ‘Tuesday’ array though, but it keeps saying ‘Monday’ because of the wrong way of writing the console.log statement. I guess, It is quite hard to put two and two together and picture logging VS actually putting in values into html elements. Here is a bit of improvement using ternary operator (yes, I switched the elements of the array, again!):

const monday = ['one', 'two', 'three', 'four', 'five', 'six', 'seven', 'eight', 'nine', 'ten'];
const tuesday = ['apple', 'orange', 'banana', 'pineapple', 'grapes', 'cherries', 'strawberries', 'mangoes', 'blueberries', 'pears'];
let hours = monday;
let currentHour = 9;
function cycleDays(currentHour) {
  let currentIndex = currentHour
  for (let i = 0; i < 14; i++) {
  hours === monday ? console.log(`day: monday, elements: ${hours[currentIndex]}`) : console.log(`day: tuesday, elements: ${hours[currentIndex]}`)
  currentIndex = (currentIndex + 1) % 10
   if(currentIndex === 0 && hours === monday){
      hours = tuesday
    } 
} //for 
}
cycleDays(currentHour);
Enter fullscreen mode Exit fullscreen mode

Finally, I can construct my code for the 3-day data I am retrieving from the api, here is the refined version:

function createHours(days){
        if (!days || days.length === 0) {
            console.error("Days array is undefined or empty");
            return; // Prevent further execution if data is missing
        }
        const btnNext = document.querySelector('.next');
        const btnPrev = document.querySelector('.previous');
        const hourContainer = document.querySelector('.tfour_hours');
        const currentHour = new Date().getHours()
        function getHoursForDay(index) {
         return days[index].hour; 
        }
        let dayIndex = 0;
        let hours = getHoursForDay(dayIndex);
        let index = currentHour;
        let displayHours = [];
        for (let i = 0; i < 12; i++) {
            // console.log(hours)
            // console.log(`index: ${index}`)
            let hourData = hours[index];
            let hourNum = index < 10 ? `0${index}` : index;

            if (index === 0) {
                displayHours.push(`
                    <div class="hour-${i} child">
                        <p class="next-day">Next Day</p>
                        <p class="next-hour-num">${hourNum}</p>
                        <img class="icon" src="https:${hourData.condition.icon}" alt="icon">
                        <p class="temp">${hourData.temp_c}°C</p>
                    </div>
                `);
            } else {
                displayHours.push(`
                    <div class="hour-${i} child">
                        <p class="hour-num">${hourNum}</p>
                        <img class="icon" src="https:${hourData.condition.icon}" alt="icon">
                        <p class="temp">${hourData.temp_c}°C</p>
                    </div>
                `);
            }
                    index = (index + 1) % 24;
            if(index === 0 && dayIndex === 0){
                dayIndex = 1;
                hours = getHoursForDay(dayIndex)
            }
        } //for loop
            displayHours = displayHours.join('');
        hourContainer.innerHTML = displayHours;
…
};
Enter fullscreen mode Exit fullscreen mode

Creating Dynamic HTML Elements

Let’s talk about generating the 12 divs. I couldn’t picture how to get the buttons on either side of the parent div while the 12 hours just float in between them. If I were to generate the 12 divs in the same parent as the buttons, then the button elements would need a different justification setting than the 12 divs.

It only made sense to let them have their own container. It took me a while to figure this out—I actually had to sleep on it. Then next day, I typed .btn-container and hit tab and from there, everything clicked. I had seen every grouped item and their own containers inside parent containers in John Smilga's tutorials, I did not know why such grouping would be necessary until I started to design the 24-hour container. It was a real 'gotcha moment'.

Now came another problem that lingered for days. The slider that I designed in the tutorials was not as challenging as these divs. In the tutorials, there was a simple translate value, but right now I have quite a few issues. On smaller screens, the divs were sticking together and starting to look like spaghetti.

And, when I used a simple translateX property, meaning when I 'guessed’ the pixels, there was a lot of space left after the divs had completely translated to the left. It meant they were translating more than their combined width. I needed to find a proper value to ensure the divs stopped exactly at the end without leaving that extra space. After searching for a long time, I came across a blog that offered various solutions.

There were a lot of solutions. A few of them were using modulo operator, which reminded me of the circular array logic I had applied when switching days in the ‘for loop’. There were a lot comments here that used Math.min and Math.max. Which basically would make the container translate until the end of its length was reached. Excellent! So no more white space? Not so fast...

One thing that differed from these examples was that my container would initially display 3 or 4 divs. So, when the offset is 0, there is already a certain amount of length in the parent container.

They were showing the image by adding the number 1. So, their carousel would slide 1 image forward according to the index number of the images in the array. For example, if there are 10 images in a container, and we add one to the currentImage variable, the value calculated by Math.min will be '1'. Then, when we add another '1', the current image will be 2 and the value will be 2 by Math.min because 2 mod 10 is 2. This particular example would change the game of the slider that I am trying to make. This was the code that caught my eye:

const imageData = [ 'image1.png', 'img2.png', 'img3.png', ... ];
let currentImage = 0;
____
const handleImageChange = (imageShift) => {
  currentImage = Math.max(
    0,
    Math.min(
      imageData.length - 1,
      (currentImage + imageShift) % imageData.length
    )
  );
}
____
const firstImage = () => handleImageChange(-imageData.length);
const prevImage = () => handleImageChange(-1);
const nextImage = () => handleImageChange(1);
const lastImage = () => handleImageChange(imageData.length);
Enter fullscreen mode Exit fullscreen mode

The brilliance behind Richard Kichenama's solution, found in the comments, lies in the use of Math.max to ensure the value doesn’t drop below 0 and Math.min to calculate the translation value until it reaches the maximum length of the image array.

Now, how was I to solve the problem of the white space? I had to consider the margins of all of the child divs and add them together to get the entire length of the children divs. Then, the slider should stop moving once the last child is reached. This means the total width is the sum of all the children's widths plus their margins.

However, I ran into another issue: some of the divs were already displayed in the container, which left me stuck again. Luckily, a friend of mine came to the rescue. After discussing the problem with them, here's what I understood:

I could not consider the entire length of the children divs. There was almost as much of white space left as the container length. The solution was to subtract the parent container's length from the total length of the children (including margins). This adjustment helped resolve the white space issue—phew!

Some of the code examples had a variable that was kind of like a ‘counter’. It acted as a ‘counter’ for translate property. When this variable increased, the translate property increased and so on. I separated the Math.min and Math.max properties for the next and previous buttons. It was more helpful and easier that way.

In the examples I referenced, the code was using the length of the array to determine the slide distance, but, as per my previous discussion with my friend, I needed to consider the white space so I have to subtract the length of the container. This way, I ensured that my divs could only move by a specific amount, avoiding the extra space at the end.

Also, thanks to john smilga's tutorials, I learned how to get the width, height, top properties of items. It was a struggle to apply the right one, it was also a struggle to find out that some of the values are strings and needed to be turned into numbers. I found that easily on google and got introduced to ‘parseFloat’.

I also came across another helpful resource that taught me how to display only three divs for large screens and two divs for small screens. The trick was to divide 100% of the container’s width by 3 (or 2 for small screens) and subtract the margins. This allowed for equally sized divs that fit perfectly within the container (so clever!). Finally, to check out the final function, please visit my GitHub. Here is the link to my repository.

The window event listener for resizing was crucial in fixing the alignment issues in my container. It addressed the "Flash of Unstyled Content" (FOUC) problem by resetting the offset on resize. I have to thank my friend for helping me understand how to calculate maxOffset—that was a real game changer.

Lastly, a shout-out to all experienced developers: every word you share helps someone new to the field. So, keep posting the information from your end, as we are waiting on the other side eager to learn. Thank you!

Top comments (0)