Build a live activity feed with Angular 7

Introduction

In this article, we are going to show how you can add a realtime activity post feed to your website to keep users engaged without the need to resort to going somewhere else or forcing a browser refresh event often. It is an important feature of social media these days is a realtime feed as it offers increased engagement among its users.

We are going to build a system where a user will be presented with a form to add a new conversation. You can find the entire source code of the application in this GitHub repository.

Here is a visual representation of what we will be building:

angular-7-feed-demo

Prerequisites

Set up the server

Let’s set up a simple Node server that will process the posts published by users of the website. While the server will perform validation to check for valid data and make sure not to allow duplications, its major job is to publish the post to Pusher Channels to enable realtime functionalities.

The first step is to create a directory to house the application. You should create a directory called pusher-angular-realtime-feed. In the newly created directory, you are to create another folder called server - this distinction will prove its worth when we are building the client in AngularJS.

We can go ahead to install the dependencies needed to build our application. Create a package.json file and paste in the following:

1// pusher-angular-realtime-feed/server/package.json
2    {
3      "name": "pusher-activity-feed-api",
4      "version": "1.0.0",
5      "description": "",
6      "main": "index.js",
7      "scripts": {
8        "test": "echo \"Error: no test specified\" && exit 1"
9      },
10      "author": "",
11      "license": "MIT",
12      "dependencies": {
13        "body-parser": "^1.18.3",
14        "cors": "^2.8.5",
15        "dotenv": "^6.1.0",
16        "express": "^4.16.4",
17        "pusher": "^2.1.3"
18      }
19    }

The dependencies defined above need to be installed. To do that, run:

    npm install

Now that we have our server dependencies installed, it is time to build the actual server itself. But before that is done, we need to make our Pusher Channels credentials accessible to the application. To do that, we will create a variable.env file and input the credentials we got from the Pusher Channels dashboard in it:

1// pusher-angular-realtime-feed/server/variable.env
2    PUSHER_APP_ID=<your app id>
3    PUSHER_APP_KEY=<your app key>
4    PUSHER_APP_SECRET=<your app secret>
5    PUSHER_APP_CLUSTER=<your app cluster>
6    PUSHER_APP_SECURE=1

Create an index.js file and paste the following code:

1// pusher-angular-realtime-feed/server/index.js
2    
3    const express = require('express');
4    const Pusher = require('pusher');
5    const cors = require('cors');
6    
7    require('dotenv').config({ path: 'variable.env' });
8    
9    const app = express();
10    const port = process.env.PORT || 3000;
11    
12    let pusher = new Pusher({
13      appId: process.env.PUSHER_APP_ID,
14      key: process.env.PUSHER_APP_KEY,
15      secret: process.env.PUSHER_APP_SECRET,
16      encrypted: process.env.PUSHER_APP_SECURE,
17      cluster: process.env.PUSHER_APP_CLUSTER,
18    });
19    
20    app.use(cors());
21    app.use(express.json());
22    
23    app.get('/', function(req, res) {
24      res.status(200).send({ service: 'Pusher activity feed API' });
25    });
26    
27    // An in memory structure to prevent posts with duplicate titles
28    const titles = [];
29    
30    app.post('/submit', (req, res) => {
31      const title = req.body.title;
32      const body = req.body.body;
33    
34      if (title === undefined) {
35        res
36          .status(400)
37          .send({ message: 'Please provide your post title', status: false });
38        return;
39      }
40    
41      if (body === undefined) {
42        res
43          .status(400)
44          .send({ message: 'Please provide your post body', status: false });
45        return;
46      }
47    
48      if (title.length <= 5) {
49        res.status(400).send({
50          message: 'Post title should be more than 5 characters',
51          status: false,
52        });
53        return;
54      }
55    
56      if (body.length <= 6) {
57        res.status(400).send({
58          message: 'Post body should be more than 6 characters',
59          status: false,
60        });
61        return;
62      }
63    
64      const index = titles.findIndex(element => {
65        return element === title;
66      });
67    
68      if (index >= 0) {
69        res
70          .status(400)
71          .send({ message: 'Post title already exists', status: false });
72        return;
73      }
74    
75      titles.push(title.trim());
76      pusher.trigger('realtime-feeds', 'posts', {
77        title: title.trim(),
78        body: body.trim(),
79        time: new Date(),
80      });
81    
82      res
83        .status(200)
84        .send({ message: 'Post was successfully created', status: true });
85    });
86    
87    app.listen(port, function() {
88      console.log(`API is running at ${port}`);
89    });

While the above code seems like a lot, here is a breakdown of what it does:

  • It loads your Pusher Channels credentials from the variable.env file we created earlier.
  • Creates two HTTP endpoints. One for the index page and the other - /submit for validating and processing users’ posts.

Please note that we are making use of an in-memory storage system hence posts that are created will not be persisted in a database. This tutorial is focused on the realtime functionalities.

You will need to run this server with the following command:

    node server/index.js

Set up the Angular app

We will be making use of Angular 7 to create the website that interacts with the backend server we have created earlier. Angular apps are usually created with a command-line tool called ng. If you don’t have that installed, you will need to run the following command in your terminal to fetch it:

    npm install -g @angular/cli

Once the installation of the ng tool is finished, you can then go ahead to set up our Angular application. To do that, you will need to run the command below in the root directory of the project - in this case, pusher-angular-realtime-feed:

     ng new client

You will need to select yes when asked to use routing. You will also need to select CSS when asked for a stylesheet format

This command will create a new folder called client in the root of your project directory, and install all the dependencies needed to build and run the Angular application.

Next, we will cd into the newly created directory and install the client SDK for Pusher Channels, which we’ll be needing to implement realtime features for our application’s frontend:

    npm install pusher-js

Now that we have all dependencies installed, it is time to actually build it. The application will consist of three pages:

  • The dashboard page located at /dashboard
  • A page to create/add a new post. This would be located at /new
  • A page to display the created posts in realtime. This would be located at /feed

Each one of the pages are components, so we will need to create them. The ng tool we installed earlier includes a generate command that would help us with the entire job. To generate them, you need to run the following commands in a terminal window:

1ng generate component FeedForm
2    ng generate component dashboard
3    ng generate component page-not-found
4    ng generate component Feed

We will need to build the dashboard page first of all as it will be the landing page. We will update it to include some relevant links to other pages of the application plus a Pusher Channels logo somewhere:

1// pusher-angular-realtime-feed-api/client/src/app/dashboard/dashboard.component.html
2    
3    <div style="text-align:center">
4      <h1>Welcome to {{ title }}!</h1>
5      <img
6        width="300"
7        alt="Pusher Logo"
8        src="https://djmmlc8gcwtqv.cloudfront.net/imgs/channels/channels-fold.png"
9      />
10    </div>
11    <div style="text-align:center">
12      <h2>Here are some links to help you start:</h2>
13    
14      <nav>
15        <a routerLink="/new" routerLinkActive="active">Create new Post</a> <br />
16        <br />
17        <a routerLink="/feed" routerLinkActive="active">View realtime feed</a>
18      </nav>
19    </div>

Since dashboards are supposed to look good enough to make the user want to explore more, we will make use of Bulma. We need to include it in the index.html page Angular loads every time our site is visited:

1// pusher-angular-realtime-feed-api/client/src/index.html
2    
3    <!DOCTYPE html>
4    <html>
5      <head>
6        <meta charset="utf-8">
7        <meta name="viewport" content="width=device-width, initial-scale=1">
8        <title>Pusher realtime feed</title>
9        <meta name="viewport" content="width=device-width, initial-scale=1" />
10        <link rel="icon" type="image/x-icon" href="favicon.ico" />
11        <base href="/" />
12        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css">
13      <body>
14          <section class="section">
15              <div class="container">
16                <app-root></app-root>
17              </div>
18           </section>
19      </body>
20    </html>

Now that we have our dashboard page, we will go ahead to create the page where users will be able to create a new post:

1// pusher-angular-realtime-feed-api/client/src/app/feed-form/feed-form.component.html
2    <div class="columns">
3      <div class="column is-5">
4        <h3 class="notification">Create a new post</h3>
5        <div *ngIf="infoMsg" class="notification is-success">{{ infoMsg }}</div>
6        <div *ngIf="errorMsg" class="is-danger notification">{{ errorMsg }}</div>
7        <form>
8          <div class="field">
9            <label class="label">Title : </label>
10            <div class="control">
11              <input
12                class="input"
13                type="text"
14                placeholder="Post title"
15                name="title"
16                [(ngModel)]="title"
17              />
18            </div>
19          </div>
20    
21          <div><label>Message: </label></div>
22          <div>
23            <textarea
24              [(ngModel)]="content"
25              rows="10"
26              cols="70"
27              [disabled]="isSending"
28              name="content"
29            ></textarea>
30          </div>
31        </form>
32      </div>
33    
34      <div class="is-7"></div>
35    </div>
36    
37    <button (click)="submit()" class="button is-info" [disabled]="isSending">
38      Send
39    </button>

The form we have created above obviously needs to be processed and sent to the backend server we have created earlier. To do that, we need to update the feed-form.component.ts file with the following content:

1// pusher-angular-realtime-feed-api/client/src/app/feed-form/feed-form.component.ts
2    import { Component, OnInit, Injectable } from '@angular/core';
3    import { HttpClient } from '@angular/common/http';
4    
5    @Component({
6      selector: 'app-feed-form',
7      templateUrl: './feed-form.component.html',
8      styleUrls: ['./feed-form.component.css'],
9    })
10    export class FeedFormComponent implements OnInit {
11      private isSending: boolean;
12      private httpClient: HttpClient;
13    
14      public content: string;
15      public errorMsg: string;
16      public infoMsg: string;
17      public title: string;
18    
19      constructor(private http: HttpClient) {
20        this.httpClient = http;
21      }
22    
23      submit() {
24        this.errorMsg = '';
25        this.isSending = true;
26        this.infoMsg = 'Processing your request.. Wait a minute';
27    
28        this.http
29          .post('http://localhost:3000/submit', {
30            title: this.title,
31            body: this.content,
32          })
33          .toPromise()
34          .then((data: { message: string; status: boolean }) => {
35            this.infoMsg = data.message;
36            setTimeout(() => {
37              this.infoMsg = '';
38            }, 1000);
39    
40            this.isSending = false;
41            this.content = '';
42            this.title = '';
43          })
44          .catch(error => {
45            this.infoMsg = '';
46            this.errorMsg = error.error.message;
47    
48            this.isSending = false;
49          });
50      }
51    
52      ngOnInit() {}
53    }

The submit method is the most interesting in the above snippet as it is responsible for sending the request to the backend and sending instructions to update the UI as needed - infoMsg and errorMsg.

While we now have the dashboard and the post creation page, we still have no way to view the posts in realtime. We need to create the feed page to complete this task.

1// pusher-angular-realtime-feed-api/client/src/app/feed/feed.component.html
2    <h1 class="notification is-info">Your feed</h1>
3    <div class="columns">
4      <div class="column is-7">
5        <div *ngFor="let feed of feeds">
6          <div class="box">
7            <article class="media">
8              <div class="media-left">
9                <figure class="image is-64x64">
10                  <img
11                    src="https://bulma.io/images/placeholders/128x128.png"
12                    alt="Image"
13                  />
14                </figure>
15              </div>
16              <div class="media-content">
17                <div class="content">
18                  <p>
19                    <strong>{{ feed.title }}</strong>
20                    <small>{{ feed.createdAt }}</small> <br />
21                    {{ feed.content }}
22                  </p>
23                </div>
24              </div>
25            </article>
26          </div>
27        </div>
28      </div>
29    </div>

If you take a proper look at the template above, you will notice that we have a section with the following content, <div *ngFor="let feed of feeds"> . This implies a for-loop of all feeds and displaying them in the UI. The next step is to generate those feeds. We will be making use of a concept called services in Angular.

We need to create a Feed service that will be responsible for fetching realtime posts from Pusher Channels and feeding them to our template. You can create the service by running the command below:

1ng generate service Feed
2    ng generate class Feed

You will need to edit both files with the following contents:

1// pusher-angular-realtime-feed/client/src/app/feed.ts
2    export class Feed {
3      constructor(
4        public title: string,
5        public content: string,
6        public createdAt: Date
7      ) {
8        this.title = title;
9        this.content = content;
10        this.createdAt = createdAt;
11      }
12    }
1// pusher-angular-realtime-feed/client/src/app/feed.service.ts
2    
3    import { Injectable } from '@angular/core';
4    import { Subject, Observable } from 'rxjs';
5    import { Feed } from './feed';
6    import Pusher from 'pusher-js';
7    
8    @Injectable({
9      providedIn: 'root',
10    })
11    export class FeedService {
12      private subject: Subject<Feed> = new Subject<Feed>();
13    
14      private pusherClient: Pusher;
15    
16      constructor() {
17        this.pusherClient = new Pusher('YOUR KEY HERE', { cluster: 'CLUSTER' });
18    
19        const channel = this.pusherClient.subscribe('realtime-feeds');
20    
21        channel.bind(
22          'posts',
23          (data: { title: string; body: string; time: string }) => {
24            this.subject.next(new Feed(data.title, data.body, new Date(data.time)));
25          }
26        );
27      }
28      
29      getFeedItems(): Observable<Feed> {
30        return this.subject.asObservable();
31      }
32    }

Kindly remember to add your credentials else this would not work as expected.

In feed.service.ts, we have created an observable we can keep on monitoring somewhere else in the application. We also subscribe to the realtime-feeds channel and posts event after which we provide a callback that adds a new entry to our observable.

The next step is to wire up the FeedService to the feed.component.html we saw above and provide it the feeds variable. To do that, we will need to update the feed.component.ts file with the following:

1// pusher-angular-realtime-feed-api/client/src/app/feed/feed.component.ts
2    import { Component, OnInit, OnDestroy } from '@angular/core';
3    import { FeedService } from '../feed.service';
4    import { Feed } from '../feed';
5    import { Subscription } from 'rxjs';
6    
7    @Component({
8      selector: 'app-feed',
9      templateUrl: './feed.component.html',
10      styleUrls: ['./feed.component.css'],
11      providers: [FeedService],
12    })
13    export class FeedComponent implements OnInit, OnDestroy {
14      public feeds: Feed[] = [];
15    
16      private feedSubscription: Subscription;
17    
18      constructor(private feedService: FeedService) {
19        this.feedSubscription = feedService
20          .getFeedItems()
21          .subscribe((feed: Feed) => {
22            this.feeds.push(feed);
23          });
24      }
25    
26      ngOnInit() {}
27    
28      ngOnDestroy() {
29        this.feedSubscription.unsubscribe();
30      }
31    }

In the above, we take a dependency on FeedService and subscribe to an observable it provides us with it’s getFeedItems method. Every single time a new Feed item is resolved from the subscription, we add it to the list of feeds we already have.

And finally, we implemented an Angular lifecycle method, ngOnDestroy where we unsubscribe from the observable. This is needed so as to prevent against a potential memory leak and ngOnDestroy just seems to be the perfect place to perform some clean up operations as can be seen in the documentation.

While we are almost done, we will need to create those endpoints we talked about earlier and also configure the routing for the application. We need to update the app.module.ts file to contain the following code:

1// pusher-angular-realtime-feed/client/src/app/app.module.ts
2    
3    import { BrowserModule } from '@angular/platform-browser';
4    import { NgModule } from '@angular/core';
5    import { FormsModule } from '@angular/forms';
6    import { RouterModule, Routes } from '@angular/router';
7    import { HttpClientModule } from '@angular/common/http';
8    
9    import { AppRoutingModule } from './app-routing.module';
10    import { AppComponent } from './app.component';
11    import { FeedFormComponent } from './feed-form/feed-form.component';
12    import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
13    import { DashboardComponent } from './dashboard/dashboard.component';
14    import { FeedComponent } from './feed/feed.component';
15    
16    const appRoutes: Routes = [
17      { path: 'new', component: FeedFormComponent },
18      {
19        path: 'feed',
20        component: FeedComponent,
21      },
22      {
23        path: '',
24        redirectTo: '/dashboard',
25        pathMatch: 'full',
26      },
27      { path: 'dashboard', component: DashboardComponent },
28      { path: '**', component: PageNotFoundComponent },
29    ];
30    
31    @NgModule({
32      declarations: [
33        AppComponent,
34        FeedFormComponent,
35        PageNotFoundComponent,
36        DashboardComponent,
37        FeedComponent,
38      ],
39      imports: [
40        RouterModule.forRoot(
41          appRoutes,
42          { enableTracing: true } 
43        ),
44        BrowserModule,
45        HttpClientModule,
46        AppRoutingModule,
47        FormsModule,
48      ],
49      exports: [RouterModule],
50    
51      bootstrap: [AppComponent],
52    })
53    export class AppModule {}

As a final step, make sure to edit app.component.html to inlcude only the following line:

1// pusher-angular-realtime-feed/client/src/app/app.component.html
2    <router-outlet></router-outlet>

You should delete whatever else is in the file

With that done, it is time to see our app in action. That can done by issuing the following commands:

1cd client
2    ng serve

The application will be visible at https://localhost:4200.

Conclusion

In this tutorial, you have learned how to build an activity feed with Angular 7 and how to set up Pusher Channels for adding realtime functionality to the feed.

Thanks for reading! Remember that you can find the source code of this app in this GitHub repository.