← Blog

AngularJS + Socket.io

AngularJS provides a very interactive user experience, just like Socket.io. When you combine them in a single page application awesomeness is squared.

But some of the AngularJS mechanics produce buggy behavior for Socket.io.

For example, if you subscribe to an event in your controller and then go to another view scope this controller is destroyed, but event subscription is still stored in a global socket.io object. So when you go back and the controller is reinstantiated, you get a second subscription. After a while of you going back and forth there’s a chaos!

You don’t want to receive 2+ messages from 1 event in your let’s say chat application. So how can we deal with it?

I’ll let the code and comments speak for themselves (coffeescript):

"use strict"
angular.module("chatApp").factory "Socket", ($rootScope) ->
  # io is a global object available if you included socket.io script:
  socket = io.connect()
  ###
    $scope - scope of a controller
    global - boolean indicating whether a change applies only
             to a local scope or to a $rootScope
             (in case perfomansc is an issues)
  ###
  resultObject = ($scope, global = true)->
    scopeOfChange = if global then $rootScope else $scope
    # create an array of listeners if there is none (notice '?' operator):
    $scope._listeners?= []
    # if controller's scope is destroyed then $destroy event is fired
    # on this event we should get rid of all listeners on this scope:
    $scope.$on '$destroy', (event)->
      for lis in $scope._listeners
        socket.removeListener(lis.eventName, lis.ngCallback)
      $scope._listeners.length=0

    # return familiar to us socket.io object that can listen to and emit events:
    return  {
      on: (eventName, callback) ->
        ngCallback = ()->
          args = arguments
          # trigger angular $digest cycle on selected scope:
          scopeOfChange.$apply ->
            callback.apply socket, args # apply function to original object
        # save listener to a list on current scope so we can remove it later:
        $scope._listeners.push {
          eventName
          ngCallback
        }
        # pass our own callback that wraps the one passed by a user
        socket.on eventName, ngCallback

      emit: (eventName, data, callback) ->
        socket.emit eventName, data, ->
          args = arguments
          scopeOfChange.$apply ->
            callback.apply socket, args  if callback
    }
  # sometimes I find reconnect to be usefull:
  resultObject.reconnect = ()->
    socket.socket.disconnect()
    socket.socket.connect()
  return resultObject

And here’s how you use it:

'use strict';

angular.module('chatApp')
  .controller 'ChatCtrl', ($scope, Socket)->
    socket = Socket($scope)

    socket.emit 'subscribe', {}
    $scope.messages = [ ]

    socket.on 'message_history', (messages)->
      $scope.messages = messages

    socket.on 'user_list', (users)->
      $scope.users = users

    socket.on 'message_created', (msg)->
      $scope.messages.push msg

    socket.on 'message_deleted', (msg)->
      removeFromArray($scope.messages, msg)

    $scope.deleteMessage = (msg)->
      socket.emit 'delete_message', {id: msg._id}

    $scope.submitMessage = (form)->
      if form.$valid
        socket.emit 'create_message', {text: $scope.currentMessage}
        $scope.currentMessage = ""

Some use case for using reconnect (it’s so simple I shouldn’t have included it):

"use strict"
angular.module("chatApp").controller "LoginCtrl", ($scope, Auth, $location, Socket) ->
  $scope.user = {}
  $scope.errors = {}
  $scope.login = (user) ->
    Auth.login(
      nickname: user.nickname
      password: user.password
    ).then ()->
      # Logged in, redirect to home
      Socket.reconnect()
      $location.path "/"
    .catch (err)->
      err = err.data;
      $scope.errors.other = err.message;