setInterval for multiple elments (Vanilla Javascript)

Viewed 44

I have created a time tracker with vanilla Javascript; however, the start button functionality is not working correctly when you have more than one activity. If you have more than one activity div, the timer starts at the same time as the last div and the first divs play button starts the last div. Can you please look at my newPlay eventListener to determine how my approach was incorrect? (Am I missing a bind, this, event, or e?

JAVASCRIPT:

let date = new Date().toLocaleDateString('en-us', { weekday:"long", year:"numeric", month:"short", day:"numeric"});
let showDate = document.getElementById("insert-date");
showDate.innerText=date;



//Get number of activity buttons for loop to add event listener to each
const btnLength = document.querySelectorAll(".opt-btn").length;

//Will append new div's here
const mainDiv = document.getElementById("activities");

//Add event listeners to each activity button & captures the buttonValue(activity name)
 for (let i = 0; i < btnLength; i++){
document.querySelectorAll(".opt-btn")[i].addEventListener("click", function() {
    const buttonValue = this.value;
    addActivity(buttonValue);
});
   

    //Onclick event creates the div, i, p , play, stop, timer text and appends to the main div
function addActivity(buttonValue){
 
 
        const newDiv = document.createElement('div');
     newDiv.classList.add("activity");

     const newP = document.createElement('p');
     newP.innerText = buttonValue;
     newP.classList.add("act-p");
     newDiv.appendChild(newP);

     const newPlay =document.createElement('i');
     newPlay.classList.add("fa-solid");
     newPlay.classList.add("fa-play");
     let activityInterval;
     newPlay.addEventListener('click', () => {
     activityInterval = setInterval(() => {
            elapsedSeconds++
            updateTimerValue()
          }, 1000)
        })
       newDiv.appendChild(newPlay);

    const newStop =document.createElement('i');
    newStop.classList.add("fa-solid");
    newStop.classList.add("fa-circle-stop");
    newStop.addEventListener("click", clearInterval(activityInterval));
    newDiv.appendChild(newStop);

    newElapsedTime = document.createElement('p');
    newElapsedTime.classList.add("elapsedTime-Text");
    newElapsedTime.innerText = "00:00:00";
    newDiv.appendChild(newElapsedTime);

          mainDiv.appendChild(newDiv);

    let elapsedSeconds = 0;
    updateTimerValue()


    function updateTimerValue() {
        // Count the seconds, minutes and hours, convert to strings and pad the strings with leading 0's
        const seconds = String(elapsedSeconds % 60).padStart(2, '0')
        const minutes = String(parseInt(elapsedSeconds / 60 % 60)).padStart(2, '0')
        const hours = String(parseInt(elapsedSeconds / 60 / 60)).padStart(2, '0')
    
        // Use string interpolation for formatting the timer string
        newElapsedTime.innerText = `${hours}:${minutes}:${seconds}`
      }

    }

 }
  

HTML:

<section class="option-sec">
<div class = "container">
    <div class = "row">
        <div class ="col col-md-2">
<button class = "opt-btn" value="BATHROOM"><img src ="/images/bathroom.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="COMMUTE"><img src ="/images/commute.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="WORK"><img src ="/images/desk.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="ENT"><img src ="/images/ent.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="FAMILY"><img src ="/images/family.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="FITNESS"><img src ="/images/gym.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="MEAL"><img src ="/images/meal.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="MEDITATE"><img src ="/images/meditate.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="RELIGION"><img src ="/images/pray.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="SHOPPING"><img src ="/images/shopping.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="SLEEP"><img src ="/images/sleep.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="STUDY"><img src ="/images/study.png" class="option"></button>
</div>
        </div>
    </div>
</section>
<!--End Of Option Buttons-->
<section class="date-area">
    <h1 class="date-hdr">CURRENT DATE: </h1>
    <h2  class="date-hdr" id="insert-date"></h2>
</section>   

<div class ="activity-container">
    <div id="activities" class="activity-list"></div>
</div>
2 Answers

TLDR;

Declare variables before use.


Writing the script in strict mode would have generate a compile time error that newElapsedTime has not been declared before use.

In sloppy mode however, assignment to newElapsedTime without declaration causes any activity's "play" click handler to create or overwrite the value of a shared global object property window.newElapsedTime with no error generated.

Declaring the variable in the execution context of the handler which added the activity to mainDiv seems to solve activity clicks interfering with each other.

This example of a solution does not implement stop (or pause and resume) and logs an error if the play <i> element is clicked multiple times for the same activity:

"use strict";
let date = new Date().toLocaleDateString('en-us', { weekday:"long", year:"numeric", month:"short", day:"numeric"});
let showDate = document.getElementById("insert-date");
showDate.innerText=date;

// debug: show value in buttons instead of images

Array.from(document.querySelectorAll(".opt-btn")).forEach(
    button=>button.innerHTML = button.value
);

//Get number of activity buttons for loop to add event listener to each
const btnLength = document.querySelectorAll(".opt-btn").length;

//Will append new div's here
const mainDiv = document.getElementById("activities");

//Add event listeners to each activity button & captures the buttonValue(activity name)
 for (let i = 0; i < btnLength; i++){
document.querySelectorAll(".opt-btn")[i].addEventListener("click", function() {
    const buttonValue = this.value;
    addActivity(buttonValue);
});
   
//Onclick event creates the div, i, p , play, stop, timer text and appends to the main div

function addActivity(buttonValue){
     const newDiv = document.createElement('div');
     newDiv.classList.add("activity");

     const newP = document.createElement('p');
     newP.innerText = buttonValue;
     newP.classList.add("act-p");
     newDiv.appendChild(newP);

     const newPlay =document.createElement('i');

     newPlay.classList.add("fa-solid");
     newPlay.classList.add("fa-play");
     let activityInterval;
     newPlay.addEventListener('click', () => {

     // what to do if newPlay clicked twice?
     if( activityInterval) {
        throw Error("no code support for clicking play multiple times for same activity");
     }
 
     activityInterval = setInterval(() => {
            elapsedSeconds++
            updateTimerValue()
          }, 1000)
        })
       newDiv.appendChild(newPlay);

    const newStop =document.createElement('i');
    newStop.classList.add("fa-solid");
    newStop.classList.add("fa-circle-stop");
    newStop.addEventListener("click", clearInterval(activityInterval));
    newDiv.appendChild(newStop);

    // declare newElapsedTime variable befor use:
    const newElapsedTime = document.createElement('p');

    newElapsedTime.classList.add("elapsedTime-Text");
    newElapsedTime.innerText = "00:00:00";
    newDiv.appendChild(newElapsedTime);

    mainDiv.appendChild(newDiv);

    let elapsedSeconds = 0;
    updateTimerValue()


    function updateTimerValue() {
        // Count the seconds, minutes and hours, convert to strings and pad the strings with leading 0's
        const seconds = String(elapsedSeconds % 60).padStart(2, '0')
        const minutes = String(parseInt(elapsedSeconds / 60 % 60)).padStart(2, '0')
        const hours = String(parseInt(elapsedSeconds / 60 / 60)).padStart(2, '0')
    
        // Use string interpolation for formatting the timer string
        newElapsedTime.innerText = `${hours}:${minutes}:${seconds}`
      }

    }

 }
button {
  text-align: center;
  min-width: 8rem;
}
.fa-play:after {
   content: "[play]"
}
<section class="option-sec">
<div class = "container">
    <div class = "row">
        <div class ="col col-md-2">
<button class = "opt-btn" value="BATHROOM"><img src ="/images/bathroom.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="COMMUTE"><img src ="/images/commute.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="WORK"><img src ="/images/desk.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="ENT"><img src ="/images/ent.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="FAMILY"><img src ="/images/family.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="FITNESS"><img src ="/images/gym.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="MEAL"><img src ="/images/meal.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="MEDITATE"><img src ="/images/meditate.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="RELIGION"><img src ="/images/pray.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="SHOPPING"><img src ="/images/shopping.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="SLEEP"><img src ="/images/sleep.png" class="option"></button>
</div>
<div class ="col col-md-2">
    <button class = "opt-btn" value="STUDY"><img src ="/images/study.png" class="option"></button>
</div>
        </div>
    </div>
</section>
<!--End Of Option Buttons-->
<section class="date-area">
    <h1 class="date-hdr">CURRENT DATE: </h1>
    <h2  class="date-hdr" id="insert-date"></h2>
</section>   

<div class ="activity-container">
    <div id="activities" class="activity-list"></div>
</div>

I thoroughly recommend writing code in strict mode and resolving any errors generated by the compiler because of it.

The issue likely is with this line:

activityInterval = setInterval(() => {
        elapsedSeconds++
        updateTimerValue()
      }, 1000)
    })

Each time you call addActivity(), you end up redefining activityInterval to the interval specific to the most recent activity created. Therefore, in stopTimer(), you only stop the most recent timer.

Instead, activityInterval needs to be a local variable. Delcare it outside the event handler first, and write a arrow function callback in stopTimer():

local declaration:

let activityInterval;
newPlay.addEventListener("click", () => {
  activityInterval = setInterval(() => {
    elapsedSeconds++;
    updateTimerValue();
  }, 1000);

callback:

newStop.addEventListener("click", () => clearInterval(activityInterval));

Here's a codesandbox too: https://codesandbox.io/s/zealous-jepsen-7gc4fn?file=/src/index.js:1100-1286 (I didn't have the images, and "date" is just a placeholder)

Related