Building a Socket.io chat
Lets Build a chatting application which will be constructed from several server event handlers, but most of the implementation will take place in your AngularJS application. We’ll begin with setting the server event handlers.
Setting the event handlers of the chat server
Before implementing the chat client in your AngularJS application, you’ll first need to create a few server event handlers. You already have a proper application structure, so you won’t implement the event handlers directly in your configuration file. Instead, it would be better to implement your chat logic by creating a new file named chat.server.controller.js
inside your app/controllers
folder. In your new file, paste the following lines of code:
module.exports = function(io, socket) { io.emit('chatMessage', { type: 'status', text: 'connected', created: Date.now(), username: socket.request.user.username }); socket.on('chatMessage', function(message) { message.type = 'message'; message.created = Date.now(); message.username = socket.request.user.username; io.emit('chatMessage', message); }); socket.on('disconnect', function() { io.emit('chatMessage', { type: 'status', text: 'disconnected', created: Date.now(), username: socket.request.user.username }); }); };
In this file, you implemented a couple of things. First, you used the io.emit()
method to inform all the connected socket clients about the newly connected user. This was done by emitting the chatMessage
event, and passing a chat message object with the user information and the message text, time, and type. Since you took care of handling the user authentication in your socket server configuration, the user information is available from the socket.request.user
object.
Next, you implemented the chatMessage
event handler that will take care of messages sent from the socket client. The event handler will add the message type, time, and user information, and it will send the modified message object to all connected socket clients using the io.emit()
method.
Our last event handler will take care of handling the disconnect
system event. When a certain user is disconnected from the server, the event handler will notify all the connected socket clients about this event by using the io.emit()
method. This will allow the chat view to present the disconnection information to other users.
You now have your server handlers implemented, but how will you configure the socket server to include these handlers? To do so, you will need to go back to your config/socketio.js
file and slightly modify it:
var config = require('./config'),
cookieParser = require('cookie-parser'),
passport = require('passport');
module.exports = function(server, io, mongoStore) {
io.use(function(socket, next) {
cookieParser(config.sessionSecret)(socket.request, {}, function(err) {
var sessionId = socket.request.signedCookies['connect.sid'];
mongoStore.get(sessionId, function(err, session) {
socket.request.session = session;
passport.initialize()(socket.request, {}, function() {
passport.session()(socket.request, {}, function() {
if (socket.request.user) {
next(null, true);
} else {
next(new Error('User is not authenticated'), false);
}
})
});
});
});
});
io.on('connection', function(socket) {
require('../app/controllers/chat.server.controller')(io, socket);
});
};
Notice how the socket server connection
event is used to load the chat controller. This will allow you to bind your event handlers directly with the connected socket.
Congratulations, you’ve successfully completed your server implementation! Next, you’ll see how easy it is to implement the AngularJS chat functionality. Let’s begin with the AngularJS service.
Creating the Socket service
The provided Socket.io client method is used to open a connection with the socket server and return a client instance that will be used to communicate with the server. Since it is not recommended to use global JavaScript objects, you can leverage the services singleton architecture and wrap your socket client.
Let’s begin by creating the public/chat
module folder. Then, create the public/chat/chat.client.module.js
initialization file with the following line of code:
angular.module('chat', []);
Now, proceed to create a public/chat/services
folder for your socket service. In the public/chat/services
folder, create a new file named socket.client.service.js
that contains the following code snippet:
angular.module('chat').service('Socket', ['Authentication', '$location', '$timeout', function(Authentication, $location, $timeout) { if (Authentication.user) { this.socket = io(); } else { $location.path('/'); } this.on = function(eventName, callback) { if (this.socket) { this.socket.on(eventName, function(data) { $timeout(function() { callback(data); }); }); } }; this.emit = function(eventName, data) { if (this.socket) { this.socket.emit(eventName, data); } }; this.removeListener = function(eventName) { if (this.socket) { this.socket.removeListener(eventName); } }; } ]);
Let’s review this code for a moment. After injecting the services, you checked whether the user is authenticated using the Authentication
service. If the user is not authenticated, you redirected the request back to the home page using the $location
service. Since AngularJS services are lazily loaded, the Socket service will only load when requested. This will prevent unauthenticated users from using the Socket service. If the user is authenticated, the service socket
property is set by calling the io()
method of Socket.io.
Next, you wrapped the socket emit()
, on()
, and removeListenter()
methods with compatible service methods. It is worth checking the service on()
method. In this method, you used a common AngularJS trick that involves the $timeout
service. The problem we need to solve here is that AngularJS data binding only works for methods that are executed inside the framework. This means that unless you notify the AngularJS compiler about third-party events, it will not know about changes they cause in the data model. In our case, the socket client is a third-party library that we integrate in a service, so any events coming from the socket client might not initiate a binding process. To solve this problem, you can use the $apply
and $digest
methods; however, this often causes an error, since a digest cycle might already be in progress. A cleaner solution is to use $timeout
trick. The $timeout
service is a wrapper around the window.setTimeout()
method, so calling it without the timeout
argument will basically take care of the binding issue without any impact on user experience
Once you have the Socket service ready, all you have to do is implement the chat controller and chat view. Let’s begin by defining the chat controller.
Creating the chat controller
The chat controller is where you implement your AngularJS chat functionality. To implement your chat controller, you’ll first need to create a public/chat/controllers
folder. In this folder, create a new file named chat.client.controller.js
that contains the following code snippet:
angular.module('chat').controller('ChatController', ['$scope', 'Socket', function($scope, Socket) { $scope.messages = []; Socket.on('chatMessage', function(message) { $scope.messages.push(message); }); $scope.sendMessage = function() { var message = { text: this.messageText, }; Socket.emit('chatMessage', message); this.messageText = ''; } $scope.$on('$destroy', function() { Socket.removeListener('chatMessage'); }) } ]);
In the controller, you first created a messages array and then implemented the chatMessage
event listener that will add retrieved messages to this array. Next, you created a sendMessage()
method that will send new messages by emitting the chatMessage
event to the socket server. Finally, you used the in-built $destroy
event to remove the chatMessage
event listener from the socket client. The $destory
event will be emitted when the controller instance is deconstructed. This is important because the event handler will still get executed unless you remove it.