The total chat app, including the ask/answer component for soliciting a name comments, etc. is 68 lines of code. There is no special code to support AJAX/Comet (all the wrapping is done automatically by lift.
When the Chat comet widget is added to the page, it needs to solict the user for a "chat name". It asks the "AskName" comet widget for the name. Until the AskName comet widget provides a name, all rendering messages are forwarded to AskName. Here's the code for the "AskName":
class AskName extends CometActor { def render = ajaxForm(<div>What is your username?</div> ++ text("",name => answer(name.trim)) ++ submit("Enter", ignore => true)) }
When the user submits the form, the question asked by the Chat comet widget is answered with the value the user submitted. This is similar to the ask/answer paradigm in Seaside, except that there's no need for continuations.
Now, onto the heart of the chat app:
class Chat extends CometActor { private var userName = "" private var currentData: List[ChatLine] = Nil private val server = { val ret = ChatServer.server ret !? ChatServerAdd(this) match { case ChatServerUpdate(value) => currentData = value case _ => {} } ret } override def lowPriority : PartialFunction[Any, Unit] = { case ChatServerUpdate(value) => { currentData = value reRender loop } } def render = { val inputName = this.uniqueId+"_msg" val ret = <span>Hello {userName}<ul>{ currentData.reverse.map{ cl => <li>{hourFormat(cl.when)} {cl.user}: {cl.msg}</li> }.toList }</ul><lift:form method="POST"> <input name={inputName} type="text" value=""/><input value="Send" type="submit"/> </lift:form></span> XmlAndMap(ret, TreeMap(inputName -> sendMessage)) } override def localSetup { if (userName.length == 0) { ask(new AskName, "what's your username") { answer => answer match { case s : String if (s.length > 2) => userName = s; true case _ => localSetup; false } } } } def waitForUpdate : Option[List[ChatLine]] = { receiveWithin(100) { case ChatServerUpdate(l) => Some(l) case TIMEOUT => None } } def sendMessage(in: List[String]) = { server ! ChatServerMsg(userName, in.head) waitForUpdate match { case Some(l : List[ChatLine]) => currentData = l ; true case _ => false } } }
server
is a value (calculated at instantiation and unchanged) that
contains the Chat server shared by all the Chat comet clients.
lowPriority
defines what happens when we receive a
message from another Actor. In this case, when the Chat server
receives a new posting, it sends a message to all the Chat comet wigets.
When the Chat comet widget receives the message, it notifies the containing
page (via reRender
) of the new content.
render
yields the XHTML and parameter to function map.
localSetup
is called when the comet widget is instantiated
or when it joins a new page (comet widgets can be shared across pages).
Chat comet widget creates an AskName and askes it a question. When
AskName "answers" the question, the attached function will be called
with the Answer to the question.
sendMessage
is called when the user submits the
form (enters a line in chat). It sends a message to the
Chat server, waits up to 100ms for the Chat server to send an
update. If the Chat server sends an update, the Chat client
updates itself.
This example demonstrates the power of Scala's Actors and lift. With very few lines of code, we've got a complete AJAX/Comet app that has Seaside style Ask/Answer for building modal dialogs.