Back to search

Build a realtime poll using Angular

  • Christian Nwamba
May 7th, 2018
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:

angular-football-poll-demo

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.

tables-angular-welcome-screen

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 to true signifying that the user has placed a vote. Also, it sets the vote 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:

angular-football-poll-stage-1

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.

angular-football-poll-complete

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.

  • Channels

© 2018 Pusher Ltd. All rights reserved.

Pusher Limited is a company registered in England and Wales (No. 07489873) whose registered office is at 28 Scrutton Street, London EC2A 4RP.