Skip to content

Commit

Permalink
Merge pull request #1846 from lift/early-tutors
Browse files Browse the repository at this point in the history
Early Tutors: Initial pass at a new tutorial living in the repo
  • Loading branch information
farmdawgnation authored Mar 29, 2018
2 parents 3ab4cc2 + 46606aa commit 5ac794a
Show file tree
Hide file tree
Showing 9 changed files with 860 additions and 0 deletions.
72 changes: 72 additions & 0 deletions docs/getting-started-tutorial/1-view-first-development.adoc
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].
32 changes: 32 additions & 0 deletions docs/getting-started-tutorial/2-the-lift-menu-system.adoc
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 docs/getting-started-tutorial/3-adding-snippet-bindings.adoc
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 docs/getting-started-tutorial/4-css-selector-transforms.adoc
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].
Loading

0 comments on commit 5ac794a

Please sign in to comment.