Build a realtime poll using Angular
You will need Node and npm or Yarn installed on your machine. A basic knowledge of Angular and Node is required.
An electronic polling system allows users cast their votes with ease without the hassle and stress of visiting a polling booth. This makes it easily accessible as it can be used by users anywhere in the world. Adding realtime functionality to the application improves the user experience as votes are seen in realtime.
Using Angular you can extend the template language with your own components and use a wide array of existing components.
To follow this tutorial a basic understanding of Angular and Node.js is required. Please ensure that you have Node and npm installed before you begin.
If you have no prior knowledge of Angular, kindly follow the tutorial here. Come back and finish the tutorial when you’re done.
We’ll be using these tools to build out our application:
We’ll build a realtime polling application using Pusher , Angular and charts.js for data visualization.
Using our application users will get to vote for their favourite soccer player in the English Premier League.
Here’s a demo of the final product:
We’ll send our votes to the server and with the help of Pusher, update our polls in realtime. To make use of Pusher you’ll have to create an account here.
Let’s build!
Setup and folder structure
To get started, we will use the CLI (command line interface) provided by the Angular team to initialize our project.
First, install the CLI by running npm install -g @angular/cli
. NPM is a package manager used for installing packages. It will be available on your PC if you have Node installed.
To create a new Angular project using the CLI, open a terminal and run
ng new angular-realtime-polling
--``style=scss
The command is simply telling the CLI to create a new project called angular-realtime-polling
and it should make use of the CSS pre-processor SCSS rather than CSS for styling.
Open the newly created angular-realtime-polling
. Your folder structure should look something like this:
angular-realtime-polling/
e2e/
node_modules/
src/
app/
app.component.html
app.component.ts
app.component.css
...
Open a terminal inside the project folder and start the application by running ng serve
. If you open your browser and visit the link http://localhost:4200
you should see the screenshot below if everything went well.
Building our server
Now that we have our Angular application running, let’s build our server.
To do this we’ll need to install Express. Express is a fast, unopinionated, minimalist web framework for Node.js. We’ll use this to receive requests from our Angular application.
Run npm install express
on a terminal inside the root folder of your project to install Express.
Create a file called server.js
in the root of the project and update it with the code snippet below
require('dotenv').config();
const express = require('express');
const bodyParser = require('body-parser');
const Pusher = require('pusher');
const app = express();
const port = process.env.PORT || 4000;
const pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_KEY,
secret: process.env.PUSHER_SECRET,
cluster: 'eu',
encrypted: true,
});
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header(
'Access-Control-Allow-Headers',
'Origin, X-Requested-With, Content-Type, Accept'
);
next();
});
app.listen(port, () => {
console.log(`Server started on port ${port}`);
});
We referenced three packages that haven’t been installed, body-parser
, pusher
and dotenv
. Install these packages by running npm i body-parser pusher dotenv
in your terminal.
The body-parser
package is used to parse incoming request bodies in a middleware before your handlers, available under the req.body
property.
The calls to our endpoint will be coming in from a different origin, therefore we need to make sure we include the CORS headers (Access-Control-Allow-Origin
). If you are unfamiliar with the concept of CORS headers, you can find more information here.
This is a standard Node application configuration, nothing specific to our app.
Also you’ll notice that we installed Pusher library as a dependency. Create a Pusher account and a new Pusher Channels app if you haven’t done so yet and get your appId
, key
and secret
.
The last package, dotenv is a zero-dependency module that loads environment variables from a .env
file into process.env.
We use this package so we don’t add sensitive information like our appId
and secret
directly into our code. To get these values loaded into our environment, we’ll create a .env
file in the root of our project.
Your .env
file should look something like the snippet below. We’ll add our Pusher appId
, key
and secret
provided here.
PUSHER_APP_ID=<APP_ID>
PUSHER_KEY=<PUSHER_KEY>
PUSHER_SECRET=<PUSHER_SECRET>
If you noticed, I added the dotenv
package at the start of our file. This is done because we need to make the variables available throughout the file.
Please ensure you replace the following placeholder values above with your Pusher appId
, key
and secret
.
Send votes
To enable users send requests to the server, we’ll create a route to handle incoming requests. Update your server.js
file with the code below.
// server.js
require('dotenv').config();
...
app.post('/vote', (req, res) => {
const { body } = req;
const { player } = body;
pusher.trigger('vote-channel', 'vote', {
player,
});
res.json({ player });
});
...
- We created a
POST /vote
route which, when hit, triggers a Pusher event. - We used object destructuring to get the body of the request and also the player info sent by the user.
- The trigger is achieved using the
trigger
method which takes the trigger identifier(vote-channel
), 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 name of the player being voted for
- We still go ahead to respond with an object containing the voted player string so we can update the frontend with the data
Polling view
Open the app.component.html
file and replace it with the content below.
// app.component.html
<div>
<h2>Vote for your player of the season</h2>
<ul>
<li *ngFor="let player of playerData">
<img [src]="player.image" [alt]="player.name" (click)="castVote(player.shortName)" [ngClass]="getVoteClasses(player.shortName)">
<h4>{{player.name}}</h4>
<p>{{player.goals}} goals</p>
<p>{{player.assists}} assists</p>
</li>
</ul>
</div>
In the code snippet above, we looped through playerData
to create a view based on the player’s information.
There are some undefined variables in code snippet above, don’t panic yet, we’ll define them in our component file below.
Styling
// app.component.scss
div {
width: 60%;
margin: auto;
text-align: center;
ul {
list-style: none;
padding-left: 0;
display: flex;
justify-content: center;
li {
padding: 20px;
img {
width: 100px;
height: 100px;
border-radius: 50%;
box-shadow: 0 3px 4px 1px rgba(0, 0, 0, 0.1);
filter: grayscale(1);
border: 4px solid rgba(0, 0, 0, 0.2);
cursor: pointer;
&.elect {
border: 3px solid rgb(204, 54, 54);
box-shadow: 0 4px 7px 1px rgba(0, 0, 0, 0.1);
filter: grayscale(0);
cursor: default;
}
&.lost {
box-shadow: unset;
border: 4px solid rgba(0, 0, 0, 0.1);
&:hover {
filter: grayscale(1);
cursor: default;
}
}
&:hover {
filter: grayscale(0);
}
}
h4 {
font-size: 16px;
opacity: 0.9;
margin-bottom: 8px;
font-weight: lighter;
}
p {
font-size: 14px;
opacity: 0.6;
font-weight: bold;
margin: 4px 0;
}
}
}
}
These styles are meant to add a bit of life to our application. It also helps distinguish between states during application use. For example: the voted player is highlighted with a red border
App component
In the HTML snippet we made reference to some variables that weren’t yet defined, we’ll create the variables here with the logic behind our application.
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
constructor(private http: HttpClient) {}
event = 'vote';
vote = '';
voted = false;
playerData = [
{
name: 'Mo. Salah',
goals: 30,
assists: 12,
shortName: 'salah',
image: 'https://platform-static-files.s3.amazonaws.com/premierleague/photos/players/250x250/p118748.png'
},
{
name: 'Christian Eriksen',
goals: 8,
assists: 13,
shortName: 'eriksen',
image: 'https://platform-static-files.s3.amazonaws.com/premierleague/photos/players/250x250/p80607.png',
},
{
name: 'Harry Kane',
goals: 26,
assists: 5,
shortName: 'kane',
image:
'https://platform-static-files.s3.amazonaws.com/premierleague/photos/players/40x40/p78830.png',
},
{
name: "Kevin De'bruyne",
goals: 10,
assists: 17,
shortName: 'kevin',
image: 'https://platform-static-files.s3.amazonaws.com/premierleague/photos/players/40x40/p61366.png',
},
];
voteCount = {
salah: 0,
kane: 0,
eriksen: 0,
kevin: 0,
};
castVote(player) {
this.http
.post(`http://localhost:4000/vote`, { player })
.subscribe((res: any) => {
this.vote = res.player;
this.voted = true;
});
}
getVoteClasses(player) {
return {
elect: this.voted && this.vote === player,
lost: this.voted && this.vote !== player,
};
}
ngOnInit() {
}
}
- castVote: this method makes use of the native httpClient service to make requests to our server. It sends the name of the player being voted for in a POST request to the server. When a response is returned, it sets the
voted
property totrue
signifying that the user has placed a vote. Also, it sets thevote
property to the name of the player being voted. - getVoteClasses: this method sets classNames on each player element based on if a player was voted for or not.
To make use of the HttpClient
service, we’ll need to import the HttpClientModule
into the app.module.ts
file. Update your app module file as follows:
// app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import {HttpClientModule} from '@angular/common/http';
....
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule],
providers: [],
bootstrap: [AppComponent],
})
....
By now our application should look like this:
Introducing Pusher
So far we have an application that enables users to cast votes but we have no way of keeping track of how others voted in realtime. We also have no way of visualizing the polling data. To solve both these problems, we’ll include the Pusher library and Chart.js for data visualization.
Open a terminal in the root folder of the project and install these packages by running the following command:
npm install pusher-js chart.js ng2-charts
To make both libraries available in our project we’ll add the libraries as third party scripts to be loaded by Angular CLI. All CLI config is stored in .angular-cli.json
file. Modify the scripts
property to include the link to pusher.min.js
. and chart.js
files.
// .angular-cli.json
...
"scripts": [
"../node_modules/pusher-js/dist/web/pusher.min.js",
"../node_modules/chart.js/src/chart.js"
]
...
After updating this file, you’ll need to restart the angular server so that the CLI compiles the new script files we’ve just added.
Now that Pusher has been made available in our project, we’ll create a Pusher service to be used application wide. The Angular CLI can aid in the service creation. Open a terminal in your project’s root folder and run the following command.
ng generate service pusher
This command simply tells the CLI to generate a service named pusher
. Now open the pusher.service.ts
file and update it with the code below.
// pusher.service.ts
import { Injectable } from '@angular/core';
declare const Pusher: any;
@Injectable()
export class PusherService {
constructor() {
var pusher = new Pusher('<PUSHER_KEY>', {
cluster: 'eu',
encrypted: true,
});
this.channel = pusher.subscribe('vote-channel');
}
channel;
public init() {
return this.channel;
}
}
- First, we initialize Pusher in the constructor.
- The
init
method returns the Pusher property we created. - Ensure you replace the
PUSHER_KEY
string with your actual Pusher key.
To make the service available application wide, import it into the module file.
// app.module.ts
import {PusherService} from './pusher.service'
...
@NgModule({
....
providers: [PusherService],
....
})
We’ll make use of this service in our component, by binding to the vote event and incrementing the votes of the voted player returned in the event. This will be done in the ngOnInit
lifecycle.
// app.component.ts
import { Component, OnInit } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { PusherService } from './pusher.service';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.scss'],
})
export class AppComponent implements OnInit {
constructor(private pusher: PusherService, private http: HttpClient) {}
...
ngOnInit() {
const channel = this.pusher.init();
channel.bind('vote', ({ player }) => {
this.voteCount[player] += 1;
});
}
}
Data visualization
Now that our application has been built out, we’ll need to visualize the voting process using charts. This is vital because we need a way to determine the winner of the polls and how each person voted.
To make use of charts in our application, we’ll import the ChartsModule
into our app.module.ts
file.
// app.module.ts
import {ChartsModule} from 'ng2-Charts';
....
@NgModule({
declarations: [AppComponent],
imports: [BrowserModule, HttpClientModule, ChartsModule],
providers: [PusherService],
bootstrap: [AppComponent],
})
....
We can then use the canvas
component to provide visualization. Make the following changes to your app.component.ts
, html and css files.
// app.component.ts
...
playerData = [
{
name: 'Mo. Salah',
goals: 30,
assists: 12,
shortName: 'salah',
image:
'https://platform-static-files.s3.amazonaws.com/premierleague/photos/players/250x250/p118748.png',
}
...
];
voteCount = {
salah: 0,
kane: 0,
eriksen: 0,
kevin: 0,
};
chartLabels: string[] = Object.keys(this.voteCount);
chartData: number[] = Object.values(this.voteCount);
chartType = 'doughnut';
...
ngOnInit() {
const channel = this.pusher.init();
channel.bind('vote', ({ player }) => {
this.voteCount[player] += 1;
// Update the chartData whenever there's a new vote
this.chartData = Object.values(this.voteCount);
});
}
}
chartLabels: we provide labels for our chart using the keys of the voteCount
object.
chartData: the chart data will make use of the values of the voteCount
object which signifies the vote count of each player.
chartType: we specify the chart type we’ll use.
We also made a few changes to the ngOnInit
lifecycle. We update the chartData values whenever there’s a new vote event.
// app.component.html
<div>
...
</li>
</ul>
<div class="chart-box" *ngIf="voted">
<h2>How others voted</h2>
<canvas baseChart [data]="chartData"
[labels]="chartLabels" [chartType]="chartType">
</canvas>
</div>
</div>
// app.component.scss
...
.chart-box{
display: flex;
flex-direction: column;
justify-content: center;
}
At this point, your application should have realtime updates when votes are placed. Ensure that the server is running alongside the Angular development server. If not, run node server
and ng serve
in two separate terminals. Both terminals should be opened in the root folder of your project.
To test the realtime functionality of the application, open two browsers side-by-side and place votes. You’ll notice that votes placed on one reflect on the other browser.
Conclusion
Using Pusher, we’ve built out an application using the pub/sub pattern without having to set up a WebSocket server. This shows how powerful Pusher is and how easy it is to set up. You can find the demo for this article on Github.
7 May 2018
by Christian Nwamba