Build a live poll using Node.js
A basic understanding of JavaScript and HTML is needed to follow this tutorial.
An electronic poll simplifies the way polls are carried out and aggregates data in realtime. (These days, nobody needs to take a bus to town just to cast a vote for their favorite soccer team!) As voters cast their votes, every connected client that is authorised to see the poll data should see the votes as they come in.
This article explains how to seamlessly add realtime features to your polling app using Pusher while visualising the data on a chart using CanvasJS, in just 5 steps.
Some of the tools we will be using to build our app are:
- Node: JavaScript on a server. Node will handle all our server related needs.
- Express: Node utility for handling HTTP requests via routes
- Body Parser: Attaches the request payload on Express’s
req
, hencereq.body
stores this payload for each request. - Pusher: Pub/Sub pattern for building realtime solutions.
- CanvasJS: A UI library to facilitate data visualization with JavaScript on the DOM.
Together, we will build a minimalist app where users can select their favourite JavaScript framework. Our app will also include an admin page where the survey owner can see the polls come in.
Let’s walk through the steps one by one:
1. Polling screen
First things first. The survey participants or voters (call them whatever fits your context) need to be served with a polling screen. This screen contains clickable items from which they are asked to pick an option.
Try not to get personal with the options, we’re just making a realtime demo. The following is the HTML behind the scenes:
<!-- ./index.html -->
<div class="main">
<div class="container">
<h1>Pick your favorite</h1>
<div class="col-md-8 col-md-offset-2">
<div class="row">
<div class="col-md-6">
<div class="poll-logo angular">
<img src="images/angular.svg" alt="">
</div>
</div>
<div class="col-md-6">
<div class="poll-logo ember">
<img src="images/ember.svg" alt="">
</div>
</div>
</div>
<div class="row">
<div class="col-md-6">
<div class="poll-logo react">
<img src="images/react.svg" alt="">
</div>
</div>
<div class="col-md-6">
<div class="poll-logo vue">
<img src="images/vue.svg" alt="">
</div>
</div>
</div>
</div>
</div>
</div>
<div class="js-logo">
<img src="images/js.png" alt="">
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js"></script>
<script src="app.js"></script>
The HTML renders the polling cards and imports axios
and our custom app.js
file. axios
will be used to make HTTP calls to a server we will create. This server is responsible for triggering/emitting realtime events using Pusher.
2. Send vote requests
When a user clicks on their chosen option, we want to react with a response. The response would be to trigger a HTTP request. This request is expected to create a Pusher event, but we are yet to implement that:
// ./app.js
window.addEventListener('load', () => {
var app = {
pollLogo: document.querySelectorAll('.poll-logo'),
frameworks: ['Angular', 'Ember', 'React', 'Vue']
}
// Sends a POST request to the
// server using axios
app.handlePollEvent = function(event, index) {
const framework = this.frameworks[index];
axios.post('http://localhost:3000/vote', {framework: framework})
.then((data) => {
alert (`Voted ${framework}`);
})
}
// Sets up click events for
// all the cards on the DOM
app.setup = function() {
this.pollLogo.forEach((pollBox, index) => {
pollBox.addEventListener('click', (event) => {
// Calls the event handler
this.handlePollEvent(event, index)
}, true)
})
}
app.setup();
})
When each of the cards are clicked, handlePollEvent
is called with the right values as argument depending on the index. The method, in turn, sends the framework name to the server as payload via the /vote
(yet to be implemented) endpoint.
3. Set up a Pusher account and app
Before we jump right into setting up a server where Pusher will trigger events based on the request sent from the client, you’ll need to create a Pusher account and app, if you don’t already have one:
-
Sign up for a free Pusher account.
-
Create a new app by selecting Apps on the sidebar and clicking Create New button on the bottom of the sidebar.
-
Configure your app by providing basic information requested in the form presented. You can also choose the environment you intend to integrate Pusher into for a better setup experience.
-
You can retrieve your app credentials from the App Keys tab
4. Realtime server
The easiest way to set up a Node server is by using the Express project generator. You need to install this generator globally on your machine using npm:
npm install express-generator -g
The generator is a scaffold tool, therefore it’s useless after installation unless we use its command to create a new Express app. We can do that by running the following command:
express poll-server
This generates a few helpful files including the important entry point (app.js
) and routes (found in the routes
folder).
We just need one route to get things moving: a /vote
route which is where the client is sending a post request.
Create a new vote.js
file in the routes folder with the following logic:
// ./routes/votes.js
var express = require('express');
var Pusher = require('pusher');
var router = express.Router();
var pusher = new Pusher({
appId: '<APP_ID>',
key: '<APP_KEY>',
secret: '<APP_SECRET>',
cluster: '<APP_CLUSTER>',
encrypted: true
});
// /* Vote
router.post('/', function(req, res, next) {
pusher.trigger('poll', 'vote', {
points: 10,
framework: req.body.framework
});
res.send('Voted');
});
module.exports = router;
For the above snippet to run successfully, we need to install the Pusher SDK using npm. The module is already used but it’s not installed yet:
npm install --save pusher
- At the top of the file, we import Express and Pusher, then configure a route with Express and a Pusher instance with the credentials we retrieved from the Pusher dashboard.
- The configured router is used to create a
POST /vote
route which, when hit, triggers a Pusher event. The trigger is achieved using thetrigger
method which takes the trigger identifier(poll
), an event name (vote
), and a payload. - The payload can be any value, but in this case we have a JS object. This object contains the points for each vote and the name of the option (in this case, a framework) being voted. The name of the framework is sent from the client and received by the server using
req.body.framework
. - We still go ahead to respond with “Voted” string so we don’t leave the server hanging in the middle of an incomplete request.
In the app.js
file, we need to import the route we have just created and add it as part of our Express middleware. We also need to configure CORS because our client lives in a different domain, therefore the requests will NOT be made from the same domain:
// ./app.js
// Other Imports
var vote = require('./routes/vote');
// CORS
app.all('/*', function(req, res, next) {
// CORS headers
res.header("Access-Control-Allow-Origin", "*");
// Only allow POST requests
res.header('Access-Control-Allow-Methods', 'POST');
// Set custom headers for CORS
res.header('Access-Control-Allow-Headers', 'Content-type,Accept,X-Access-Token,X-Key');
});
// Ensure that the CORS configuration
// above comes before the route middleware
// below
app.use('/vote', vote);
module.exports = app;
5. Connect a dashboard
The last step is the most interesting aspect of the example. We will create another page in the browser which displays a chart of the votes for each framework. We intend to access this dashboard via the client domain but on the /admin.html
route.
Here is the markup for the chart:
<!-- ./admin.html -->
<div class="main">
<div class="container">
<h1>Chart</h1>
<div id="chartContainer" style="height: 300px; width: 100%;"></div>
</div>
</div>
<script src="https://js.pusher.com/4.0/pusher.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.js"></script>
<script src="app.js"></script>
- The div with the id
charContainer
is where we will mount the chart. - We have imported Pusher and Canvas JS (for the chart) via CDN as well as the same
app.js
that our home page uses.
We need to initialize the chart with a default dataset. Because this is a simple example, we won’t bother with persisted data, rather we can just start at empty (zeros):
// ./app.js
window.addEventListener('load', () => {
// Event handlers for
// vote cards was here.
// Just truncated for brevity
let dataPoints = [
{ label: "Angular", y: 0 },
{ label: "Ember", y: 0 },
{ label: "React", y: 0 },
{ label: "Vue", y: 0 },
]
const chartContainer = document.querySelector('#chartContainer');
if(chartContainer) {
var chart = new CanvasJS.Chart("chartContainer",
{
animationEnabled: true,
theme: "theme2",
data: [
{
type: "column",
dataPoints: dataPoints
}
]
});
chart.render();
}
// Here:
// - Configure Pusher
// - Subscribe to Pusher events
// - Update chart
})
- The
dataPoints
array is the data source for the chart. The objects in the array have a uniform structure oflabel
which stores the frameworks andy
which stores the points. - We check if the
chartContainer
exists before creating the chart because theindex.html
file doesn’t have achartContainer
. - We use the
Chart
constructor function to create a chart by passing the configuration for the chart which includes the data. The chart is rendered by callingrender()
on constructor function instance.
We can start listening to Pusher events in the comment placeholder at the end:
// ./app.js
// ...continued
// Allow information to be
// logged to console
Pusher.logToConsole = true;
// Configure Pusher instance
var pusher = new Pusher('<APP_KEY>', {
cluster: '<APP_CLUSTER>',
encrypted: true
});
// Subscribe to poll trigger
var channel = pusher.subscribe('poll');
// Listen to vote event
channel.bind('vote', function(data) {
dataPoints = dataPoints.map(x => {
if(x.label == data.framework) {
// VOTE
x.y += data.points;
return x
} else {
return x
}
});
// Re-render chart
chart.render()
});
- First we ask Pusher to log every information about realtime transfers to the console. You can leave that out in production.
- We then configure Pusher with our credentials by passing the app key and config object as arguments to the Pusher constructor function.
- The name of our trigger is
poll
, so we subscribe to it and listen to itsvote
event. Hence, when the event is triggered, we update thedataPoints
variable and re-render the chart withrender()
Conclusion
We didn’t spend time building a full app with identity and all, but you should now understand the model for building a fully fleshed poll system. We just made a simple realtime poll app with Pusher showing how powerful Pusher can be.
6 June 2017
by Christian Nwamba