Objective
Create a Counter
class to be able to easily create multiple, self-contained, counters in a web page.
Difficulty
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'));
New file
Inside the directory you created for this course on JavaScript, create a new folder called Exercise-50-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'));
It works absolutely flawlessly!