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:
req
, hence req.body
stores this payload for each request.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:
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:
1<!-- ./index.html --> 2<div class="main"> 3 <div class="container"> 4 <h1>Pick your favorite</h1> 5 <div class="col-md-8 col-md-offset-2"> 6 <div class="row"> 7 <div class="col-md-6"> 8 <div class="poll-logo angular"> 9 <img src="images/angular.svg" alt=""> 10 </div> 11 </div> 12 <div class="col-md-6"> 13 <div class="poll-logo ember"> 14 <img src="images/ember.svg" alt=""> 15 </div> 16 </div> 17 </div> 18 <div class="row"> 19 <div class="col-md-6"> 20 <div class="poll-logo react"> 21 <img src="images/react.svg" alt=""> 22 </div> 23 </div> 24 <div class="col-md-6"> 25 <div class="poll-logo vue"> 26 <img src="images/vue.svg" alt=""> 27 </div> 28 </div> 29 </div> 30 </div> 31 </div> 32 </div> 33 <div class="js-logo"> 34 <img src="images/js.png" alt=""> 35 </div> 36 <script src="https://cdnjs.cloudflare.com/ajax/libs/axios/0.16.2/axios.js"></script> 37 <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.
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:
1// ./app.js 2window.addEventListener('load', () => { 3 var app = { 4 pollLogo: document.querySelectorAll('.poll-logo'), 5 frameworks: ['Angular', 'Ember', 'React', 'Vue'] 6 } 7 8 // Sends a POST request to the 9 // server using axios 10 app.handlePollEvent = function(event, index) { 11 const framework = this.frameworks[index]; 12 axios.post('http://localhost:3000/vote', {framework: framework}) 13 .then((data) => { 14 alert (`Voted ${framework}`); 15 }) 16 } 17 18 // Sets up click events for 19 // all the cards on the DOM 20 app.setup = function() { 21 this.pollLogo.forEach((pollBox, index) => { 22 pollBox.addEventListener('click', (event) => { 23 // Calls the event handler 24 this.handlePollEvent(event, index) 25 }, true) 26 }) 27 } 28 29 app.setup(); 30 31})
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.
Before we jump right into setting up a server where Pusher will trigger events based on the request sent from the client, sign up for a free Pusher account. Then go to the dashboard and create a Channels app instance.
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
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:
1// ./routes/votes.js 2var express = require('express'); 3var Pusher = require('pusher'); 4 5var router = express.Router(); 6var pusher = new Pusher({ 7 appId: '<APP_ID>', 8 key: '<APP_KEY>', 9 secret: '<APP_SECRET>', 10 cluster: '<APP_CLUSTER>', 11 encrypted: true 12}); 13// /* Vote 14router.post('/', function(req, res, next) { 15 pusher.trigger('poll', 'vote', { 16 points: 10, 17 framework: req.body.framework 18 }); 19 res.send('Voted'); 20}); 21module.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
POST /vote
route which, when hit, triggers a Pusher event. The trigger is achieved using the trigger
method which takes the trigger identifier(poll
), an event name (vote
), and a payload.req.body.framework
.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
1// Other Imports 2var vote = require('./routes/vote'); 3 4// CORS 5app.all('/*', function(req, res, next) { 6 // CORS headers 7 res.header("Access-Control-Allow-Origin", "*"); 8 // Only allow POST requests 9 res.header('Access-Control-Allow-Methods', 'POST'); 10 // Set custom headers for CORS 11 res.header('Access-Control-Allow-Headers', 'Content-type,Accept,X-Access-Token,X-Key'); 12}); 13 14// Ensure that the CORS configuration 15// above comes before the route middleware 16// below 17app.use('/vote', vote); 18 19module.exports = app;
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:
1<!-- ./admin.html --> 2<div class="main"> 3 <div class="container"> 4 <h1>Chart</h1> 5 <div id="chartContainer" style="height: 300px; width: 100%;"></div> 6 </div> 7</div> 8<script src="https://js.pusher.com/4.0/pusher.min.js"></script> 9<script src="https://cdnjs.cloudflare.com/ajax/libs/canvasjs/1.7.0/canvasjs.js"></script> 10<script src="app.js"></script>
charContainer
is where we will mount the chart.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):
1// ./app.js 2window.addEventListener('load', () => { 3 // Event handlers for 4 // vote cards was here. 5 // Just truncated for brevity 6 7 let dataPoints = [ 8 { label: "Angular", y: 0 }, 9 { label: "Ember", y: 0 }, 10 { label: "React", y: 0 }, 11 { label: "Vue", y: 0 }, 12 ] 13 const chartContainer = document.querySelector('#chartContainer'); 14 15 if(chartContainer) { 16 var chart = new CanvasJS.Chart("chartContainer", 17 { 18 animationEnabled: true, 19 theme: "theme2", 20 data: [ 21 { 22 type: "column", 23 dataPoints: dataPoints 24 } 25 ] 26 }); 27 chart.render(); 28 } 29 30 // Here: 31 // - Configure Pusher 32 // - Subscribe to Pusher events 33 // - Update chart 34})
dataPoints
array is the data source for the chart. The objects in the array have a uniform structure of label
which stores the frameworks and y
which stores the points.chartContainer
exists before creating the chart because the index.html
file doesn’t have a chartContainer
.Chart
constructor function to create a chart by passing the configuration for the chart which includes the data. The chart is rendered by calling render()
on constructor function instance.We can start listening to Pusher events in the comment placeholder at the end:
1// ./app.js 2// ...continued 3// Allow information to be 4// logged to console 5Pusher.logToConsole = true; 6 7// Configure Pusher instance 8var pusher = new Pusher('<APP_KEY>', { 9 cluster: '<APP_CLUSTER>', 10 encrypted: true 11}); 12 13// Subscribe to poll trigger 14var channel = pusher.subscribe('poll'); 15// Listen to vote event 16channel.bind('vote', function(data) { 17 dataPoints = dataPoints.map(x => { 18 if(x.label == data.framework) { 19 // VOTE 20 x.y += data.points; 21 return x 22 } else { 23 return x 24 } 25 }); 26 27 // Re-render chart 28 chart.render() 29});
poll
, so we subscribe to it and listen to its vote
event. Hence, when the event is triggered, we update the dataPoints
variable and re-render the chart with render()
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 Channels showing how powerful it can be.