Neither HTML nor HTTP were created for dynamic web applications. We basically rely on hacks, on top of hacks to give our apps a responsive user interface. AngularJS removes some limitations from HTML, allowing us to create and manage user-interface code easier. Socket.IO, on the other hand helps us send data from the server not only when client requests it, but also when the server needs to. In this article I will show you how to combine these two, to improve the responsiveness of your single-page apps.
Introduction
In the first part of this tutorial we will create a reusable AngularJS service for Socket.IO. Because of that reusable part, this will be a little trickier than just using module.service()
or module.factory()
. These two functions are just syntactic sugar on top of the more low-level module.provider()
method, which we will use to provide some configuration options. If you've never used AngularJS before, I strongly advise you to at least follow the official tutorial and some of the tutorials here on Tuts+.
Preparation: The Back-End
Before we start writing our AngularJS module, we need a simple back-end for testing. If you are already familiar with Socket.IO you can just scroll down to the end of this section, copy the back-end source and proceed to the next one, if not - read on.
Required Modules
We will only need socket.io
. You can either install it directly using the npm
command like this:
npm install socket.io
Or create a package.json
file, put this line in the dependencies
section:
"socket.io": "0.9.x"
And execute the npm install
command.
Creating the Socket.IO Server
Since we don't need any complicated web framework like Express, we can create the server using Socket.IO:
var io = require('socket.io')(8080);
That's all you need to setup the Socket.IO server. If you start your app, you should see similar output in the console:
And you should be able to access the socket.io.js
file in your browser at http://localhost:8080/socket.io/socket.io.js:
Handling Connections
We will handle all incoming connections in the connection
event listener of the io.sockets
object:
io.sockets.on('connection', function (socket) { });
The socket
attribute passed to the callback is the client that connected and we can listen to events on it.
A Basic Listener
Now we will add a basic event listener in the callback above. It will send the data received, back to the client using the socket.emit()
method:
socket.on('echo', function (data) { socket.emit('echo', data); });
echo
is the custom event name that we will use later.
A Listener With Acknowledgment
We will also use acknowledgments in our library. This feature allows you to pass a function as the third parameter of the socket.emit()
method. This function can be called on the server to send some data back to the client:
socket.on('echo-ack', function (data, callback) { callback(data); });
This allows you to respond to the client without requiring it to listen to any events (which is useful if you want to just request some data from the server).
Now our test back-end is complete. The code should look like this (this is the code you should copy if you omitted this section):
var io = require('socket.io')(8080); io.sockets.on('connection', function (socket) { socket.on('echo', function (data) { socket.emit('echo', data); }); socket.on('echo-ack', function (data, callback) { callback(data); }); });
You should now run the app and leave it running before proceeding with the rest of the tutorial.
Preparation: The Front-End
We will of course need some HTML to test our library. We have to include AngularJS, socket.io.js
from our back-end, our angular-socket.js
library and a basic AngularJS controller to run some tests. The controller will be inlined in the <head>
of the document to simplify the workflow:
<!DOCTYPE html> <html> <head> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.16/angular.min.js"></script> <script src="http://localhost:8080/socket.io/socket.io.js"></script> <script src="angular-socket.js"></script> <script type="application/javascript"> </script> </head> <body> </body> </html>
This is all we need for now, we will get back to the empty script tag later since we don't have the library yet.
Creating the AngularJS Socket.IO Library
In this section we will create the angular-socket.js
library. All of the code must be insterted into this file.
The Module
Let's start with creating the module for our lib:
var module = angular.module('socket.io', []);
We don't have any dependencies, so the array in the second argument of angular.module()
is empty, but do not remove it completely or you will get an $injector:nomod
error. This happens because the one-argument form of angular.module()
retrieves a reference to the already existing module, instead of creating a new one.
The Provider
Providers are one of the ways to create AngularJS services. The syntax is simple: the first argument is the name of the service (not the name of the provider!) and second one is the constructor function for the provider:
module.provider('$socket', $socketProvider() { });
Configuration Options
To make the library reusable, we will need to allow changes in Socket.IO's configuration. First let's define two variables that will hold the URL for the connection and the configuration object (code in this step goes to the $socketProvider()
function):
var ioUrl = ''; var ioConfig = {};
Now since these variables are not available outside of the $socketProvider()
function (they are kind of private), we have to create methods (setters) to change them. We could of course just make them public like this:
this.ioUrl = ''; this.ioConfig = {};
But:
- We would have to use
Function.bind()
later to access the appropriate context forthis
- If we use setters, we can validate to make sure the proper values are set - we don't want to put
false
as the'connect timeout'
option
A full list of options for Socket.IO's Client can be seen on their GitHub wiki. We will create a setter for each of them plus one for the URL. All of the methods look similar, so I will explain the code for one of them and put the rest below.
Let's define the first method:
this.setConnectionUrl = function setConnectionUrl(url) {
It should check the type of parameter passed in:
if (typeof url == 'string') {
If it's the one we expected, set the option:
ioUrl = url;
If not, it should throw TypeError
:
} else { throw new TypeError('url must be of type string'); } };
For the rest of them, we can create a helper function to keep it DRY:
function setOption(name, value, type) { if (typeof value != type) { throw new TypeError("'"+ name +"' must be of type '"+ type + "'"); } ioConfig[name] = value; }
It just throws TypeError
if the type is wrong, otherwise sets the value. Here is the code for the rest of the options:
this.setResource = function setResource(value) { setOption('resource', value, 'string'); }; this.setConnectTimeout = function setConnectTimeout(value) { setOption('connect timeout', value, 'number'); }; this.setTryMultipleTransports = function setTryMultipleTransports(value) { setOption('try multiple transports', value, 'boolean'); }; this.setReconnect = function setReconnect(value) { setOption('reconnect', value, 'boolean'); }; this.setReconnectionDelay = function setReconnectionDelay(value) { setOption('reconnection delay', value, 'number'); }; this.setReconnectionLimit = function setReconnectionLimit(value) { setOption('reconnection limit', value, 'number'); }; this.setMaxReconnectionAttempts = function setMaxReconnectionAttempts(value) { setOption('max reconnection attempts', value, 'number'); }; this.setSyncDisconnectOnUnload = function setSyncDisconnectOnUnload(value) { setOption('sync disconnect on unload', value, 'boolean'); }; this.setAutoConnect = function setAutoConnect(value) { setOption('auto connect', value, 'boolean'); }; this.setFlashPolicyPort = function setFlashPolicyPort(value) { setOption('flash policy port', value, 'number') }; this.setForceNewConnection = function setForceNewConnection(value) { setOption('force new connection', value, 'boolean'); };
You could replace it with a single setOption()
method, but it seems easier to type the option's name in camel case, rather than pass it as a string with spaces.
The Factory Function
This function will create the service object that we can use later (for example in controllers). First, let's call the io()
function to connect to the Socket.IO server:
this.$get = function $socketFactory($rootScope) { var socket = io(ioUrl, ioConfig);
Note that we are assigning the function to the $get
property of the object created by the provider - this is important since AngularJS uses that property to call it. We also put $rootScope
as its parameter. At this point, we can use AngularJS's dependency injection to access other services. We will use it to propagate changes to any models in Socket.IO callbacks.
Now the function needs to return an object:
return { }; };
We will put all methods for the service in it.
The on()
Method
This method will attach an event listener to the socket object, so we can utilize any data sent from the server:
on: function on(event, callback) {
We will use Socket.IO's socket.on()
to attach our callback and call it in AngularJS's $scope.$apply()
method. This is very important, because models can only be modified inside of it:
socket.on(event, function () {
First, we have to copy the arguments to a temporary variable so we can use them later. Arguments are of course everything that the server sent to us:
var args = arguments;
Next, we can call our callback using Function.apply()
to pass arguments to it:
$rootScope.$apply(function () { callback.apply(socket, args); }); }); },
When socket
's event emitter calls the listener function it uses $rootScope.$apply()
to call the callback provided as the second argument to the .on()
method. This way you can write your event listeners like you would for any other app using Socket.IO, but you can modify AngularJS's models in them.
The off()
Method
This method will remove one or all event listeners for a given event. This helps you to avoid memory leaks and unexpected behavior. Imagine that you are using ngRoute
and you attach few listeners in every controller. If the user navigates to another view, your controller is destroyed, but the event listener remains attached. After a few navigations and we'll have a memory leak.
off: function off(event, callback) {
We only have to check if the callback
was provided and call socket.removeListener()
or socket.removeAllListeners()
:
if (typeof callback == 'function') { socket.removeListener(event, callback); } else { socket.removeAllListeners(event); } },
The emit()
Method
This is the last method that we need. As the name suggests, this method will send data to the server:
emit: function emit(event, data, callback) {
Since Socket.IO supports acknowledgments, we will check if the callback
was provided. If it was, we will use the same pattern as in the on()
method to call the callback inside of $scope.$apply()
:
if (typeof callback == 'function') { socket.emit(event, data, function () { var args = arguments; $rootScope.$apply(function () { callback.apply(socket, args); }); });
If there is no callback
we can just call socket.emit()
:
} else { socket.emit(event, data); } }
Usage
To test the library, we will create a simple form that will send some data to the server and display the response. All of the JavaScript code in this section should go in the <script>
tag in the <head>
of your document and all HTML goes in its <body>
.
Creating the Module
First we have to create a module for our app:
var app = angular.module('example', [ 'socket.io' ]);
Notice that 'socket.io'
in the array, in the second parameter, tells AngularJS that this module depends on our Socket.IO library.
The Config Function
Since we will be running from a static HTML file, we have to specify the connection URL for Socket.IO. We can do this using the config()
method of the module:
app.config(function ($socketProvider) { $socketProvider.setConnectionUrl('http://localhost:8080'); });
As you can see, our $socketProvider
is automatically injected by AngularJS.
The Controller
The controller will be responsible for all of the app's logic (the application is small, so we only need one):
app.controller('Ctrl', function Ctrl($scope, $socket) {
$scope
is an object that holds all of the controller's models, it's the base of AngularJS's bi-directional data binding. $socket
is our Socket.IO service.
First, we will create a listener for the 'echo'
event that will be emited by our test server:
$socket.on('echo', function (data) { $scope.serverResponse = data; });
We will display $scope.serverResponse
later, in HTML, using AngularJS's expressions.
Now there will also be two functions that will send the data - one using the basic emit()
method and one using emit()
with acknowledgment callback:
$scope.emitBasic = function emitBasic() { $socket.emit('echo', $scope.dataToSend); $scope.dataToSend = ''; }; $scope.emitACK = function emitACK() { $socket.emit('echo-ack', $scope.dataToSend, function (data) { $scope.serverResponseACK = data; }); $scope.dataToSend = ''; }; });
We have to define them as methods of $scope
so that we can call them from the ngClick
directive in HTML.
The HTML
This is where AngularJS shines - we can use standard HTML with some custom attributes to bind everything together.
Let's start by defining the main module using an ngApp
directive. Place this attribute in the <body>
tag of your document:
<body ng-app="example">
This tells AngularJS that it should bootstrap your app using the example
module.
After that, we can create a basic form to send data to the server:
<div ng-controller="Ctrl"> <input ng-model="dataToSend"> <button ng-click="emitBasic()">Send</button> <button ng-click="emitACK()">Send (ACK)</button> <div>Server Response: {{ serverResponse }}</div> <div>Server Response (ACK): {{ serverResponseACK }}</div> </div>
We used a few custom attributes and AngularJS directives there:
-
ng-controller
- binds the specified controller to this element, allowing you to use values from its scope -
ng-model
- creates a bi-directional data bind between the element and the specified scope property (a model), which allows you to get values from this element as well as modifying it inside of the controller -
ng-click
- attaches aclick
event listener that executes a specified expression (read more on AngularJS expressions)
The double curly braces are also AngularJS expressions, they will be evaluated (don't worry, not using JavaScript's eval()
) and their value will be inserted in there.
If you have done everything correctly, you should be able to send data to the server by clicking the buttons and see the response in the appropriate <div>
tags.
In Summary
In this first part of the tutorial, we've created the Socket.IO library for AngularJS that will allow us to take advantage of WebSockets in our single-page apps. In the second part, I will show you how you can improve the responsiveness of your apps using this combination.
Comments