Welcome!

AJAX & REA Authors: Lee Novak, Brad Abrams, Alin Irimie, Jonny Defh, RealWire News Distribution

Related Topics: AJAX & REA

AJAX & REA: Article

AJAX Load Buddies

When the AJAX request is complete, it calls the display() function

This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs for the special pre-order price, click here for more information. Aimed at everyone from enterprise developers to self-taught scripters, Real-World AJAX: Secrets of the Masters is the perfect book for anyone who wants to start developing AJAX applications.

Load Buddies
The load buddies function uses the Prototype library's Ajax.Request() to get the user's buddy list, which includes the current status of each buddy. Again, we use Prototype's bindAsEventListener to ensure that, when the AJAX request is complete, it calls the display() function on the instance of the Buddies object that started the AJAX request.

This load() function gets called regularly by our refresh() loop. And, it's not until refresh() is called for the first time that the buddy list is loaded. This is why refresh() is called at the end of the login() function, see Listing12.13b.

Listing 12.18 Load

load: function () {
buddylistURL = baseURL + '/' + responseFormat+ '/' + activeUser+'/buddies';
log('Loading Buddies', 'debug');

new Ajax.Request(
   buddylistURL,
   {
     method: 'get',
     parameters: '',
     onComplete: this.display.bindAsEventListener(this)
   });
},

As you can see in the example, there are no parameters to pass in the request; all of the values the API requires get encoded in the request URL.

This function makes requests that look like /im.php/html/John/buddies.

Display Buddies
The display() function takes the response returned by the AJAX call initiated in load(), and then actually writes it into the HTML. We use the Prototype library's Element.update() method to display these results.

We also attach a new Event listener to each buddy name that appears in the buddy list. These listen for the "click" event, and trigger our goChat() function (see Listing 12.23).

You will notice that we also use two nice features of the Prototype library: document.getElements-ByClassName() method and the Ruby-style iterator that runs through the buddy list.

Listing 12.19 Display

budlist: '',
display: function (xhr) {
   Element.update('buddylist', xhr.responseText);
   this.budlist = document.getElementsByClassName('budlisting', 'buddylist');
   this.budlist.each(function(bud) {
     Event.observe(bud, 'click', goChat, true);
     });
},

Change Status
The changeStatus() function is what's called on the change event to the <select id="status"> control. This function captures selected options and calls the Buddies object postStatusUpdate() method, which we'll explore next.

If the user sets the status to "Offline", this function also calls the global logout() function (which we'll cover in more detail in Listing 12.34), which more fully "takes the user offline" from the point of view of both the IM Client and the IM Server.

Listing 12.20 Change Status

changeStatus: function () {
   idx = this.statusControl.options.selectedIndex;
   status = this.statusControl.options[idx].value;
   status_msg = '';
   log('Updating Status to: ' + status, 'debug');
   this.postStatusUpdate(status, status_msg);
   if (status=='Offline') {
     logout();
   }
},

Notice how the "this" keyword refers to the instance of our Buddies object, and not the HTML element the Event listener is bound to. Since we do need to access that HTML element, we do so through the object's this.statusControl property, in which we stored that HTML element exactly for this kind of access to it.

Also note: we have a status_msg variable set to the empty string. We had planned to add the means for the user to set a comment when they updated their status. This feature is built-out in the IM Server code, and there are stubs for it in the Buddies object. But, it was not built into the interface, and so there is currently no way for the user to set a comment.

Post Status Update
Our postStatusUpdate() function uses the Prototype library's Ajax.Request to update the server with the user's status. This function is called both after login (which we discussed earlier, see Listing 12.17), and each time the user changes their status as described in the previous section.

In this case, we are using the POST method on the AJAX requests, and so we use the postBody option to set the request parameters. Again note that in our current implementation, status_msg passed to this function will always be the empty string. But, you can see in this code that this value is encoded and sent to the server.

Listing 12.21 Post Status Update

postStatusUpdate: function (status, status_msg) {
   updatestatusURL = baseURL + '/updatestatus';
   var myAjax = new Ajax.Request(
   updatestatusURL,
   {
     method: 'post',
     postBody: 'from='+activeUser+'&status='+status+'&status_
msg='+encodeURI(status_msg)
   });
   log('Status Updated to: ' + status, 'info');
},

Destroy Buddies
We also have added a destroy() function to the Buddies class to handle clean-up on log out. Naturally, this method will be called by our logout() function that we'll discuss in Listings 12.34.

Besides setting the buddy list display style to "none" in order to hide the buddy window, this function has the important job of stopping all of the Event listeners we've added.

To do this, we use the Prototype library's Event.stopObserving() function, which has the reverse effect of Event.observe().

This is a context where, again, there can be issues around the binding of the "this" keyword. Our this.changeStatusObserver property stores the properly referenced object that needs to be passed into Event.stopObserving(), and we recommend you compare how we use Event.observe() in Listing 12.17 with how we use Event.stopObserving() in Listing 12.22.

Listing 12.22 Destroy

destroy: function() {
   this.listWindow.style.display = 'none';
   this.postStatusUpdate('Offline', 'Logged Out');
   this.budlist.each(function(bud) {
     Event.stopObserving(bud, 'click', goChat, true);
     });
     Event.stopObserving('status', 'change', this.changeStatusObserver, true);
}

Go Chat
Now we get to the exciting part, our goChat() function that connects the buddy list with the chat windows. This function is bound to the click event on each buddy's name in the buddy list via Event.observe(), as seen in Listing 12.17.

Because goChat() does some management of our chat windows, we decided not to encapsulate it within the Buddies object. Conceivably, one could encapsulate this in Buddies, and each chat window would then be an object dependent on the buddy list.

This dependency is somewhat the case, but we felt there might be other cases when chat windows are managed outside of the buddy list. So, we made goChat() an independent function.

Becuase goChat() is bound to the HTML elements that represent the buddy names, the "this" keyword refers to the element where the event was triggered.

Listing 12.23 Go Chat

function goChat() {
   log('Clicked ' + this.id + ' to chat', 'debug');

   if (windowMgr[this.id] == undefined) {
     windowsUsed++;
     handle = windowsUsed;
     windowMgr[this.id] = new ChatWindow(handle, this.id);
     log('Created a new handle for window #' + handle, 'debug');
   }

   windowMgr[this.id].load();
   windowMgr[this.id].openOrFocus();
}

In Listing 12.10c, we showed the HTML returned from the server, and how each buddy name is embedded in the DIV id, like: <div class="budlisting" id="buddy_Ringo">.

In goChat() we use these IDs to uniquely identify the chat windows. We have an array windowMgr that is defined globally in im.js, and we check to see if windowMgr[this.id] exists, where this.ID is an ID like buddy_Ringo.

If it does not already exist, we append a new item on the windowMgr array identified by the ID, and we set this new item to be a ChatWindow() object (which we'll cover below, Listing 12.24-32).

We also have a windowsUsed counter variable that is global in im.js. We use this to give each chat window a number. These numbers, each of which we call a "handle," connect the ChatWindow object with the "index" of the HTML elements that we described at the beginning of the chapter, in Listing 12. 5.

Finally, we call the load() and openOrFocus() methods on the chat window.

ChatWindow Objects
Like our Buddies object, the ChatWindow objects utilize the Prototype library's extended class features, notably the initialize() method that executes at the time of object creation.

Also like the Buddies class, the ChatWindow class' initialize() method sets a number of properties that we'll use in other methods of the ChatWindow, and also sets a number of Event listeners.

The Event listeners in the ChatWindow listen for the following:

  • Click on the Send button
  • Hit the enter key in the input field
  • Focus on the input field
  • Blur on the input field
  • Click the close button on the chat window
Listing 12.24 Initialize

ChatWindow.prototype = {
   initialize: function (handle, to) {
     toPrefix = 'buddy_';
     this.chatwindow = $('chat' + handle);
     this.chatmsg = $('chatmsg' + handle);
     this.chatin = $('chatin' + handle);
     this.chatsend = $('chatsend' + handle);
     this.chatinfo = $('chatinfo' + handle);
     this.closer = $('close' + handle);
     this.to = to.substring(toPrefix.length);
     log('Initializing window #' + handle + ' for chat with ' + this.to, 'debug');

     Element.update(this.chatinfo, 'Chat with '+ this.to);
     this.open = false;
     this.content = '';

     this.chatSendObserver = this.chatSend.bindAsEventListener(this);
     this.inputFocusObserver = this.inputFocus.bindAsEventListener(this);
     this.inputBlurObserver = this.inputBlur.bindAsEventListener(this);
     this.hideObserver = this.hide.bindAsEventListener(this);
     Event.observe(this.chatin, 'focus', this.inputFocusObserver, true);
     Event.observe(this.chatin, 'blur', this.inputBlurObserver, true);
     Event.observe(this.chatin, 'keypress', this.chatSendObserver, true);

     Event.observe(this.chatsend, 'click', this.chatSendObserver, true);
     Event.observe(this.closer, 'click', this.hideObserver, true);
     }
   }

One trick we do in intialize() is parse out the name of the buddy from the ID that is passed to the ChatWindow at the time of object creation. Our IDs look like buddy_Ringo, and we just parse out what comes after the buddy_ prefix and take that to be the buddy name. This name is used in requests to the server as the "to" parameter.

Open or Focus Chat Windows
By default, as set in our im.css, our chat windows are not displayed. When the user clicks on a buddy name, we then display (open) the chat window. If a user clicks on a buddy name where there is already a chat window visible for that buddy, we shift focus to the already open window.

Listing 12.25 Open or Focus

openOrFocus: function () {
   if (!this.open) {
     log('Opened window to ' + this.to, 'debug');
     this.chatwindow.style.display = "block";
     this.chatin.focus();
     this.open = true;
   } else {
     log('Focused window to ' + this.to, 'debug');
     this.chatin.focus();
   }
},

Load Chat Messages
The load() function uses the Prototype library's Ajax.Request() to get the messages between the user and the buddy associated with the chat window. Again, we use Prototype's bindAsEventListener to ensure that, when the AJAX request is complete, that it calls the display() function on the instance of the ChatWindow object that started the AJAX request.

This load() function gets called regularly by our refresh() loop.

Listing 12.26 Load

load: function() {
   msgsURL = baseURL + '/' + responseFormat+ '/' + activeUser+'/msgs/'+ this.to;
   log('Loading Messages to '+ this.to +' via AJAX', 'debug');
   new Ajax.Request(
     msgsURL,
   {
     method: 'get',
     parameters: '',
     onComplete: this.display.bindAsEventListener(this)
   });
},

Note that the requests to the server will be in the form /im.php/html/John/msgs/Clarence.

Display Messages
The display() function takes the response returned by the AJAX call initiated in load(), and then actually writes it into the HTML. We use the Prototype library's Element.update() method to display these results.

We also scroll the chat window to the bottom.

Listing 12.27 Display

display: function(xhr) {
   log('Displaying Messages for '+this.to, 'debug');
   this.content = xhr.responseText;
   Element.update(this.chatmsg, this.content);
   this.chatmsg.scrollTop = this.chatmsg.scrollHeight;
},

Chat Send Message
There are two ways the user can initiate the sending of a message that they have typed in the chat window: they may hit the "send" button, or hit the enter key while their cursor is in the text input.

Our chatSent() function captures these events and then calls the object's sendMessage() method.

Notice how the Event is passed as a parameter to this function. This is a feature of Prototype's bindAsEventListener() method that we're using. It makes the "this" keyword refer to an object other than the HTML Element to which the Event is bound, but it also passes that Event as a parameter so it still can be accessed.

Listing 12.28 Chat Send

chatSend: function(ev) {
   if (ev!=null && (
     (ev.type == 'click') ||
     (ev.type=='keypress' && ev.keyCode == Event.KEY_RETURN)
     )) {
     e = Event.element(ev);
     log(activeUser + ' Sending message from ' + this.chatwindow.id +' to: ' + this.
to, 'debug');
     this.sendMessage();
     this.chatin.value = '';
     this.chatin.focus();
   }
},

Another small feature of this function is that it resets the value of the text input and resets the focus on the input after the message is sent. This is an expected behavior that must be explicitly created - especially for the case where the user is clicking the send button, which otherwise has no connection to this input.

Send Message
The sendMessage() function uses the Prototype library's Ajax.Request to update the server with the new message.

In this case, we are again using the POST method on the AJAX requests, and so we use the postBody option to set the request parameters.

Listing 12.29 Send Message

sendMessage: function() {
   sendmsgURL = baseURL + '/sendmsg';
   new Ajax.Request(
     sendmsgURL,
     {
       method: 'post',
       postBody: 'from='+activeUser+'&to='+this.to+'&msg='+encodeURI(this.chatin.
value)
     });
   log('Message sent to: ' + this.to, 'info');
},

This content is reprinted from Real-World AJAX: Secrets of the Masters published by SYS-CON Books. To order the entire book now along with companion DVDs, click here to order.

More Stories By James Benson

Jim Benson, AICP, is the COO of Gray Hill Solutions in Seattle. Gray Hill creates tools for government and industry to harness and utilize real-time data. Jim has always driven applications for his clients to store and provide information in easily extensible ways. Web 2.0 has therefore been a natural environment for him. He is also involved with the Cooperation Commons and the Institute for the Future's Future Commons to study human cooperation and envision the future of cooperation. Jim's tags: Gray Hill Solutions (www.grayhillsolutions.com), Jim's Blog (http://ourfounder.typepad.com), Cooperation Commons (www.cooperationcommons.org), Institute for the Future (www.iftf.org).

More Stories By Jay Fienberg

Jay Fienberg is co-founder of Juxtaprose where he designs information architecture and user experience for websites and information systems.

He specializes in design for content and information-rich websites and web-based social and collaboration systems. His current preferred CMS is ExpressionEngine, though he also works with Wordpress and Joomla, and still gets called upon to make SharePoint do interesting CMS-like things for enterprise intranets.

Since the early 1990s, Jay also has designed and developed hypertext, database, and content management systems and worked in a wide range of programming languages including XML, SQL, SGML, Python, PHP, Javascript, Java, HTML, CSS and APL.

For anything more official, please visit jayfienberg.com.

Comments (0)

Share your thoughts on this story.

Add your comment
You must be signed in to add a comment. Sign-in | Register

In accordance with our Comment Policy, we encourage comments that are on topic, relevant and to-the-point. We will remove comments that include profanity, personal attacks, racial slurs, threats of violence, or other inappropriate material that violates our Terms and Conditions, and will block users who make repeated violations. We ask all readers to expect diversity of opinion and to treat one another with dignity and respect.