diff --git a/docs/getting-started-tutorial/1-view-first-development.adoc b/docs/getting-started-tutorial/1-view-first-development.adoc new file mode 100644 index 0000000000..a8e33a34d1 --- /dev/null +++ b/docs/getting-started-tutorial/1-view-first-development.adoc @@ -0,0 +1,72 @@ +:idprefix: +:idseparator: - +:toc: right +:toclevels: 2 + +# View-first Development + +If you're developing a user-facing web site or application, one of Lift's +greatest improvements over existing systems is view-first development. +View-first development thoroughly separates the process of creating the user +interface from the process of putting data from the system into it. This way, +you can stay focused on users when you're creating the user interface and +focus on the interface between your backend and the HTML only when +you're working on the backend. + +The flip side of view-first development is that it takes some getting used to +if you are accustomed to the typical web MVC framework. The first stop when +figuring out what's going on in a typical web MVC setup is the controller. In +Lift, your first stop is your HTML file. Everything starts in the HTML, where +you decide what it is that you want to present to the user. You don't just think +about user interactions first, you *build* them first, and let them guide your +development forward and inform it at every step of the way. Turning a usability +tested, high-fidelity mockup into a live page has never been so +straightforward. + +For our chat app, we're going to focus first on two use cases, formulated as +user stories: + + - As a chatter, I want to post a message so that others can see it. + - As a chatter, I want to see messages from me and others so that I can keep + track of the conversation and contribute in context. + +To start with, we'll set up a simple `chat.html` page in our `src/main/webapp` +directory (where all HTML files go). All we really need in there for now is a +list of chat messages so far, and a box to put our own chat message into. So, +here's some base HTML to get us going: + +```html:src/main/webapp/index.html + + + + Chat! + + + +
+
    +
  1. Hi!
  2. +
  3. Oh, hey there.
  4. +
  5. How are you?
  6. +
  7. Good, you?
  8. +
+
+ + + +
+
+ + +``` + +While we're not using it here, it's probably a good idea to start off with +http://html5boilerplate.com[HTML5 Boilerplate]. Indeed, the default Lift +templates all start with exactly that footnote:[Ok, so not exactly. IE +conditional comments need a little additional work in Lift, because Lift is +smart enough to strip all HTML comments in production mode.]. + +When it comes to user testing, notice that our view is fully-valid HTML, with +placeholder data. It is, in effect, a high-fidelity mockup. And now that we've +got our view sorted out (and, ideally, tested with users), we can start hooking +up link:2-the-lift-menu-system.adoc[the Lift side]. diff --git a/docs/getting-started-tutorial/2-the-lift-menu-system.adoc b/docs/getting-started-tutorial/2-the-lift-menu-system.adoc new file mode 100644 index 0000000000..373db1f283 --- /dev/null +++ b/docs/getting-started-tutorial/2-the-lift-menu-system.adoc @@ -0,0 +1,32 @@ +:idprefix: +:idseparator: - +:toc: right +:toclevels: 2 + +# The Lift Menu System + +Another distinguishing characteristic of Lift is that it is *secure by +default*. Amongst other things, this means that if you enable Lift's `SiteMap` +menu system, you can't access a file in your `src/main/webapp` directory through +your application unless you explicitly define that it's meant to be accessed. + +Hooking up a simple page in `SiteMap` is easy, and seems redundant; rest +assured, we'll explore the real power of `SiteMap` as the application becomes +more complicated. All you have to do for the chat page is add a line to your +`SiteMap.scala` that names the page and points to the file in the `webapp` +directory: + +```src/scala/bootstrap/liftweb/Boot.scala +... + Menu.i("Chat") / "chat" +... +``` + +The string passed to `i` is the name of this menu. We can use that to +link:menu-links[automatically render links for our menu]. It gets processed +through Lift's internationalization system, but since we've got no +internationalization set up for now it'll just go through unchanged. The part +after the `/` specifies where the template will be found—in our case, in the +`chat.html` file directly under `src/main/webapp`. + +With that out of the way, we can move on to link:3-adding-snippet-bindings.adoc[bringing our HTML to life]. diff --git a/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc b/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc new file mode 100644 index 0000000000..00334f75c9 --- /dev/null +++ b/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc @@ -0,0 +1,74 @@ +:idprefix: +:idseparator: - +:toc: right +:toclevels: 2 + +# Adding Snippet Bindings + +In most frameworks, a page's data is looked up by a controller, and backend +code clutters the HTML to produce the correct rendering of the data. This +process is usually done through what amounts to little more than string +munging. Lift throws this paradigm away entirely in favor of a much better +approach based on entities called snippets. + +Snippets let you refer to a block of code that is responsible for rendering a +particular part of your page. You add these references by augmenting your HTML +with a few completely valid `data-` attributes that get stripped before the +HTML is then sent to the browser. These snippets then take your HTML, fully +parsed into a valid DOM tree, and transform it, providing true decoupling +between your business logic and your template, and an extra level of +security footnote:[We already mentioned that Lift is secure by default, and +another way that manifests is that the template HTML is turned into a +first-class XML tree early in the processing cycle, and snippets just transform +that tree. That means script injection and a variety of other attacks are +significantly more difficult against a Lift codebase.]. + + +Let's look at our chat app specifically. We're going to bind two things: the +list of chat messages, and the text input that lets us actually chat. To the +`ol` that contains the chat messages, we add: + +```html:src/main/webapp/index.html +
    +``` + +And to the input form: + +```html:src/main/webapp/index.html +
    +``` + +The two references in the `data-lift` attributes we added indicate two methods +in a class called `Chat`, which Lift searches for in the `code.snippet` package +footnote:[This can be changed using +link:++https://liftweb.net/api/30/api/index.html#net.liftweb.http.LiftRules@addToPackages(what:String):Unit++[`LiftRules.addPackage`.]. +We'll write a very basic version that just passes through the contents of the +list and form unchanged, and then in the next section we'll start adding some +behavior. In `src/main/scala/code/snippet/Chat.scala`, add: + +```scala:src/main/scala/code/snippet/Chat.scala +package code +package snippet + +import scala.xml._ + +object Chat { + def messages(contents: NodeSeq) = contents + def sendMessage(contents: NodeSeq) = contents +} +``` + +Note that the methods referred to from the template can either take a +`NodeSeq` footnote:[What's a `NodeSeq`? Scala uses a `NodeSeq` to represent an +arbitrary block of XML. It is a __seq___uence of >= 1 __node___s, which can in +turn have children.] and return a `NodeSeq`, or they can take no parameters and +return a `(NodeSeq)=>NodeSeq` function. The `NodeSeq` that is passed in is the +element that invoked the snippet in the template, minus the `data-lift` +attribute. The `NodeSeq` that is returned replaces that element completely in +the resulting output. + +Now that we have our snippet methods set up, we can move on to actually showing +some data in them. Right now all they do is pass their contents through +unchanged, so rendering this page in Lift will look just the same as if we just +opened the template directly. To transform them and display our data easily, we +use link:4-css-selector-transforms.adoc[CSS Selector Transforms]. diff --git a/docs/getting-started-tutorial/4-css-selector-transforms.adoc b/docs/getting-started-tutorial/4-css-selector-transforms.adoc new file mode 100644 index 0000000000..2d7b7ee4bb --- /dev/null +++ b/docs/getting-started-tutorial/4-css-selector-transforms.adoc @@ -0,0 +1,172 @@ +:idprefix: +:idseparator: - +:toc: right +:toclevels: 2 + +# CSS Selector Transforms + +Because Lift operates by transforming HTML trees, we need an easy way to +specify those transformations. Otherwise we'd be doing a bunch of recursive +tree searches and munges, which would get ugly, unpleasant, and probably end up +being a performance nightmare. To deal with transformations easily, we instead +use a small subset of CSS selectors, with a few Lift variations that allow us to +maximize performance and address additional use cases around tree +transformation. + +We'll leave forms for the next section, as forms always come with a catalog of +related functionality, and focus on binding the list of chat messages in this +section. We'll also add a new message before every page load, so that we can see +the list changing. + +First, we'll define a variable to hold the messages: + +```scala:src/main/scala/code/snippet/Chat.scala +... +object Chat { + var messageEntries = List[String]() +... +} +``` + +Then, we can change the definition of the `messages` method to bind the +contents of the message list: + +```scala:src/main/scala/code/snippet/Chat.scala +... + +import net.liftweb.util.Helpers._ + +... + def messages = { + "li *" #> messageEntries + } +... +``` + +In the previous section, we mentioned that Lift snippets can return +`(NodeSeq)=>NodeSeq` functions. That is what's happening here: Lift's CSS +selector transforms are actually functions that take a `NodeSeq` and return a +`NodeSeq`, constructed using an easy-to-read syntax. + +What we do in this particular transformation is select all ``li``s. We then +specify that we want to transform them by replacing their contents (`*`) by +whatever is on the right. The right side, however, is a list, in this case of +``String``s. When there's a list on the right side of a transformation, Lift +repeats the matched element or elements once for each entry in the list, and +binds the contents of each element in turn. + +Let's start up Lift and see what's going on. In your terminal, enter the +directory of the chat app and start up the application: + +``` +$ sbt +> jetty:start +[info] Compiling 4 Scala sources to /Users/Shadowfiend/github/lift-example/target/scala-2.9.2/classes... +[info] jetty-8.1.7.v20120910 +[info] NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet +[info] started o.e.j.w.WebAppContext{/,[file:/Users/Shadowfiend/github/lift-example/src/main/webapp/]} +[info] started o.e.j.w.WebAppContext{/,[file:/Users/Shadowfiend/github/lift-example/src/main/webapp/]} +[info] Started SelectChannelConnector@0.0.0.0:8080 +[success] Total time: 4 s, completed Oct 6, 2013 2:31:01 PM +> +``` + +Once you see the success message, point your browser to +`http://localhost:8080/`. You should see an empty chat list, since currently +there are no message entries. To fix this, we're going to add a chat message +every time we render the message list: + +```scala:src/main/scala/code/snippet/Chat.scala +... + def messages = { + messageEntries :+= "It is now " + formattedTimeNow + "li *" #> messageEntries + } +... +``` + +Let's recompile and restart the server: + +``` +> jetty:stop +[info] stopped o.e.j.w.WebAppContext{/,[file:/Users/Shadowfiend/github/lift-example/src/main/webapp/]} +[success] Total time: 0 s, completed Oct 6, 2013 2:36:48 PM +> container:start +[info] Compiling 1 Scala source to /Users/Shadowfiend/github/lift-example/target/scala-2.9.2/classes... +[info] jetty-8.1.7.v20120910 +[info] NO JSP Support for /, did not find org.apache.jasper.servlet.JspServlet +[info] started o.e.j.w.WebAppContext{/,[file:/Users/Shadowfiend/github/lift-example/src/main/webapp/]} +[info] started o.e.j.w.WebAppContext{/,[file:/Users/Shadowfiend/github/lift-example/src/main/webapp/]} +[info] Started SelectChannelConnector@0.0.0.0:8080 +``` + +Now if you pull up the page you'll see something that doesn't look quite right. +The markup we're producing should look something like: + +``` +
  1. It is now 13:25 UTC
  2. +
  3. It is now 13:25 UTC
  4. +
  5. It is now 13:25 UTC
  6. +
  7. It is now 13:25 UTC
  8. +``` + +If you reload the page, you'll get something like: + +``` +
  9. It is now 13:25 UTC
  10. +
  11. It is now 13:25 UTC
  12. +
  13. It is now 13:25 UTC
  14. +
  15. It is now 13:25 UTC
  16. +
  17. It is now 13:26 UTC
  18. +
  19. It is now 13:26 UTC
  20. +
  21. It is now 13:26 UTC
  22. +
  23. It is now 13:26 UTC
  24. +``` + +What's causing all the repetition? Well, remember when we described what the +CSS Selector Transform was doing, we said we “select all ``li``s”. We also said +that the list on the right side means “Lift repeats the matched element **or +elements**”. So we select all the ``li``s, but in the template there are 4, so +that the template when viewed alone (say, for a user test, or when a frontend +developer is editing it) has some content in it. How do we bridge the two +without getting nasty in our HTML? + +Lift lets us tag the extra elements with a class `clearable`: + +```html:src/main/webapp/index.html +... +
  25. Hi!
  26. +
  27. Oh, hey there.
  28. +
  29. How are you?
  30. +
  31. Good, you?
  32. +... +``` + +Then, in our snippet, we can use a special transform called `ClearClearable`, +which will remove all of the tagged elements before we start transforming the +template: + +```scala:src/main/scala/code/snippet/Chat.scala +... + def messages = { + messageEntries :+= "It is now " + formattedTimeNow + + ClearClearable & + "li *" #> messageEntries + } +... +``` + +Notice that we combine the two CSS selector transforms here by using `&`. You +can chain together as many CSS selector transforms as you want this way, as long +as they don't modify the same parts of the same element. We'll deal with that +limitation link:13-who-knows[a little later] footnote:[This is because CSS +selector transforms are optimized for speed, and pass through the nodes a +single time to make all of the transformations happen.]. + +Now if we restart the server and look at the results, we'll see the right thing +happening: one entry per message, and every time we reload the page we get a +new entry. + +Now that we've got the list of messages rendering, it's time to get into the +bread and butter of web development: link:5-basic-forms.adoc[forms]. diff --git a/docs/getting-started-tutorial/5-basic-forms.adoc b/docs/getting-started-tutorial/5-basic-forms.adoc new file mode 100644 index 0000000000..0cb9dcaaed --- /dev/null +++ b/docs/getting-started-tutorial/5-basic-forms.adoc @@ -0,0 +1,106 @@ +:idprefix: +:idseparator: - +:toc: right +:toclevels: 2 + +# Basic Forms + +It's a recurring and important theme that Lift is secure by default. This +manifests in the way that forms are constructed as well. Form fields in Lift +are associated with a callback function that runs when the field is submitted +with the form. On the client, the field name is always unique to this page +load, and this unique field name is a cryptographically secure random value +that is associated to the callback function you specify on the server +footnote:[What about using your own field names, you may ask? You can always do +that. To access a submitted field with a given name, you can use +`S.param("field name")`. You'll get back a `Box` that will be `Full` and +contain the value you submitted if it was submitted. The `Box` will be `Empty` +if the field wasn't submitted in this request. However, be very careful about +using this method, since it exposes you to CSRF attacks.]. This makes Lift +forms resistant to many cross-site request forgery (CSRF) attacks, and +resistant to https://liftweb.net/lift_and_breach[the BREACH attack that typical +CSRF tokens are vulnerable to when served with gzip compression over a secure +connection. + +Let's look at a simple example with our chat application. Currently our form +looks like this: + +```html + + + + +
    +``` + +Our `sendMessage` snippet looks like this: + +```scala +... + def sendMessage(contents: NodeSeq) = contents +... +``` + +We want to bind two things above. The first is the text field, which we want to +bind so that we can get a message from the user, and the second is the submit +button, so that we can process the new message. Here's how we can do that: + +```scala:src/main/scala/code/snippet/Chat.scala +... +import net.liftweb.http.SHtml + +... + def sendMessage = { + var message: String = "" + + "#new-message" #> SHtml.text(message, message = _) & + "type=submit" #> SHtml.submitButton(() => { + messageEntries ::= message + }) + } +... +``` + +First things first, we're using the `SHtml` singleton. This singleton contains +Lift's form handling helpers. We're using two of them here. The first is +`SHtml.text`. This returns an `input type="text"` whose initial value is the +first parameter you pass to it. The second parameter is a function that runs +when the field is submitted. It takes in a single `String`, which is the value +the user submitted, and does something with it. In our case, we use Scala +shorthand to indicate the field's handler will just assign the value submitted +by the user to the `message` variable. + +The second form helper we're using is `SHtml.submitButton`. This returns an +`input type="submit"` that runs the function you pass to it when the form is +submitted. In this case, when the form submits, we're going to prepend the +value of `message` to the existing message entries list. + +Before continuing, let's change the `messages` snippet so it doesn't keep +adding a new message on each page load: + +```scala:src/main/scala/code/snippet/Chat.scala +... + def messages = { + ClearClearable & + "li *" #> messageEntries + } +... +``` + +Now we can restart the server, and when we reload the page we'll be able to +post messages and see them appear on the list of entries. + +So now we have a basic chat page. We've fulfilled our two initial use cases: + + - As a chatter, I want to post a message so that others can see it. + - As a chatter, I want to see messages from me and others so that I can keep + track of the conversation and contribute in context. + +But, this clearly isn't a particularly usable chat. For one, we don't actually +know who's posting what message. For another, the current implementation of the +messages relies on a single variable that updates when a user posts to it. This +works fine when there's just one user posting a time, but once multiple users +start submitting the post form simultaneously, we start getting into serious +threading and data consistency issues. + +Let's link:6-adding-usernames.adoc[deal with usernames first]. diff --git a/docs/getting-started-tutorial/6-adding-usernames.adoc b/docs/getting-started-tutorial/6-adding-usernames.adoc new file mode 100644 index 0000000000..2289cfe850 --- /dev/null +++ b/docs/getting-started-tutorial/6-adding-usernames.adoc @@ -0,0 +1,153 @@ +:idprefix: +:idseparator: - +:toc: right +:toclevels: 2 + +# Adding Usernames + +We're about to add another use case to our chat system: + + - As a chatter, I want to see who posted a message so that I have better + context for the conversation. + +The first thing we'll do is change the HTML to look like we want it to. Let's +add the username: + +```html:src/main/webapp/index.html +... +
  33. + Antonio Hi! +
  34. +
  35. + David Oh, hey there. +
  36. +
  37. + Antonio How are you? +
  38. +
  39. + Antonio Good, you? +
  40. +... +``` + +Initially, we'll generate a username for the current user. We can store it in a +`SessionVar`. `SessionVar`s in Lift are used to store things that should exist +for the duration of a user's session. A user's session exists as long as Lift +is aware of the user viewing a page related to that session. If Lift sees no +activity from a given session after 20 minutes foonote:[This is configurable, +of course. See `LiftRules.sessionInactivityTimeout`.], the session will be +thrown away, as will the associated `SessionVar` values and related data. + +For now, let's look at adding the `SessionVar` to the `Chat` snippet: + +```scala:src/main/scala/code/snippet/Chat.scala +... +object username extends SessionVar[String]("username") + +object Chat { +... +``` + +Here, we create a new `SessionVar`, whose default value will be “username” if it +is accessed without having been set. We can change that to be random: + +```scala:src/main/scala/code/snippet/Chat.scala +object username extends SessionVar[String]("User " + randomString(5)) +``` + +We're using a Lift helper called `randomString`. We just pass it a length and +it gives us back a random string of that length. This'll make sure that each +user session has a (reasonably) unique username. + +Now, we need to store usernames alongside messages. Let's do that by making the +messageEntries list contain a case class instance instead of a simple `String`: + +```scala:src/main/scala/code/snippet/Chat.scala +... +case class ChatMessage(poster: String, body: String) // <1> +class Chat { + var messageEntries = List[ChatMessage]() // <2> + + def messages = { + ClearClearable & + "li" #> messageEntries.map { entry => // <3> + ".poster *" #> entry.poster & + ".body *" #> entry.body + } + } +... +``` +<1> First, we introduce a new case class, `ChatMessage`, that carries a poster + and a message body. +<2> We also update `messageEntries` to be a list of ``ChatMessage``s instead of + plain ``String``s. +<3> One of the big changes here is how we update the `messages` snippet method. + Before, we just mapped the content of `li` to the list of ``String``s. + However, `ChatMessage` objects can't be dealt with so simply. Instead, the + left side becomes a simple selection of `li`. The right side is now a list + of CSS selector transforms -- one for each `ChatMessage`. As before, Lift + copies the contents of the `li` once for each entry in the list, and then + transforms it according to that particular entry. In this case, rather than + just putting a string into the `li`, we set the contents of the `.poster` + and `.body` elements inside it. + +Now let's update the binding of the `sendMessage` form to deal with the new +`ChatMessage` class: + +```scala:src/main/scala/code/snippet/Chat.scala + def sendMessage = { + var message = ChatMessage("", "") // <1> + + "#new-message" #> SHtml.text( // <2> + message, + { messageBody: String => message = Full(ChatMessage(username.get, messageBody)) } // <3> + ) & + "type=submit" #> SHtml.submitButton(() => { // <4> + for (body <- message) { + messageEntries ::= message + } + }) + } +} +``` +<1> Before, we used an empty `String` as our starting value for the message. + However, we can't do that anymore here. Our only option would be to use + `null`, but `null` is very dangerous footnote:[Why is not dealing with + `null` desirable? Using a `Box` lets you deal with "this value isn't there" + as an inherent type. `null`, on the other hand, is something that can + masquerade as any value (for example, you can put `null` into either a + `ChatMessage` or a `String`), and the compiler can't check for you that you + made sure this optional value was set before using it. With a `Box`, the + compiler will enforce the checks so that you'll know if there's a + possibility of a value not being set.] and, as a rule, we avoid using it in + Scala. Instead, we use an `Empty` `Box`, and, when we receive a message + body, we create a `Full` `Box` with the newly posted `ChatMessage`. +<2> Here, we update the handler for the `#new-message` text field. Before, the + handler function was `message = _`; when `message` was a `String`, we could + simply assign the message the user sent directly to it, and we were good to + go. However, `message` is now a `ChatMessage` -- it has to carry not only + the message body that the user typed, but also their username. To do that, + we write a complete handler function that takes in the body that the user + submitted with the form and, combined with the current user's username, + creates a `ChatMessage`. This `ChatMessage` is what we now put into the + `message` variable. +<3> Notably, `username.get` is how you fetch the current value of the `username` + `SessionVar`. Don't confuse it with the `.get` on `Option`, which is very + dangerous! If you prefer to use a method that is less easily confused with + ``Option``'s `.get` (as many Lift developers and committers do), you can use + `.is` instead, which does the same thing. +<4> As a result of the `Box` wrapping the submitted `ChatMessage`, we have to + update the submission handler. We use a `for` comprehension to unpack the + value of `message`. The body of that comprehension won't run unless + `message` is `Full`, so we can't try to insert an empty message into the + message list. + +Now that we have a reasonably nice chat system with actual usernames, it's time +to look at the underlying issue of *consistency*. If two users posted a chat +message at the same time right now, who knows what would happen to the +`messageEntries` list? We could end up with only one of their messages, or with +both, or with an undefined state of nastiness. + +Before letting a user set their own username, let's deal with this issue by +serializing the posting of and access to messages using a simple mechanism: +link:7-using-actors-for-chat.adoc[an actor]. diff --git a/docs/getting-started-tutorial/7-using-actors-for-chat.adoc b/docs/getting-started-tutorial/7-using-actors-for-chat.adoc new file mode 100644 index 0000000000..35d3ef4004 --- /dev/null +++ b/docs/getting-started-tutorial/7-using-actors-for-chat.adoc @@ -0,0 +1,160 @@ +:idprefix: +:idseparator: - +:toc: right +:toclevels: 2 + +# Using Actors for Chats + +Actors are fairly simple: they receive messages, do something with them, and +potentially respond to them. This isn't unlike the process that you go through +when you call a method on a regular class; however, when you send a message to +an actor, it is processed after any earlier messages you sent, never at the +same time. This makes it a lot easier to reason about what's going on at any +given point, even if multiple threads are sending messages to the actor. + +## Storing the Message List + +We're going to use a fairly basic actor for now. As before, we're going to have +a single chat room for the entire site we're building, so we'll use a singleton +object. Lift provides a very simple actor implementation, which we'll be using +here. There are more complicated actor systems, like the one provided by +http://aka.io[Akka], but they're only necessary in cases where you need more +flexibility or fault tolerance. We'll stick to the easy stuff, starting with a +new file at `src/main/scala/code/actor/ChatActor.scala`: + +```scala:src/main/scala/code/actor/ChatActor.scala +package code +package actor + +import net.liftweb.actor._ + +case class ChatMessage(poster: String, body: String) + +case class MessagePosted(message: ChatMessage) + +object ChatActor extends LiftActor { + private var messageEntries = List[ChatMessage]() + + def messageHandler = { + case MessagePosted(newMessage) => + messageEntries ::= newMessage + } +} +``` + +This provides a very basic actor that can receive a new message and add it to +its internal list. We've moved the `ChatMessage` class from the `Chat` snippet +to this file. Typically, messages to actors are case classes. This is because +they're easy to pattern match (as you can see, message handling is done via +pattern matching footnote:[Strictly speaking, `messageHandler` is a +`PartialFunction`. This means that it can match any subset of objects that it +wants to.]) and because they're generally immutable, so there's no chance of +someone else trying to modify the message as we're processing it. + +To ask the actor to add a message, we'll send it the `MessagePosted` message +using the `!` operator. Here's how we can update our code in the `Chat` +snippet: + +```scala:src/main/scala/code/snippet/Chat.scala +... +import actor._ +... + def sendMessage = { + ... + "type=submit" #> SHtml.submitButton(() => { + for (body <- message) { + ChatActor ! MessagePosted(message) + } + }) + } +... +``` + +Now, whenever a message is posted, it will be sent to the `ChatActor`, which +will update its internal list. + +This is, however, only half of the equation. Putting messages into the actor +isn't useful if we can't get them back out! + +## Retrieving Messages + +To retrieve messages, we can add a new message for the `ChatActor`: + +```scala:src/main/scala/code/actor/ChatActor.scala +... +case class MessagePosted(message: ChatMessage) +case object GetMessages +... +``` + +And a handler for it: + +```scala:src/main/scala/code/actor/ChatActor.scala +... + def messageHandler = { + ... + case GetMessages => + reply(messageEntries) + } +... +``` + +When handling `GetMessages`, we use the `reply` method. This method lets us +send an answer back to the person who sent us this message. By default, +messages don't send answers, and the `!` operator is non-blocking, meaning it +adds the message to the end of the actor's list of messages to process and then +lets the original code continue running without waiting for the actor to deal +with it. + +To wait for a reply, we have to use the `!?` operator instead. We do this when +listing messages by updating the `Chat` snippet: + +```scala:src/main/scala/code/snippet/Chat.scala +... + def messages = { + val messageEntries = Box.asA[List[ChatMessage]](ChatActor !? GetMessages) openOr List() + + ClearClearable & + "li" #> messageEntries.map { entry => + ".poster *" #> entry.poster & + ".body *" #> entry.body + } + } +... +``` + +Two things to notice here. First off, we use `ChatActor !? GetMessages` to +retrieve the messages. This will block until the `ChatActor` can process our +message and send the reply back to us. Unfortunately, because we're not +invoking a method, there is no type safety in the `!?` operator, so the +compiler doesn't know what the type that `GetMessages` will return to us is. +Because of that, we have to do some casting. To deal with this, Lift provides a +very handy utility function, `Box.asA[T]`; it attempts to convert its parameter +to the type `T`, and, if it succeeds, provides a `Full` `Box` with the +converted value of the appropriate type. If it fails, it provides an `Empty` +`Box` instead. + +To deal with the fact that the `Box` may be `Full` or `Empty`, we use `openOr` +on the `Box`. We do this because the type of `messageEntries` is now a +`Box[List[ChatMessage]]`, meaning a box that *contains* a list of chat +messages. `openOr` will give us the plain list of messages if the `Box` is +`Full`, and return the second parameter, an empty `List`, if the `Box` is +`Empty`. + +It's worth mentioning that it seems like we *know* we'll be getting a +`List[ChatMessage]` from the actor. However, the compiler *doesn't*, and that +means it can't guarantee to us that future changes won't render that assumption +false. Using `Box.asA` ensures that, if someone changes the `ChatActor` later +to reply with something else, our snippet won't blow up in the user's face—it +will just not display the existing messages. The intrepid reader can then go +and fix the issue. + +Another annoyance in the code as it stands now is that if 8000 people are +posting messages and I log into the site, my page won't load until those 8000 +messages are processed by the actor. That's because of how `reply` works: we +wait until the actor gets to our message and then replies to it. There are far +better ways of dealing with both of these issues, which we'll talk about when +we talk about using `CometActor`s link:9-comet-actors[later]. + +First, though, let's go back and look at how we can let the user link:8-customizable-usernames.adoc[change their +username so they don't have to use our nasty automatically-generated name]. diff --git a/docs/getting-started-tutorial/8-customizable-usernames.adoc b/docs/getting-started-tutorial/8-customizable-usernames.adoc new file mode 100644 index 0000000000..76a2cab454 --- /dev/null +++ b/docs/getting-started-tutorial/8-customizable-usernames.adoc @@ -0,0 +1,69 @@ +:idprefix: +:idseparator: - +:toc: right +:toclevels: 2 + +# Customizable Usernames + +Let's deal with the next use case: + + - As a chatter, I want to change what name other users see for me when I post + a message. + +What we really want is a text box on the client that will let us edit the name. +We'll add it to the top of our chat area in `chat.html`: + +```html:src/main/webapp/index.html +... +
    +
    + Posting as: + +
    +... +``` + +The ideal way for this to work would be for you to be able to change the value +of the field and have it save once the cursor leaves the field (i.e., on blur). +We can do exactly that using Lift's `ajaxText` helper in `Chat.scala`: + +```scala:src/main/scala/code/snippet/Chat.scala +... + def nameField = { + "input" #> SHtml.ajaxText(username.is, username.set _) + } +... +``` + +How's that for ludicrously easy? We create an `ajaxText` whose initial value +will be the value of the `username` `SessionVar` that we created initially to +track the user's name. The second parameter to `ajaxText` is what gets run when +a change occurs on the client, and we hook it up directly to the ``SessionVar``'s +`set` method, so that changing the text field on the client changes the +`SessionVar`. + +However, maybe we want to provide some feedback to the user to let them know +the name has been updated. We can get a little more detailed: + +```scala:src/main/scala/code/snippet/Chat.scala +... + def nameField = { + "input" #> SHtml.ajaxText(username.is, { updatedUsername: String => + username.set(updatedUsername) + + Alert("Updated your username!") + } + } +... +``` + +Now, when the change gets saved, the user will get a popup that will say +“Updated your username!”. Note that `ajaxText` fields are set up to submit +their changes on blur *or* when the user hits `enter` in the field. + +Now that the user can update their name, it's time to make things truly real +time. Until now, to see the messages someone else has posted, we'd have to +reload the page. Only our messages were posted to the page in real time. Not +much of a chat at all, is it! + +It's time to link:9-comet-actors.adoc[break out the `CometActor`]. diff --git a/docs/getting-started-tutorial/9-comet-actors.adoc b/docs/getting-started-tutorial/9-comet-actors.adoc new file mode 100644 index 0000000000..4d0fce4ba0 --- /dev/null +++ b/docs/getting-started-tutorial/9-comet-actors.adoc @@ -0,0 +1,22 @@ +:idprefix: +:idseparator: - +:toc: right +:toclevels: 2 + +# Comet Actors + +Lift has very robust support for pushing information to the client without the +user having to take explicit action, and it's all mediated through a +`CometActor`. `CometActor` works in many ways exactly like the regular actor +we're already using to track chat messages, only `CometActor`s have a couple +more tricks up their sleeve. Most notably for our purposes: they can re-render +themselves in response to stuff going on inside the server, and send the +updated version to the client. This finally gives us a way to update Jane's +chat message list when Jill posts a message, without Jane having to do +anything. + +Our first move will be to change how exactly we handle binding chat messages. +First, we'll do a quick conversion that puts everything in a `CometActor`, but +doesn't add any additional functionality. Instead of calling + +TODO Apparently I stopped midsentence here, so there's more to fill in ;)