Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

README.md

Shared TODO Application using plain Javascript

This repository contains a tutorial for writing simple TODO application written using with Javascript and jQuery that can be used with Kaazing AMQP or JMS brokers.

The application loads 5 todo items from JSON file and notifies other users when

  • An item is marked complete/incomplete
  • An item is being edited by one client. This disables editing on all other clients.

The application uses the Kaazing Universal Clients for Javascript open source library - for specifics how to use the library with plain Javascript see Kaazing JavaScript Universal Client for Javascript

Tutorial

In this tutorial we will guide you through development of a simple shared TODO application using AngularJS. We assume that you are familiar with AngularJS framework and will mostly focus on WebSocket-related features of an application.

Preparation

In order to develop an application we need to:

  • Create directory structure:

    • root
      • js
      • css
      • data
  • Copy bower.json to your root directory. Open command line at your root directory and execute the following command (note, you need to have NodeJS installed).

bower install
  • Create root/data/todo.json file with todo items with the following or similar content:
[
  {
    "id": 1,
    "action": "Get groceries",
    "complete": false
  },
  {
    "id": 2,
    "action": "Call plumber",
    "complete": false
  },
  {
    "id": 3,
    "action": "Buy running shoes",
    "complete": false
  },
  {
    "id": 4,
    "action": "Buy flowers",
    "complete": false
  },
  {
    "id": 5,
    "action": "Call family",
    "complete": false
  }
]	

Developing single-user TODO application

  1. Create root/todo.html with the following code:

    <!DOCTYPE html>
    <html ng-app="webSocketApp">
    <head>
        <title>WebSocket Test Application</title>
        <script src="bower_components/jquery/dist/jquery.js"></script>
        <script src="bower_components/requirejs/require.js"></script>
        <link href="bower_components/bootstrap/dist/css/bootstrap.css" rel="stylesheet"/>
        <link href="bower_components/bootstrap/dist/css/bootstrap-theme.css" rel="stylesheet"/>
        <link href="css/app.css" rel="stylesheet"/>
        <script src="bower_components/kaazing-jms-client-javascript/javascript/src/JmsClient.js"></script>
        <script src="bower_components/kaazing-javascript-universal-client/javascript/src/JavascriptUniversalClient.js"></script>
        <script src="js/main.js"></script>
    </head>
    <body>
    <div id="todoPanel" class="panel">
        <h3 class="panel-header">To Do List</h3>
        <table id="todoTable" class="table">
            <thead>
            <tr>
                <th>#</th>
                <th>Action</th>
                <th>Done</th>
            </tr>
            </thead>
            <tbody></tbody>
        </table>
        <h4>Local Messages</h4>
        <div id="localMessages" class="msg-container">
        </div>
        <h4>WebSocket Messages</h4>
        <div id="wsMessages" class="msg-container">
        </div>
    </div>
    
    </body>
    </html>

    As you can see, the page consists of a table where we will be adding items with checkboxes - that is where Todo items will go and two divs for to log events.

  2. Create root/css/app.css - the stylesheet for the application

    /* app css stylesheet */
    
    .menu {
    	list-style: none;
    	border-bottom: 0.1em solid black;
    	margin-bottom: 2em;
    	padding: 0 0 0.5em;
    }
    
    .menu:before {
    	content: "[";
    }
    
    .menu:after {
    	content: "]";
    }
    
    .menu > li {
    	display: inline;
    }
    
    .menu > li:before {
    	content: "|";
    	padding-right: 0.3em;
    }
    
    .menu > li:nth-child(1):before {
    	content: "";
    	padding: 0;
    }
    
    tr.Red {background-color: lightcoral;}
    tr.Green {background-color: lightgreen;}
    tr.Blue {background-color: lightblue;}
    
    .Gray {background-color: lightgray;}
    .Yellow {background-color: lightgoldenrodyellow}
    .Blue {background-color: lightblue;}
    
    .Done {background-color: lightgreen;}
    .NotDone {background-color: lightcoral;}
    .MouseOverNotDone {background-color: lightsalmon}
    .MouseOverDone {background-color: lavender;}
    .Busy {background-color: gray;}
    
    .msg-container {
    	width: 80%;
    	height: 200px;
    	overflow-y: scroll;
    	overflow-x:hidden;
    }
    
    .msg-error {color:red}
    .msg-info {color:blue}
    .msg-sent {color:goldenrod}
    .msg-received {color:green}
  3. Create root/js/main.js We will use some basic jQuery functions - for more information see jQuery documentation.

    var createTableRow = function (item) {
        var row = "<tr class='Blue'>";
        row += "<td>" + item.id + "</td>";
        row += "<td>" + item.action + "</td>";
        row += "<td class='action' id='"+item.id+"'>";
        row += "<input type='checkbox' id='"+item.id+"'>";
        row += "</td>";
        row += "</tr>";
        return row;
    }
    
    var selectedItemIndex=-1;
    
    var getDoneColor = function (item) {œ
        if (!item.available) {
            return "Busy";
        }
        else {
            if (item.id==selectedItemIndex) {
                if (item.complete)
                    return 'MouseOverDone';
                else
                    return 'MouseOverNotDone';
            }
            else if (item.complete)
                return 'Done';
            else
                return 'NotDone';
        }
    }
    
    var setItemColor=function(item){
        $('#'+item.id+".action").removeClass().addClass('action').addClass(getDoneColor(item));
    }
    var createTable=function(items){
        for(var i=0;i<items.length;i++){
            var row=createTableRow(items[i]);
            $('#todoTable > tbody:last-child').append(row);
        }
    }
    
    var todos=null;
    
    var findItem=function(id){
        for(var i=0;i<todos.length;i++){
            if (todos[i].id===id){
                return todos[i];
            }
        }
        return null;
    }
    
    
    
    
    var itemClicked = function (item) {
    	var msg = "Item " + item.id + " is now " + ((item.complete) ? "completed" : "incompleted!");
    	console.info(msg);
    
    	$('#localMessages').append("<div>"+msg+"</div>");
    
    	//Send command "complete" or "incomplete" for this item
    	sendCommand(item, ((item.complete) ? "complete" : "incomplete"));
    }
    
    
    var checkItem=function(item){
        $(":checkbox").filter("#"+item.id).prop('checked', item.complete);
    }
    
    var sendCommand=function(item, command){
    	var cmd = {
    		command: command,
    		item: item.id
    	}
    
    	sendMessage(cmd);
    }
    
    //TODO: Add code to create client
    
    var sendMessage=function(msg){
    	// TODO: Add code to send messages
    	
    }
    
    $(document).ready(function () {
        $.get('data/todo.json', function( r ) {
            todos=r;
            for(var i=0;i<todos.length;i++){
                todos[i].available=true;
            }
            createTable(todos);
            for(var i=0;i<todos.length;i++){
                setItemColor(todos[i]);
            }
            $(":checkbox").change(function() {
                var item=findItem(Number(this.id));
                item.complete=this.checked;
                itemClicked(item);
            });
    
            $(".action").mouseover(function(){
                var item=findItem(Number(this.id));
                selectedItemIndex=Number(this.id);
                setItemColor(item);
                sendCommand(item, "busy");
            });
    
            $(".action").mouseout(function(){
                var item=findItem(Number(this.id));
                selectedItemIndex=-1;
                setItemColor(item);
                sendCommand(item, "available");
            });
    
    		//TODO: Add code to connect
        });
    });

    The does the following:

    • Create helper functions to
      • Create table of items: createTableRow and createTable
      • Set item color: getDoneColor and setItemColor
      • Handle item being checked/unchecked: itemClicked. This function uses function sendCommand that, in turn calls function sendMessage that currently does nothing.
    • As soon the document is loaded
      • Loads todo items from root/data/todo.json into $scope.todos that is bound to the table. Initially all the items are considered to be available as no-one else could edit them yet.
      • Once the items are successfully loaded:
        • Creates table using the helper function and sets the initial color of each item.
        • Creates event handler for mouseover and mouseout events. Event handlers use function sendCommand that, in turn calls function sendMessage that currently does nothing.

Now we should have a complete single-user application.

Developing multi-user TODO application with WebSockets

To make things easier, we are going to use Kaazing Universal Client for Javascript library that implements the facade for Kaazing JavaScript client libraries.

All our changes will be done in main.js

  1. We need to create an instance of the Kaazing Universal Client for Javascript KaazingClientService service and create two configurations for connecting with Gateways - one for JMS and one for AMQP. We will detect which protocol to use by using the first URL parameter and then pass it to the Universal Client library.

    ...
    //TODO: Add code to create client
    var protocol=window.location.search.replace("?", "").split("&")[0];
    
    var client=UniversalClientDef(protocol);
    var connectionInfo=null;
    
    var noLocal=true;
    var TOPIC_PUB=null;
    var	TOPIC_SUB=null;
    if (protocol=="amqp") {
        connectionInfo = {
            url: "ws://localhost:8001/amqp",
            username: "guest",
            password: "guest"
        };
        TOPIC_PUB="todo";
        TOPIC_SUB="todo";
    }
    else if (protocol=="jms") {
        connectionInfo = {
            url: "ws://localhost:8001/jms",
            username: "",
            password: ""
        };
        TOPIC_PUB="/topic/Todo";
        TOPIC_SUB="/topic/Todo";
    }
    else{
        alert("Use: http://<host/port>/todo.html?<protocol>. Unknown protocol: "+protocol);
    }
    
    var subscription={};
    ...
    $(document).ready(...)

    This code can be placed anywhere before $(document).ready function.

  2. Let's add the functionality to log WebSocket messages that will be returned from the Universal Client.

    ...
    var logWebSocketMessage = function (cls, msg) {
        if (cls === undefined || cls == null)
            cls = "info";
        cls=cls.toLowerCase();
        console.info("From WebSocket: " + msg);
        $('#wsMessages').append("<div class='msg-"+cls+"'>"+msg+"</div>");
    }
    ...
    $(document).ready(...)

    This code can be placed anywhere before $(document).ready function.

  3. Now we need to add the function that will be processing received messages.

    ...
    var processReceivedCommand=function(cmd){
        logWebSocketMessage("received","Received command: "+cmd.command+", item id: "+cmd.item)
        var item=findItem(cmd.item);
        if (cmd.command==="busy"){
            item.available=false;
        }
        else if (cmd.command==="available"){
            item.available=true;
        }
        else if (cmd.command==="complete"){
            item.complete=true;
        }
        else if (cmd.command==="incomplete"){
            item.complete=false;
        }
        setItemColor(item);
        checkItem(item);
    
    }
    
    ...
    $(document).ready(...)

    This code can be placed anywhere before $(document).ready function.

    As you can see from the code above function received and object that contains command and item (that contains id), locate the matching item and executes the command that could be \

    • 'busy' : somebody is working on the item (their mouse is over it).
    • 'available' : item is no longer worked on
    • 'complete' : item is checked
    • 'incomplete' : item is unchecked
  4. Now we need to establish connection with the Gateway once the document is ready and the data is loaded.

    ...
    
    $(document).ready(function () {
        $.get('data/todo.json', function( r ) {
            ...
            // Set the logger function
            client.loggerFuncHandle=logWebSocketMessage;
            
            var exceptionHandler=function(err){
            	alert(err);
            }
    
            client.connect(connectionInfo, exceptionHandler, function(connection){
            	connection.subscribe(TOPIC_PUB, TOPIC_SUB,processReceivedCommand, noLocal, function(subscr){
            				console.info("Subscription is created "+subscr);
            				subscription=subscr;
            	});
            });
            
            $( window ).unload(function() {
                // TODO: Disconnect
            });
    
        });
    ...

    First of all we use loggerFuncHandle property to declare the external logger. Then we declare the function to handle WebSockets errors and exceptions and establish the connection.

    As you can see we pass to the connect function the following parameters - Connection information that includes - URL - user name - user password - function to process errors - callback function that will return connection object that will be used to create subscriptions

    Once connection is established, we use connection object to create subscription. We use subscribe function with the following parameters: - name of publishing endpoint - name of subscription endpoint - in our case it is the same as publishing - function to receive and process messages - noLocal flag (set to true) indicating that we do not want to receive our own messages - callback function that will receive subscription object once it is created. In this function we will store the received object to a subscription variable.

  5. In order to send messages, all we need to do is add AngularUniversalClient.sendMessage(msg) to our $scope.sendCommand function

    ...	
    
    var sendMessage=function(msg){
        // TODO: Add code to send messages
        subscription.sendMessage(msg);
    }
    
    $(document).ready(...)
    
    ...
  6. And, finally, disconnect function to notify gateway when the application is closed.

    ...
    $(document).ready(function () {
        $.get('data/todo.json', function( r ) {
            ...
    
            //TODO: Add code to connect
            // Set the logger function
            client.loggerFuncHandle=logWebSocketMessage;
            
            var exceptionHandler=function(err){
            	alert(err);
            }
    
            client.connect(connectionInfo, exceptionHandler, function(connection){
            	connection.subscribe(TOPIC_PUB, TOPIC_SUB,processReceivedCommand, noLocal, function(subscr){
            				console.info("Subscription is created "+subscr);
            				subscription=subscr;
            	});
            });
            
            $( window ).unload(function() {
                // TODO: Disconnect
                client.disconnect();
            });
    
        });
    ...

Now we have a fully functional shared TODO application that can be tested by opening multiple browser instances and using url

http://localhost:<your server port>/<path on your server/todo.html?amqp 

or

http://localhost:<your server port>/<path on your server/todo.html?jms

Note: Default configuration for JMS and AMQP gateways uses the same port 8001 so you cannot run them at the same time.