Socket.io in Mean Angular4 Todo App

Handle Real-time Todos using socket.io.

Complete Source Code for this App:

In this tutorial we are going to add socket.io in our Mean (MongoDB, Express, Angular.js, Node.js) Todo App tutorial for the realtime communications. Here we are not going to build new app, we will clone our already available Mean App hosted on Github repo:

So, it is highly recommend to read the Mean tutorial first:

and then continue to this tutorial.

One of hottest topic in node.js is building realtime web applications and Socket.io, A realtime library for Node.js, is very popular to handle real-time data.

First of all clone the Mern app from Github:

git clone https://github.com/bipinswarnkar1989/mean_stack_tutorial_todo_app.git

Make sure you are in the mean_stack_tutorial_todo_app directory

cd mean_stack_tutorial_todo_app

Open the server repository in your current terminal and install the server dependencies:

cd express-server && npm install

Install socket.io in server:

npm install socket.io --save

Then in a new terminal Open the Angular4 frontend repository and install the reactjs dependencies:

cd angular-client && npm install

Then run your express server and react frontend in their terminals by running in each of these (don’t forget to run MongoDb instance first):

npm start

Our cloned app is running now will add socket in our three major functionalities Add Todo, Edit Todo and Delete Todo one by one . But first of all we need to know what socket.io is and its working flow.

Socket.IO is a JavaScript library for real-time web applications. It enables bidirectional communication between web clients and server. Both sides have identical API and are event-driven like Node.js. To open this interactive communication session between the browser and a server, we have to create an HTTP server to enable real-time communication. It will allow us to emit and receive messages. The socket is the object that handles this communication between web clients and server.

Great we just discussed about the Socket.IO , Let’s start adding socket to our Mean App.

Open up main express server file app.js and revise, add following code for socket connection and to receive Todo from client:

./express-server/app.js

// ./express-server/app.js
import express from 'express';
import path from 'path';
import bodyParser from 'body-parser';
import logger from 'morgan';
import mongoose from 'mongoose';
import SourceMapSupport from 'source-map-support';
import bb from 'express-busboy';
import http from 'http';
import socket from 'socket.io';
// import routes
import todoRoutes from './routes/todo.server.route';
//import controller file
import * as todoController from './controllers/todo.server.controller';
// define our app using express
const app = express();
const server = http.Server(app);
const io = socket(server);
// express-busboy to parse multipart/form-data
bb.extend(app);
// socket.io connection
io.on('connection', (socket) => {
console.log("Connected to Socket!!"+ socket.id);
// Receiving Todos from client
socket.on('addTodo', (Todo) => {
console.log('socketData: '+JSON.stringify(Todo));
todoController.addTodo(io,Todo);
});
// Receiving Updated Todo from client
socket.on('updateTodo', (Todo) => {
console.log('socketData: '+JSON.stringify(Todo));
todoController.updateTodo(io,Todo);
});
// Receiving Todo to Delete
socket.on('deleteTodo', (Todo) => {
console.log('socketData: '+JSON.stringify(Todo));
todoController.deleteTodo(io,Todo);
});
})
// allow-cors
app.use(function(req,res,next){
res.header("Access-Control-Allow-Origin", "*");
res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
next();
})
// configure app
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended:true }));
app.use(express.static(path.join(__dirname, 'public')));
// set the port
const port = process.env.PORT || 3001;
// connect to database
mongoose.Promise = global.Promise;
mongoose.connect('mongodb://localhost/mern-todo-app', {
useMongoClient: true,
});
// add Source Map Support
SourceMapSupport.install();
app.use('/api', todoRoutes);
app.get('/', (req,res) => {
return res.end('Api working');
});
// catch 404
app.use((req, res, next) => {
res.status(404).send('<h2 align=center>Page Not Found!</h2>');
});
// start the server
server.listen(port,() => {
console.log(`App Server Listening at ${port}`);
});

In the above code we imported our controller file todo.server.controller directly in the main file because we are receiving Todo in this main file and skipping the router, forwarding Todo directly to our server controller because router has no role in socket communications, but we need router for getting All Todos and single Todo.

  • io.on(‘connection’, callback) is Used to establish connection to the server.
  • socket.on(message, callback) is Used to receive Todos from the client. callback is invoked on reception.

Revise the express server router file as following:

./express-server/routes/todo.server.route.js

// ./express-server/routes/todo.server.route.js
import express from 'express';
//import controller file
import * as todoController from '../controllers/todo.server.controller';
// get an instance of express router
const router = express.Router();
router.route('/')
.get(todoController.getTodos);
router.route('/:id')
.get(todoController.getTodo);
export default router;

Open up your server controller file todo.server.controller.js, modify addTodo, updateTodo and deleteTodo arrow function.

./express-server/controllers/todo.server.controller.js

// ./express-server/controllers/todo.server.controller.js
import mongoose from 'mongoose';
//import models
import Todo from '../models/todo.server.model';
export const getTodos = (req,res) => {
Todo.find().exec((err,todos) => {
if(err){
return res.json({'success':false,'message':'Some Error'});
}
return res.json({'success':true,'message':'Todos fetched successfully',todos});
});
}
export const addTodo = (io,T) => {
let result;
const newTodo = new Todo(T);
newTodo.save((err,todo) => {
if(err){
result = {'success':false,'message':'Some Error','error':err};
console.log(result);
}
else{
const result = {'success':true,'message':'Todo Added Successfully',todo}
io.emit('TodoAdded', result);
}
})
}
export const updateTodo = (io,T) => {
let result;
Todo.findOneAndUpdate({ _id:T.id }, T, { new:true }, (err,todo) => {
if(err){
result = {'success':false,'message':'Some Error','error':err};
console.log(result);
}
else{
result = {'success':true,'message':'Todo Updated Successfully',todo};
io.emit('TodoUpdated', result);
}
})
}
export const getTodo = (req,res) => {
Todo.find({_id:req.params.id}).exec((err,todo) => {
if(err){
return res.json({'success':false,'message':'Some Error'});
}
if(todo.length){
return res.json({'success':true,'message':'Todo fetched by id successfully',todo});
}
else{
return res.json({'success':false,'message':'Todo with the given id not found'});
}
})
}
export const deleteTodo = (io,T) => {
let result;
Todo.findByIdAndRemove(T.id, (err,todo) => {
if(err){
result = {'success':false,'message':'Some Error','error':err};
console.log(result);
}
result = {'success':true,'message':todo.todoText+'Todo deleted successfully'};
io.emit('TodoDeleted', result);
})
}

We just finished our server side code for socket.io. Let’s move to Angular4 client side.

Install socket.io in Angular4 Client:

npm install socket.io --save

Run the Angular App:

ng serve

Now socket.io is installed in our client side also. Let’s start adding socket.io code in Angular4.

Add Todo

Open up todo-list.component.ts and import socket.io, connect it from server url and receive the Added Todo from server:

./angular-client/src/app/todo/todo-list/todo-list.component.ts

// ./angular-client/src/app/todo/todo-list/todo-list.component.ts
import { Component, OnInit } from '@angular/core';
import { TodoService } from '../todo.service';
import io from "socket.io-client";
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {
todos:any[] = [];
todo:any = {};
todoToEdit:any = {};
todoToDelete:any = {};
fetchingData:boolean = false;
apiMessage:string;
private url = 'http://localhost:3001';
private socket;
constructor(private todoService:TodoService) { }
ngOnInit(): void {
this.todoService.showAddTodoBox = true;
this.todoService.getTodos()
.then(td => this.todos = td.todos );
this.socket = io.connect(this.url);
this.socket.on('TodoAdded', (data) => {
console.log('TodoAdded: '+JSON.stringify(data));
this.todos.push(data.todo);
});

}
AddTodo(todo:any):void{
if(!todo){ return; }
this.todoService.createTodo(todo,this.socket);
}
showEditTodo(todo:any):void{
this.todoToEdit = todo;
this.apiMessage = "";
}
EditTodo(todo:any):void{
if(!todo){ return; }
todo.id = this.todoToEdit._id;
this.todoService.updateTodo(todo)
.then(td => {
const updatedTodos = this.todos.map(t => {
if(td.todo._id !== t._id){
return t;
}
return { ...t, ...td.todo };
})
this.apiMessage = td.message;
this.todos = updatedTodos;
})
}
showDeleteTodo(todo:any):void{
this.todoToDelete = todo;
this.apiMessage = "";
}
DeleteTodo(todo:any):void{
if(!todo){ return; }
this.todoService.deleteTodo(todo)
.then(td => {
const filteredTodos = this.todos.filter(t => t._id !== td.todo._id);
this.apiMessage = td.message;
this.todos = filteredTodos;
})
}
}

Send Todo to the server using Todo Service method createTodo:

./angular-client/src/app/todo/todo.service.ts

// ./angular-client/src/app/todo/todo.service.ts
import { Injectable } from '@angular/core';
import { Headers, Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
@Injectable()
export class TodoService {
private apiUrl = 'http://localhost:3001/api/';
showAddTodoBox:boolean = true;
constructor(private http: Http){ }
getTodos(): Promise<any>{
return this.http.get(this.apiUrl)
.toPromise()
.then(this.handleData)
.catch(this.handleError)
}
getTodo(id:string): Promise<any>{
return this.http.get(this.apiUrl + id)
.toPromise()
.then(this.handleData)
.catch(this.handleError)
}
createTodo(todo:any,socket:any): void{
socket.emit('addTodo', todo);
}
updateTodo(todo:any):Promise<any>{
return this.http
.put(this.apiUrl, todo)
.toPromise()
.then(this.handleData)
.catch(this.handleData);
}
deleteTodo(todo:any):Promise<any>{
return this.http
.delete(this.apiUrl + todo._id)
.toPromise()
.then(this.handleData)
.catch(this.handleError);
}
private handleData(res: any) {
let body = res.json();
console.log(body); // for development purposes only
return body || {};
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for development purposes only
return Promise.reject(error.message || error);
}
}

Now you can test adding real time todos in two or more browser tabs.

Edit Todo

Send and Receive the Updated Todo:

./angular-client/src/app/todo/todo-list/todo-list.component.ts

// ./angular-client/src/app/todo/todo-list/todo-list.component.ts
import { Component, OnInit } from '@angular/core';
import { TodoService } from '../todo.service';
import io from "socket.io-client";
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {
todos:any[] = [];
todo:any = {};
todoToEdit:any = {};
todoToDelete:any = {};
fetchingData:boolean = false;
apiMessage:string;
private url = 'http://localhost:3001';
private socket;
constructor(private todoService:TodoService) { }
ngOnInit(): void {
this.todoService.showAddTodoBox = true;
this.todoService.getTodos()
.then(td => this.todos = td.todos );
this.socket = io.connect(this.url);
// Receive Added Todo
this.socket.on('TodoAdded', (data) => {
console.log('TodoAdded: '+JSON.stringify(data));
this.todos.push(data.todo);
});
//Receive Updated Todo
this.socket.on('TodoUpdated', (data) => {
console.log('TodoUpdated: '+JSON.stringify(data));
const updatedTodos = this.todos.map(t => {
if(data.todo._id !== t._id){
return t;
}
return { ...t, ...data.todo };
})
this.apiMessage = data.message;
this.todos = updatedTodos;
});

}
AddTodo(todo:any):void{
if(!todo){ return; }
this.todoService.createTodo(todo,this.socket);
}
showEditTodo(todo:any):void{
this.todoToEdit = todo;
this.apiMessage = "";
}
EditTodo(todo:any):void{
if(!todo){ return; }
todo.id = this.todoToEdit._id;
this.todoService.updateTodo(todo,this.socket);
}
showDeleteTodo(todo:any):void{
this.todoToDelete = todo;
this.apiMessage = "";
}
DeleteTodo(todo:any):void{
if(!todo){ return; }
this.todoService.deleteTodo(todo)
.then(td => {
const filteredTodos = this.todos.filter(t => t._id !== td.todo._id);
this.apiMessage = td.message;
this.todos = filteredTodos;
})
}
}

./angular-client/src/app/todo/todo.service.ts

// ./angular-client/src/app/todo/todo.service.ts
import { Injectable } from '@angular/core';
import { Headers, Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
@Injectable()
export class TodoService {
private apiUrl = 'http://localhost:3001/api/';
showAddTodoBox:boolean = true;
constructor(private http: Http){ }
getTodos(): Promise<any>{
return this.http.get(this.apiUrl)
.toPromise()
.then(this.handleData)
.catch(this.handleError)
}
getTodo(id:string): Promise<any>{
return this.http.get(this.apiUrl + id)
.toPromise()
.then(this.handleData)
.catch(this.handleError)
}
createTodo(todo:any,socket:any): void{
socket.emit('addTodo', todo);
}
updateTodo(todo:any,socket:any):void{
socket.emit('updateTodo', todo);
}
deleteTodo(todo:any):Promise<any>{
return this.http
.delete(this.apiUrl + todo._id)
.toPromise()
.then(this.handleData)
.catch(this.handleError);
}
private handleData(res: any) {
let body = res.json();
console.log(body); // for development purposes only
return body || {};
}
private handleError(error: any): Promise<any> {
console.error('An error occurred', error); // for development purposes only
return Promise.reject(error.message || error);
}
}

We just finished Adding and Updating Todos in Real Time. You can open the app in two browsers or tabs and test the real time communication. Let’s move to Delete Todos in Real Time.

Delete Todo

Send and Receive the Deleted Todo and Remove it from Todos List:

./angular-client/src/app/todo/todo-list/todo-list.component.ts

// ./angular-client/src/app/todo/todo-list/todo-list.component.ts
import { Component, OnInit } from '@angular/core';
import { TodoService } from '../todo.service';
import io from "socket.io-client";
@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.css']
})
export class TodoListComponent implements OnInit {
todos:any[] = [];
todo:any = {};
todoToEdit:any = {};
todoToDelete:any = {};
fetchingData:boolean = false;
apiMessage:string;
private url = 'http://localhost:3001';
private socket;
constructor(private todoService:TodoService) { }
ngOnInit(): void {
this.todoService.showAddTodoBox = true;
this.todoService.getTodos()
.then(td => this.todos = td.todos );
this.socket = io.connect(this.url);
// Receive Added Todo
this.socket.on('TodoAdded', (data) => {
console.log('TodoAdded: '+JSON.stringify(data));
this.todos.push(data.todo);
});
//Receive Updated Todo
this.socket.on('TodoUpdated', (data) => {
console.log('TodoUpdated: '+JSON.stringify(data));
const updatedTodos = this.todos.map(t => {
if(data.todo._id !== t._id){
return t;
}
return { ...t, ...data.todo };
})
this.apiMessage = data.message;
this.todos = updatedTodos;
});
//Receive Deleted Todo and remove it from liste
this.socket.on('TodoDeleted', (data) => {
console.log('TodoDeleted: '+JSON.stringify(data));
const filteredTodos = this.todos.filter(t => t._id !== data.todo._id);
this.apiMessage = data.message;
this.todos = filteredTodos;
});

}
AddTodo(todo:any):void{
if(!todo){ return; }
this.todoService.createTodo(todo,this.socket);
}
showEditTodo(todo:any):void{
this.todoToEdit = todo;
this.apiMessage = "";
}
EditTodo(todo:any):void{
if(!todo){ return; }
todo.id = this.todoToEdit._id;
this.todoService.updateTodo(todo,this.socket);
}
showDeleteTodo(todo:any):void{
this.todoToDelete = todo;
this.apiMessage = "";
}
DeleteTodo(todo:any):void{
if(!todo){ return; }
this.todoService.deleteTodo(todo,this.socket);
}
}

./angular-client/src/app/todo/todo.service.ts

// ./angular-client/src/app/todo/todo.service.ts
import { Injectable } from '@angular/core';
import { Headers, Http } from '@angular/http';
import 'rxjs/add/operator/toPromise';
@Injectable()
export class TodoService {
private apiUrl = 'http://localhost:3001/api/';
showAddTodoBox:boolean = true;
constructor(private http: Http){ }
getTodos(): Promise<any>{
return this.http.get(this.apiUrl)
.toPromise()
.then(this.handleData)
.catch(this.handleError)
}
getTodo(id:string): Promise<any>{
return this.http.get(this.apiUrl + id)
.toPromise()
.then(this.handleData)
.catch(this.handleError)
}
createTodo(todo:any,socket:any): void{
socket.emit('addTodo', todo);
}
updateTodo(todo:any,socket:any):void{
socket.emit('updateTodo', todo);
}
deleteTodo(todo:any,socket:any):void{
socket.emit('deleteTodo', todo);
}
}

I am glad we made it to the end. We added real time communication to all our three major functionalities Add Todo, Edit Todo and Delete Todo using socket.io. Hope you enjoyed.

Complete Source code is available:

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.