Build a live voting app with Ionic
You should have Node and npm installed on your machine. Familiarity with Ionic development will be helpful.
Data visualization is viewed by many disciplines as a modern equivalent of visual communication. It involves the creation and study of the visual representation of data.
An important advantage of data visualization is how it enables users to more effectively see connections as they are occurring between operating conditions and business performance. Adding realtime functionality using Pusher improves this experience as data changes are witnessed in realtime.
We’ll be creating an application that will present data about how football fans predict who wins the current running World Cup. Using our application, users will complete a poll and then see the data from the polls in realtime.
Here’s a screenshot of the final product:
Prerequisites
To follow this tutorial a basic understanding of Angular, Ionic and Node.js is required. Please ensure that you have Node and npm installed before you begin.
If you have no prior knowledge of Ionic, kindly follow the tutorial here. Come back and finish the tutorial when you’re done.
We’ll be using these tools to build our application:
We’ll be sending data to the server and using Pusher’s pub/sub pattern, we’ll listen to and receive data in realtime. To make use of Pusher you’ll have to create an account here.
After account creation, visit the dashboard. Click Create new Channels app, fill out the details, click Create my app, and make a note of the details on the App Keys tab.
Let’s build!
Setup and folder structure
We’ll initialize our project using the Ionic CLI (command line interface). First, install the CLI by running npm install -g ionic
in your terminal. 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 Ionic project called chat-app
using the CLI, open a terminal and run:
ionic start ionic-polls blank
The command is simply telling the CLI to create a new project called ionic-polls
without a template.
Follow the prompt and integrate your app with Cordova to target IOS and Android.
Type Y to integrate Cordova into the application. The next prompt will ask if you want to integrate Ionic pro into the application. If you have a pro account type Y and N if you don’t.
The Ionic team provides three ready made starter templates. You can check out the rest of the templates here.
Open the newly created folder, your folder structure should look something like this:
chat-app/
resources/
node_modules/
src/
app/
app.component.html
app.module.ts
app.scss
...
Open a terminal inside the project folder and start the application by running ionic serve
. A browser window should pop up and you should see a page like this.
Installing dependencies
Next, run the following commands in the root folder of the project to install dependencies.
// install depencies required to build the server
npm install express body-parser dotenv pusher
// front-end dependencies
npm install ng2-charts pusher-js
Start the Ionic development server by running ionic serve
in a terminal in the root folder of your project.
Building our server
We’ll build our server using Express. Express is a fast, unopinionated, minimalist web framework for Node.js.
Create a file called server.js
in the root of the project and update it with the code snippet below
// server.js
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: process.env.PUSHER_CLUSTER,
});
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}`);
});
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.
Create a Pusher account and a new Pusher Channels app if you haven’t done so yet and get your appId
, key
and secret
. Create a file in the root folder of the project and name it .env
. Copy the the following snippet into the .env
file and ensure to replace the placeholder values with your Pusher credentials.
// .env
// Replace the placeholder values with your actual pusher credentials
PUSHER_APP_ID=PUSHER_APP_ID
PUSHER_KEY=PUSHER_KEY
PUSHER_SECRET=PUSHER_SECRET
PUSHER_CLUSTER=PUSHER_CLUSTER
We’ll make use of the dotenv
library to load the variables contained in the .env
file into the Node environment. The dotenv
library should be initalized as early as possible in the application.
Sending votes
To let 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.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
...
});
app.post('/vote', (req, res) => {
const {body} = req;
const data = {
...body,
// set the selected property of the body to true
selected: true,
};
// trigger a new-entry event on the vote-channel
pusher.trigger('vote-channel', 'new-entry', data);
res.json(data);
});
...
- We added a POST route(
/vote
) to handle incoming requests. - Using object destructuring, we got the body of the request.
- The trigger is achieved using the
trigger
method which takes the trigger identifier(vote-channel
), an event name (new-entry
), and a payload. - The payload being sent contains the body of the request sent in. The
selected
property of the payload is set totrue
.
Start the server by running node server
in a terminal in the root folder of your project.
Home view
The home view of the project will house both the polling area and the area where the data is visualized. We’ll present the user with options and a submit button to place vote.
Open the home.html
file and replace it with the content below. The home.html
file is in the src/pages/home/
directory.
<!-- src/pages/home/home.html -->
<ion-header>
<ion-navbar>
<ion-title>
Vote
</ion-title>
</ion-navbar>
</ion-header>
<ion-content>
<div padding>
<h1 class="header">Who will win the world cup?</h1>
<p class="sub-header">* Place vote to see results</p>
</div>
<div class="vote-area" *ngIf="!voted">
<div class="options">
<button ion-button full class="option" color="light" [ngClass]="{active: selectedOption === option}" *ngFor="let option of optionsArray"
(click)="selectOption(option)">{{options[option].name}}</button>
</div>
<div>
<button ion-button block class="submit" (click)="vote()">Submit Vote!</button>
</div>
</div>
<div class="result-area" *ngIf="voted">
<!-- Charts area -->
</div>
</ion-content>
- In the code snippet above, we looped through
optionsArray
to create a view based on the player’s information. - The
vote
method will make use of theHttpClient
to send the user’s selection as a request to the server. - An option is active if the current
selectedOption
is equal to the option’s name.
Variables used will be defined in the component’s TypeScript file.
Styling
Replace the contents of home.scss
with the following:
// src/pages/home/home.scss
page-home {
.toolbar-background {
background: #1cd8d2; /* fallback for old browsers */
background: linear-gradient(to right, #93edc7, #1cd8d2);
}
.toolbar-title {
color: white;
}
.header {
font-size: 35px;
line-height: 1.1;
}
.sub-header{
margin: 0;
opacity: 0.5;
font-size: 13px;
font-weight: bold;
}
.options {
margin-top: 1.5rem;
padding: 0 17px 5px;
.option {
margin: 15px 0;
padding-top: 32px;
padding-bottom: 32px;
opacity: 0.6;
font-size: 17px;
font-weight: bold;
box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.2);
&.active {
border-left-width: 5px;
border-left-style: solid;
border-image: linear-gradient(to right, #93edc7, #1cd8d2) 1 100%;
box-shadow: 0 2px 2px 0 rgba(0, 0, 0, 0.2);
opacity: 0.9;
}
}
}
.submit {
background: #1cd8d2; /* fallback for old browsers */
background: linear-gradient(to right, #93edc7, #1cd8d2);
border-radius: 0;
padding-top: 35px;
padding-bottom: 35px;
margin-bottom: 0;
font-size: 19px;
font-weight: bold;
}
.response{
@extend .submit;
margin-top: 5rem;
}
}
These styles are meant to add a bit of life to our application. It also helps distinguish between states during application use.
Home 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.
// src/pages/home/home.ts
import { Component, OnInit } from '@angular/core';
import { NavController } from 'ionic-angular';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'page-home',
templateUrl: 'home.html',
})
export class HomePage implements OnInit {
constructor(
public navCtrl: NavController,
private http: HttpClient,
) {}
options = {
germany: { name: 'Germany', votes: 0 },
spain: { name: 'Spain', votes: 0 },
france: { name: 'France', votes: 0 },
nigeria: { name: 'Nigeria', votes: 0 },
};
optionsArray = Object.keys(this.options);
chartData = this.optionsArray.map((val) => this.options[val].votes);
selectedOption = '';
chartType = 'doughnut';
voted = false;
selectOption(option) {
this.selectedOption = this.selectedOption !== option ? option : '';
}
computeData(option) {
this.options = {
...this.options,
[option]: {
...this.options[option],
votes: ++this.options[option].votes,
},
};
this.chartData = this.optionsArray.map((val) => this.options[val].votes);
}
vote() {
if (this.selectedOption) {
this.http
.post('http://localhost:4000/vote', { option: this.selectedOption })
.subscribe((res) => {
this.voted = true;
});
}
}
ngOnInit() {
}
}
-
vote
: this method makes use of the native HttpClient service to make requests to our server. A request is sent, only if the user has made a selection. When a response is returned thevoted
property is set to true. -
computeData
: when a response is returned, this function takes theoption
and increments the votes for the selected option. -
selectOption
: this method will be used to set theselectedOption
property to theoption
param passed it.
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:
// src/app/app.module.ts
...
import { HomePage } from '../pages/home/home';
import {HttpClient, HttpClientModule} from '@angular/common/http';
@NgModule({
...
imports: [
...
HttpClientModule,
],
...
providers: [
...
HttpClient
]
})
export class AppModule {}
Let’s check how our application looks at this point. Make sure the server(node server
) and Ionic dev server(ionic serve
) are both running.
Introducing Pusher
So far we have an application that allows users be a part of the polling process but data updates aren’t happening in realtime. Let’s create a provider that will make it easier to include Pusher in our components.
We’ll create a Pusher provider to be used application wide. The Ionic CLI can aid in the provider creation. Open a terminal in your project’s root folder and run the following command.
ionic generate provider pusher
This command simply tells the CLI to generate a service named pusher
. Now open the pusher.ts
file in the src/providers/pusher/
directory and update it with the code below.
// src/providers/pusher/pusher.ts
import { Injectable } from '@angular/core';
import Pusher from 'pusher-js';
@Injectable()
export class PusherProvider {
constructor() {
const pusher = new Pusher('PUSHER_KEY', {
cluster: 'PUSHER_CLUSTER',
});
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.
Note: ensure you replace the
PUSHER_KEY
andPUSHER_CLUSTER
string with your actual credentials.
To make the service available application wide, import it into the app.module.ts
file.
// app.module.ts
...
import {HttpClient, HttpClientModule} from '@angular/common/http';
import { PusherProvider } from '../providers/pusher/pusher';
@NgModule({
...
providers: [
...
PusherProvider,
]
})
export class AppModule {}
The next step is to include the provider in the home.ts
file. Using the PusherProvider
, we’ll listen for vote
events from the server and update our app in real time according to votes placed by users.
Open the home.ts
file and update the ngOnInit
lifecycle to listen for Pusher events.
// src/pages/home/home.ts
...
import { PusherProvider } from '../../providers/pusher/pusher';
@Component({
selector: 'page-home',
templateUrl: 'home.html',
})
export class HomePage implements OnInit {
constructor(
...
private pusher: PusherProvider
) {}
...
ngOnInit() {
const channel = this.pusher.init();
channel.bind('new-entry', (data) => {
this.computeData(data.option);
});
}
Now our application should receive vote updates in realtime. Let’s include a chart component to visualize the data in the application.
Charts component
To visualize the data in our application, we’ll be making use of ng2-charts to create charts and present the data in a graphical format. Let’s make use of the components provided by the ng2-charts library. Update the home.html
file to include the canvas
provided by ng2-charts.
Open the home.html
file and update it with the contents below:
// src/pages/home/home.html
...
<ion-content>
...
<div class="result-area" *ngIf="voted">
<canvas baseChart [data]="chartData" [labels]="optionsArray" [chartType]="chartType"></canvas>
<div ion-button block class="response">Thank you for voting!</div>
</div>
</ion-content>
To make use of the ng2-charts
package, we’ll have to import the ChartsModule
into our module file.
Update the app.module.ts
file like so:
// src/app/app.module.ts
...
import {HttpClient, HttpClientModule} from '@angular/common/http';
import {ChartsModule} from 'ng2-charts';
@NgModule({
...
imports: [
...
HttpClientModule,
ChartsModule
],
...
})
...
At this point, your application should have realtime updates when votes are placed. Ensure that the server is running alongside the Ionic development server. If not, run node server
and ionic 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 engage the application. Data updates should be in realtime.
Testing on mobile devices
To test the application on your mobile device, download the IonicDevApp on your mobile device. Make sure your computer and your mobile device are connected to the same network. When you open the IonicDevApp, you should see Ionic apps running on your network listed.
Note: Both the server(
node server
), ngrok for proxying our server and the Ionic dev server(ionic serve
) must be running to get the application working. Run the commands in separate terminal sessions if you haven’t done so already.
To view the application, click on it and you should see a similar view to what was in the browser. Sending messages to the server might have worked in the browser but localhost doesn’t exist on your phone, so we’ll need to create a proxy to be able to send messages from mobile.
Using Ngrok as a proxy
To create a proxy for our server, we’ll download Ngrok. Visit the download page on the Ngrok website. Download the client for your OS. Unzip it and run the following command in the folder where Ngrok can be found:
./ngrok http 4000
Copy the forwarding url with https
and place it in the home.ts
file that previously had http://localhost:4000/vote
. Please do not copy mine from the screenshot above.
// src/pages/home/home.ts
...
export class ChatComponent implements OnInit {
...
vote() {
...
this.http
.post('<NGROK_URL>/vote', data)
.subscribe((res) => {
this.voted = true;
});
}
...
}
...
Ensure to include the forwarding URL you copied where the placeholder string is
Now you should be receiving messages sent from the phone on the browser. Preferably you can test it with two mobile devices connected to the same network.
Note: Both the server(
node server
), ngrok for proxying our server and the Ionic dev server(ionic serve
) must be running to get the application working. Run the commands in separate terminal sessions if you haven’t done so already.
To build your application to deploy on either the AppStore or PlayStore, follow the instructions found here.
Conclusion
Using Pusher, we’ve built out an application using the pub/sub pattern to recieve realtime updates. With the help of Chart.js, our data was well presented using charts. You can check out the repo containing the demo on GitHub.
2 July 2018
by Christian Nwamba