The Room Widget
In previous examples, we have built a few widgets with the dojo.Declaration tag. This is a fast way to add functionality, but the problem is sharing it with other pages. Although Laura knows the Operator and Client pages will be different, she senses common elements in the way they chat. So she will define a widget class through JavaScript instead. That way, both pages can use it.
The Widget Skeleton
A widget is a way to tie Dojo API calls into one displayable element. The way Laura designs it, the display is pretty much the same on both sides. The only difference is the container. Here's a cocktail napkin view of the client side:

And the operator side:
To be added once we get the public cometd server up.
The message display area, the input box for messages and the send button are exactly the same. So we can package those up as a widget, which we will call dijit.demos.chat.room. Using a little object oriented analysis, Laura stubs out the object code:
dojo.provide("dijit.demos.chat.room"); dojo.require("dojox.cometd"); dojo.require("dijit._Widget"); dojo.require("dijit._Templated"); dojo.declare("dijit.demos.chat.Room", // All widgets inherit from _Widget, and all templated widgets mix in _Templated [dijit._Widget,dijit._Templated], { // Username of client _username: null, // Default room id. Will become the client's username for our tech support line roomId: "public", // For future expansion into public chat rooms isPrivate: false, // Constant prompt: "Name:", join: function(name){ // Join a room }, _join: function(/* Event */e){ // Respond to someone joining a room (only operator does this) }, leave: function(){ // leave a room }, chat: function(text){ // Send a message }, _chat: function(message){ // Receive a message }, startup: function(){ // Required function for a widget. Called on startup }, });
The Widget Template
As we did in Example 1 for i18n, we build a template for the widget. Essentially this template will replace the tag with dojoType="dijit.demos.chat.room"
<div dojoAttachPoint="chatNode" class="chat"></div>
<div dojoAttachPoint="input" class="input">
<div dojoAttachPoint="joining">
<span>${prompt}</span>
<input class="username" dojoAttachPoint="username" type="text" dojoAttachEvent="onkeyup: _join">
<input dojoAttachPoint="joinB" class="button" type="submit" name="join"
value="Contact" dojoAttachEvent="onclick: _join"/>
</div>
<div dojoAttachPoint="joined" class="hidden">
<input type="text" class="phrase" dojoAttachPoint="phrase" dojoAttachEvent="onkeyup: _cleanInput" />
<input type="submit" class="button" value="Send" dojoAttachPoint="sendB"
dojoAttachEvent="onclick: _sendPhrase"/>
</div>
</div>
</div>
The placeholders ${id} and ${prompt} look familiar. These are replaced with the properties id and prompt from the widget instance. A few of the properties look unfamiliar, but they all start with the prefix "dojo".
- dojoAttachPoint creates a property in the widget containing the DOM node. For example, the DIV tag with dojoattachpoint="joining" creates a joining property, which you can pick up with "this.joining" in your widget code. You can do with anything you can do with a DOM node, for instance set its styles or CSS class.
- dojoAttachEvent connects an event and DOM node with a function. So dojoAttachEvent="onkeyup: _cleanInput" means "when the onkeyup event happens here, call _cleanInput."
Only one of the "joining" node:

and the "joined" node:

is visible at any one time. That gives the illusion that the "joined" and "joining" nodes toggle back and forth, occupying the same screen real estate. This is a fairly common trick in widget templates.
Starting the Widget
Every widget has extension points that get called during widget creation. The widget class designer can hook into these by providing methods, as we do here with startup():
startup: function(){ this.joining.className=''; this.joined.className='hidden'; //this.username.focus(); this.username.setAttribute("autocomplete","OFF"); if (this.registeredAs) { this.join(this.registeredAs); } this.inherited("startup",arguments); },
this.inherited() acts like Java's super() operator, but more generally. Here it calls dijit._Widget.startup(). This is a good practice to get into for the widget extension points.
Joining and Leaving a Room
Dojo calls the join method upon clicking the Submit button. Just as we outlined on the previous page, it establishes the chat connection with the operator (only the client calls this particular method). "_join" does the same thing in response to keystrokes.
Note how the dojoAttachPoints come in handy here: they make flipping the nodes on and off straightforward.
join: function(name){ if(name == null || name.length==0){ alert('Please enter a username!'); }else{ if(this.isPrivate){ this.roomId = name; } this._username=name; this.joining.className='hidden'; this.joined.className=''; this.phrase.focus(); dojox.cometd.subscribe("/chat/demo/" + this.roomId, this, "_chat"); dojox.cometd.publish("/chat/demo/" + this.roomId, { user: this._username, join: true, chat : this._username+" has joined the room."} ); dojox.cometd.publish("/chat/demo", { user: this._username, joined: this.roomId }); } }, _join: function(/* Event */e){ var key = (e.charCode == dojo.keys.SPACE ? dojo.keys.SPACE : e.keyCode); if (key == dojo.keys.ENTER || e.type=="click"){ this.join(this.username.value); } },
Leaving, for the most part, just reverses the join sequence:
leave: function(){ dojox.cometd.unsubscribe("/chat/demo/" + this.roomId, this, "_chat"); dojox.cometd.publish("/chat/demo/" + this.roomId, { user: this._username, leave: true, chat : this._username+" has left the chat."} ); // switch the input form back to login mode this.joining.className=''; this.joined.className='hidden'; this.username.focus(); this._username=null; },
Sending and Receiving a Message
Finally, the meaty part of the Room widget. Sending is a fairly simple matter over our protocol:
chat: function(text){ // summary: publish a text message to the room if(text != null && text.length>0){ // lame attempt to prevent markup text=text.replace(//g,'>'); dojox.cometd.publish("/chat/demo/" + this.roomId, { user: this._username, chat: text}); } },
Receive is handled by the topic subscriptions, which we connected in join(). The event mediator sends receive() a message, which becomes the parameter "message" here. Then message.data contains the object that the publisher sent over.
_chat: function(message){ // summary: process an incoming message if (!message.data){ console.warn("bad message format "+message); return; } var from=message.data.user; var special=message.data.join || message.data.leave; var text=message.data.chat; if(text!=null){ if(!special && from == this._last ){ from="..."; }else{ this._last=from; from+=":"; } if(special){ this.chatNode.innerHTML += ""+from+ " "+text+"
"; this._last=""; }else{ this.chatNode.innerHTML += ""+from+" "+text+ "
"; this.chatNode.scrollTop = this.chatNode.scrollHeight - this.chatNode.clientHeight; } } },
Entering and leaving messages use the alert CSS class to distinguish them from regular chat messages.
There's our bouncing baby widget! Now let's make use of it...
Details for the Impatient
- Printer-friendly version
- Login or register to post comments
- Subscribe post
source code
It would be nice to include the source code (files attached) for this example similar to previous examples.