Build a live activity feed with Angular 7
You will need Node 6+ and the Angular 7 CLI installed on your machine.
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:
Prerequisites
- Node.js (v6 and above)
- Angular ( v7 )
- A Pusher Channels application. Create one here.
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:
// pusher-angular-realtime-feed/server/package.json
{
"name": "pusher-activity-feed-api",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "MIT",
"dependencies": {
"body-parser": "^1.18.3",
"cors": "^2.8.5",
"dotenv": "^6.1.0",
"express": "^4.16.4",
"pusher": "^2.1.3"
}
}
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:
// pusher-angular-realtime-feed/server/variable.env
PUSHER_APP_ID=<your app id>
PUSHER_APP_KEY=<your app key>
PUSHER_APP_SECRET=<your app secret>
PUSHER_APP_CLUSTER=<your app cluster>
PUSHER_APP_SECURE=1
Create an index.js
file and paste the following code:
// pusher-angular-realtime-feed/server/index.js
const express = require('express');
const Pusher = require('pusher');
const cors = require('cors');
require('dotenv').config({ path: 'variable.env' });
const app = express();
const port = process.env.PORT || 3000;
let pusher = new Pusher({
appId: process.env.PUSHER_APP_ID,
key: process.env.PUSHER_APP_KEY,
secret: process.env.PUSHER_APP_SECRET,
encrypted: process.env.PUSHER_APP_SECURE,
cluster: process.env.PUSHER_APP_CLUSTER,
});
app.use(cors());
app.use(express.json());
app.get('/', function(req, res) {
res.status(200).send({ service: 'Pusher activity feed API' });
});
// An in memory structure to prevent posts with duplicate titles
const titles = [];
app.post('/submit', (req, res) => {
const title = req.body.title;
const body = req.body.body;
if (title === undefined) {
res
.status(400)
.send({ message: 'Please provide your post title', status: false });
return;
}
if (body === undefined) {
res
.status(400)
.send({ message: 'Please provide your post body', status: false });
return;
}
if (title.length <= 5) {
res.status(400).send({
message: 'Post title should be more than 5 characters',
status: false,
});
return;
}
if (body.length <= 6) {
res.status(400).send({
message: 'Post body should be more than 6 characters',
status: false,
});
return;
}
const index = titles.findIndex(element => {
return element === title;
});
if (index >= 0) {
res
.status(400)
.send({ message: 'Post title already exists', status: false });
return;
}
titles.push(title.trim());
pusher.trigger('realtime-feeds', 'posts', {
title: title.trim(),
body: body.trim(),
time: new Date(),
});
res
.status(200)
.send({ message: 'Post was successfully created', status: true });
});
app.listen(port, function() {
console.log(`API is running at ${port}`);
});
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:
ng generate component FeedForm
ng generate component dashboard
ng generate component page-not-found
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:
// pusher-angular-realtime-feed-api/client/src/app/dashboard/dashboard.component.html
<div style="text-align:center">
<h1>Welcome to {{ title }}!</h1>
<img
width="300"
alt="Pusher Logo"
src="https://djmmlc8gcwtqv.cloudfront.net/imgs/channels/channels-fold.png"
/>
</div>
<div style="text-align:center">
<h2>Here are some links to help you start:</h2>
<nav>
<a routerLink="/new" routerLinkActive="active">Create new Post</a> <br />
<br />
<a routerLink="/feed" routerLinkActive="active">View realtime feed</a>
</nav>
</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:
// pusher-angular-realtime-feed-api/client/src/index.html
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Pusher realtime feed</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" type="image/x-icon" href="favicon.ico" />
<base href="/" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/bulma/0.7.2/css/bulma.min.css">
<body>
<section class="section">
<div class="container">
<app-root></app-root>
</div>
</section>
</body>
</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:
// pusher-angular-realtime-feed-api/client/src/app/feed-form/feed-form.component.html
<div class="columns">
<div class="column is-5">
<h3 class="notification">Create a new post</h3>
<div *ngIf="infoMsg" class="notification is-success">{{ infoMsg }}</div>
<div *ngIf="errorMsg" class="is-danger notification">{{ errorMsg }}</div>
<form>
<div class="field">
<label class="label">Title : </label>
<div class="control">
<input
class="input"
type="text"
placeholder="Post title"
name="title"
[(ngModel)]="title"
/>
</div>
</div>
<div><label>Message: </label></div>
<div>
<textarea
[(ngModel)]="content"
rows="10"
cols="70"
[disabled]="isSending"
name="content"
></textarea>
</div>
</form>
</div>
<div class="is-7"></div>
</div>
<button (click)="submit()" class="button is-info" [disabled]="isSending">
Send
</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:
// pusher-angular-realtime-feed-api/client/src/app/feed-form/feed-form.component.ts
import { Component, OnInit, Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-feed-form',
templateUrl: './feed-form.component.html',
styleUrls: ['./feed-form.component.css'],
})
export class FeedFormComponent implements OnInit {
private isSending: boolean;
private httpClient: HttpClient;
public content: string;
public errorMsg: string;
public infoMsg: string;
public title: string;
constructor(private http: HttpClient) {
this.httpClient = http;
}
submit() {
this.errorMsg = '';
this.isSending = true;
this.infoMsg = 'Processing your request.. Wait a minute';
this.http
.post('http://localhost:3000/submit', {
title: this.title,
body: this.content,
})
.toPromise()
.then((data: { message: string; status: boolean }) => {
this.infoMsg = data.message;
setTimeout(() => {
this.infoMsg = '';
}, 1000);
this.isSending = false;
this.content = '';
this.title = '';
})
.catch(error => {
this.infoMsg = '';
this.errorMsg = error.error.message;
this.isSending = false;
});
}
ngOnInit() {}
}
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.
// pusher-angular-realtime-feed-api/client/src/app/feed/feed.component.html
<h1 class="notification is-info">Your feed</h1>
<div class="columns">
<div class="column is-7">
<div *ngFor="let feed of feeds">
<div class="box">
<article class="media">
<div class="media-left">
<figure class="image is-64x64">
<img
src="https://bulma.io/images/placeholders/128x128.png"
alt="Image"
/>
</figure>
</div>
<div class="media-content">
<div class="content">
<p>
<strong>{{ feed.title }}</strong>
<small>{{ feed.createdAt }}</small> <br />
{{ feed.content }}
</p>
</div>
</div>
</article>
</div>
</div>
</div>
</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:
ng generate service Feed
ng generate class Feed
You will need to edit both files with the following contents:
// pusher-angular-realtime-feed/client/src/app/feed.ts
export class Feed {
constructor(
public title: string,
public content: string,
public createdAt: Date
) {
this.title = title;
this.content = content;
this.createdAt = createdAt;
}
}
// pusher-angular-realtime-feed/client/src/app/feed.service.ts
import { Injectable } from '@angular/core';
import { Subject, Observable } from 'rxjs';
import { Feed } from './feed';
import Pusher from 'pusher-js';
@Injectable({
providedIn: 'root',
})
export class FeedService {
private subject: Subject<Feed> = new Subject<Feed>();
private pusherClient: Pusher;
constructor() {
this.pusherClient = new Pusher('YOUR KEY HERE', { cluster: 'CLUSTER' });
const channel = this.pusherClient.subscribe('realtime-feeds');
channel.bind(
'posts',
(data: { title: string; body: string; time: string }) => {
this.subject.next(new Feed(data.title, data.body, new Date(data.time)));
}
);
}
getFeedItems(): Observable<Feed> {
return this.subject.asObservable();
}
}
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:
// pusher-angular-realtime-feed-api/client/src/app/feed/feed.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { FeedService } from '../feed.service';
import { Feed } from '../feed';
import { Subscription } from 'rxjs';
@Component({
selector: 'app-feed',
templateUrl: './feed.component.html',
styleUrls: ['./feed.component.css'],
providers: [FeedService],
})
export class FeedComponent implements OnInit, OnDestroy {
public feeds: Feed[] = [];
private feedSubscription: Subscription;
constructor(private feedService: FeedService) {
this.feedSubscription = feedService
.getFeedItems()
.subscribe((feed: Feed) => {
this.feeds.push(feed);
});
}
ngOnInit() {}
ngOnDestroy() {
this.feedSubscription.unsubscribe();
}
}
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:
// pusher-angular-realtime-feed/client/src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { RouterModule, Routes } from '@angular/router';
import { HttpClientModule } from '@angular/common/http';
import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { FeedFormComponent } from './feed-form/feed-form.component';
import { PageNotFoundComponent } from './page-not-found/page-not-found.component';
import { DashboardComponent } from './dashboard/dashboard.component';
import { FeedComponent } from './feed/feed.component';
const appRoutes: Routes = [
{ path: 'new', component: FeedFormComponent },
{
path: 'feed',
component: FeedComponent,
},
{
path: '',
redirectTo: '/dashboard',
pathMatch: 'full',
},
{ path: 'dashboard', component: DashboardComponent },
{ path: '**', component: PageNotFoundComponent },
];
@NgModule({
declarations: [
AppComponent,
FeedFormComponent,
PageNotFoundComponent,
DashboardComponent,
FeedComponent,
],
imports: [
RouterModule.forRoot(
appRoutes,
{ enableTracing: true }
),
BrowserModule,
HttpClientModule,
AppRoutingModule,
FormsModule,
],
exports: [RouterModule],
bootstrap: [AppComponent],
})
export class AppModule {}
As a final step, make sure to edit app.component.html
to inlcude only the following line:
// pusher-angular-realtime-feed/client/src/app/app.component.html
<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:
cd client
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.
6 December 2018
by Lanre Adelowo