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.
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.
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 apackage.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.
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}!`)});
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>
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
.
Here is what the final app looks like:
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.