Part 3: Real-Time Browser to Browser Communication, ExtJS Based Chat Application Using Node.Js and Socket.Io

In the previous entries, found here and here, I wrote about how to enable browser to server communication, using Node.Js and Socket.Io and how to emit events and return responses. In this entry I will describe how to route those events among different connected browsers, and how to create a basic chat application using Sencha ExtJS as a user interface, to notify clients when a new user is connected/disconnected and enable chat messaging.

Sencha ExtJS is a JavaScript framework, used for creating Rich Desktop Web Applications. It provides out of the box widgets, such as Windows, Grids, Panels, model data handling and others.

Server side: Handling and routing messages

Step1: Create a new object, that provides event handlers and routing logic.

/**
 * Chat application.
 * @class Provides message routing, along with other chat functionality.
 * @constructor
 */
var ChatApplication = function() {
	// Prepare the client object (storing socket objects, by id)
	this._clientSockets = {};
};

Step 2: Add a client connection handler to this object used for keeping track of active connections, and notifying user about a new.

/**
 * Method used for keeping track of client connections.
 * @param {Object} socket Socket object, storing the client data.
 * @function
 */
ChatApplication.prototype.clientConnectionHandler = function( socket ) {
	// Push data to the client
	this._clientSockets[socket.id] = socket;

	// Notify others, by routing the 'newClient' event, along with a socket id
	this.emitEvent( 'newClient', { id: socket.id }, socket );

	// Send a list of client ids to the new client
	this.clientListHandler( {}, socket );
}

Step 3: Add an event handler 'clientList' that returns a list of connected sockets.

/**
 * Method used for handling a client list request.
 * @param {Object} data Data object.
 * @param {Object} socket Socket object, storing the client data.
 * @function
 */
ChatApplication.prototype.clientListHandler = function( data, socket ) {
	var clientIds = [];
	for ( var socketId in this._clientSockets ) {
		if ( this._clientSockets[socketId] ) {
			clientIds.push( { id: socketId } );
		}
	}
	socket.emit( 'clientList', { ids: clientIds } );
}

Step 4: Add an event handler 'clientMessage' that handles messages.

/**
 * Method used for handling a client text message event.
 * @param {Object} data Data object. Always null for a disconnecting client.
 * @param {Object} socket Socket object, storing the client data.
 * @function
 */
ChatApplication.prototype.clientMessageHandler = function( data, socket ) {
	// Notify others, by routing the 'clientMessage' event, along with a socket id and text value
	this.emitEvent( 'clientMessage', { id: socket.id, text: data.text }, socket );
}

Step 5: Add a client disconnect handler that updates the list of connected sockets.

/**
 * Method used for keeping track of disconnecting clients.
 * @param {Object} data Data object. Always null for a disconnecting client.
 * @param {Object} socket Socket object, storing the client data.
 * @function
 */
ChatApplication.prototype.clientDisconnectHandler = function( data, socket ) {
	// Remove from sockets objects, to ignore it upon next notification
	this._clientSockets[socket.id] = false;

	// Notify existing clients, by routing the 'disconnectingClient' event, along with a socket id
	this.emitEvent( 'disconnectingClient', { id: socket.id }, socket );
}

Step 6: Add a function that distributes messages among connected sockets.

/**
 * Method used for emitting events, to all clients.
 * @param {String} eventName Event name.
 * @param {Object} data Data object to push to clients.
 * @param {Object} ignoreSocket Optional socket object to ignore. Used when sending messages to all users, except the specified socket user.
 */
ChatApplication.prototype.emitEvent = function( eventName, data, ignoreSocket ) {
	// Loop all existing connections
	var me = this;
	Object.keys( this._clientSockets ).forEach( function( socketId ) {
		// Verify if we must ignore a socket
		if ( typeof ignoreSocket !== "undefined" ) {
			// Verify if that socket is the current one
			if ( me._clientSockets[socketId] && me._clientSockets[socketId].id == ignoreSocket.id ) {
				return; // Ignore
			}
		}

		// Emit event
		if ( me._clientSockets[socketId] ) {
			me._clientSockets[socketId].emit( eventName, data );
		}
	} );
}

Step 7: Instantiate the object, and register the event handlers with the Server object.

var ChatApp = new ChatApplication();

// Create server
ChatServer = new Server( {
	port: 10000 // Listening port
	,socket: { // Socket configuration
		log: false // Disable loggings
	}
	// Set the scope to the instance of ChatApp
	,scope: ChatApp
	// Add event handlers
	,events: {
		// Disconnecting client event
		disconnect: ChatApp.clientDisconnectHandler
		// Message event handler
		,clientMessage: ChatApp.clientMessageHandler
		// Client list handler
		,clientList: ChatApp.clientListHandler
	}
	// Connection handler
	,connectionHandler: ChatApp.clientConnectionHandler
 } ).init();

Client side: Display the user interface, initiate a connection, send and receive messages

Step 1: Create a new class, that provides the chat application.

/**
 * Chat Application Object.
 * @class Provides chat functionality.
 * @constructor
 */
var ChatJs = function() {
	// Client id array
	this._clients = [];

	// Client instance
	this.client = {};

	// Create the UI as soon as ExtJS is ready
	Ext.onReady( function() {
		// Prepare the client list
		this.clientList = Ext.create( 'Ext.grid.Panel', {
			region: 'west'
			,width: 180
			,columns: [
				{ header: 'Client Id',  dataIndex: 'id', flex: 1 }
			]
			,store: Ext.create( 'Ext.data.Store', {
				fields: [ 'id' ]
				,data: []
			} )
		} );

		// Handle a text sending UI action
		var handleSendText = function() {
			if ( this.textField.getValue() ) {
				this.addText( "<b>Me:</b> " + Ext.htmlEncode( this.textField.getValue() ) );

				// Emit event
				this.client.emit( 'clientMessage', { text: this.textField.getValue() } );
			}
			this.textField.setValue( "" );
		}

		// Text field
		this.textField = Ext.create( 'Ext.form.field.Text', {
			width: 560
			,enableKeyEvents: true
			,listeners: {
				keydown: function( field, e, eOpts ) {
					if ( e.getKey() === 13 ) {
						handleSendText.bind( this )();
					}
				}.bind( this )
			}
		} );

		// Prepare the text window
		this._firstTime = true; // Prevent the welcome text from displaying twice
		this.textPanel = Ext.create( 'Ext.panel.Panel', {
			region: 'center'
			,border: false
			,autoScroll: true
			,html: '<div style="height: 900px;">&nbsp;</div>'
			,bbar: [
				this.textField
				, '-'
				, Ext.create( 'Ext.button.Button', {
					text: 'Send'
					,handler: handleSendText.bind( this )
				} )
			]
			,listeners: {
				// Display welcome text
				afterlayout: function() {
					if ( this._firstTime === true ) {
						this.addText( '<b>Welcome to ChatJS.</b>' );
						this._firstTime = false;
					}
				}.bind( this )
			}
		} );

		// Prepare the window
		this.chatWindow = Ext.create( 'Ext.window.Window', {
			title: 'ChatJS'
			,closable: false
			,maximizable: false
			,minimizable: false
			,resizable: false
			,height: 500
			,width: 800
			,layout: 'border'
			,items: [
				this.clientList
				,this.textPanel
			]
		} );

		// Show
		this.chatWindow.show();
	}.bind( this ) );
};

Step 2: Add an event handler 'clientMessageHandler'.

/**
 * Method used for handling an incoming message.
 * @param {Object} data Data object.
 * @function
 */
ChatJs.prototype.clientMessageHandler = function( data ) {
	// Add text to window
	this.addText( '<b>' + data.id + ':</b> ' + Ext.htmlEncode( data.text ) );
}

Step 3: Add an event handler 'clientListMessageHandler'.

/**
 * Method used for handling a client list event.
 * @param {Object} data Data object.
 * @function
 */
ChatJs.prototype.clientListMessageHandler = function( data ) {
	// Store the list of clients, for later use
	this._clientList = data.ids;

	// Reload UI list
	this.clientList.getStore().loadRawData( data.ids );
}

Step 4: Add a method used for updating the chat text.

/**
 * Method used for appending text.
 * @param {String} text String to add to window.
 * @function
 */
ChatJs.prototype.addText = function( text ) {
	// Get DOM component
	var obj = Ext.get( 'messageArea' );

	this.textPanel.body.insertHtml( "beforeEnd", text + '<br>' );
	this.textPanel.body.scroll( 'b', Infinity );
}

Step 5: Add an event handler 'newClientHandler'.

/**
 * Method used for handling a new client connection.
 * @param {Object} data Data object.
 * @function
 */
ChatJs.prototype.newClientHandler = function( data ) {
	// Add text to window
	this.addText( '<b>Client connected:</b> ' + data.id );

	// Request a new list of clients
	this.client.emit( 'clientList', {} );
}

Step 6: Add an event handler 'disconnectingClientHanlder'.

/**
 * Method used for handling a disconnecting client event.
 * @param {Object} data Data object.
 * @function
 */
ChatJs.prototype.disconnectingClientHandler = function( data ) {
	// Add text to window
	this.addText( '<b>Client left:</b> ' + data.id );

	// Request a new list of clients
	this.client.emit( 'clientList', {} );
}

Step 7: Update the Client object to make use of the new events.

// Create a new instance of the chat application
var ChatApplication = new ChatJs();

var Example = new Client( {
	port: 10000
	,host: 'http://localhost'
	,scope: ChatApplication
	// Example event handlers, not bound to any scope
	,events: {
		// Client message handler
		clientMessage: ChatApplication.clientMessageHandler
		// Client disconnection handler
		,disconnectingClient: ChatApplication.disconnectingClientHandler
		// Connecting clients handler
		,newClient: ChatApplication.newClientHandler
		// Client list handler
		,clientList: ChatApplication.clientListMessageHandler
	}
} );

// Initialise the server
Example.init();

// Add the chat server to the chat application
ChatApplication.client = Example;

Update the HTML file, to include ExtJS, and the new client.js file

<html>
	<head>
		<link rel="stylesheet" type="text/css" href="extjs/resources/css/ext-all.css"/>
		<script type="text/javascript" src="extjs/ext-all.js"></script>

		<script src="http://localhost:10000/socket.io/socket.io.js"></script>
		<script src="chat.js"></script>
		<script src="client.js"></script>
	</head>
	<body>
	</body>
</html>

While the three articles provides a rough introduction to node.js and socket.io, without going into details, this example can be used as a starting point for enabling browser to browser communication. This can be used either for real time chat systems, gaming or other event handling mechanism. 

The application can be seen here:

http://grosan.co.uk/chatjs/

The source code can be viewed or forked here:

https://github.com/fgheorghe/jsIRC/tree/64a288921fc0170644e68c5d7e135798b4b8ea5a

Here is a screenshot of the user interface, displaying a list of connected socket ids, a message from a second browser, from the current browser and a client disconnecting and connecting:

3 thoughts on “Part 3: Real-Time Browser to Browser Communication, ExtJS Based Chat Application Using Node.Js and Socket.Io”

    1. Thanks for reporting the issue. I haven’t been able to replicate it, however, try changing the server.js code at line 217 to ,connectionHandler: ChatApp.clientConnectionHandler.bind(ChatApp). This will bind the clientConnectionHandler function to ChatApp’s scope, ensuring this._clientSockets is available.

      I’ve updated the github link to point to the right commit, as this project evolved into an IRC server:
      https://github.com/fgheorghe/jsIRC/tree/64a288921fc0170644e68c5d7e135798b4b8ea5a

Leave a Reply

Your email address will not be published. Required fields are marked *