AngularChat

One of the most visited projects of mine is the node.js/socket.io chat application that I have created and blogged about a few times before. I'm glad to announce that I have made some significant improvements to the code by using Angular as the frontend framework. In this post I'd like to walk you through the changes, their benefits and also share some code snippets with you.

Let's discuss the backend first in this article.

The code has not changed that much but there are several improvements over the old codebase. I have noticed that in some cases even though the application functioned correctly the actual functionality that was in place was broken. (You know the popular meme: "My code doesn't work, I don't know why. My code works, I don't know why.")

Previously the whole chat server was running in one single JavaScript file which is not considered to be a good application architecture and structure. I have rethought this a bit and created an app.js file that bootstraps the application and initiates the routes. This particular functionality is achieved by using Express.

First of all I have added a route support so that I can call the render() function outside of app.js. This may not have been absolutely necessary as I only have one single route for the root of the site but in all my other projects I have used routes so why not do the same here.

Second, I have created a file called chatServer.js that contains all the functionality pertaining to the chat server itself. The way this chatServer is initialised is a bit tricky. In my app.js I had to initialise a variable and pass on the http server to it as a parameter. This is how I have achieved the variable initialisation in app.js:

var express = require('express')
, app = express()
, server = require('http').createServer(app)
, routes = require('./routes')
, chatServer = require('./chatServer')(server);

The app.get('/', routes.index) portion is the bit that defines what route to call if someone accesses the / (root) of the site.

Let's have a closer look at chatServer = require('./chatServer')(server) and see why the server parameter had to be sent.

chatServer.js contains the first initialisation of the io object. Of course, in order to have the socket listening on a particular server (and port) it needs to be initialised in the following way:

var io = require('socket.io').listen(server);

This is exactly why I need to pass the server on to the chatServer.js.

Now I have app.js responsible for starting up the application and handling the requests via routes and I also have a file solely responsible for the chat server - they are nicely separated. However, I could still do better.

I can also move the purge() function outside from the server so that the code doesn't look that cluttered. (If you haven't seen my previous code, the purge() function just takes care of the various socket removal scenarios - e.g. what to do when a person who owns a room disconnects from the server? In this case it has to delete the room, send out a notification to all connected people in the room, remove them from the room as well as do numerous updates.)

I have created a standalone JavaScript file purge.js under a folder named utils and included that in chatServer.js:

var purgatory = require('./utils/purge');

(Innovative naming convention, isn't it?)

I have also created some helper functions and placed them in a file called utils/utils.js. The functions in this file are only responsible for broadcasting various messages to the connected sockets based on some criteria. In other words, as opposed to me having to use socket.emit() or io.sockets.emit() to distribute messages I came up with wrapper functions with more meaningful names:

module.exports.sendToSelf = function(socket, method, data) {
  socket.emit(method, data);
}

module.exports.sendToAllConnectedClients = function(io, method, data) {
  io.sockets.emit(method, data);
}

module.exports.sendToAllClientsInRoom = function(io, room, method, data) {
  io.sockets.in(room).emit(method, data);
}

The other modifications at the backend are not that significant. I have updated the logic behind a few functions, added more meaningful methods for the socket communication. I have also simplified some parts of the code. Before, in the case when a chat room owner has either disconnected, left the room or removed the room I used various underscore.js methods to remove all other people connected to that room (from the room.people array). I realised that this is a way too expensive operations - I could just remove all the data from the array and I found this solution:

room.people = 0

This not only empties the array but also keeps all the references intact so no funny things can happen.

The frontend portion

I think I wrote enough about the backend, let's also discuss the frontend components. I have to add that while working on this project I had multiple 'wow' moments, all thanks to Angular. I love the framework and I think it's awesome. You may not agree with me (and you don't have to) but I hope I will be able to prove my point by detailing my experience here.

The inspiration to rework my code into Angular came from Brian Ford's article on Socket.io & Angular. I have in fact used his sample code as a baseline and I've added my features on top of that.

In order to get socket.io working with Angular I had to use Brian's service and extend that a bit. Essentially the service wraps the socket object returned by socket.io. Each of the socket ballbacks are wrapped in $scope.apply which tells Angular that it needs to check the state of the application and update the templates (views) if there was a change. As mentioned in his article he did not wrap all the functions of socket.io but I have added a few more (such as disconnect. The tricky bit here was that the on method is called asynchronously therefore when the socket.disconnect() was called from within my application there was already an angular context therefore the code complained with an error $apply already in progress. To overcom this I have implemented the socket.disconnect() in the following way:

app.factory('socket', function ($rootScope) {
  var socket = io.connect();
  var disconnect = false;
  return {
    //more code
    disconnect: function() {
      disconnect = true;
      socket.disconnect();
    }
  }
} //etc

I have added two more services as well - one to do a geo-lookup (I've reused the service from my Twitter GeoTrending app) and I've created a brand new one to detect the user agent so that I can display icons indicating if a user is connected from a PC or a mobile device.

app.factory('useragent', ['$q', '$rootScope', '$window', 'useragentmsgs', function ($q,$rootScope,$window,useragentmsgs) {
  return {
    getUserAgent: function () {
      var deferred = $q.defer();
      if ($window.navigator && $window.navigator.userAgent) {
        var ua = $window.navigator.userAgent;
        deferred.resolve(ua);
      }
      else {
        $rootScope.$broadcast('error',useragentmsgs['errors.useragent.notFound']);
        $rootScope.$apply(function(){deferred.reject(useragentmsgs['errors.useragent.notFound']);});
      }
      return deferred.promise;
    },
    getIcon: function(ua) {
      var deferred = $q.defer();
      var icon = '';
      if (ua.match(/Android|BlackBerry|iPhone|iPad|iPod|Opera Mini|IEMobile/i)) {
        icon = 'mobile';
        deferred.resolve(icon);
      } else {
        icon = 'desktop';
        deferred.resolve(icon)
      }
      return deferred.promise;
    }
  };
}]);

On top of the services I have also created a few directives. The first one is responsible for auto scrolling the overfown container div element that holds the chat messages.

app.directive('autoscroll', function () {
  return function(scope, element, attrs) {
    var pos = element[0].parentNode.parentNode.scrollHeight;
    $(element).parent().parent().animate({
      scrollTop : pos
    }, 1000);
  }
});

The other two capture focus and blur events on input boxes. I think this functionality is missing from the current* Angular release (*current at the time of writing is 1.2.14)

app.directive('onFocus', function() {
  return {
    restrict: 'A',
    link: function(scope, el, attrs) {
      el.bind('focus', function() {
        scope.$apply(attrs.onFocus);
      });
    }
  };
});

app.directive('onBlur', function() {
  return {
    restrict: 'A',
    link: function(scope, el, attrs) {
      el.bind('blur', function() {
        scope.$apply(attrs.onBlur);
      });
    }
  };
});

I am using these two directives to allow the isTyping functionality whereby a message is displayed if a person is typing a message - the functionality is only enabled if a particular field has focus, if a user is in a room:

$scope.typing = function(event, room) {
    if (event.which !== 13) {
      if (typing === false && $scope.focussed && room !== null) {
        typing = true;
        socket.emit('typing', true);
      } else {
        clearTimeout(timeout);
        timeout = setTimeout(timeoutFunction, 1000);
      }
    }
  }

Let's discuss how Angular interacts with socket.io. Most of the functionality is of course written inside the controller however the initial connection to the chat server (socket server) is setup inside the services.js file.

The best thing about working with Angular is that I could utilise the built in directives such as ng-show, ng-hide and ng-if. I will describe why I enjoyed using them in a moment.

The interaction between the frontend and the backend is done by passing around objects. These object either contain data about the people connected or the rooms that are available. This gives a lot of flexibility but also poses a few dangers. The positive things are that the user data can be immediately attached to the $scope and this also means that any updates to the people object will automatically be refreshed in the view as well. Furthermore it also allows me to manipulate the view using the previously mentioned directivies. Have a look at this great example whereby I can control when to display the 'join', 'leave' or 'delete' buttons for rooms. Essentially a person can 'join' a room if he/she is not part of any room, a user who is already in a room but does not own it can 'leave' a room and finally a user who owns a room cannot 'join' or 'leave' a room but can 'delete'

<li ng-repeat="room in rooms" ng-cloak>
  <form class="form-inline" role="form"><div class="form-group"><p class="white">{{ room.name }}</p></div>
  <button class="btn btn-success btn-xs" type="submit" ng-click='joinRoom(room)' ng-hide='room.id === user.owns || room.id === user.inroom || user.owns || user.inroom'>Join</button>
    <button type="submit" ng-click='deleteRoom(room)' class="btn btn-xs btn-danger" ng-show='room.id === user.owns'>Delete</button>
    <button type="submit" ng-click="leaveRoom(room)" class="btn btn-xs btn-info" ng-hide='room.id === user.owns || !user.inroom || user.owns || user.inroom !== room.id'>Leave</button>
  </form>
</li>

Beautiful isn't it?

I mentioned dangers before. In web applications you should have security features at the backend. If you have security applied to your frontend someone with Chrome's Dev Tools could easily start manipulating your code. The challenge for me was to make sure that if I remove a particular user from the $scope.users object in the frontend, I remove them from the backend as well. So essentially I had to mirror some functionality from the frontend to the backend as well.

The code (as always) is available on GitHub with the appropriate installation instructions.

And for the first time I am also giving you guys a demo to try out. You can access that under this url: http://angularchat-tamasnode.rhcloud.com/ (as this is a free Openshift instance after some time the application may go idle. In that case you will see a 502 error, just wait a moment and refresh the page to see it in action again.)

Please note that I am still testing the app and the mobile version does not work 100% and of course I'm still not a web designer so there may be some visual deficiencies. If you find a bug, please report it.

And finally if you can't access the page nor do the installation here are a few screenshots:

I look forward to extending the functionality of this application and share it with you guys. Laters!

Show Comments