Build a realtime counter using JavaScript

Introduction

Dashboards are a common feature of modern day applications. Users like to see an overview of their performance, on a single page, and a really great way to improve that experience is to make the information shown to them be dynamic, or realtime!

Counters are one of the prominent components of user dashboards, and in this tutorial, I will be showing you how to create a realtime counter, using the broadcasting prowess of Pusher and the simplicity of plain JavaScript. We will build a simple vote counter to count the number of votes an item gets, in real time.

First, we will set up Pusher, then create our Node.js Application, and finally we will create our view and listen for changes to the number of votes for an item.

Setting up Pusher

Pusher's APIs make it very easy to add realtime functionality to your applications. You should signup to a free account (if you haven't already done so), create an app, and copy out the app credentials (App ID, Key and Secret) from the “App Keys” section, as we will be needing these for our app interaction with Pusher.

Creating Our App

We will be building our backend on Node.js, make sure you have it installed, then you can initialise the new app with:

npm init -y

Tip: The -y or --yes flag helps to create a package.json file with default values.

Next, we will install Express and Pusher and save as dependencies in our package.json file, via npm:

npm install -S express pusher

Now, we can create the files needed for our application:

1./server.js
2./index.html

The server.js file will contain our server-side code, and index.html will contain our view and event listener script.

Building the Backend

In our server.js file, first we will initialise Express, require the path module and require Pusher:

1const express = require('express');
2const path = require('path');
3const app = express();
4app.use(express.static(path.join(__dirname)));
5const Pusher = require('pusher');

Next, we will Initialise Pusher with our app credentials, gotten from the Pusher dashboard:

1const pusher = new Pusher({
2  appId: 'YOUR_APP_ID',
3  key: 'YOUR_APP_KEY',
4  secret: 'YOUR_APP_SECRET',
5  cluster: 'eu',
6  encrypted: true
7});

Note: If you created your app in a different cluster to the default us-east-1, you must configure the cluster option. It is optional if you chose the default option. encrypted is also optional.

Now we can start defining our app's routes and responses.

When a user visits the homepage, we want to serve our index.html file, so we define a route for /:

1app.get('/', (req,res) => {  
2  res.sendFile('index.html', {root: __dirname});
3});

Tip: res.sendFile is used to deliver files in Express applications.

Next, we will define a route to handle votes. When a request with an item_id is sent to this route, we want to increase the number of votes on that item, and broadcast the change to all our users.

1app.get('/vote', (req, res) => {
2  let item = req.query.item_id;
3  pusher.trigger('counter', 'vote', {item: item});
4  res.status(200).send();
5});

In the code above, when a request is made to the /vote route, it gets the value of the item from the item_id key in the query string, then triggers a vote event on the counter channel, sending the item information as data to be broadcasted.

The trigger method has this syntax: pusher.trigger( channels, event, data, socketId, callback );. You can read more in it here.

We are broadcasting on a public channel as we want the data to be accessible to everyone. Pusher also allows broadcasting on private and presence channels, which provide functionalities that require authentication. Their channel names are prefixed by private- and presence- respectively, unlike public channels that require no prefix.

Typically, we should also save the new value of the number of votes to a database of some sort, so the data is persisted, but that is a bit out of the scope of this tutorial. You can implement this on your version!

Now we can start the server and listen on port 5000 for connections:

1const port = 5000;
2app.listen(port, () => { console.log(`App listening on port ${port}!`)});

The final server.js file will look like this:

1/*
2 * Initialise Express
3 */
4const express = require('express');
5const path = require('path');
6const Pusher = require('pusher');
7const app = express();
8app.use(express.static(path.join(__dirname)));
9
10/*
11 * Initialise Pusher
12 */
13const pusher = new Pusher({
14  appId: 'YOUR_APP_ID',
15  key: 'YOUR_APP_KEY',
16  secret: 'YOUR_APP_SECRET',
17  cluster: 'eu',
18  encrypted: true
19});
20
21/*
22 * Define app routes and reponses
23 */
24app.get('/', (req,res) => {  
25  res.sendFile('index.html', {root: __dirname});
26});
27
28app.get('/vote', (req, res) => {
29  let item = req.query.item_id;
30  pusher.trigger('counter', 'vote', {item: item});
31  res.status(200).send();
32});
33
34/*
35 * Run app
36 */
37const port = 5000;
38app.listen(port, () => { console.log(`App listening on port ${port}!`)});

Creating the App View

Now, we can fill index.html with some markup. I also included Foundation to take advantage of some preset styles:

1<!DOCTYPE html>
2<html lang="en">
3<head>
4  <meta charset="utf-8">
5  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/6.3.1/css/foundation.min.css">
6  <title>JavaScript Decides</title>
7</head>
8<body>
9  <div class="container text-center">
10    <h3 class="title">
11      Pusher Real-time Vote Counter.
12      <h5 class="subheader">JavaScript Decides</h5>
13    </h3>
14
15    <div class="row">
16      <div class="columns medium-6">
17        <div class="stat" id="vote-1">0</div>
18        <p class="subheader"><small>number of votes</small></p>
19        <button class="button vote-button" data-vote="1">Vote for me</button>
20      </div>
21      <div class="columns medium-6">
22        <div class="stat" id="vote-2">0</div>
23        <p class="subheader"><small>number of votes</small></p>
24        <button class="button vote-button" data-vote="2">Nah, Vote for me</button>
25      </div>
26    </div>
27    <hr>
28  </div>
29</body>
30</html>

Listening for events from Pusher and updating the vote count

To work with Pusher on the client side, we need to include its JavaScript library. We'll do so at the bottom of index.html:

<script src="https://js.pusher.com/4.0/pusher.min.js"></script>

Then, initialising Pusher with our app credentials:

1const pusher = new Pusher('YOUR_APP_KEY', {
2  cluster: 'eu',
3  encrypted: true
4});

Note: Don't forget to replace 'YOUR_APP_KEY' with its actual value

Next, we will subscribe to the counter public channel, which is the same channel we publish to on our server-side, and listen for vote events:

1const channel = pusher.subscribe('counter');
2
3channel.bind('vote', data => {
4  let elem = document.querySelector(`#vote-${data.item}`),
5      votes = parseInt(elem.innerText);
6  elem.innerText = votes + 1;
7});

Tip: You can also do Pusher.logToConsole = true; to debug locally

In the above code, we also defined a callback function, which accepts the data broadcast through Pusher as its parameter. We used this data to update the DOM with the new values of the vote counts.

Finally, we define an event listener for click events on our vote buttons. We also define a voteItem() function which will be fired whenever the buttons are clicked.

1const voteButtons = document.getElementsByClassName("vote-button");
2
3function voteItem() { 
4  let vote_id = this.getAttribute("data-vote");
5
6  // Make Ajax call with JavaScript Fetch API
7  fetch(`/vote?item_id=${vote_id}`)
8      .catch( e => { console.log(e); });
9}
10
11// IIFE - Executes on page load
12(function() {
13  for (var i = 0; i < voteButtons.length; i++) {
14    voteButtons[i].addEventListener('click', voteItem);
15  }
16})();

Note: We make use of the JavaScript Fetch API for making an Ajax request. It is promise-based, and more powerful than the regular XMLHttpRequest, although a Polyfill might be needed for older browsers.

The final index.html file will look like this:

1<!DOCTYPE html>
2<html lang="en">
3<head>
4  <meta charset="utf-8">
5  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/foundation/6.3.1/css/foundation.min.css">
6  <title>JavaScript Decides</title>
7</head>
8<body>
9  <div class="container text-center">
10    <h3 class="title">
11      Pusher Real-time Vote Counter.
12      <h5 class="subheader">JavaScript Decides</h5>
13    </h3>
14
15    <div class="row">
16      <div class="columns medium-6">
17        <div class="stat" id="vote-1">0</div>
18        <p class="subheader"><small>number of votes</small></p>
19        <button class="button vote-button" data-vote="1">Vote for me</button>
20      </div>
21      <div class="columns medium-6">
22        <div class="stat" id="vote-2">0</div>
23        <p class="subheader"><small>number of votes</small></p>
24        <button class="button vote-button" data-vote="2">Nah, Vote for me</button>
25      </div>
26    </div>
27    <hr>
28  </div>
29
30  <script src="https://js.pusher.com/4.0/pusher.min.js"></script>
31  <script>
32    const pusher = new Pusher('YOUR_APP_KEY', {
33      cluster: 'eu',
34      encrypted: true
35    });
36
37    const channel = pusher.subscribe('counter');
38
39    channel.bind('vote', data => {
40      let elem = document.querySelector(`#vote-${data.item}`),
41          votes = parseInt(elem.innerText);
42      elem.innerText = votes + 1;
43    });
44
45    const voteButtons = document.getElementsByClassName("vote-button");
46
47    function voteItem() { 
48      let vote_id = this.getAttribute("data-vote");
49
50      // Make Ajax call with JavaScript Fetch API
51      fetch(`/vote?item_id=${vote_id}`)
52          .catch( e => { console.log(e); });
53    }
54
55    // IIFE - Executes on page load
56    (function() {
57      for (var i = 0; i < voteButtons.length; i++) {
58        voteButtons[i].addEventListener('click', voteItem);
59      }
60    })();
61  </script>
62  </body>
63</html>

And that's it, we have a functional realtime vote counter!

To run the app:

node server.js

You can also get nodemon, so you can have automatic reloads on changes to your file. So instead, you could do: nodemon server.js.

Demo

Here is what the final app looks like:

counter-javascript-demo

Conclusion

In this tutorial, we have learned how to start a basic JavaScript project, and give it realtime functionality using Pusher. We have also learned about Public channels, and how we can trigger events on these channels on the server-side, and listen for them on the client-side.

There are a lot of possibilities, with Pusher providing realtime functionality for our applications, especially in the creation of dashboard components. In the same way as a counter was created, we can also create tables, charts, and so on.

Pusher's presence channels can also be used to implement a view counter, whenever a user visits your app.