From 348ce3aac0d2ae2c9a3799a049d9fffde0151814 Mon Sep 17 00:00:00 2001 From: Antonio Salazar Cardozo Date: Tue, 25 Apr 2017 21:52:09 -0400 Subject: [PATCH 1/7] Add first drafts of tutorial sections 1-9. --- .../1-view-first-development.adoc | 71 ++++++++ .../2-the-lift-menu-system.adoc | 33 ++++ .../3-adding-snippet-bindings.adoc | 73 ++++++++ .../4-css-selector-transforms.adoc | 171 ++++++++++++++++++ .../5-basic-forms.adoc | 105 +++++++++++ .../6-adding-usernames.adoc | 146 +++++++++++++++ .../7-using-actors-for-chat.adoc | 159 ++++++++++++++++ .../8-customizable-usernames.adoc | 69 +++++++ .../9-comet-actors.adoc | 20 ++ 9 files changed, 847 insertions(+) create mode 100644 docs/getting-started-tutorial/1-view-first-development.adoc create mode 100644 docs/getting-started-tutorial/2-the-lift-menu-system.adoc create mode 100644 docs/getting-started-tutorial/3-adding-snippet-bindings.adoc create mode 100644 docs/getting-started-tutorial/4-css-selector-transforms.adoc create mode 100644 docs/getting-started-tutorial/5-basic-forms.adoc create mode 100644 docs/getting-started-tutorial/6-adding-usernames.adoc create mode 100644 docs/getting-started-tutorial/7-using-actors-for-chat.adoc create mode 100644 docs/getting-started-tutorial/8-customizable-usernames.adoc create mode 100644 docs/getting-started-tutorial/9-comet-actors.adoc 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..b52d43b49d --- /dev/null +++ b/docs/getting-started-tutorial/1-view-first-development.adoc @@ -0,0 +1,71 @@ +: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, in a way +that lets you stay focused on users when you're creating the user interface and +worry about the 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 one is accustomed 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, and in +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 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..d4ca29a0df --- /dev/null +++ b/docs/getting-started-tutorial/2-the-lift-menu-system.adoc @@ -0,0 +1,33 @@ +: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 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. You define this using Lift's menu +system, called `SiteMap`. + +Hooking up a simple page like this one 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: + +``` +... + Menu.i("Chat") / "chat" +... +``` + +The string passed to `i` is the name of this menu. We can use that to +[automatically render links for our menu](#section-12). 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 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..1564b0908c --- /dev/null +++ b/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc @@ -0,0 +1,73 @@ +: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: + +``` +
    +``` + +And to the input form: + +``` +
    +``` + +These two indicate two methods in a class called `Chat`, which Lift searches +for in the `code.snippet` package footnote:[This can be changed using +`Lift.addPackage`. See ...]. 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: + +``` +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 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..867e606006 --- /dev/null +++ b/docs/getting-started-tutorial/4-css-selector-transforms.adoc @@ -0,0 +1,171 @@ +:idprefix: +:idseparator: - +:toc: right +:toclevels: 2 + +# CSS Selector Transforms + +Because Lift operates by transforming HTML trees, it needs an easy way to +specify those transformations. Otherwise we'd be doing a bunch of recursive +tree searches and munges and it would get ugly and probably slow to boot. To +deal with transformations easily, we use a small subset of CSS selectors, with +a few Lift idiosyncrasies to maximize performance and address some use cases +that are particularly useful when transforming trees. + +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 one before every page load, so that we can see +the list changing. + +First, we'll define a variable to hold the messages: + +``` +... +object Chat { + var messageEntries = List[String]() +... +} +``` + +Then, we can change the definition of the `messages` method to bind the +contents of the message list: + +``` +... + +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 +> container: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: + +``` +... + def messages = { + messageEntries += "It is now " + formattedTimeNow + "li *" #> messageEntries + } +... +``` + +Let's recompile and restart the server: + +``` +> container: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 rendered 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`: + +``` +... +
  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: + +``` +... + def messages = { + messageEntries ::= "It is now " + formattedTimeNow + + ClearClearable & + "li *" #> messageEntries + } +... +``` + +Notice that we combine the two CSS selector transforms here by using the `&`. +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 [a little later](#section13) 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: 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..0945efe1b3 --- /dev/null +++ b/docs/getting-started-tutorial/5-basic-forms.adoc @@ -0,0 +1,105 @@ +: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 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: + +``` + + + + +
    +``` + +Our `sendMessage` snippet looks like this: + +``` +... + 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: + +``` +... +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: + +``` +... + 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 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..53124e8561 --- /dev/null +++ b/docs/getting-started-tutorial/6-adding-usernames.adoc @@ -0,0 +1,146 @@ +: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: + +``` +... +
  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: + +``` +... +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: + +``` +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`: + +``` +... +case class ChatMessage(poster: String, body: String) +class Chat { + var messageEntries = List[ChatMessage]() + + def messages = { + ClearClearable & + "li" #> messageEntries.map { entry => + ".poster *" #> entry.poster & + ".body *" #> entry.body + } + } + + def sendMessage = { + var message = ChatMessage("", "") + + "#new-message" #> SHtml.text(message, { body: String => message = ChatMessage(username.is, body) }) & + "type=submit" #> SHtml.submitButton(() => { + messageEntries ::= message + }) + } +} +``` + +We introduce a new case class, `ChatMessage`, that carries a poster and a +message body. We also update `messageEntries` to be a list of those. + +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, the trained eye might notice that `sendMessage` never checks whether the +client submitted the form without including the message in the submission. This +is a relatively obscure/weird corner case, but one that's worth dealing with +because it's so easy. To deal with it, we can change `message` from being a +`ChatMessage` to being a `Box[ChatMessage]` that starts off `Empty`. We can +then only add the message to the list if the box has been set to `Full`. This +ensures that we never add a weird blank message to the list, and lets us do it +without having to deal with an initial value of `null` for the `message` +variable 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.]: + +``` +... + def sendMessage = { + var message: Box[ChatMessage] = Empty + + "#new-message" #> SHtml.text(message, { body: String => message = Full(ChatMessage(username.is, body)) }) & + "type=submit" #> SHtml.submitButton(() => { + for (body <- message) { + messageEntries ::= message + } + }) + } +... +``` + +We use a `for` comprehension to unpack the value of `message`. The body of that +comprehension won't run unless `message` is a `Full` box containing a +`ChatMessage` sent by the client. + +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: 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..73773e1229 --- /dev/null +++ b/docs/getting-started-tutorial/7-using-actors-for-chat.adoc @@ -0,0 +1,159 @@ +: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 +[Akka](http://akka.io), 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`: + +``` +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: + +``` +... +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`: + +``` +... +case class MessagePosted(message: ChatMessage) +case object GetMessages +... +``` + +And a handler for it: + +``` +... + 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: + +``` +... + def messages = { + val messageEntries = Box.asA[List[ChatMessage]](ChatActor !? GetMessages).flatten + + 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 `flatten` +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. `flatten` will give us the plain list of messages if the `Box` is +`Full`, and an empty list if it's `Empty`, which is perfect. + +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 [later](link-to-comet-actors). + +First, though, let's go back and look at how we can let the user 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..9f1df267d3 --- /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 +... +
    +
    + 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. We can do exactly that using Lift's `ajaxText` +helper in `Chat.scala`: + +```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 +... + 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 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..ae4a4bbd7c --- /dev/null +++ b/docs/getting-started-tutorial/9-comet-actors.adoc @@ -0,0 +1,20 @@ +: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 From 4eb79939d74f83976fdb8b34e46f1f7ab2f8440a Mon Sep 17 00:00:00 2001 From: Antonio Salazar Cardozo Date: Tue, 25 Apr 2017 22:10:16 -0400 Subject: [PATCH 2/7] Fix typos and such in sections 1-4. --- .../1-view-first-development.adoc | 4 ++-- .../2-the-lift-menu-system.adoc | 2 +- .../3-adding-snippet-bindings.adoc | 14 ++++++------ .../4-css-selector-transforms.adoc | 22 +++++++++---------- 4 files changed, 21 insertions(+), 21 deletions(-) diff --git a/docs/getting-started-tutorial/1-view-first-development.adoc b/docs/getting-started-tutorial/1-view-first-development.adoc index b52d43b49d..6b844d0001 100644 --- a/docs/getting-started-tutorial/1-view-first-development.adoc +++ b/docs/getting-started-tutorial/1-view-first-development.adoc @@ -14,7 +14,7 @@ worry about the 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 one is accustomed the typical web MVC framework. The first stop when +if one is 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, and in what it is that you want to present to the user. You don't just think about @@ -29,7 +29,7 @@ user stories: - 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` +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: diff --git a/docs/getting-started-tutorial/2-the-lift-menu-system.adoc b/docs/getting-started-tutorial/2-the-lift-menu-system.adoc index d4ca29a0df..5e533e4673 100644 --- a/docs/getting-started-tutorial/2-the-lift-menu-system.adoc +++ b/docs/getting-started-tutorial/2-the-lift-menu-system.adoc @@ -24,7 +24,7 @@ directory: ``` The string passed to `i` is the name of this menu. We can use that to -[automatically render links for our menu](#section-12). It gets processed +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 diff --git a/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc b/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc index 1564b0908c..25e30e638b 100644 --- a/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc +++ b/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc @@ -29,7 +29,7 @@ list of chat messages, and the text input that lets us actually chat. To the `ol` that contains the chat messages, we add: ``` -
      +
        ``` And to the input form: @@ -40,10 +40,10 @@ And to the input form: These two indicate two methods in a class called `Chat`, which Lift searches for in the `code.snippet` package footnote:[This can be changed using -`Lift.addPackage`. See ...]. 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: +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: ``` package code @@ -59,8 +59,8 @@ object Chat { 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 +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 diff --git a/docs/getting-started-tutorial/4-css-selector-transforms.adoc b/docs/getting-started-tutorial/4-css-selector-transforms.adoc index 867e606006..a4286667b1 100644 --- a/docs/getting-started-tutorial/4-css-selector-transforms.adoc +++ b/docs/getting-started-tutorial/4-css-selector-transforms.adoc @@ -47,10 +47,10 @@ In the previous section, we mentioned that Lift snippets can return 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 +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 +``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. @@ -78,7 +78,7 @@ every time we render the message list: ``` ... def messages = { - messageEntries += "It is now " + formattedTimeNow + messageEntries :+= "It is now " + formattedTimeNow "li *" #> messageEntries } ... @@ -123,10 +123,10 @@ If you reload the page, you'll get something like: ``` 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 +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 rendered alone (say, for a user test, or when a frontend +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? @@ -148,7 +148,7 @@ template: ``` ... def messages = { - messageEntries ::= "It is now " + formattedTimeNow + messageEntries :+= "It is now " + formattedTimeNow ClearClearable & "li *" #> messageEntries @@ -156,10 +156,10 @@ template: ... ``` -Notice that we combine the two CSS selector transforms here by using the `&`. -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 [a little later](#section13) footnote:[This is because CSS +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.]. From 088b38689ebee4919927a35838e4520d82ab007a Mon Sep 17 00:00:00 2001 From: Antonio Salazar Cardozo Date: Wed, 26 Apr 2017 22:45:35 -0400 Subject: [PATCH 3/7] Fix typos and such in sections 5-8. --- docs/getting-started-tutorial/5-basic-forms.adoc | 5 +++-- docs/getting-started-tutorial/7-using-actors-for-chat.adoc | 4 ++-- docs/getting-started-tutorial/8-customizable-usernames.adoc | 2 +- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/getting-started-tutorial/5-basic-forms.adoc b/docs/getting-started-tutorial/5-basic-forms.adoc index 0945efe1b3..9159dad38e 100644 --- a/docs/getting-started-tutorial/5-basic-forms.adoc +++ b/docs/getting-started-tutorial/5-basic-forms.adoc @@ -18,8 +18,9 @@ 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 the BREACH attack that typical CSRF tokens are vulnerable to when -served with gzip compression over a secure connection. +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: diff --git a/docs/getting-started-tutorial/7-using-actors-for-chat.adoc b/docs/getting-started-tutorial/7-using-actors-for-chat.adoc index 73773e1229..5a56d6066a 100644 --- a/docs/getting-started-tutorial/7-using-actors-for-chat.adoc +++ b/docs/getting-started-tutorial/7-using-actors-for-chat.adoc @@ -18,7 +18,7 @@ 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 -[Akka](http://akka.io), but they're only necessary in cases where you need more +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`: @@ -153,7 +153,7 @@ 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 [later](link-to-comet-actors). +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 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 index 9f1df267d3..3c6219b189 100644 --- a/docs/getting-started-tutorial/8-customizable-usernames.adoc +++ b/docs/getting-started-tutorial/8-customizable-usernames.adoc @@ -38,7 +38,7 @@ helper in `Chat.scala`: 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 +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`. From d77f4006d5860cda59a6bc309ed8c013139dcb1b Mon Sep 17 00:00:00 2001 From: Antonio Salazar Cardozo Date: Tue, 2 May 2017 22:04:23 -0400 Subject: [PATCH 4/7] Add interlinking between tutorial parts. --- docs/getting-started-tutorial/1-view-first-development.adoc | 2 +- docs/getting-started-tutorial/2-the-lift-menu-system.adoc | 2 +- docs/getting-started-tutorial/3-adding-snippet-bindings.adoc | 2 +- docs/getting-started-tutorial/4-css-selector-transforms.adoc | 2 +- docs/getting-started-tutorial/5-basic-forms.adoc | 2 +- docs/getting-started-tutorial/6-adding-usernames.adoc | 4 ++-- docs/getting-started-tutorial/7-using-actors-for-chat.adoc | 4 ++-- docs/getting-started-tutorial/8-customizable-usernames.adoc | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/getting-started-tutorial/1-view-first-development.adoc b/docs/getting-started-tutorial/1-view-first-development.adoc index 6b844d0001..e60bdf9887 100644 --- a/docs/getting-started-tutorial/1-view-first-development.adoc +++ b/docs/getting-started-tutorial/1-view-first-development.adoc @@ -68,4 +68,4 @@ 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 the Lift side. +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 index 5e533e4673..cddf382db2 100644 --- a/docs/getting-started-tutorial/2-the-lift-menu-system.adoc +++ b/docs/getting-started-tutorial/2-the-lift-menu-system.adoc @@ -30,4 +30,4 @@ 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 bringing our HTML to life. +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 index 25e30e638b..96e004dbb1 100644 --- a/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc +++ b/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc @@ -70,4 +70,4 @@ 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 CSS Selector Transforms. +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 index a4286667b1..01f8e8cbee 100644 --- a/docs/getting-started-tutorial/4-css-selector-transforms.adoc +++ b/docs/getting-started-tutorial/4-css-selector-transforms.adoc @@ -168,4 +168,4 @@ 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: forms. +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 index 9159dad38e..a2b10c8304 100644 --- a/docs/getting-started-tutorial/5-basic-forms.adoc +++ b/docs/getting-started-tutorial/5-basic-forms.adoc @@ -103,4 +103,4 @@ 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 deal with usernames first. +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 index 53124e8561..7fdbe07f4e 100644 --- a/docs/getting-started-tutorial/6-adding-usernames.adoc +++ b/docs/getting-started-tutorial/6-adding-usernames.adoc @@ -142,5 +142,5 @@ message at the same time right now, who knows what would happen to the 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: an -actor. +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 index 5a56d6066a..5ca26ff7e5 100644 --- a/docs/getting-started-tutorial/7-using-actors-for-chat.adoc +++ b/docs/getting-started-tutorial/7-using-actors-for-chat.adoc @@ -155,5 +155,5 @@ 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 change their -username so they don't have to use our nasty automatically-generated name. +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 index 3c6219b189..c426f1294e 100644 --- a/docs/getting-started-tutorial/8-customizable-usernames.adoc +++ b/docs/getting-started-tutorial/8-customizable-usernames.adoc @@ -66,4 +66,4 @@ 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 break out the `CometActor`. +It's time to link:9-comet-actors.adoc[break out the `CometActor`]. From b6668a9728da9bd1ad58a14fd1b9277f6980d822 Mon Sep 17 00:00:00 2001 From: Antonio Salazar Cardozo Date: Wed, 3 May 2017 22:23:45 -0400 Subject: [PATCH 5/7] Significantly rework the introduction + explanation of `ChatMessage`. We now use asciidoc callouts to more clearly link back to particular parts of the code that was updated, and break it down into two more digestible chunks. --- .../6-adding-usernames.adoc | 115 ++++++++++-------- 1 file changed, 61 insertions(+), 54 deletions(-) diff --git a/docs/getting-started-tutorial/6-adding-usernames.adoc b/docs/getting-started-tutorial/6-adding-usernames.adoc index 7fdbe07f4e..9935bd523b 100644 --- a/docs/getting-started-tutorial/6-adding-usernames.adoc +++ b/docs/getting-started-tutorial/6-adding-usernames.adoc @@ -62,78 +62,85 @@ 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 ... -case class ChatMessage(poster: String, body: String) +case class ChatMessage(poster: String, body: String) // <1> class Chat { - var messageEntries = List[ChatMessage]() + var messageEntries = List[ChatMessage]() // <2> def messages = { ClearClearable & - "li" #> messageEntries.map { entry => + "li" #> messageEntries.map { entry => // <3> ".poster *" #> entry.poster & ".body *" #> entry.body } } - - def sendMessage = { - var message = ChatMessage("", "") - - "#new-message" #> SHtml.text(message, { body: String => message = ChatMessage(username.is, body) }) & - "type=submit" #> SHtml.submitButton(() => { - messageEntries ::= message - }) - } -} -``` - -We introduce a new case class, `ChatMessage`, that carries a poster and a -message body. We also update `messageEntries` to be a list of those. - -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, the trained eye might notice that `sendMessage` never checks whether the -client submitted the form without including the message in the submission. This -is a relatively obscure/weird corner case, but one that's worth dealing with -because it's so easy. To deal with it, we can change `message` from being a -`ChatMessage` to being a `Box[ChatMessage]` that starts off `Empty`. We can -then only add the message to the list if the box has been set to `Full`. This -ensures that we never add a weird blank message to the list, and lets us do it -without having to deal with an initial value of `null` for the `message` -variable 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.]: - -``` ... +``` +<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 def sendMessage = { - var message: Box[ChatMessage] = Empty + var message = ChatMessage("", "") // <1> - "#new-message" #> SHtml.text(message, { body: String => message = Full(ChatMessage(username.is, body)) }) & - "type=submit" #> SHtml.submitButton(() => { + "#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 } }) } -... +} ``` - -We use a `for` comprehension to unpack the value of `message`. The body of that -comprehension won't run unless `message` is a `Full` box containing a -`ChatMessage` sent by the client. +<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 From c4160a74652868bd975d0929bd3aacc24b1d167e Mon Sep 17 00:00:00 2001 From: Antonio Salazar Cardozo Date: Sat, 3 Mar 2018 10:00:18 -0500 Subject: [PATCH 6/7] Added some rephrasings and tweaks based on excellent review feedback --- .../1-view-first-development.adoc | 17 +++++++++-------- .../2-the-lift-menu-system.adoc | 9 ++++----- .../3-adding-snippet-bindings.adoc | 5 +++-- .../4-css-selector-transforms.adoc | 17 +++++++++-------- .../7-using-actors-for-chat.adoc | 9 +++++---- .../8-customizable-usernames.adoc | 4 ++-- .../9-comet-actors.adoc | 2 ++ 7 files changed, 34 insertions(+), 29 deletions(-) diff --git a/docs/getting-started-tutorial/1-view-first-development.adoc b/docs/getting-started-tutorial/1-view-first-development.adoc index e60bdf9887..a8e33a34d1 100644 --- a/docs/getting-started-tutorial/1-view-first-development.adoc +++ b/docs/getting-started-tutorial/1-view-first-development.adoc @@ -8,19 +8,20 @@ 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, in a way -that lets you stay focused on users when you're creating the user interface and -worry about the the interface between your backend and the HTML only when +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 one is accustomed to the typical web MVC framework. The first stop when +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, and in -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 +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. +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: diff --git a/docs/getting-started-tutorial/2-the-lift-menu-system.adoc b/docs/getting-started-tutorial/2-the-lift-menu-system.adoc index cddf382db2..dc28ac1559 100644 --- a/docs/getting-started-tutorial/2-the-lift-menu-system.adoc +++ b/docs/getting-started-tutorial/2-the-lift-menu-system.adoc @@ -6,12 +6,11 @@ # The Lift Menu System Another distinguishing characteristic of Lift is that it is *secure by -default*. Amongst other things, this means that 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. You define this using Lift's menu -system, called `SiteMap`. +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 like this one is easy, and seems redundant; rest +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` diff --git a/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc b/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc index 96e004dbb1..7dc67ac51f 100644 --- a/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc +++ b/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc @@ -38,8 +38,9 @@ And to the input form:
        ``` -These two indicate two methods in a class called `Chat`, which Lift searches -for in the `code.snippet` package footnote:[This can be changed using +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 diff --git a/docs/getting-started-tutorial/4-css-selector-transforms.adoc b/docs/getting-started-tutorial/4-css-selector-transforms.adoc index 01f8e8cbee..f7bd576e55 100644 --- a/docs/getting-started-tutorial/4-css-selector-transforms.adoc +++ b/docs/getting-started-tutorial/4-css-selector-transforms.adoc @@ -5,16 +5,17 @@ # CSS Selector Transforms -Because Lift operates by transforming HTML trees, it needs an easy way to +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 and it would get ugly and probably slow to boot. To -deal with transformations easily, we use a small subset of CSS selectors, with -a few Lift idiosyncrasies to maximize performance and address some use cases -that are particularly useful when transforming trees. +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 one before every page load, so that we can see +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: @@ -59,7 +60,7 @@ directory of the chat app and start up the application: ``` $ sbt -> container:start +> 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 @@ -87,7 +88,7 @@ every time we render the message list: Let's recompile and restart the server: ``` -> container:stop +> 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 diff --git a/docs/getting-started-tutorial/7-using-actors-for-chat.adoc b/docs/getting-started-tutorial/7-using-actors-for-chat.adoc index 5ca26ff7e5..fd4e2f17e8 100644 --- a/docs/getting-started-tutorial/7-using-actors-for-chat.adoc +++ b/docs/getting-started-tutorial/7-using-actors-for-chat.adoc @@ -112,7 +112,7 @@ listing messages by updating the `Chat` snippet: ``` ... def messages = { - val messageEntries = Box.asA[List[ChatMessage]](ChatActor !? GetMessages).flatten + val messageEntries = Box.asA[List[ChatMessage]](ChatActor !? GetMessages) openOr List() ClearClearable & "li" #> messageEntries.map { entry => @@ -134,11 +134,12 @@ 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 `flatten` +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. `flatten` will give us the plain list of messages if the `Box` is -`Full`, and an empty list if it's `Empty`, which is perfect. +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 diff --git a/docs/getting-started-tutorial/8-customizable-usernames.adoc b/docs/getting-started-tutorial/8-customizable-usernames.adoc index c426f1294e..cbf62441c0 100644 --- a/docs/getting-started-tutorial/8-customizable-usernames.adoc +++ b/docs/getting-started-tutorial/8-customizable-usernames.adoc @@ -24,8 +24,8 @@ We'll add it to the top of our chat area in `chat.html`: ``` 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. We can do exactly that using Lift's `ajaxText` -helper in `Chat.scala`: +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 ... diff --git a/docs/getting-started-tutorial/9-comet-actors.adoc b/docs/getting-started-tutorial/9-comet-actors.adoc index ae4a4bbd7c..4d0fce4ba0 100644 --- a/docs/getting-started-tutorial/9-comet-actors.adoc +++ b/docs/getting-started-tutorial/9-comet-actors.adoc @@ -18,3 +18,5 @@ 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 ;) From 46606aa946a5cf8fb28e8d890af5c77016233cb9 Mon Sep 17 00:00:00 2001 From: Antonio Salazar Cardozo Date: Sat, 3 Mar 2018 10:05:42 -0500 Subject: [PATCH 7/7] Fill in code block filenames where relevant This sets us up for some nice preprocessing later to fill in context around our example blocks. --- .../2-the-lift-menu-system.adoc | 2 +- .../3-adding-snippet-bindings.adoc | 6 +++--- .../4-css-selector-transforms.adoc | 10 +++++----- docs/getting-started-tutorial/5-basic-forms.adoc | 8 ++++---- docs/getting-started-tutorial/6-adding-usernames.adoc | 10 +++++----- .../7-using-actors-for-chat.adoc | 10 +++++----- .../8-customizable-usernames.adoc | 6 +++--- 7 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/getting-started-tutorial/2-the-lift-menu-system.adoc b/docs/getting-started-tutorial/2-the-lift-menu-system.adoc index dc28ac1559..373db1f283 100644 --- a/docs/getting-started-tutorial/2-the-lift-menu-system.adoc +++ b/docs/getting-started-tutorial/2-the-lift-menu-system.adoc @@ -16,7 +16,7 @@ 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" ... diff --git a/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc b/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc index 7dc67ac51f..00334f75c9 100644 --- a/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc +++ b/docs/getting-started-tutorial/3-adding-snippet-bindings.adoc @@ -28,13 +28,13 @@ 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 ``` @@ -46,7 +46,7 @@ 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 diff --git a/docs/getting-started-tutorial/4-css-selector-transforms.adoc b/docs/getting-started-tutorial/4-css-selector-transforms.adoc index f7bd576e55..2d7b7ee4bb 100644 --- a/docs/getting-started-tutorial/4-css-selector-transforms.adoc +++ b/docs/getting-started-tutorial/4-css-selector-transforms.adoc @@ -20,7 +20,7 @@ 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]() @@ -31,7 +31,7 @@ object Chat { 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._ @@ -76,7 +76,7 @@ Once you see the success message, point your browser to 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 @@ -133,7 +133,7 @@ without getting nasty in our HTML? Lift lets us tag the extra elements with a class `clearable`: -``` +```html:src/main/webapp/index.html ...
        1. Hi!
        2. Oh, hey there.
        3. @@ -146,7 +146,7 @@ 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 diff --git a/docs/getting-started-tutorial/5-basic-forms.adoc b/docs/getting-started-tutorial/5-basic-forms.adoc index a2b10c8304..0cb9dcaaed 100644 --- a/docs/getting-started-tutorial/5-basic-forms.adoc +++ b/docs/getting-started-tutorial/5-basic-forms.adoc @@ -25,7 +25,7 @@ connection. Let's look at a simple example with our chat application. Currently our form looks like this: -``` +```html @@ -35,7 +35,7 @@ looks like this: Our `sendMessage` snippet looks like this: -``` +```scala ... def sendMessage(contents: NodeSeq) = contents ... @@ -45,7 +45,7 @@ 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 @@ -78,7 +78,7 @@ 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 & diff --git a/docs/getting-started-tutorial/6-adding-usernames.adoc b/docs/getting-started-tutorial/6-adding-usernames.adoc index 9935bd523b..2289cfe850 100644 --- a/docs/getting-started-tutorial/6-adding-usernames.adoc +++ b/docs/getting-started-tutorial/6-adding-usernames.adoc @@ -13,7 +13,7 @@ We're about to add another use case to our chat system: 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 ...
        4. Antonio Hi! @@ -40,7 +40,7 @@ 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") @@ -51,7 +51,7 @@ 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)) ``` @@ -62,7 +62,7 @@ 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 +```scala:src/main/scala/code/snippet/Chat.scala ... case class ChatMessage(poster: String, body: String) // <1> class Chat { @@ -94,7 +94,7 @@ class Chat { Now let's update the binding of the `sendMessage` form to deal with the new `ChatMessage` class: -```scala +```scala:src/main/scala/code/snippet/Chat.scala def sendMessage = { var message = ChatMessage("", "") // <1> diff --git a/docs/getting-started-tutorial/7-using-actors-for-chat.adoc b/docs/getting-started-tutorial/7-using-actors-for-chat.adoc index fd4e2f17e8..35d3ef4004 100644 --- a/docs/getting-started-tutorial/7-using-actors-for-chat.adoc +++ b/docs/getting-started-tutorial/7-using-actors-for-chat.adoc @@ -22,7 +22,7 @@ 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 @@ -55,7 +55,7 @@ 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._ ... @@ -80,7 +80,7 @@ isn't useful if we can't get them back out! 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 @@ -89,7 +89,7 @@ case object GetMessages And a handler for it: -``` +```scala:src/main/scala/code/actor/ChatActor.scala ... def messageHandler = { ... @@ -109,7 +109,7 @@ 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() diff --git a/docs/getting-started-tutorial/8-customizable-usernames.adoc b/docs/getting-started-tutorial/8-customizable-usernames.adoc index cbf62441c0..76a2cab454 100644 --- a/docs/getting-started-tutorial/8-customizable-usernames.adoc +++ b/docs/getting-started-tutorial/8-customizable-usernames.adoc @@ -13,7 +13,7 @@ Let's deal with the next use case: 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 +```html:src/main/webapp/index.html ...
          @@ -27,7 +27,7 @@ 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 +```scala:src/main/scala/code/snippet/Chat.scala ... def nameField = { "input" #> SHtml.ajaxText(username.is, username.set _) @@ -45,7 +45,7 @@ a change occurs on the client, and we hook it up directly to 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 +```scala:src/main/scala/code/snippet/Chat.scala ... def nameField = { "input" #> SHtml.ajaxText(username.is, { updatedUsername: String =>