Learn JavaScript Object-Oriented Programming by Building a Timer Application

Object-oriented programming (OOP) is a powerful paradigm that allows developers to organize code into reusable, self-contained units called objects. Objects encapsulate related data and functions into a single entity, making code more modular and easier to reason about.

JavaScript is a multi-paradigm language that supports OOP via prototypes and, more recently, classes. Learning to "think in objects" is an important skill for any JavaScript developer.

In this post, we‘ll explore the key concepts of object-oriented programming in JavaScript as we build a timer application from scratch. You‘ll learn how to:

  • Structure an application using objects
  • Define classes to serve as reusable object templates
  • Leverage OOP principles like encapsulation and inheritance
  • Emit and subscribe to events between objects

Whether you‘re new to object-oriented programming or looking to deepen your understanding, building a JavaScript timer is an excellent way to learn OOP concepts in a practical context. Let‘s get started!

Creating the Timer Interface with HTML and CSS

Before diving into the JavaScript, let‘s set up a basic interface for our timer. We‘ll need:

  • An input box for the user to specify the countdown time
  • Start, Pause, and Stop buttons to control the timer
  • A display to show the current time

Here‘s the HTML:

<div class="timer">
  <input type="number" id="duration" min="0" value="30">
  <button id="start">Start</button>  
  <button id="pause">Pause</button>
  <button id="stop">Stop</button>
  <div id="display">00:00:00</div>
</div>

And some simple CSS to style it:

.timer {
  text-align: center;
  font-family: sans-serif;
}

input {
  font-size: 2em; 
  width: 6em;
}

button {
  font-size: 1.2em;
  margin: 1em auto;
}

#display {
  font-size: 3em;
  font-weight: bold;  
}

With that, we have a functional (if not yet very attractive) interface for our timer. Now let‘s make it actually do something!

Building the Timer with Procedural JavaScript

To start, we‘ll implement the timer functionality using regular procedural JavaScript functions. First we‘ll get references to the key elements on the page:

const durationInput = document.getElementById("duration");
const startButton = document.getElementById("start");
const pauseButton = document.getElementById("pause");
const stopButton = document.getElementById("stop");
const display = document.getElementById("display");

Next we‘ll define some variables to track the state of our timer:

let startTime; 
let elapsedTime = 0;
let timerInterval;

startTime will record the timestamp when the timer is started, so we can calculate the elapsed time. elapsedTime tracks the total time in seconds. And timerInterval will store a reference to the interval that updates the timer every second.

Now let‘s implement the start function, which fires when the user clicks the Start button:

function start() {
  startTime = Date.now() - elapsedTime;
  timerInterval = setInterval(function printTime() {
    elapsedTime = Math.floor((Date.now() - startTime) / 1000);
    display.textContent = formatTime(elapsedTime);
  }, 1000);
  showButton("PAUSE");
}

This function does a few things:

  1. Records the start time, offset by any elapsed time if the timer was previously paused
  2. Sets up an interval to update the elapsed time and display every second
  3. Shows the Pause button (hiding Start)

The formatTime helper function mentioned takes a number of seconds and returns a string in HH:MM:SS format:

function formatTime(seconds) {
  return [
    parseInt(seconds / 60 / 60),
    parseInt(seconds / 60 % 60),
    parseInt(seconds % 60)
  ].join(":").replace(/\b(\d)\b/g, "0$1");
}

We also have a showButton helper that shows/hides the relevant buttons based on the state of the timer:

function showButton(buttonName) {
  [startButton, pauseButton, stopButton].forEach(button => {
    button.style.display = ‘none‘;
  });
  if (buttonName === "START") startButton.style.display = "inline-block";
  else if (buttonName === "PAUSE") pauseButton.style.display = "inline-block";
  else if (buttonName === "STOP") stopButton.style.display = "inline-block";
}

The Pause and Stop button event handlers are similar:

function pause() {
  clearInterval(timerInterval);
  showButton("START");
}

function stop() {  
  clearInterval(timerInterval);
  elapsedTime = 0;
  display.textContent = formatTime(elapsedTime);
  showButton("START");
}

Pause clears the update interval and shows the Start button. Stop additionally resets the elapsed time to zero.

With these functions in place, our timer is fully functional! We can specify a duration, start, pause and stop the countdown.

However, all the functionality is jumbled together in the global scope. As our application grows in complexity, this will quickly become unwieldy. Let‘s see how we can refactor this code to be more modular and maintainable using object-oriented programming.

Refactoring the Timer with Object-Oriented JavaScript

The key idea behind OOP is to group related data and functions into reusable objects. So let‘s start by defining a Timer class that encapsulates the state and behavior of our timer:

class Timer {
  constructor(durationInput, startButton, pauseButton, stopButton, display) {
    this.durationInput = durationInput;
    this.startButton = startButton;
    this.pauseButton = pauseButton;
    this.stopButton = stopButton;
    this.display = display;

    this.startButton.addEventListener(‘click‘, this.start);    
    this.pauseButton.addEventListener(‘click‘, this.pause);
    this.stopButton.addEventListener(‘click‘, this.stop);    
  }

  start = () => {
    // timer starts  
  }

  pause = () => {
    // timer pauses
  }

  stop = () => {
    // timer stops
  } 
}

The constructor function is called when a new Timer instance is created. It takes the key DOM elements as parameters and saves them as properties on the instance (which is what this refers to inside the constructor).

We also attach event listeners to the buttons, so the corresponding methods on the Timer instance are called when the buttons are clicked.

Speaking of those methods, they look very similar to our previous functions – we‘re just moving the logic inside the Timer class:

start = () => {
  this.startTime = Date.now() - this.elapsedTime;
  this.timerInterval = setInterval(() => this.printTime(), 1000);
  this.showButton("PAUSE");
}

pause = () => {
  clearInterval(this.timerInterval);
  this.showButton("START");
}

stop = () => {
  clearInterval(this.timerInterval);
  this.elapsedTime = 0;
  this.display.textContent = this.formatTime(this.elapsedTime);
  this.showButton("START");
}

Notice how instead of referring to global variables like startTime and elapsedTime, the methods refer to this.startTime and this.elapsedTime. This is because those variables are now properties on the Timer instance.

We prefix the methods with this. when passing them to addEventListener inside the constructor. This ensures that this continues to refer to the Timer instance when the methods are called in response to button clicks.

The helper methods formatTime and showButton also move inside the Timer class:

formatTime = (seconds) => {
  return [
    parseInt(seconds / 60 / 60),
    parseInt(seconds / 60 % 60),
    parseInt(seconds % 60)
  ].join(":").replace(/\b(\d)\b/g, "0$1");  
}

showButton = (buttonName) => {
  [this.startButton, this.pauseButton, this.stopButton].forEach(button => {
    button.style.display = ‘none‘;
  });
  if (buttonName === "START") this.startButton.style.display = "inline-block";
  else if (buttonName === "PAUSE") this.pauseButton.style.display = "inline-block";  
  else if (buttonName === "STOP") this.stopButton.style.display = "inline-block";
}

Finally, to actually use our Timer class, we create a new instance, passing in the relevant DOM elements:

const durationInput = document.getElementById("duration");
const startButton = document.getElementById("start");
const pauseButton = document.getElementById("pause");
const stopButton = document.getElementById("stop");
const display = document.getElementById("display");

const timer = new Timer(durationInput, startButton, pauseButton, stopButton, display);

And with that, our refactored object-oriented timer is complete! The functionality is exactly the same as before, but now our code is more organized and reusable.

Adding Features with Inheritance and Events

One of the key benefits of object-oriented programming is code reusability through inheritance. Let‘s say we wanted to create a special type of timer that plays a sound when the time is up. Instead of copying and modifying the Timer class, we can create a SoundTimer that inherits from Timer and adds the sound functionality:

class SoundTimer extends Timer {
  constructor(durationInput, startButton, pauseButton, stopButton, display, soundUrl) {
    super(durationInput, startButton, pauseButton, stopButton, display);
    this.soundUrl = soundUrl;
  }

  stop = () => {
    super.stop();
    this.playSound();
  }

  playSound = () => {
    const audio = new Audio(this.soundUrl);
    audio.play();
  }
}

The SoundTimer constructor takes an additional soundUrl parameter, which it saves as a property after calling super() to invoke the parent Timer constructor.

It also overrides the stop method to play a sound after calling the original stop method via super.stop(). This is an example of polymorphism, another key OOP concept.

We can create a SoundTimer instance like so:

const soundTimer = new SoundTimer(durationInput, startButton, pauseButton, stopButton, display, ‘path/to/sound.mp3‘);

Another way to extend the functionality of our timer would be to use events to notify interested parties when the timer starts, pauses, stops, or completes. We can define a custom EventEmitter class:

class EventEmitter {
  constructor() {
    this.listeners = {};
  }

  on(event, callback) {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event].push(callback);
  }

  emit(event, ...args) {
    if (this.listeners[event]) {
      this.listeners[event].forEach(callback => {
        callback(...args);
      });
    }
  }
}

And then have our Timer class inherit from EventEmitter:

class Timer extends EventEmitter {
  constructor(durationInput, startButton, pauseButton, stopButton, display) {
    super();
    // rest of constructor
  }

  start = () => {
    // existing start logic
    this.emit(‘start‘);
  }

  pause = () => {
    // existing pause logic  
    this.emit(‘pause‘);
  }

  stop = () => {
    // existing stop logic
    this.emit(‘stop‘);
    if (this.elapsedTime >= this.durationInput.value) {
      this.emit(‘complete‘);
    }
  }
}

Now any code can subscribe to timer events and react accordingly:

const timer = new Timer(durationInput, startButton, pauseButton, display);

timer.on(‘start‘, () => {
  console.log(‘Timer started‘);
});

timer.on(‘complete‘, () => {
  console.log(‘Time is up!‘);
});

These are just a couple examples of how object-oriented programming principles can be used to create flexible, extensible code. With OOP, you can architect your application as a collection of interacting objects, making it easier to manage complexity and adapt to changing requirements.

Conclusion

In this post, we‘ve explored how to use object-oriented programming in JavaScript to build a timer application. We started with a procedural approach, then refactored our code to use classes, encapsulation, inheritance, and events – all key concepts in OOP.

Remember, object-oriented programming is a way of organizing and designing code, not a specific feature of JavaScript. The principles of abstraction, encapsulation, inheritance, and polymorphism can be applied in any language that supports OOP.

My challenge to you is to take the concepts you‘ve learned here and apply them to your own projects. Look for opportunities to group related data and behavior into reusable classes. Consider how inheritance and polymorphism can help you create specialized variations of base functionality. And don‘t forget the power of events for creating loosely coupled, extensible code.

Object-oriented programming is a powerful tool in any developer‘s toolbox. Mastering it will make you a more effective programmer, capable of tackling complex problems and building robust, maintainable applications. So keep practicing and happy coding!

Similar Posts