-
Notifications
You must be signed in to change notification settings - Fork 278
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #1846 from lift/early-tutors
Early Tutors: Initial pass at a new tutorial living in the repo
- Loading branch information
Showing
9 changed files
with
860 additions
and
0 deletions.
There are no files selected for viewing
72 changes: 72 additions & 0 deletions
72
docs/getting-started-tutorial/1-view-first-development.adoc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
<!DOCTYPE html> | ||
<html> | ||
<head> | ||
<title>Chat!</title> | ||
</head> | ||
|
||
<body> | ||
<section id="chat"> | ||
<ol class="messages"> | ||
<li>Hi!</li> | ||
<li>Oh, hey there.</li> | ||
<li>How are you?</li> | ||
<li>Good, you?</li> | ||
</ol> | ||
<form class="send-message"> | ||
<label for="new-message">Post message</label> | ||
<input id="new-message" type="text"> | ||
<input type="submit" value="Post"> | ||
</form> | ||
</section> | ||
</body> | ||
</html> | ||
``` | ||
|
||
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]. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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]. |
74 changes: 74 additions & 0 deletions
74
docs/getting-started-tutorial/3-adding-snippet-bindings.adoc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
<ol class="messages" data-lift="Chat.messages"> | ||
``` | ||
|
||
And to the input form: | ||
|
||
```html:src/main/webapp/index.html | ||
<form class="send-message" data-lift="Chat.sendMessage"> | ||
``` | ||
|
||
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]. |
172 changes: 172 additions & 0 deletions
172
docs/getting-started-tutorial/4-css-selector-transforms.adoc
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: | ||
|
||
``` | ||
<li>It is now 13:25 UTC</li> | ||
<li>It is now 13:25 UTC</li> | ||
<li>It is now 13:25 UTC</li> | ||
<li>It is now 13:25 UTC</li> | ||
``` | ||
|
||
If you reload the page, you'll get something like: | ||
|
||
``` | ||
<li>It is now 13:25 UTC</li> | ||
<li>It is now 13:25 UTC</li> | ||
<li>It is now 13:25 UTC</li> | ||
<li>It is now 13:25 UTC</li> | ||
<li>It is now 13:26 UTC</li> | ||
<li>It is now 13:26 UTC</li> | ||
<li>It is now 13:26 UTC</li> | ||
<li>It is now 13:26 UTC</li> | ||
``` | ||
|
||
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 | ||
... | ||
<li>Hi!</li> | ||
<li class="clearable">Oh, hey there.</li> | ||
<li class="clearable">How are you?</li> | ||
<li class="clearable">Good, you?</li> | ||
... | ||
``` | ||
|
||
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]. |
Oops, something went wrong.