← Blog
AngularJS + Socket.io
- Angular
- JavaScript
- Programming
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;