Getting Chatty with Angular, Socket.IO, Node/Express and Bootstrap

by
Tags: , , , ,
Category:

Now that we’re well into the client/server age of the web with client-side frameworks such as AngularJS and Ember, it’s time to start revisiting those sample projects and reviewing how they’ll change with a more intelligent client. 

This little demonstration app, which you can clone/download at https://github.com/krimple/angular-socketio-chat, sets up a NodeJS-based chat server, and serves an AngularJS client which communicates with the server via the Socket.IO library.

Socket.IO is a library that supports a variety of clients and servers; in our case the app should respond using the underlying WebSocket API, however you can modify the socket server and client to transport data using long polling, flash, or other mechanisms as well.

The Server

Let’s get started by reviewing the server itself. I’m using NodeJS and Express (version 3.5) to provide the plumbing. I cheated and used Yeoman and the Yeoman Express Generator to build out a simple NodeJS server (because in other demos and my training courses I’ll be working on more demos, specifically RESTful server code).  I did this with:

yo express

I then realized it was running on Express 4.0, and so I backed off using:

npm install --save express@3.5

This began a bit of customization to undo the work the generator did.   More experienced Express experts probably would have coded something quickly out of the box, but my focus was a minimal Express server with basic middleware, which is what I mostly got.  I still have a bit of cleanup to go in app.js, but it’s functional now.  Not pretty, but functional…

Next, I added Socket.IO support:

npm install --save socket.io

Now with the dependencies installed, I moved to setting up the socket server…

The Socket Server

Let’s cut to the chase so we can focus on the Angular side. I created a socket server (chained to the express engine) in the app.js file like this (it’s a bit expurgated, look at the original app.js on GitHub):

var express = require('express'),
    io = require('socket.io'),
    http = require('http'),
    app = express(),
    server = http.createServer(app),
    io = io.listen(server);
server.listen(3000);

I also added this a bit later to mount the development-time AngularJS application root as a URI, /angular-dev:

app.use('/angular-dev',
    express.static(__dirname  + '/angular-frontend/app'));

Next, I decided to separate out the actual configuration of socket.io in another module, so next I referenced it (see /sockets/base.js):

require('./sockets/base')(io);

Here is the file:

module.exports = function (io) {
  'use strict';
  io.on('connection', function (socket) {
    socket.on('message', function (from, msg) {
      console.log('recieved message from',
                  from, 'msg', JSON.stringify(msg));
      console.log('broadcasting message');
      console.log('payload is', msg);
      io.sockets.emit('broadcast', {
        payload: msg,
        source: from
      });
      console.log('broadcast complete');
    });
  });
};

The console logging is for debugging purposes and to show you how the messages are received.

How it works

Socket.IO opens a server-side socket listener. Clients connect to the server, and when connected Socket.IO invokes io.on with the ‘connection’ event. We hook an event to the newly connected client, waiting on the client-side ‘message’ event – an incoming message is being sent. Once received, we use io.sockets.emit to send a message to all clients with the message category of ‘broadcast’ – which is our key to know on the client-side that a new chat message is being broadcasted to the chatters.

Our incoming message is sent with two parameters – the incoming chatter nickname, and the payload. So we broadcast the answer back in JSON format with two keys: ‘payload’, the message, and ‘source’, the sender.

[If I was less time-constrained a nice little diagram would appear here. Maybe it will in the future!]

At this point, we can connect a socket.io client to our server on the default URL (/socket.io) and begin chatting.

The AngularJS application

We’re going to configure AngularJS using the Twitter Bootstrap AngularJS Yeoman generator, if only to get all of our favorite libraries. I created the app in a subdirectory of my generated express application and deleted the .gitignore and other settings.

yo angular

I took all defaults, although we’re not currently using routing for example.

Let’s set up the application. We’ll be using Brian Ford’s Angular SocketIO library as a dependency, removed the routing code and config method, and set a default nickname, these are the major changes to the application creation script, angular-frontend/app/scripts/app.js:

angular
  .module('chatApp', [
    'ngCookies',
    'ngResource',
    'ngSanitize',
    'btford.socket-io'
  ])
  .value('nickName', 'anonymous');

A quick word about Brian Ford’s module, btford.socket-io. We’re going to use it to create our client-side socket, and to take any incoming messages and convert them into broadcasts to the AngularJS scope system. This adapter is really useful, and lets us focus mostly on Angular-based messaging and not directly touch the Socket.IO scripts for receiving of messages. Brian delegates to Socket.IO for sending, so the messages have the same rough for sending. We’ll see this when we review our socket service.

NB: If you want to play with routes, simply add back the ngRoute dependency here.

The Angular Controller

The magic happens in a combination of several files – the AngularJS controller, where we coordinate the setup of the data and events in our scope, and several other supporting services. We start by initializing the chat controller:

angular.module('chatApp')
.controller('SocketCtrl',
  function ($log, $scope, chatSocket,
            messageFormatter, nickName) {
  $scope.nickName = nickName;
  $scope.messageLog = 'Ready to chat!';

Quickly scanning the controller declaration, you see we’re using Angular’s $log abstraction to perform logging, $scope to access our page view model, and the nickName value to fetch our default chat name, anonymous.

We then assign the nickName as a scoped variable, so we can manage it. We make our messageLog, which will show up as a text area on the view, default to ‘Ready to chat!’.

Sending a message

Let’s see how the controller handles a message sending event. When we type in a chat message and click the button, the following controller function is called (note the AngularJS helper functions isArray and isDefined):

  $scope.sendMessage = function() {
    var match = $scope.message.match('^\/nick (.*)');
    if (angular.isDefined(match) &&
        angular.isArray(match) && match.length === 2) {
      var oldNick = nickName;
      nickName = match[1];
      $scope.message = '';
      $scope.messageLog = messageFormatter(new Date(),
                      nickName, 'nickname changed - from ' +
                      oldNick + ' to ' + nickName + '!') +
                      $scope.messageLog;
      $scope.nickName = nickName;
    }
    $log.debug('sending message', $scope.message);
    chatSocket.emit('message', nickName, $scope.message);
    $log.debug('message sent', $scope.message);
    $scope.message = '';
  };

We begin by checking the value of our form field, $scope.message. If it starts with /nick, we update our nickname, and use a service we’ll define below called messageFormatter to pretty print the change.

Next, we send our message using chatSocket.emit – this delegates to the Socket.IO client API emit method and sends our message, with the payload of our nickName and message text.

Finally, we clear out the message textbox value.

Incoming! Dealing with chat messages

Now we’re ready to receive incoming messages. This is where Brian’s socket client library really shines – it just treats incoming socket messages as message broadcasts in Angular:

 $scope.$on('socket:broadcast', function(event, data) {
    $log.debug('got a message', event.name);
    if (!data.payload) {
      $log.error('invalid message', 'event', event,
                 'data', JSON.stringify(data));
      return;
    }
    $scope.$apply(function() {
      $scope.messageLog = messageFormatter(
            new Date(), data.source,
            data.payload) + $scope.messageLog;
    });
  });
});  // end of controller

All messages coming from Socket.IO are prefixed with socket: and then suffixed with the message type. When our server receives a message, it sends it back to all clients as a broadcast, so we use the message event name of broadcast. Hence we respond to socket:broadcast. The function returns the event, which includes the name (in our case always socket:broadcast, and the data that was sent.

We pluck the payload and source fields out of the data response, and send them to our formatter to pretty-print. Finally, we append the message to the log.

The socket service

We defined a separate service, socket to invoke Brian’s library and return a client socket. It lives in app/services/socket.js

'use strict';
angular.module('chatApp')
.factory('chatSocket', function (socketFactory) {
      var socket = socketFactory();
      socket.forward('broadcast');
      return socket;
  });

The key call here is the socket.forward line – it tells the library to forward any messages of the event name of broadcast to the $rootScope in Angular. If we want to be more precise we can add a second parameter, the scope that we wish to broadcast into (keep in mind that scopes broadcast downward, so the root scope sends to all view scopes, so this is ok, albeit brute force).

The formatter

For completeness, here is our formatter. It simply prints a nicely formatted message:

angular.module('chatApp')
  .value('messageFormatter', function(date, nick, message) {
    return date.toLocaleTimeString() + ' - ' +
           nick + ' - ' +
           message + '\n';
  });

The view

We’re using a slimmed down view, without the Router. It’s bootstrap-laden, which makes it look nice without any particular CSS of my own. Here is the Angular-specific stuff:

<div class='container'>
  <div ng-controller='SocketCtrl'>
    You are known as: <span ng-bind="nickName"></span>.
    <br/>
    Change your nickname with <code>/nick [yourNewNick]</code>
    <div class='row'>
      <div class='col-md-9'>
        <textarea style='width: 100%; height: 200px'
             ng-disable='true' ng-model='messageLog'></textarea>
      </div>
    </div>
    <div class='row'>
      &nbsp;
    </div>
    <div class='row'>
      <div class='col-md-8'>
        <form role='form' novalidate name='form'>
          <div class='form-group'>
            <input type='text' style='width: 80%'
            required
            ng-max='50'
            ng-model='message'
            focus>
            <button class='btn btn-sm btn-primary'
              ng-enable='form.$valid'
              ng-click="sendMessage(message)">Send!</button>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>

Wrap-up

That’s it. Please feel free to download the code, and follow the README.md instruction guide for building and playing around. Comments welcome!