Build a realtime table with Angular
You will need Node and npm installed on your machine. A basic understanding of JavaScript will be helpful.
Introduction
In a traditional web app, the clients (browser) has no idea when the state of the server may have changed. It either polls for changes at a particular interval or waits for the user to initiate the change. With realtime features, information is transmitted (almost) instantaneously between the users and the server. Pusher helps to bring realtime apps to the masses with their simple APIs from the client end down to the server.
In this tutorial, we’ll demonstrate how to integrate Pusher with an Angular application. We will create a realtime table, where we don’t need to refresh our page or component anytime there’s a change to our table’s data, our table should immediately update based on the current state of the data.
Prerequisites
We are going to make use of the following dependencies:
-
Angular 4+: a platform that makes it easy to build applications with the web. Angular combines declarative templates, dependency injection, end to end tooling, and integrated best practices to solve development challenges.
-
Pusher: a hosted service that makes it super-easy to add realtime data and functionality to web and mobile applications. It has different products based on the need of your application.
-
Bootstrap 4: an open source toolkit for developing with HTML and CSS. It includes HTML and CSS based design templates for typography, forms, buttons, tables, navigation, modals, image carousels and many other, as well as optional JavaScript plugins.
-
open-iconic: an open source icon set with 223 marks in SVG, webfont and raster formats
Please ensure you have Node and npm installed before starting the tutorial.
No knowledge of Angular is required, but a basic understanding of Javascript (not necessarily Typescript) may be helpful.
Let’s build our realtime application
Bootstrapping with Angular-cli:
npm install -g @angular/cli
ng new realtimeNgTable
cd realtimeNgTable
We installed a command line interface to scaffold and build Angular apps globally. It exposes ng
in our terminal for us to make use of the commands available. To confirm everything went well, run the below command on your terminal within the newly created app folder /realtimeNgTable
.
ng serve
You should see this:
Installing dependencies:
npm install bootstrap open-iconic @theo4u/ng-alert pusher-js
Open .angular-cli.json
within our application root folder and update the styles
field to look like so:
"styles": [
"../node_modules/bootstrap/dist/css/bootstrap.css",
"../node_modules/open-iconic/font/css/open-iconic-bootstrap.css",
"../node_modules/@theo4u/ng-alert/style.css",
"styles.css"
]
Open src/styles.css
and add the below CSS to it:
/* You can add global styles to this file, and also import other style files */
.box-shadow { box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .05); }
/* ngAlert customization */
.app-level-alert {
padding-top: 10px;
}
We added a utility style for adding shadows to our div and also for making our alert align later on.
Finally, open src/app/app.module.ts
to add NgAlertModule
to the imports
array:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgAlertModule } from '@theo4u/ng-alert';
import { AppComponent } from './app.component';
@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
NgAlertModule
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
At this point, we have successfully fixed our app dependencies and styling.
Next, we need to create two services to manage our Pusher instance (we don’t have to always create a new instance of Pusher anytime we want to use it, we can use just one instance of it throughout the entire app) and our employees (interaction with the remote server and vice versa for employee’s data):
ng g s services/pusher --spec false
ng g s services/employee --spec false
ng g i interfaces/iemployee
We used another command of Angular CLI, which in full is ng generate service path/name
. This creates a folder if not existing called services/
and place our service files there. We also used --spec false
to let the Angular CLI know we don’t need to generate test file for the service file. The last comand generates an employee interface to help give an idea of what an employee object should have.
Open src/app/services/pusher.service.ts
and update it to look like the one below:
import { Injectable } from '@angular/core';
import * as Pusher from 'pusher-js';
// this is here to discourage the instantianting of pusher any where its
// needed, better to reference it from one place
@Injectable()
export class PusherService {
private _pusher: any;
constructor() {
this._pusher = new Pusher(API_KEY, {
cluster: CLUSTER,
encrypted: true
});
}
// any time it is needed we simply call this method
getPusher() {
return this._pusher;
}
}
We insert the free API_KEY
and CLUSTER
we get after signing up and creating a channel app with Pusher.
After creating an app from Pusher’s dashboard, navigate to
App Keys
tab to see your app credentials
To ensure that connection traffic is encrypted, we set encrypted
to the Boolean true
in our app. Read more about client configuration here.
Open src/app/interfaces/iemployee.ts
and update it to look like so:
export interface IEmployee {
id?: number;
name: string;
position: string;
salary: string;
createdAt?: string;
}
Let’s now move over to our src/app/services/employee.service.ts
. This file will manage everything relating to employee and our server:
// src/app/services/employee.service.ts
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpClient } from '@angular/common/http';
import { IEmployee } from '../interfaces/iemployee';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/mapTo';
import { PusherService } from './pusher.service';
@Injectable()
export class EmployeeService {
private _endPoint = 'http://localhost:2000/employee'
private _channel: any;
constructor(private _http: HttpClient, private _pusherService: PusherService) {
this._channel = this._pusherService.getPusher().subscribe('employee');
}
/**
* @return employee's channel for the different event available under employee
*/
getChannel () {
return this._channel;
}
list (): Observable<IEmployee[]> {
return this._http.get(this._endPoint)
.map(res => <IEmployee[]> res);
}
/**
* Create new employee
* @param param
* @return Observable<IEmployee> with the id
*/
create(param: IEmployee): Observable<IEmployee> {
return this._http.post(this._endPoint, param)
.map(res => <IEmployee> res);
}
/**
* Remove an employee
* @param employee to remove
* @return Observable<IEmployee> the employee just removed
*/
delete(employee: IEmployee): Observable<IEmployee> {
return this._http.delete(`${this._endPoint}/${employee.id}`)
.mapTo(employee);
}
}
Observe that we attached this service to the employee’s channel, which means anything that has to do with an employee and Pusher is within our employee’s service file.
Let’s make our services available to be used by others from our app.module.ts
by providing them also adding HTTPClientModule since it was used within employee.service.ts
for http requests:
// src/app/app.module.ts
import { EmployeeService } from './services/employee.service';
import { PusherService } from './services/pusher.service';
import { HttpClientModule } from '@angular/common/http';
...
imports: [
...,
HttpClientModule
],
providers:[EmployeeService, PusherService]
Angular components to manage employees
We’ll now create two more components to list employees and create employees:
ng g c listEmployee -is --spec false
ng g c createEmployee -is --spec false
Using the Angular CLI, we generated a component with no test attached as explained earlier. It will create a folder for each containing a .ts
and a .html
. The -is
command means don’t create a .css
file instead use inline styling within our .ts
. Since, we are not going to do alot or no styling within each component.
Let’s start with the list employee component, it should look like our gif above.
Open list-employee.component.html
and update it to look like so:
<!-- src/app/list-employee/list-employee.component.html -->
<h6 class="pb-2 mb-0">Employees</h6>
<table class="table">
<thead>
<tr>
<th scope="col">Name</th>
<th scope="col">Position</th>
<th scope="col">Salary</th>
<th scope="col">Created At</th>
<th scope="col">Actions</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let employee of employees">
<td>
<span *ngIf="employee.new" class="badge badge-primary">new</span>
{{employee.name}}
</td>
<td>{{employee.position}}</td>
<td>{{employee.salary}}</td>
<td>{{employee.createdAt | date:'yyyy/MM/dd'}}</td>
<td>
<button (click)="delete(employee)" class="btn btn-danger btn-sm">
<span class="oi oi-trash"></span>
</button>
</td>
</tr>
<tr *ngIf="loading">
<td colspan="5" align="center">Fetching Employees</td>
</tr>
</tbody>
</table>
Next open list-employee.component.ts
and update it with the code below:
// src/app/list-employee/list-employee.component.ts
import { Component, OnInit } from '@angular/core';
import { EmployeeService } from '../services/employee.service';
import { IEmployee } from '../interfaces/iemployee';
import { NgAlertService, MessageType } from '@theo4u/ng-alert';
@Component({
selector: 'app-list-employee',
templateUrl: './list-employee.component.html',
styles: []
})
export class ListEmployeeComponent implements OnInit {
employees: IEmployee[] = [];
loading = true;
constructor(private _employeeService: EmployeeService, private _ngAlert: NgAlertService) { }
ngOnInit() {
this.loading = true;
this._employeeService.list()
.subscribe(employees => {
this.loading = false;
this.employees = employees;
});
}
delete(employee: IEmployee) {
// show delete confirmation with ngAlert
this._ngAlert.push({
message: `<strong>Are you sure!</strong> you want to delele this employee with name <strong>${employee.name}</strong>`,
type: MessageType.warning,
buttons: [
{
label: 'Continue',
action: () => {
this._actualDelete(employee);
},
css: 'btn btn-danger'
}
]
});
}
private _actualDelete (employee: IEmployee) {
this._employeeService.delete(employee)
.subscribe(() => {
// remove the employee if removed successfully
this.employees = this.employees.filter(item => item !== employee);
this._ngAlert.push({
message: `${employee.name} removed`,
type: MessageType.success
});
});
}
}
Here, we are simply performing a normal loading and deleting of employees from our remote server via EmployeeService
.
Let’s go into our create-employee.component.html
. Here, we’ll make use of Angular’s reactive form:
<!-- src/app/create-employee/create-employee.component.html -->
<h6 class="pb-2 mb-0">Create Employees</h6>
<form [formGroup]="employeeForm" (ngSubmit)="onSubmit()" novalidate>
<div class="form-group">
<label for="name">Name</label>
<input formControlName="name" type="text" class="form-control" id="name" placeholder="Christian Nwamba">
<small *ngIf="employeeForm.get('name').hasError('required')" class="form-text text-danger">Name is required.</small>
</div>
<div class="form-group">
<label for="position">Position</label>
<select formControlName="position" class="form-control" id="position">
<option>Manager</option>
<option>Hr</option>
<option>Developer</option>
</select>
</div>
<div class="form-group">
<label for="salary">Salary</label>
<input formControlName="salary" type="text" class="form-control" id="salary" placeholder="$12,000">
<small *ngIf="employeeForm.get('salary').hasError('required')" class="form-text text-danger">Salary is required.</small>
</div>
<button type="submit" [disabled]="loader || employeeForm.invalid" class="btn btn-primary">{{loader?'Adding':'Add'}}</button>
</form>
Open create-employee.component.ts
to manage our HTML template above. We are making use of the .create
method in our employee’s service with reactive forms to validate entries easily under the _createForm
method.
// src/app/create-employee/create-employee.component.ts
import { Component, OnInit } from '@angular/core';
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
import { EmployeeService } from '../services/employee.service';
import { IEmployee } from '../interfaces/iemployee';
@Component({
selector: 'app-create-employee',
templateUrl: './create-employee.component.html',
styles: []
})
export class CreateEmployeeComponent implements OnInit {
employeeForm: FormGroup;
loader: boolean;
constructor(private _fb: FormBuilder, private _employeeService: EmployeeService) { }
ngOnInit() {
this._createForm();
}
/**
* create our reactive form here
*/
private _createForm() {
this.employeeForm = this._fb.group({
name: ['', Validators.required],
position: ['Manager', Validators.required],
salary: ['', Validators.required]
});
}
/**
* submit new employee to server
*/
onSubmit() {
const param = this.employeeForm.value;
this._employeeService.create(param)
.subscribe((employee: IEmployee) => {
this.loader = false;
this.employeeForm.reset({position: 'Manager'});
},
(error) => {
console.error(error);
this.loader = false;
});
}
}
Making use of our components
Let’s call our component so we can test easily with our browser, open up app.component.ts
and update it to look like so:
// src/app/app.component.ts
import { Component, OnInit, OnDestroy } from '@angular/core';
import { IMessage, MessageType, CloseType, NgAlertService } from '@theo4u/ng-alert';
import { Subscription } from 'rxjs/Subscription';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit, OnDestroy {
message: IMessage;
closeTypes = CloseType;
private _alertSub: Subscription;
constructor(private _ngAlert: NgAlertService) {
}
ngOnInit () {
this._alertSub = this._ngAlert.getSource().subscribe(message => {
this.message = message;
});
}
ngOnDestroy () {
this._alertSub.unsubscribe();
}
}
Most of what is going on here is about managing our app level alerts. Anytime an alert is triggered from any component, we can easily get it here and we can also unsubscribe from it once we are done with the component. Check the delete
method in list-employee.component.ts
to see where we push a confirmation message before deleting any employee.
Open app.components.html
and let’s call our components side by side:
<div class="container">
<!-- Alert here -->
<div class="app-level-alert">
<ng-alert [(message)]="message" [dismissable]="true" [closeType]="closeTypes.TIMES"></ng-alert>
</div>
<!-- /Alert here -->
<div class="row">
<div class="col-md-8">
<div class="my-3 p-3 bg-white rounded box-shadow">
<app-list-employee></app-list-employee>
</div>
</div>
<div class="col-md-4">
<div class="my-3 p-3 bg-white rounded box-shadow">
<app-create-employee></app-create-employee>
</div>
</div>
</div>
</div>
Our final app.module.ts
should now be like this:
// src/app/app.module.ts
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { NgAlertModule } from '@theo4u/ng-alert';
import { AppComponent } from './app.component';
import { CreateEmployeeComponent } from './create-employee/create-employee.component';
import { ListEmployeeComponent } from './list-employee/list-employee.component';
import { EmployeeService } from './services/employee.service';
import { ReactiveFormsModule } from '@angular/forms';
import { HttpClientModule } from '@angular/common/http';
import { PusherService } from './services/pusher.service';
@NgModule({
declarations: [
AppComponent,
CreateEmployeeComponent,
ListEmployeeComponent
],
imports: [
BrowserModule,
ReactiveFormsModule,
NgAlertModule,
HttpClientModule
],
providers: [EmployeeService, PusherService],
bootstrap: [AppComponent]
})
export class AppModule { }
Our app behaviour should now look like this:
We have to always refresh our page to get newly added employees or deleted employees, next section will handle this.
Integrating Pusher for realtime table updates
Pusher is a hosted service that makes it super-easy to add realtime data and functionality to web and mobile applications. Pusher sits as a realtime layer between your servers and your clients. Pusher also maintains persistent connections to the clients over WebSocket if possible and falling back to HTTP-based connectivity so that as soon as your servers have new data that they want to push to the clients they can do, instantly via Pusher.
We’ll use Pusher’s event based API know as Pusher Channels. All we need to do is to subscribe to a particular channel like employee
( employee.service.ts
getChannel()
method) and watch for any event type (new, deleted) emitted and know how to handle the data coming along with the event.
Open list-employee.component.ts
to spice it up with Pusher. We are watching for new and deleted employees, add the following to ngOnInit
method:
// subscribe to pusher's event
this._employeeService.getChannel().bind('new', data => {
data.new = true;
this.employees.push(data);
});
this._employeeService.getChannel().bind('deleted', data => {
this.employees = this.employees.filter(emp => emp.id !== data.id);
});
Anytime a new/deleted record is received we get the data as well and then manipulate our array of employees. When a record is added we need to let the user know that this record just came in using data.new=true
, which was used in our list-employee.component.html
to show a new label.
Now, try deleting or adding a record from another browser window, the current one should be updated.
Integrating Pusher to our Node server
Our server is simply going to simulate an actual DB and routes to create, delete and get employees from a mock array object as our DB. Also, obeying the twelve-factor approach to handling configurations for our server. Open your terminal within our application folder /realtimeNgTable
and run the following commands:
npm install express body-parser cors pusher dotenv shortid
touch server.js
touch mocks.js
touch .env
Mock is simply going to export our initial list of employees, like so:
// src/mocks.js
module.exports = [
{
id: 'S1234X',
name: 'Christian Nwamba',
position: 'Manager',
salary: '$13,000',
createdAt: new Date().toISOString()
},
{
id: 'S1234Y',
name: 'Prosper Otemuyiwa',
position: 'Hr',
salary: '$12,500',
createdAt: new Date().toISOString()
},
{
id: 'S1234Z',
name: 'Theophilus Omoregbee',
position: 'Developer',
salary: '$10,500',
createdAt: new Date().toISOString()
}
]
Let’s setup our server in server.js
:
//server.js
const express = require('express')
const bodyParser = require('body-parser')
const Pusher = require('pusher')
const cors = require('cors')
const dotenv = require('dotenv').config()
const shortId = require('shortid')
let mocks = require('./mocks')
const app = express()
app.use(cors())
app.use(bodyParser.urlencoded({ extended: false }))
app.use(bodyParser.json())
const pusher = new Pusher({
appId: process.env.PUSHER_APPID,
key: process.env.PUSHER_KEY,
secret: process.env.PUSHER_SECRET,
cluster: process.env.PUSHER_CLUSTER,
encrypted: true
})
app.post('/employee', (req, res) => {
// simulate actual db save with id (using shortId) and createdAt added
const employee = {
id: shortId.generate(),
createdAt: new Date().toISOString(),
...req.body
}
mocks.push(employee) // like our db
// trigger this update to our pushers listeners
pusher.trigger('employee', 'new', employee)
res.send(employee)
})
app.delete('/employee/:id', (req, res) => {
const employee = mocks.find(emp => emp.id === req.params.id)
mocks = mocks.filter(emp => emp.id !== employee.id)
pusher.trigger('employee', 'deleted', employee)
res.send(employee)
})
app.get('/employee', (req, res) => {
res.send(mocks)
})
app.listen(2000, () => console.log('Listening at 2000'))
We instantiated Pusher with environment parameters, as said earlier on we are sticking with the twelve-factor approach, where we use .env
file to pass environment variables to our server.js
.
Let’s populate our .env
file with our Pusher credentials
PUSHER_APPID=YOUR_APP_ID
PUSHER_KEY=YOUR_APP_KEY
PUSHER_SECRET=YOUR_APP_SECRET
PUSHER_CLUSTER=YOUR_APP_CLUSTER
Finally, running the below command in our terminal should start the node server
node server.js
Any time a new record is created we trigger the event to everyone who subscribed to the channel with the event type and additional data to pass across.
Conclusion
With this sample realtime Angular table, we can definitely build more with Pusher’s channel event-based APIs to handle any form of realtime update needed in our Angular application. You can extend this sample application to handle edit by triggering another type of event and the data as {id, data}
. With the id
of the record that got updated from the server and the data
as the changes. Source code for both the frontend and the backend node server is located on Github.
7 May 2018
by Christian Nwamba