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( "Me: " + 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: ' ' ,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( 'Welcome to ChatJS.' ); 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( '' + data.id + ': ' + 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 + '
' ); 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( 'Client connected: ' + 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( 'Client left: ' + 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
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: