stopwatch using javascript - accurate and easy
Tags: javascript, stopwatch
Published: 2021-04-02 15:16:47

###Overview
On a recent project, I needed to create a stopwatch. My initial thought was just to run javascript `setInterval` to keep track of time. While `setInterval` will be useful in our case, I made the mistake of depending on the `setInterval` alone to estimate time. Here is an example of what my code looked like in the beginning.

``` javascript
let totalSeconds = 0
setInterval(() => {
   totalSeconds += 1
}, 1000)  
```

The problem was that the code above is not a good way to keep track of time. Because, even though our `setInterval` will call our callback function every second, it won't always execute the function at exactly one-second intervals. This is because our callback function will only be added to our call stack, but if the call stack has other work to execute, it will cause a delay in our time. Instead, let's build a more accurate stopwatch using javascript `Date()` constructor.

##HTML and CSS
First, lets create a canvas we can work on. Here are the HTML and CSS used for this exercise.

####HTML
```html
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <title></title>
    <meta name="description" content="">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="stylesheet" href="/styles.css">
  </head>
  <body>

    <div class="stopwatch-wrapper">
      <p class="stopwatch">00:00:00:00</p>
      <div class="control-buttons-wrapper">
        <button id="main-button" class="control-buttons">Start</button>
        <button id="clear-button" class="control-buttons">Clear</button>
      </div>
    </div>
    <script src="/stopwatch.js" ></script>
  </body>
</html>
```

####CSS
```css
* {
  font-family: sans-serif;
}

.stopwatch-wrapper {
  display: flex;
  justify-content: center;
  flex-direction: column;
  margin: 100px auto 0;
  width: fit-content;
  padding: 10px;
  box-shadow: 0 0px 2.2px rgba(0, 0, 0, 0.031), 0 0px 5.3px rgba(0, 0, 0, 0.044),
    0 0px 10px rgba(0, 0, 0, 0.055), 0 0px 17.9px rgba(0, 0, 0, 0.066),
    0 0px 33.4px rgba(0, 0, 0, 0.079), 0 0px 80px rgba(0, 0, 0, 0.11);
  border-radius: 5px;
}

.stopwatch {
  margin: 0 auto;
  text-align: center;
  font-size: 60px;
}

.control-buttons-wrapper {
  display: flex;
  justify-content: center;
  flex-wrap: wrap;
}
.control-buttons-wrapper button {
  outline: none;
  cursor: pointer;
  color: #fff;
  border: none;
  border-radius: 5px;
  font-size: 25px;
  margin: 0 10px;
  padding: 3px 8px;
}
.control-buttons-wrapper button:active {
  opacity: 0.7;
}
#clear-button {
  background: rgb(187, 187, 187);
}
#main-button {
  background: rgb(0, 146, 231);
}

```

##Writing our program
We will break down our program into four steps
1. Creating our variables
2. Add event listener to our buttons
4. Creating our stopwatch function
5. Creating a function to display our time to the DOM

So let's get started!


###1.Creating our variables
create our constants variables to store the elements we'll be using. And an additional object variable called `stopwatch` to store all data required for our program.
```javascript

const time = document.querySelector('.stopwatch')
const mainButton = document.querySelector('#main-button')
const clearButton = document.querySelector('#clear-button')
const stopwatch = { elapsedTime: 0 }
```

###2. Adding Event Listiners to buttons
for our main button we will create a condition based on the text of the button. If the user clicks our `mainButton` with `Start` text; we will call our `startSTopwatch()` function and update the button text. Else we will keep track of our elasped time in `stopwatch.elapsedTime` and stop our stopwatch interval.
```javascript
mainButton.addEventListener('click', () => {
  if (mainButton.innerHTML === 'Start') {
    startStopwatch();
    mainButton.innerHTML = 'Stop'
  } else {
    stopwatch.elapsedTime += Date.now() - stopwatch.startTime
    clearInterval(stopwatch.intervalId)
    mainButton.innerHTML = 'Start'
  }
})
```

Our second event listener will trigger when our `clear` button is clicked. 
```javascript
clearButton.addEventListener('click', () => {
  stopwatch.elapsedTime = 0
  stopwatch.startTime = Date.now()
  displayTime(0, 0, 0, 0)
})
```

###3. Creating our stopwatch function
Here is our stopwatch function. I've added inline comments for a better explanation. 

Instead of relying solely on the `setInterval()`, we are comparing the start time and calculation the difference based on the current time. I have set the interval to 100 milliseconds, but you may change this if you wish. If accuracy is not an issue, you can increase the interval to 1,000; otherwise, the shorter the intervals, the more accurate your time records will be. I

```javascript
function startStopwatch() {
  //reset start time
  stopwatch.startTime = Date.now();
  // run `setInterval()` and save the ID
  stopwatch.intervalId = setInterval(() => {
    //calculate elapsed time
    const elapsedTime = Date.now() - stopwatch.startTime + stopwatch.elapsedTime
    //calculate different time measurements based on elapsed time
    const milliseconds = parseInt((elapsedTime%1000)/10)
    const seconds = parseInt((elapsedTime/1000)%60)
    const minutes = parseInt((elapsedTime/(1000*60))%60)
    const hour = parseInt((elapsedTime/(1000*60*60))%24);
    displayTime(hour, minutes, seconds, milliseconds)
  }, 100);
}
```


###4. Creating a function to display our time to the DOM

Lastly, we need to display our time to the user. First, I am adding a leading zero if any time measurement is less than 10(optional). Then I am updating the text within our time HTML element.

```javascript
function displayTime(hour, minutes, seconds, milliseconds) {
  const leadZeroTime = [hour, minutes, seconds, milliseconds].map(time => time < 10 ? `0${time}` : time)
  time.innerHTML = leadZeroTime.join(':')
}
```

### Final Code

```javascript
const time = document.querySelector('.stopwatch')
const mainButton = document.querySelector('#main-button')
const clearButton = document.querySelector('#clear-button')
const stopwatch = { elapsedTime: 0 }

mainButton.addEventListener('click', () => {
  if (mainButton.innerHTML === 'Start') {
    startStopwatch();
    mainButton.innerHTML = 'Stop'
  } else {
    stopwatch.elapsedTime += Date.now() - stopwatch.startTime
    clearInterval(stopwatch.intervalId)
    mainButton.innerHTML = 'Start'
  }
})

clearButton.addEventListener('click', () => {
  stopwatch.elapsedTime = 0
  stopwatch.startTime = Date.now()
  displayTime(0, 0, 0, 0)
})

function startStopwatch() {
  //reset start time
  stopwatch.startTime = Date.now();
  //run `setInterval()` and save id
  stopwatch.intervalId = setInterval(() => {
    //calculate elapsed time
    const elapsedTime = Date.now() - stopwatch.startTime + stopwatch.elapsedTime
    //calculate different time measurements based on elapsed time
    const milliseconds = parseInt((elapsedTime%1000)/10)
    const seconds = parseInt((elapsedTime/1000)%60)
    const minutes = parseInt((elapsedTime/(1000*60))%60)
    const hour = parseInt((elapsedTime/(1000*60*60))%24);
    //display time
    displayTime(hour, minutes, seconds, milliseconds)
  }, 100);
}

function displayTime(hour, minutes, seconds, milliseconds) {
  const leadZeroTime = [hour, minutes, seconds, milliseconds].map(time => time < 10 ? `0${time}` : time)
  time.innerHTML = leadZeroTime.join(':')
}
```

Though this is a pretty simple exercise, it's a great exercise for programmers new to javascript. We follow the Unobtrusive JavaScript principle by using event handlers. And most importantly, we went over some gotchas of working with javascript call stack and some workarounds.

Here is the repo for this exercise: https://github.com/chrislemus/stopwatch-using-javascript