Course: JavaScript

Progress (0%)

  1. Foundation

  2. Numbers

  3. Strings

  4. Conditions

  5. Loops

  6. Arrays

  7. Functions

  8. Objects

  9. Exceptions

  10. HTML DOM

  11. CSSOM

  12. Events

  13. Drag and Drop

  14. opt Touch Events

  15. Misc

  16. Project: Analog Clock

Exercise: Many Counters

Exercise 53 Easy

Prerequisites for the exercise

  1. JavaScript Events — Basics
  2. All previous chapters

Objective

Create a Counter class to be able to easily create multiple, self-contained, counters in a web page.

Description

In the previous exercise, JavaScript Exercise — A Simple Counter, you created a counter program that applied to the entire web page (with a global variable count to hold the current count of the counter).

Now, you have to take that very idea one step further and allow the creation of multiple counters, each one self-contained, working with its own count.

In this exercise, you have to construct a Counter class that helps us create a self-contained counter, with its own display and functioning buttons.

What properties and/or methods you need is totally up to you — it's kind of like a mini-test to see how well can you abstract the idea of a counter into a class construct.

The way the class's constructor should work is as follows. It should accept an optional argument which specifies the element node inside which the counter must be placed (as the last child). If the argument isn't provided, it should be taken to be the <body> element.

Shown below is an example of the counter's usage:

<section id="s1" style="border: 1px solid black">
   <h1>Counter 1</h1>
</section>

<section id="s2" style="border: 1px solid black">
   <h1>Counter 2</h1>
</section>
// Assume that Counter has been already defined.

new Counter(document.getElementById('s1'));
new Counter(document.getElementById('s2'));

Live Example

View Solution

New file

Inside the directory you created for this course on JavaScript, create a new folder called Exercise-53-Many-Counters and put the .html solution files for this exercise within it.

Solution

The exercise seems quite basic so let's get coding.

But wait! We haven't yet designed the Counter class in our minds. If we do get into the coding at this stage, it would eventually turn out to be more troublesome than fruitful.

So first things first, let's get thinking.

What does a Counter object really need to know? What state does it need to maintain?

Well, to start with, a counter needs to know of its current count. Hence, this gives us the clue that we need an instance property, let's call it count, defined on Counter to hold this current count.

Besides count, at least at this stage, there isn't any other property that we can think of, and so we'll move on to the behavior part of the class, keeping in mind that later on we might need to add more properties.

So, what actions could we possibly want to perform on this counter? Think about it.

Well, they're quite apparent: increment, decrement, and reset.

This gives us the clue that we need to define three methods on the Counter class to accomplish these very actions, with the same meaningful names, i.e. increment(), decrement() and reset(), respectively.

Quite simple, wasn't this?

increment() must increment the count property and then display it in the rendered display element (whatever we choose it to be) of the counter.

But where does this display element come from?

Well, we need to create it manually in the JavaScript, and since it's required by each of the three methods discussed above, we need to store it as well, as a property. Let's call it displayElement.

So in total, our Counter class has the following properties/methods: count holding the current count, displayElement holding the element representing the display of the counter, increment() to increment the counter, decrement() to decrement it, and finally reset() to reset it back to 0.

Simple.

Now, let's get to define the Counter class:

We'll start with the basic class wireframe, including the definition of the constructor and the three aforementioned methods:

class Counter {
   constructor() {}

   increment() {}
   decrement() {}
   reset() {}
}

With this done, it's time to talk about the constructor.

In the constructor, we need to initialize count to 0, and then set displayElement to the display element of the counter, which is obtained while we construct the markup of the counter. Essentially, all this markup has to be created from scratch by the JavaScript code.

One way is to do it all directly inside the constructor, as shown below:

class Counter {
   constructor(parentElement = document.body) {
      this.count = 0; // Initialize count

      var counterElement = document.createElement('div');

      var displayElement = document.createElement('h1');
      displayElement.textContent = 0;
      this.displayElement = displayElement; // Initialize displayElement
      counterElement.appendChild(displayElement);

      var buttonElement;
      buttonElement = document.createElement('button');
      buttonElement.textContent = '+';
      buttonElement.onclick = this.increment.bind(this);
      counterElement.appendChild(buttonElement);
      counterElement.appendChild(document.createTextNode(' '));

      buttonElement = document.createElement('button');
      buttonElement.textContent = '-';
      buttonElement.onclick = this.decrement.bind(this);
      counterElement.appendChild(buttonElement);
      counterElement.appendChild(document.createTextNode(' '));

      buttonElement = document.createElement('button');
      buttonElement.textContent = 'Reset';
      buttonElement.onclick = this.reset.bind(this);
      counterElement.appendChild(buttonElement);

      parentElement.appendChild(counterElement);
   }

   increment() {}
   decrement() {}
   reset() {}
}

The code here shouldn't be difficult to comprehend.

It starts by initializing count to 0 and then constructing the markup of the counter by creating element nodes manually. In this process, the displayElement property is set to the <h1> element that represents the display of the counter.

Now, although the code above works absolutely fine in setting up the counter, you would agree that it's quite verbose. We're just doing a lot of work in the constructor. We need something better.

But there isn't any definite answer to what exactly is better in this regard.

For example, one way to simplify the code above is to DRY out (i.e. prevent repetition of) the last three sets of statements, that create the three buttons, into a loop, as demonstrated below:

class Counter {
   constructor(parentElement = document.body) {
      this.count = 0; // Initialize count

      var counterElement = document.createElement('div');

      var displayElement = document.createElement('h1');
      displayElement.textContent = 0;
      this.displayElement = displayElement; // Initialize displayElement
      counterElement.appendChild(displayElement);

      var buttonTextContents = ['+', '-', 'Reset'];
      var buttonHandlerNames = ['increment', 'decrement', 'reset'];
      for (var i = 0; i < 3; i++) {
         var buttonElement = document.createElement('button');
         buttonElement.textContent = buttonTextContents[i];
         buttonElement.onclick = this[buttonHandlerNames[i]].bind(this);
         counterElement.appendChild(buttonElement);
         counterElement.appendChild(document.createTextNode(' '));
      }

      parentElement.appendChild(counterElement);
   }

   increment() {}
   decrement() {}
   reset() {}
}

For this approach, as you can see, we ought to create two arrays: buttonTextContents, holding the values of the three buttons, and buttonHandlerNames, holding the names of the methods to be called on the corresponding button's click event.

Besides this, another way to simplify the verbose constructor above could be to extract out all the logic of creating the HTML elements of the counter from the constructor into a new method. This can be seen as follows:

class Counter {
   constructor(parentElement = document.body) {
      this.count = 0;
      this.displayElement = null;

      this.createCounterElement(parentElement);
   }

   createCounterElement(parentElement) {
      var counterElement = document.createElement('div');

      var displayElement = document.createElement('h1');
      displayElement.textContent = 0;
      this.displayElement = displayElement;
      counterElement.appendChild(displayElement);

      var buttonTextContents = ['+', '-', 'Reset'];
      var buttonHandlerNames = ['increment', 'decrement', 'reset'];
      for (var i = 0; i < 3; i++) {
         var buttonElement = document.createElement('button');
         buttonElement.textContent = buttonTextContents[i];
         buttonElement.onclick = this[buttonHandlerNames[i]].bind(this);
         counterElement.appendChild(buttonElement);
         counterElement.appendChild(document.createTextNode(' '));
      }

      parentElement.appendChild(counterElement);
   }

   increment() {}
   decrement() {}
   reset() {}
}

The createCounterElement() method here nicely deals with abstracting the logic, of how the counter element is actually created, out of the constructor. Obviously, since it needs to add the created counterElement node into parentElement, which is accessible only inside the constructor, we need to send parentElement down to the method for it to be able to access it.

As you can see, each way of simplication has its own additional requirements. And there's no hard and fast rule in following the methodology showed here.

For example, instead of using buttonHandlerNames, we could go with a buttonHandlers array that directly contains the function to be assigned to each button's onclick property in the loop.

Alright, with this done, let's return to implementing the three remaining methods. Their definition would be quite similar to the ones from the last exercise, JavaScript Events — A Simple Counter.

Here's the code:

class Counter {
   /* ... */

   increment() {
      this.count++;
      this.displayElement.textContent = this.count;
   }

   decrement() {
      if (this.count !== 0) {
         this.count--;
      }
      this.displayElement.textContent = this.count;
   }

   reset() {
      this.count = 0;
      this.displayElement.textContent = this.count;
   }
}

And this completes the implementation of our Counter class.

As always, it's time to test it.

And for this, we'll simply use the code shown in the exercise's description above:

<section id="s1" style="border: 1px solid black">
   <h1>Counter 1</h1>
</section>

<section id="s2" style="border: 1px solid black">
   <h1>Counter 2</h1>
</section>
class Counter {
   /* ... */
}

new Counter(document.getElementById('s1'));
new Counter(document.getElementById('s2'));

Live Example

It works absolutely flawlessly!