AngularJS collaboration board with Socket.io
Lukas Ruebbelke demonstrates all the steps necessary to build a real-time collaboration board powered by AngularJS and Socket.io.
- Knowledge needed: Intermediate JavaScript
- Requires: Node.js, NPM
- Project Time: 2 hours
AngularJS is particularly well-suited for creating rich client-side applications in the browser and, when you add in a little Socket.io into the mix, things get really interesting. In this article we are going to build a real-time collaboration board that uses AngularJS for the client-side application and Socket.io to share state between all connected clients.
Let's cover a bit of housekeeping before we get started. I'm going to presume that you have a fundamental grasp of HTML and JavaScript as I'm not going to cover every little corner of the code. For instance, I'm not going to call out the CSS and JavaScript files I've included in the head of the HTML file as there is no new information there.
Also, I encourage you to grab the code from my GitHub account to follow along. My good friend Brian Ford also has an excellent Socket.io seed, which I based some of my original ideas on.
The four main features we want in the collaboration board is the ability to create a note, read the notes, update a note, delete a note and, for fun, move a note on the board. Yes, that's correct, we're focusing on standard CRUD features. I believe that by focusing on these fundamental features, we will have covered enough code for patterns to emerge so that you can take them and apply them elsewhere.
01. The server
We're going to start with the Node.js server first since it'll serve as the foundation that we're going to build everything else on.
We're going to be building a Node.js server with Express and Socket.io. The reason we're using Express is that it provides a nice mechanism for setting up a static asset server within Node.js. Express comes with a bunch of really awesome features but, in this case, we're going to use it to bisect the application cleanly between the server and client.
(I'm operating under the assumption that you have Node.js and NPM installed. A quick Google search will show you how to get these installed if you don't.)
02. The bare bones
So to build the bare bones of the server, we need to do a couple things to get up and running.
// app.js
// A.1
var express = require('express'),
app = express();
server = require('http').createServer(app),
io = require('socket.io').listen(server);
// A.2
app.configure(function() {
app.use(express.static(__dirname + '/public'));
});
// A.3
server.listen(1337);
A.1 We are declaring and instantiating our Node.js modules so that we can use them in our application. We are declaring Express, instantiating Express and then creating an HTTP server and sending in the Express instance into it. And from there we're instantiating Socket.io and telling it to keep an eye on our server instance.
A.2 We're then telling our Express app to use our public directory to serve files from.
A.3 We start up the server and tell it to listen on port 1337.
So far that has been pretty painless and quick. I believe we're less than 10 lines into the code and already we have a functional Node.js server. Onward!
03. Declare your dependencies
// packages.json
{
"name": "angular-collab-board",
"description": "AngularJS Collaboration Board",
"version": "0.0.1-1",
"private": true,
"dependencies": {
"express": "3.x",
"socket.io": "0.9.x"
}
}
One of the nicest features of NPM is the ability to declare your dependencies in a packages.json file and then automatically install them via npm install on the command line.
Get the Creative Bloq Newsletter
Daily design news, reviews, how-tos and more, as picked by the editors.
04. Wire up Socket.io
We have already defined the core features that we want in the application and so we need to set up Socket.io event listeners and an appropriate closure to handle the event for each operation.
In the code below you will notice that it's essentially a configuration of event listeners and callbacks. The first event is the connection event, which we use to wire up our other events in the closure.
io.sockets.on('connection', function(socket) {
socket.on('createNote', function(data) {
socket.broadcast.emit('onNoteCreated', data);
});
socket.on('updateNote', function(data) {
socket.broadcast.emit('onNoteUpdated', data);
});
socket.on('deleteNote', function(data){
socket.broadcast.emit('onNoteDeleted', data);
});
socket.on('moveNote', function(data){
socket.broadcast.emit('onNoteMoved', data);
});
});
From here we add listeners to createNote, updateNote, deleteNote and moveNote. And in the callback function, we're simply broadcasting what event happened so that any client listening can be notified that the event happened.
There are a few things worth pointing out about the callback functions in the individual event handlers. One, if you want to send an event to everyone else but the client that emitted the event you insert broadcast before the emit function call. Secondly, we're simply passing the payload of the event on to the interested parties so that they can process it how they see fit.
05. Start your engines!
Now that we have defined our dependencies and set up our Node.js application with Express and Socket.io powers, it's quite simple to initialise the Node.js server.
First you install your Node.js dependencies like so:
npm install
And then you start the server like this:
node app.js
And then! You go to this address in your browser. Bam!
06. A few candid thoughts before moving on
I'm primarily a frontend developer and I was initially a bit intimidated with hooking up a Node.js server to my application. The AngularJS part was a snap but server side JavaScript? Queue the creepy music from a horror flick.
But, I was absolutely floored to discover I could set up a static web server in just a few lines of code and in a few more lines use Socket.io to handle all the events between the browsers. And it was still just JavaScript! For the sake of timeliness, we're only covering a few features, but I hope that it by the end of the article you will see that it is easy to swim - and the deep end of the pool is not so scary.
07. The client
Now that we have our solid foundation in place with our server, let's move on to my favourite part - the client! We're going to be using AngularJS, jQueryUI for the draggable part and Twitter Bootstrap for a style base.
08. The bare bones
As a matter of personal preference, when I start a new AngularJS application I like to quickly define the bare minimum that I know I'm going to need to get started and then start iterating over that as quickly as possible.
Every AngularJS application needs to be bootstrapped with at least one controller present and so this is generally where I always start.
To automatically bootstrap the application you need to simply add ng-app to the HTML node in which you want the application to live. Most of the time, adding it to the HTML tag is going to be perfectly acceptable. I've also added an attribute to ng-app to tell it that I want to use the app module, which I will define in just a moment.
// public/index.html
<html ng-app="app">
I know I'm going to need at least one controller and so I will call that out using ng-controller and assigning it a property of MainCtrl.
<body ng-controller="MainCtrl"></body>
So now we're on the hook for a module named app and a controller named MainCtrl. Let us go ahead and create them now.
Creating a module is fairly straightforward. You define it by calling angular.module and giving it a name. For future reference, the second parameter of an empty array is where you can inject sub-modules for use in the application. It is out of the scope of this tutorial, but is handy when your application starts to grow in complexity and needs.
// public/js/collab.js
var app = angular.module('app', []);
We're going to declare a few empty placeholders in the app module starting with the MainCtrl below. We will fill these all in later but I wanted to illustrate the basic structure from the onset.
app.controller('MainCtrl', function($scope) { });
We are also going to wrap the Socket.io functionality in a socket service so that we can encapsulate that object and not leave it floating around on the global namespace.
app.factory('socket', function($rootScope) { });
And while we are at it, we're going to declare a directive called stickyNote that we are going to use to encapsulate the sticky note functionality in.
app.directive('stickyNote', function(socket) { });
So let us review what we have done so far. We have bootstrapped the application using ng-app and declared our application controller in the HTML. We've also defined the application module and created the MainCtrl controller, the socket service and the stickyNote directive.
09. Creating a sticky note
Now that we have the skeleton of the AngularJS application in place, we will start building out the creation feature.
app.controller('MainCtrl', function($scope, socket) { // B.1
$scope.notes = []; // B.2
// Incoming
socket.on('onNoteCreated', function(data) { // B.3
$scope.notes.push(data);
});
// Outgoing
$scope.createNote = function() { // B.4
var note = {
id: new Date().getTime(),
title: 'New Note',
body: 'Pending'
};
$scope.notes.push(note);
socket.emit('createNote', note);
};
B.1 AngularJS has a dependency injection feature built into it so we're injecting a $scope object and the socket service. The $scope object serves as a ViewModel and is basically a JavaScript object with some events baked into it to enable two-way databinding.
B.2 We're declaring the array in which we will use to bind the view to.
B.3 We're adding a listener for the onNoteCreated event on the socket service and pushing the event payload into the $scope.notes array.
B.4 We've declared a createNote method that creates a default note object and pushes it into the $scope.notes array. It also uses the socket service to emit the createNote event and pass the new note object along.
So now that we have a method to create the note, how do we call it? That is a good question! In the HTML file, we add the built in AngularJS directive ng-click to the button and then add the createNote method call as the attribute value.
<button id="createButton" ng-click="createNote()" class="btn btn-info pull-right">Create Note</button>
Time for a quick review of what we have done so far. We've added an array to the $scope object in the MainCtrl that's going to hold all the notes for the application. We have also added a createNote method on the $scope object to create a new local note and then broadcast that note to the other clients via the socket service. We've also added an event listener on the socket service so we can know when other clients have created a note so we can add it to our collection.
10. Displaying the sticky notes
We now have the ability to create a note object and share it between browsers but how do we actually display it? This is where directives come in.
Directives and their intricacies is a vast subject, but the short version is that they provide a way to extend elements and attributes with custom functionality. Directives are easily my favourite part about AngularJS because it allows you to essentially create an entire DSL (Domain Specific Language) around your application in HTML.
It's natural that since we are going to be creating sticky notes for our collaboration board that we should create a stickyNote directive. Directives are defined by calling the directive method on a module you want to declare it on and passing in a name and a function that return a directive definition object. The directive definition object has lots of possible properties you can define on it, but we're going to use just a few for our purposes here.
I recommend that you check out the AngularJS documentation to see the entire lists of properties you can define on the directive definition object.
app.directive('stickyNote', function(socket) {
var linker = function(scope, element, attrs) { };
var controller = function($scope) { };
return {
restrict: 'A', // C.1
link: linker, // C.2
controller: controller, // C.3
scope: { // C.4
note: '=',
ondelete: '&'
}
};
});
C.1 You can restrict your directive to a certain type of HTML element. The two most common are element or attribute, which you declare using E and A respectively. You can also restrict it to a CSS class or a comment, but these are not as common.
C.2 The link function is where you put all your DOM manipulation code. There are a few exceptions that I have found, but this is always true (at least 99 per cent of the time). This is a fundamental ground rule of AngularJS and is why I have emphasised it.
C.3 The controller function works just like the main controller we defined for the application but the $scope object we're passing in is specific to the DOM element the directive lives on.
C.4 AngularJS has a concept of isolated scope, which allows you to explicitly define how a directive’s scope communicates with the outside world. If we had not declared scope the directive would have implicitly inherited from the parent scope with a parent-child relationship. In a lot of cases this is not optimal. By isolating the scope we mitigate the chances that the outside world can inadvertently and adversely affect the state of your directive.
I have declared two-way data-binding to note with the = symbol and an expression binding to ondelete with the & symbol. Please read the AngularJS documentation for a full explanation of isolated scope as it is one of the more complicated subjects in the framework.
So let’s actually add a sticky note to the DOM.
Like any good framework, AngularJS comes with some really great features right out of the box. One of the handiest features is ng-repeat. This AngularJS directive allows you to pass in an array of objects and it duplicates whatever tag it is on as many times as there are items in the array. In the case below, we are iterating over the notes array and duplicating the div element and its children for the length of the notes array.
<div sticky-note ng-repeat="note in notes" note="note" class="alert alert-block sticky-note" ondelete="deleteNote(id)">
<button type="button" class="close" ng-click="deleteNote(note.id)">×</button>
<input ng-model="note.title" ng-change="updateNote(note)" type="text" class="title" >
<textarea ng-model="note.body" ng-change="updateNote(note)"
class="body">{{note.body}}</textarea>
</div>
The beauty of ng-repeat is that it is bound to whatever array you pass in and, when you add an item to the array, your DOM element will automatically update. You can take this a step further and repeat not only standard DOM elements but other custom directives as well. That is why you see sticky-note as an attribute on the element.
There are two other bits of custom code that need to be clarified. We have isolated the scope on the sticky-notes directive on two properties. The first one is the binding defined isolated scope on the note property. This means that whenever the note object changes in the parent scope, it will automatically update the corresponding note object in the directive and vice versa. The other defined isolated scope is on the ondelete attribute. What this means is that when ondelete is called in the directive, it will call whatever expression is in the ondelete attribute on the DOM element that instantiates the directive.
When a directive is instantiated it's added to the DOM and the link function is called. This is a perfect opportunity to set some default DOM properties on the element. The element parameter we are passing in is actually a jQuery object and so we can perform jQuery operations on it.
(AngularJS actually comes with a subset of jQuery built into it but if you have already included the full version of jQuery, AngularJS will defer to that.)
app.directive('stickyNote', function(socket) {
var linker = function(scope, element, attrs) {
// Some DOM initiation to make it nice
element.css('left', '10px');
element.css('top', '50px');
element.hide().fadeIn();
};
});
In the above code we are simply positioning the sticky note on the stage and fading it in.
11.Deleting a sticky note
So now that we can add and display a sticky note, it is time to delete sticky notes. The creation and deletion of sticky notes is a matter of adding and deleting items from the array that the notes are bound to. This is the responsibility of the parent scope to maintain that array, which is why we originate the delete request from within the directive, but let the parent scope do the actual heavy lifting.
This is why we went through all the trouble of creating expression defined isolated scope on the directive: so the directive could receive the delete event internally and pass it on to its parent for processing.
Notice the HTML inside the directive.
<button type="button" class="close" ng-click="deleteNote(note.id)">×</button>
The very next thing I am going to say may seem like a long way around but remember we are on the same side and it will make sense after I elaborate. When the button in the upper right hand corner of the sticky note is clicked we are calling deleteNote on the directive’s controller and passing in the note.id value. The controller then calls ondelete, which then executes whatever expression we wired up to it. So far so good? We're calling a local method on the controller which then hands it off to by calling whatever expression was defined in the isolated scope. The expression that gets called on the parent just happens to be called deleteNote as well.
app.directive('stickyNote', function(socket) {
var controller = function($scope) {
$scope.deleteNote = function(id) {
$scope.ondelete({
id: id
});
};
};
return {
restrict: 'A',
link: linker,
controller: controller,
scope: {
note: '=',
ondelete: '&'
}
};
});
(When using expression-defined isolated scope, parameters are sent in an object map.)
In the parent scope, deleteNote gets called and does a fairly standard deletion using the angular.forEach utility function to iterate over the notes array. Once the function has handled its local business it goes ahead and emits the event for the rest of the world to react accordingly.
app.controller('MainCtrl', function($scope, socket) {
$scope.notes = [];
// Incoming
socket.on('onNoteDeleted', function(data) {
$scope.deleteNote(data.id);
});
// Outgoing
$scope.deleteNote = function(id) {
var oldNotes = $scope.notes,
newNotes = [];
angular.forEach(oldNotes, function(note) {
if(note.id !== id) newNotes.push(note);
});
$scope.notes = newNotes;
socket.emit('deleteNote', {id: id});
};
});
12. Updating a sticky note
We're making fantastic progress! By now I hope that you are starting to see some patterns emerging from this whirlwind tour we're taking. Next item on the list is the update feature.
We're going to start at the actual DOM elements and follow it up all the way to the server and back down to the client. First we need to know when the title or body of the sticky note is being changed. AngularJS treats form elements as part of the data model so you can hook up two-way data-binding in a snap. To do this use the ng-model directive and put in the property you want to bind to. In this case we're going to use note.title and note.body respectively.
When either of these properties change we want to capture that information to pass along. We accomplish this with the ng-change directive and use it to call updateNote and pass in the note object itself. AngularJS does some very clever dirty checking to detect if the value of whatever is in ng-model has changed and then executes the expression that is in ng-change.
<input ng-model="note.title" ng-change="updateNote(note)" type="text" class="title" >
<textarea ng-model="note.body" ng-change="updateNote(note)" class="body">{{note.body}}</textarea>
The upside of using ng-change is that the local transformation has already happened and we are just responsible for relaying the message. In the controller, updateNote is called and from there we are going to emit the updateNote event for our server to broadcast to the other clients.
app.directive('stickyNote', function(socket) {
var controller = function($scope) {
$scope.updateNote = function(note) {
socket.emit('updateNote', note);
};
};
});
And in the directive controller, we are listening for the onNoteUpdated event to know when a note from another client has updated so that we can update our local version.
var controller = function($scope) {
// Incoming
socket.on('onNoteUpdated', function(data) {
// Update if the same note
if(data.id == $scope.note.id) {
$scope.note.title = data.title;
$scope.note.body = data.body;
}
});
};
13. Moving a sticky note
At this point we have basically done a lap around the CRUD kiddie pool and life is good! Just for the sake of a parlor trick to impress your friends, we're going to add in the ability to move notes around the screen and update coordinates in real time. Don’t panic - it's just a few more lines of code. All this hard work is going to pay off. I promise!
We've invited special guest, jQueryUI, to the party, and we did it all for the draggables. Adding in the ability to drag a note locally only takes one line of code. If you add in element.draggable(); to your linker function you will start hearing 'Eye of the Tiger' by Survivor because you can now drag your notes around.
We want to know when the dragging has stopped and capture the new coordinates to pass along. jQueryUI was built by some very smart people, so when the dragging stops you simply need to define a callback function for the stop event. We grab the note.id off the scope object and the left and top CSS values from the ui object. With that knowledge we do what we have been doing all along: emit!
app.directive('stickyNote', function(socket) {
var linker = function(scope, element, attrs) {
element.draggable({
stop: function(event, ui) {
socket.emit('moveNote', {
id: scope.note.id,
x: ui.position.left,
y: ui.position.top
});
}
});
socket.on('onNoteMoved', function(data) {
// Update if the same note
if(data.id == scope.note.id) {
element.animate({
left: data.x,
top: data.y
});
}
});
};
});
At this point it should come as no surprise that we're also listening for a move related event from the socket service. In this case it is the onNoteMoved event and if the note is a match then we update the left and top CSS properties. Bam! Done!
14. The bonus
This is a bonus section that I would not include if I were not absolutely confident you could achieve it in less than 10 minutes. We're going to deploy to a live server (I am still amazed at how easy it is to do).
First, you need to go sign up for a free Nodejitsu trial. The trial is free for 30 days, which is perfect for the sake of getting your feet wet.
Once you have created your account you need to install the jitsu package, which you can do from the command line via $ npm install jitsu -g.
Then you need to login in from the command line via $ jitsu login and enter your credentials.
Make sure you are in your app directly, type $ jitsu deploy and step through the questions. I usually leave as much to default as possible, which means I give my application a name but not a subdomain etc.
And, my dear friends, that is all there is to it! You will get the URL to your application from the output of the server once it has deployed and it is ready to go.
15. Conclusion
We've covered a lot of AngularJS ground in this article and I hope you had a lot of fun in the process. I think it's really neat what you can accomplish with AngularJS and Socket.io in approximately 200 lines of code.
There were a few things I didn't cover for the sake of focusing on the main points, but I encourage you to pull down the source and play around with the application. We have built a strong foundation, but there are still a lot of features you could add. Get hacking!
Lukas Ruebbelke is a technology enthusiast and is co-authoring AngularJS in Action for Manning Publications. His favorite thing to do is get people as excited about new technology as he is. He runs the Phoenix Web Application User Group and has hosted multiple hackathons with his fellow partners in crime.
Liked this? Read these!
- How to make an app
- Our favourite web fonts - and they don't cost a penny
- Discover what's next for Augmented Reality
- Download free textures: high resolution and ready to use now
Thank you for reading 5 articles this month* Join now for unlimited access
Enjoy your first month for just £1 / $1 / €1
*Read 5 free articles per month without a subscription
Join now for unlimited access
Try first month for just £1 / $1 / €1
The Creative Bloq team is made up of a group of design fans, and has changed and evolved since Creative Bloq began back in 2012. The current website team consists of eight full-time members of staff: Editor Georgia Coggan, Deputy Editor Rosie Hilder, Ecommerce Editor Beren Neale, Senior News Editor Daniel Piper, Editor, Digital Art and 3D Ian Dean, Tech Reviews Editor Erlingur Einarsson and Ecommerce Writer Beth Nicholls and Staff Writer Natalie Fear, as well as a roster of freelancers from around the world. The 3D World and ImagineFX magazine teams also pitch in, ensuring that content from 3D World and ImagineFX is represented on Creative Bloq.