Reputation: 23
I am trying to implement a timer where the user can choose a number of hours and a number of minutes, which will then count down in real-time when the user pushes a button. I am using the setInterval() function to calculate the remaining time and update the page. SetInterval() runs once every second as it should, but the timer values are only updated once (when the button is pushed).
This is odd because I have tested other functions which write to the same areas of the page with the same scope, and those will updated repeatedly (such as continually getting the current date and outputting it; I can see the seconds and minutes columns tick up). This leads me to believe the problem lies in how the code views the hours/minutes/seconds variables when it exits the countDown() function, but I cannot seem to isolate any cause.
Here is my code. For reference, I am just trying to do this within a single HTMl file with no network connection necessary.
HTML:
<body>
<!--section for controlling & displaying round timer-->
<div name="timeLeft" id="D3">
Round Timer<br>
Hours:
<select name="hours" id="S1">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
</select>
Minutes:
<select name="minutes" id="S2">
<option value="0">0</option>
<option value="5">5</option>
<option value="10">10</option>
<option value="15">15</option>
<option value="20">20</option>
<option value="25">25</option>
<option value="30">30</option>
<option value="35">35</option>
<option value="40">40</option>
<option value="45">45</option>
<option value="50">50</option>
<option value="55">55</option>
</select>
<br>
<input type="button" name="countDownStart" id="B2" onclick="countDown()" value="Start Countdown">
<br>
Your round will end in <span name="timerDisplay" id="span1">00:00:00</span>
</div>
</body>
Javascript:
<script type="application/javascript">
var timerInterval = null;
var hours;
var minutes;
var seconds;
function countDown() {
timerInterval = setInterval(function() {
//get user's inputs for hours and minutes
hours = parseInt(document.getElementById("S1").value);
minutes = parseInt(document.getElementById("S2").value);
seconds = 0; //the user cannot select the # of seconds
//get current date
var currentDate = new Date();
//get the countDownDate, which is the current date plus the user's values
var countDownDate = new Date();
countDownDate.setHours(currentDate.getHours() + hours);
countDownDate.setMinutes(currentDate.getMinutes() + minutes);
//find the difference between the two dates in hours, minutes, seconds
var dateDifference = new Date();
dateDifference = countDownDate - currentDate;
//convert the values in dateDifference to milliseconds
//math.floor() here returns the largest integer less than or equal to the appropriate
//measurement of time, in milliseconds
hours = Math.floor((dateDifference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
minutes = Math.floor((dateDifference % (1000 * 60 * 60)) / (1000 * 60));
seconds = Math.floor((dateDifference % (1000 * 60)) / 1000);
//update the countdown timer with these new values every 1000 milliseconds
document.getElementById("span1").innerHTML = hours + ":" + minutes + ":" + seconds;
}, 1000);
</script>
Upvotes: 1
Views: 1195
Reputation: 56965
Your interval is running fine, but the problem is that the one-time initialization code is combined with the update code. You're resetting the timer on every tick of the setInterval
callback and the timer always appears expired. A fix is to separate the one-time initialization code from the recurring update code, giving the code a fixed future time to approach.
I also recommend making the timer start conditional on whether an interval already exists, otherwise, if the user hammers on the [Start Countdown] button, a bunch of intervals will be created and will run concurrently.
I moved your variables out of the global scope, but you might also consider using a closure to maintain the interval state, an event listener and giving more descriptive element ids than "S1"
, "S2"
, "B2"
etc as well.
var timerInterval = null;
function countDown() {
if (!timerInterval) {
var hours = parseInt(document.getElementById("S1").value);
var minutes = parseInt(document.getElementById("S2").value);
var countDownDate = new Date();
countDownDate.setHours(countDownDate.getHours() + hours);
countDownDate.setMinutes(countDownDate.getMinutes() + minutes);
timerInterval = setInterval(function () {
var dateDifference = countDownDate - new Date();
var hours = Math.floor((dateDifference % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
var minutes = Math.floor((dateDifference % (1000 * 60 * 60)) / (1000 * 60));
var seconds = Math.floor((dateDifference % (1000 * 60)) / 1000);
document.getElementById("span1").innerHTML = `${hours}:${minutes}:${seconds}`;
}, 1000);
}
}
<body>
<!--section for controlling & displaying round timer-->
<div name="timeLeft" id="D3">
Round Timer<br> Hours:
<select name="hours" id="S1">
<option value="0">0</option>
<option value="1">1</option>
<option value="2">2</option>
</select>
Minutes:
<select name="minutes" id="S2">
<option value="0">0</option>
<option value="5">5</option>
<option value="10">10</option>
<option value="15">15</option>
<option value="20">20</option>
<option value="25">25</option>
<option value="30">30</option>
<option value="35">35</option>
<option value="40">40</option>
<option value="45">45</option>
<option value="50">50</option>
<option value="55">55</option>
</select>
<br>
<input type="button" name="countDownStart" id="B2" onclick="countDown()" value="Start Countdown">
<br> Your round will end in <span name="timerDisplay" id="span1">00:00:00</span>
</div>
</body>
Since setInterval
only guarantees the callback won't fire sooner than its second parameter, be wary of skipped tick renders. This answer shows how to use requestAnimationFrame
to run the loop at a higher resolution.
Upvotes: 1
Reputation: 985
Here is a simple fix. I don't think you need all the date logic. Everything else can remain the same. This stores the current time based on seconds in the total time. Each loop it extrapolates that to an hour/min/sec total, and then increments how many seconds have passed since you started. It will dramatically simplify the process, and probably be a good bit more readable. Try this out:
var startedTimeStamp;
var timerInterval = null;
var secondsAccrued = 0;
var initialTimer = 0;
function countDown() {
//Let's set this once and be done with it. It is the total number of seconds that the user has selected for their count down.
initialTimer = (parseInt(document.getElementById("S1").value) * 3600) + (parseInt(document.getElementById("S2").value) * 60);
startedTimeStamp = new Date();
timerInterval = setInterval(function() {
//How many seconds are left in the countdown?
secondsAccrued = (new Date() - startedTimeStamp) / 1000;
var currentTime = initialTimer - secondsAccrued;
//How many hours/minutes/seconds? Convert these to seconds, and make sure to subtract the hours from the total, and then minutes from the total, before determining the seconds the user should see.
var hours = Math.floor(currentTime / 3600);
var minutes = Math.floor((currentTime - (hours * 3600)) / 60);
var seconds = Math.round(Math.abs(currentTime - (currentTime - (hours * 3600)) - (currentTime -(minutes * 60))));
document.getElementById("span1").innerHTML = hours + ":" + minutes + ":" + seconds;
}, 1000);
}
Edit: Edited to use DateTimes again for accuracy per comment. Tested and working.
Upvotes: 0