Build a live subscription count down with Angular

Introduction

In this tutorial, I will walk you through how you can add a realtime subscription count down functionality to your Angular application. In the app, we’ll have a page that displays the subscribers status, that is, the number of slots left. Then we will have another page for users to register themselves. Once the number of users that are registered is equal to the target of users that we want, then we close the registration in realtime.

At times, we need to present things to our users in a realtime manner so they know beforehand when things tend to happen. Consider that we are building an app where there are limited resources and we need maybe a limited amount of user. In such cases, It’s a better idea to let the user know what is happening in realtime. That is showing the user the slots that are remaining.

Here is a preview of what we are going to build:

angular-sub-count-demo-1

Prerequisite

You are required to have a basic knowledge of the following technologies to be able to follow along with this tutorial comfortably:

Setting up

Let’s get your system ready. First check that you have Node installed by typing the below command in a terminal on your system:

    $ node --version

The above command should print out the version of Node you have installed. If otherwise, you don’t have Node, visit the Node.js’s website and install the latest version of Node to your system.

Next, let’s install the Angular CLI. Angular provides a CLI that makes scaffolding of new Angular project and also working with Angular easier.

Install the Angular CLI globally to your system if you don’t have it installed already by executing the below command to a terminal on your system.

    $ npm install -g @angular/cli

If that was successful, you should now have the Angular command available globally on your system as ng.

Now use the Angular CLI command to create a new Angular project:

    $ ng new subscription-count-down

Choose yes for the prompt that asks if you Would like to add Angular routing and choose CSS for the stylesheet format. Then give it some minute to finalize the process.

angular-sub-count-options

Finally, cd into the newly created project and start up the app:

1$ cd subscription-count-down
2    $ ng serve --open

The app should now be available at http://localhost:4200 displaying a default Angular page like below:

angular-sub-count-landing-page

The src/app/app.component.ts file is the default component file that renders the page above.

Getting your Pusher Channels keys

We’ll be using the Channels pub/sub messaging feature to add realtime functionality to our app. The next thing we’ll do is to get our Channels API keys.

To get started with Pusher Channels, sign up for a free Pusher account. Then go to the dashboard and create a new Channels app. Now get the API keys of the app.

The keys are in this format:

1appId=<appId>
2    key=<key>
3    secret=<secret>
4    cluster=<cluster>

Take note of these keys because we’ll be making use of them soon.

Next, add the API key to the environment file so we can reference it from other files when we need it by replacing the content with below:

1// src/environments/environment.ts
2    
3    export const environment = {
4      production: false,
5      apiUrl: 'http://localhost:3000',
6      PUSHER_API_KEY: '<PUSHER_API_KEY>',
7      PUSHER_API_CLUSTER: '<PUSHER_APP_CLUSTER>'
8    };

Make sure to replace <PUSHER_API_KEY> and <PUSHER_APP_CLUSTER> placeholders with your correct API details.

In the object file above, the apiUrl property is the URL where our Node server will be running on which we’ll be creating later on.

Finally, add the pusher client SDK to the Angular app:

    $ npm install pusher-js

NOTE: Run the command while in the root folder of the project from a command line.

Creating the Node server

We need a server to be able to trigger events to Channels and also for creating and storing users. For the sake of brevity, we’ll use SQLite for our database. And we’ll be using Node for our server.

To set up a Node server, open up a new terminal, then run the following command in your terminal:

1# Create a new folder
2    $ mkdir subscription-count-down-server
3    
4    # Navigate to the folder
5    $ cd subscription-count-down-server
6    
7    # Create the Node entry file
8    $ touch app.js
9    
10    # Create a package.js file
11    $ touch package.json
12    
13    # Create the database file
14    $ touch app.db
15    
16    # Create the environment file for holding sensitive data
17    $ touch .env

These are the basic files we will need for the Node server.

Now add to the package.json file the necessary dependencies for the app:

1{
2      "name": "count-down-server",
3      "version": "1.0.0",
4      "description": "Count Down Server",
5      "main": "app.js",
6      "scripts": {
7        "test": "echo \"Error: no test specified\" && exit 1",
8        "serve": "node app.js"
9      },
10      "keywords": [
11        "Node",
12        "Count-Down",
13        "Pusher"
14      ],
15      "author": "Onwuka Gideon",
16      "license": "ISC",
17      "dependencies": {
18        "body-parser": "^1.18.3",
19        "cors": "^2.8.5",
20        "dotenv": "^6.2.0",
21        "express": "^4.16.4",
22        "pusher": "^2.2.0",
23        "sqlite3": "^4.0.6"
24      }
25    }

Next, add your Channels key to the .env file:

1PUSHER_APP_ID=<appId>
2    PUSHER_APP_KEY=<key>
3    PUSHER_APP_SECRET=<secret>
4    PUSHER_APP_CLUSTER=<cluster>

Make sure to replace <appId>, <key>, <secret>, and <cluster> placeholders with your correct API details.

Now import the dependencies we added earlier to the app.js file:

1// app.js
2    
3    require('dotenv').config()
4    
5    const express = require('express')
6    const cors = require('cors')
7    const bodyParser = require('body-parser')
8    const sqlite3 = require('sqlite3').verbose();
9    const Pusher = require('pusher');

Then, set up express, which is a Node.js web application framework for building web apps.

1// app.js
2    
3    // [...]
4    
5    const app = express()
6    const port = 3000
7    
8    app.use(cors())
9    app.use(bodyParser.json());
10    app.use(bodyParser.urlencoded({ extended: true }));
11    
12    app.get('/', (req, res) => res.status(200).send({msg: "Count down server!"}))
13    
14    app.listen(port, () => console.log(`Example app listening on port ${port}!`))
15    
16    // [...]

In the above code, we created a new route - / - which will render a JSON content once visited. We are only using it to test if express is working.

Now install the dependencies and start up the app:

1# Instal dependencies
2    $ npm install
3    
4    # Start up the app
5    $ npm run serve

If everything went well, the app should be accessible from http://localhost:3000/. If you visit the URL, you should get an output as below which shows that it works!

1{
2      "msg": "Count down server!"
3    }

Next, initialize the database:

1// app.js
2    // [...]
3    
4    const db = new sqlite3.Database('./app.db', sqlite3.OPEN_READWRITE);
5    
6    db.run("CREATE TABLE IF NOT EXISTS subscriptions (email VARCHAR(90), name VARCHAR(90))")
7    
8    // [...]

The first line above opens a new SQLite connection. While the second line checks if the subscriptions table exists, if it does not exists, it will create it.

Next, initialize Pusher server SDK:

1// app.js
2    
3    // [...]
4    
5    const pusher = new Pusher({
6        appId: process.env.PUSHER_APP_ID,
7        key: process.env.PUSHER_APP_KEY,
8        secret: process.env.PUSHER_APP_SECRET,
9        cluster: process.env.PUSHER_APP_CLUSTER,
10        encrypted: true
11    });
12    
13    // [...]

Now create a new route that we can use to get the total number of users that have subscribed and the number of targets we want. The target is the maximum number of users that we want to be able to subscribe:

1// app.js
2    
3    // [...]
4    
5    app.get('/userCount', (req, res) => {
6        db.each(`SELECT count(*) AS userCount FROM subscriptions`, (err, row) => {
7            res.status(201).send({userCount: row.userCount, targetCount: 5}) 
8        });
9    })
10    
11    // [...]

Here, we hard-coded the targetCount to five. If the total number of registered user reaches five, no other user should be able to register again.

Next, create a new endpoint named addUser for adding new users:

1// app.js
2    
3    // [...]
4    
5    app.post('/addUser',  (req, res) => {
6        const email = req.body.email;
7        const name = req.body.name;
8    
9        db.run(`INSERT INTO subscriptions (name, email) values ('${name}', '${email}')`)
10        
11        db.serialize(function() {
12            db.each(`SELECT count(*) AS userCount FROM subscriptions`, (err, row) => {
13                res.status(201).send({userCount: row.userCount}) 
14            });
15        });
16    })
17    
18    // [...]

Finally, create a new endpoint named /pusher/trigger for triggering events to Channels.

1// app.js
2    
3    // [...]
4    
5    app.post('/pusher/trigger', (req, res) => {
6        const channel_name = req.body.channel_name;
7        const event_name = req.body.event_name;
8        const data = req.body.data;
9     
10        pusher.trigger(channel_name, event_name, data);
11        
12        res.status(200).send(data)
13    })
14    
15    // [...]

To trigger events to Channels, we call the trigger method from the Pusher SDK passing along the name of the channel where we want to trigger the event to, the name of the event, and some data to pass along with the event.

Restart the server so the new changes will be picked up.

Creating the app client

Before we start building the app components, let’s create the service for our app. We’ll create two services - count-down service and pusher service. The count-down service will contain services for the entire component while the pusher service will contain services that are related to Channels, say we want to trigger event or listen to an event.

Creating our app service

In Angular, services are great ways of sharing information among classes that don't know each other.

Now, create the count-down service using the Angular CLI command:

1# Make sure you are in the root folder of the project
2    $ ng generate service count-down

You should now see a new file that is created named src/app/count-down.service.ts.

Inside the file, replace its content with the below code:

1// src/app/count-down.service.ts
2    
3    import { Injectable } from '@angular/core';
4    import { HttpClient } from '@angular/common/http';
5    import { Observable } from 'rxjs';
6    
7    import { environment } from '../environments/environment';
8    
9    interface userCount {
10      userCount: number,
11      targetCount: number
12    }
13    
14    export interface userData {
15      name: String;
16      email: String;
17    }
18    
19    @Injectable({
20      providedIn: 'root'
21    })
22    export class CountDownService {
23    
24      constructor(private http: HttpClient) { }
25    
26      getUserCount (): Observable<userCount> {
27        return this.http.get<userCount>(`${environment.apiUrl}/userCount`)
28      }
29    
30      addNewUser (userData: userData): Observable<userData> {
31        return this.http.post<userData>(`${environment.apiUrl}/addUser`, userData)
32      }
33    }

In the preceding code:

We imported the following modules:

  • HttpClient - the HttpClient is an Angular module that helps users to communicate with backend services over the HTTP protocol.
  • Observable - we’ll use the Observable module to handle asynchronous requests.
  • environment - this is the environment key that we add to the src/app/environments/environment.ts file earlier.

Next, we created two interfaces, namely userCount and userData, which defines the type of datatype they accept.

  • Recall from our Node server, we created an endpoint named /userCount which returns the userCount and targetCount as an object. This is the format that we have defined in the userCount interface.
  • The userData defines the data for a new user.

Next, we injected the HttpClient class into the class using constructor(private http: HttpClient) { } so that we can access it using *this*.http in any part in the class.

Finally, we created two methods:

  • getUserCount - this method will make a request to the /userCount endpoint to get the number of registered users and the target count.
  • addNewUser - when we want to add a new user, we will call the function, passing along the user’s name and email as an object.

Pusher service

Next, create the Pusher service

    $ ng generate service pusher

Now update the content of the service file:

1// src/app/pusher.service.ts
2    
3    import { Injectable } from '@angular/core';
4    import Pusher from 'pusher-js';
5    import { HttpClient } from '@angular/common/http';
6    import { Observable } from 'rxjs';
7    
8    import { environment } from '../environments/environment';
9    
10    @Injectable({
11      providedIn: 'root'
12    })
13    export class PusherService {
14    
15      pusher: any
16    
17      constructor(private http: HttpClient) { 
18        this.pusher = new Pusher(environment.PUSHER_API_KEY, {
19          cluster: environment.PUSHER_API_CLUSTER,
20          forceTLS: true
21        });
22      }
23    
24      subScribeToChannel(channelName: String, events: String[], cb: Function) {
25        var channel = this.pusher.subscribe(channelName);
26        
27        events.forEach( event => {
28          channel.bind(event, function(data) {
29            cb(data)
30          });
31        })
32      }
33    
34      triggerEvent(channelName: String, event: String, data: Object): Observable<Object> {
35        return this.http.post(`${environment.apiUrl}/pusher/trigger`, {
36          channel_name: channelName,
37          event_name: event,
38          data: data
39        })
40      }
41    }

In the preceding code:

  • We injected the HttpClient module just as we did in the previous service file.
  • Next, we initialize the Pusher client JavaScript SDK in the constructor of the class.
  • Next, we created two functions:
    • subScribeToChannel() - using this function we can subscribe to a channel and start to listen for events on that channel. The function accepts three parameters. The first one which is the channel name you want to subscribe to. The second parameter is an array of events you want to listen for. The last parameter is a callback function that is called when an event is triggered.
    • triggerEvent() - if we want to trigger an event, we only need to call this function passing along the channel name, the event name, and the data we want to send along. The function makes an HTTP request to our Node server to trigger the event.

Now let’s build the client-facing part of the app which we are doing with Angular. We’ll divide the app into two components:

  • CountDown component. This component will hold the count number and is the first page that will be shown to users.
  • Register component. This component will contain the form and logic for adding new users.

Before you can use the HttpClient, we need to import the Angular HttpClientModule. Import the HttpClientModule to the app.module.ts file:

1// src/app/app.module.ts
2    
3    [...]
4    import { HttpClientModule } from '@angular/common/http';
5    
6      [...]
7      imports: [
8        [...]
9        HttpClientModule,
10        [...]
11      ],
12    
13    [...]

CountDown component

Create the CountDown component using the Angular CLI command:

    $ ng generate component count-down

The above command will create a new folder, src/app/count-down/, and generates the four files for the CountDownComponent.

The count-down.component.ts is the main component class, which includes the other three files to itself. This is the file that will contain the logic for the component.

All our markup definitions go inside the count-down.component.html file. CSS styles for the component will reside in the count-down.component.css file. If we need to write a test for the component, we can do that in the count-down.component.spec.ts file.

Update the route to render this component:

1// src/app/app-routing.module.ts
2    
3    // [...]
4    
5    import { CountDownComponent } from './count-down/count-down.component';
6    
7    const routes: Routes = [
8      { path: '', component: CountDownComponent }
9    ];
10    
11    // [...]

Next, remove the default rendered page and replace it with the below mark-up:

1<!-- src/app/app.component.html -->
2    
3    <div class="container">
4      <div class="content"> 
5        <router-outlet></router-outlet>
6      </div>
7    </div>

If you now reload the page, you see it renders the html file for the CountDown component:

angular-sub-count-countdown-works

Next, update the markup for the CountDown component:

1<!-- src/app/count-down/count-down.component.html -->
2    
3    <div>
4        <div *ngIf="!countDown"> Registration closed! </div>
5        <nav *ngIf="countDown">
6          <a routerLink="/register">Register</a>
7        </nav>
8      <h1>Subscription count down:</h1>
9      <div class="count-down"> {{ countDown }} </div>
10    </div>

Now add some styling to the app:

1/* src/style.css */
2    
3    h1 {
4        color: #369;
5        font-family: Arial, Helvetica, sans-serif;
6        font-size: 200%;
7    }
8    h2, h3 {
9        color: #369;
10        font-family: Arial, Helvetica, sans-serif;
11        font-weight: lighter;
12    }
13    body {
14        overflow: hidden;
15    }
16    body, input[type="text"], button {
17        color: #888;
18        font-family: Cambria, Georgia;
19    }
20    /* everywhere else */
21    * {
22        font-family: Arial, Helvetica, sans-serif;
23    }
24    .container {
25        display: grid;
26        height: 100vh;
27        width: 100vw;
28    }
29    .content {
30        align-self: center;
31        justify-self: center;
32    }
33    .count-down {
34        text-align: center;
35        font-size: 300%;
36    }
37    .from-input {
38        display: block;
39        width: 300px;
40        margin: 8px;
41        padding: 15px;
42        font-size: 100%;
43    }
44    .from-submit {
45        background: #369;
46        color: white;
47        font-family: Arial, Helvetica, sans-serif;
48        font-size: 140%;
49        border-radius: 3px;
50        width: 100%;
51        padding: 10px;
52    }
53    .success-message {
54        background: green; 
55        color: antiquewhite; 
56        padding: 10px;
57        border-radius: 3px;
58        margin: 4px;
59    }

Reload the page to see how it looks now.

We have been able to render the CountDown component, but it does not show real data yet. And also it is showing “registration is closed!”. It should show that when the users registered is equal to the target users and otherwise show a registration form.

Now, let’s work on this component.

Import the two services we created earlier to the component file:

1// src/app/count-down/count-down.component.ts
2    
3    // [...]
4    
5    import { CountDownService } from '../count-down.service';
6    import { PusherService } from '../pusher.service';
7    
8    // [...]

Notice that we are rendering the {{ countDown }} variable to the markup file, which does not have any effect because we have not defined the variable. This variable will hold the number of slots remaining.

Define the variable:

1// src/app/count-down/count-down.component.ts
2    
3    // [...]
4    
5    export class CountDownComponent implements OnInit {
6      countDown: number;
7    
8    // [...]

Next, inject the services we imported to the component class so we can access them easily:

1// src/app/count-down.component.ts
2    
3    // [....]
4      constructor(
5        private countDownService: CountDownService,
6        private pusherService: PusherService
7      ) { }
8    // [....]

Now we want to get the countDown value from the Node server when the page loads and also listen to new events when a new user subscribes.

1// src/app/count-down.component.ts
2    
3    // [....] 
4     ngOnInit() {
5        this.countDownService.getUserCount()
6          .subscribe(data => {
7            this.countDown = data.targetCount - data.userCount
8          });
9        
10        this.pusherService.subScribeToChannel('count-down', ['newSub'], (data) => {
11          // console.log(data)
12          this.countDown -= 1;
13        });
14      }
15    // [....]

Now when the component is mounted, we call the getUserCount() function from the countDownService service to get the targetCount and userCount, which we then use to calculate the number of the slots that are remaining.

Then, we call the pusherService.subScribeToChannel() function to the count-down and start listening for a newSub event. Once there is a newSub event, we reduce the countDown value by one. And all this happens in realtime. Note that the channel name ('count-down') and event name ('newSub') can be anything you like. You only need to make sure that you trigger the same value on the Node server if you change it.

If you reload the page, you should see now that it shows the remaining slots and also a link where a user can register form.

angular-sub-count-sub-count

Register component

We also need another component that renders the form for a user to subscribe.

Create the Register component using the Angular CLI command:

    $ ng generate component register

Add a route that renders the Register component:

1// src/app/app-routing.module.ts
2    [...]
3    
4    import { RegisterComponent } from './register/register.component';
5    
6    [...]
7      const routes: Routes = [
8        { path: 'register', component: RegisterComponent },
9        { path: '', component: CountDownComponent}
10      ];
11    [...]

Now, if we visit http://localhost:4200/register, it should show the register page.

Next, import the two services we created earlier to the component file:

1// src/app/register/register.component.ts
2    
3    // [...]
4    
5    import { CountDownService, userData } from '../count-down.service';
6    import { PusherService } from '../pusher.service';
7    
8    // [...]

Define the input form detail for two-way binding:

1// [...]
2    
3    export class RegisterComponent implements OnInit {
4      userData: userData = {
5        name: '',
6        email: ''
7      };
8    
9      userAdded: Boolean = false
10    
11    // [...]

The userData is the input we are expecting from the user as they fill the registration form. We’ll use userAdded Boolean to toggle between when to show the user a success message as the submit the form.

Next, inject the service we imported to the class:

1// src/app/register/register.component.ts
2    
3    // [....]
4      constructor(
5        private countDownService: CountDownService,
6        private pusherService: PusherService
7      ) { }
8    // [....]

Next, add a function that will be called when a user clicks to submit the form:

1// [...] 
2      ngOnInit() {}
3    
4      addUser(): void {
5        this.countDownService.addNewUser(this.userData)
6          .subscribe( data => {
7            this.userAdded = true 
8            this.userData = {name:'', email:''}
9          })
10        
11        this.pusherService.triggerEvent('count-down', 'newSub', this.userData)
12            .subscribe( data => {
13              console.log(data)
14            })
15      }
16    
17    // [...]

In the function we created above, we call the addNewUser function from the countDownService to register the user. Then finally, we trigger an event to Channels so it notifies all connected user that a new user has just registered so that the count down number is updated.

Next, update the HTML mark up for the form:

1<!-- src/app/register/register.component.html -->
2    <div>
3      <nav>
4        <a routerLink="/">Got to count-down</a>
5      </nav>
6      <div>
7        <div class="success-message" *ngIf="userAdded"> User Created Successfully! </div>
8        <form> 
9          <input 
10            type="text" 
11            class="from-input" 
12            placeholder="Email" 
13            [(ngModel)]="userData.email" 
14            name="email"
15          />
16          <input 
17            type="text" 
18            class="from-input" 
19            placeholder="Name" 
20            [(ngModel)]="userData.name" 
21            name="name"
22          />
23          <button class="from-submit" (click)="addUser()"> Submit </button>
24        </form>
25      </div>
26    </div>

Finally, add the FormsModule, which is required when working with forms:

1// src/app/app.module.ts
2    
3    [...]
4    import { FormsModule } from '@angular/forms';
5    
6      [...]
7      imports: [
8        [...]
9        FormsModule,
10        [...]
11      ],
12    
13    [...]

And that is it! Let’s test what we have built:

  • Start up the Node server if it is not running already - npm run serve
  • Start the frontend app if it’s not running, then open the app in two or more different tabs in your browser.
  • In one of the tabs, navigate to the register page and then fill the form and submit. The number of slots remaining should reduce in realtime.

Conclusion

In this tutorial, we have learned how to add realtime functionality to our Angular apps by building a subscription count down app. There are other use-cases where this same approach can be applied to. Feel free to them explore with the knowledge that you have gained.

You can get the complete code of the app on GitHub.