Squeaky Clean Ajax and Comet with Lift

Focus on application development, not the plumbing

Lift is a web framework for Scala, and is probably best known for having great Comet and Ajax support.

I’ve been touring the features of Lift that I find appealing. Initially I looked at designer-friendly templates and REST services. Recently, I highlighted the great features for organising and controlling access to content.

Let’s now take a look at the Ajax and Comet features of Lift.

Ajax

When I think of Ajax, I think of interacting with a web server, but avoiding page reloads.

An example: suppose we had a site that allowed you to write poems, and a feature on that site might be a button you could click to get an associated word from a thesaurus. The thesaurus we have is large, we don’t want it loaded in the browser, so we need to call the server to use it.

The HTML template would be a field and a button:

<div data-lift="Associate">
  <input id="word" type="text" placeholder="Type a word">
  <input type="submit" name="submit">
</div>

So far, that’s probably similar across many web frameworks. What you might now expect is for us to define a REST endpoint, do some jQuery wiring to connect the service to the button.

In Lift, we can do that, but often the Ajax goodness comes via this kind of Scala code:

class Associate {
  def render =
    "@submit [onclick]" #> ajaxCall(
      ValById("word"),
      w => SetValById("word", Thesaurus.association(w))
    )
}

This Associate snippet is binding the submit button’s click event to a Lift Ajax call. That is, the left side of the replace function (#>) is a CSS selector targeting the “onclick”, and the right side is arranging for the Ajax call to hit the server.

The first parameter to ajaxCall is the value passed from the browser to the server. We’ve asked for the value of the input field with an ID of “word”. The second parameter is the function to run when the button is pressed. Here we’re taking the word we’ve been sent, and updating the field with whatever our Thesaurus.association function gives us back.

With this app running, maybe you’d type in “Lift”, press submit and you’d be given back “soar”; if you pressed submit again you might get “aeroplane”. Free association could help poets over a writing block.

That’s all the code you need. Lift is handling the Ajax connections, including retrying if needed, and arranging for your code to get the data it needs, and shuttling the result back to the browser.

Hopefully you can see the appeal: a small amount of code to write, and a lot of plumbing taken care of.

Comet

Comet supplements Ajax by providing a route from the server back to the browser. That is, something can happen on the server, and we update the browser without user intervention. Push, effectively.

As an example, let’s implement a rudimentary version of Google Docs that works in a textarea. Our collaborative poetry authoring interface (poetry by committee–what could possibly go wrong) would look like this:

<div data-lift="comet?type=Editor">
  <textarea id="poem">There once was a man from Nantucket,</textarea>
</div>

The only Lift-ish thing here is the data-lift attribute which is informing Lift that we want a Comet actor, and in particular we want the defined by a class called Editor.

The way this is going to work is based on the Actor model for concurrency. That is, each author will be an Editor comet actor, which is what we’ve said in the HTML above. The Editor will send each edit as JSON to an EditServer, and that EditServer will fan-out edits to all the authors.

Lift’s representation for JSON is JValue, so that’s what the EditServer is going to be sent:

object EditServer extends LiftActor with ListenerManager {

  // All the edits so far:
  var history = Vector[JValue]()

  // Handle messages from comet actors:
  override def lowPriority = {

    // When we receive an edit, save it in the history and pass it on to all browsers:
    case change : JValue =>
      history = history :+ change
      updateListeners(change)
  }

  // When a collaborator joins, send them this:
  def createUpdate = history
}

The LiftActor and ListenerManager traits are doing most of the work for us. All we’re doing is listening for a JValue message, keeping a record of it, and updating any authors (listeners, comet actors) who have connected to this EditServer.

When someone visits our HTML page, Lift will give them an instance of the Editor comet actor:

class Editor extends CometActor with CometListener {

  // Where we send messages to, and receive messages from:
  def registerWith = EditServer

  // When shown, bind key presses to server events:
  def render = "#poem [onkeypress]" #> 
    SHtml.jsonCall(Call("charAndPos(arguments)"), updateServer _)

  // When we get a keyboard event, notify the server:
  def updateServer(json: JValue) : JsCmd = {
    EditServer ! json
    Noop // No need to send anything back to the client that made the edit
  }

  // When we receive an event from the server, update our browser display:
  override def lowPriority = {

    // Process an edit: send the change via a JavaScript Call:
    case edit : JValue => partialUpdate(Call("accept", edit))

    // If we've joined late, apply all the changes so far:
    case edits: Vector[JValue] =>
      val js = edits.foldLeft(Noop) { (z,edit) => z & Call("accept",edit) }
      partialUpdate(js)
  }

}

This is a little longer, but I can talk you through it. The essence is two responsibilities:

  • Detecting a change we’ve made (an edit), and passing it on.
  • Responding to someone else’s edit by generating JavaScript to update our view.

Like a snippet, this comet actor has a render method which can modify the HTML text area. We use it to connect an Ajax call to the key press event of the text area (recall: the HTML contains a text area with an ID of “poem”).

We’re saying every key press triggers a jsonCall. As you can probably guess, we need some JSON data for this, and that’s coming from a JavaScript function call to charAndPos. I’m not going to describe charAndPos because it’s not Lift-specific, other than to say when a key is pressed it will generate JSON something like this:

{ command: "insert", sender: "some identifier", pos: 14, char: "A" } 

In other words, when you press a key in the text area, that JSON is sent to Lift.

What do we do with that JSON? We pass it to a function called updateServer, and that sends it on to the EditServer. (The ! symbol is the standard message sending symbol for Actors, but you can use the send method name if you prefer.)

In other applications you’ll probably do more in updateServer, such as validating what you’ve been sent, or looking up some other information, before sending it on. But for this example, we’ll just work with the raw JSON JValue.

Glance back at the EditServer and see what it does when it receives the message: it send it on to all comet actors via updateListeners.

To recap, so far, I’ve added the character “A” to my copy of the poem (let’s say), that’s been sent to the EditServer, and the EditServer has passed the update to your instance of the comet actor. What happens there? Your comet actor matches on the edit in lowPriority, and does a partialUpdate of the edit. That means, your browser is sent the content of the partialUpdate, which is a call to a JavaScript function called accept with the JSON as an argument. As with charAndPos, I won’t go into the details of this function, but it’s regular JavaScript to modify the DOM to reflect the change in the text area.

That’s what you need to start a simple collaborative editing app (well, more inserting than editing, as we’ve not implemented delete, selection, or all the other things you need). I’m not going to tell you that it’s trivial, but having a pattern of comet actors and a comet server to work with makes this kind of application achievable in not a lot of code.

Learning About Lift

These two features, Comet and Ajax, have been in Lift for years. They look after low-level details, such as retries and delivery order, and continue to be appealing because they let us focus on application development, and not the plumbing.

If you want to find out more:

Lift 2.5 has just been released, and 3.0 is in the pipeline: it’s a great time to get involved.

tags: , , ,