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.
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:
[inline:chat1.png]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
},
});
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"
/* GeSHi (C) 2004 - 2007 Nigel McNie (http://qbnz.com/highlighter) */ .geshifilter {font-family: monospace;} .geshifilter .imp {font-weight: bold; color: red;} .geshifilter .kw1 {color: #b1b100;} .geshifilter .kw2 {color: #000000; font-weight: bold;} .geshifilter .kw3 {color: #000066;} .geshifilter .coMULTI {color: #808080; font-style: italic;} .geshifilter .es0 {color: #000099; font-weight: bold;} .geshifilter .br0 {color: #66cc66;} .geshifilter .st0 {color: #ff0000;} .geshifilter .nu0 {color: #cc66cc;} .geshifilter .sc0 {color: #00bbdd;} .geshifilter .sc1 {color: #ddbb00;} .geshifilter .sc2 {color: #009900;}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".
Only one of the "joining" node:
[inline:chat2.png]and the "joined" node:
[inline:chat3.png]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.
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.
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;
},
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...