-
Notifications
You must be signed in to change notification settings - Fork 278
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Early Tutors: Initial pass at a new tutorial living in the repo #1846
Changes from 5 commits
348ce3a
4eb7993
088b386
d77f400
b6668a9
c4160a7
46606aa
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 to the typical web MVC framework. The first stop when | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I prefer no contractions, since this is written, but I'm down to go second-person to make it less academic. |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This sentence took me a few reads to comprehend correctly. It might be a good candidate for some tweaking. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rephrased, great point. |
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Comma after There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🙇 |
||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here you use There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Your eye for detail brings a tear to my eye ;) |
||
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]. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This isn't entirely true. If a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. ! Nice catch. Rephrased a bit to take that into account. |
||
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 | ||
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]. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would a link to the HTML5 specification or some other explanation for data attributes be useful here? |
||
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.]. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm torn on whether or not the contents of this footnote should be part of the regular paragraph. No further comment than that, just wanted to bring it to your attention. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Because this is a tutorial, I decided I wanted to keep the top-level content super-focused on implementation rather than reasoning. |
||
|
||
|
||
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: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The original markup was two pages back. Perhaps we should reproduce it here so the reader is reminded of the overall structure? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes… But let me punt on that a bit. I'm wanting to do a little preprocessing pass on these guys that handles building some of that for you. |
||
|
||
``` | ||
<ol class="messages" data-lift="Chat.messages"> | ||
``` | ||
|
||
And to the input form: | ||
|
||
``` | ||
<form class="send-message" data-lift="Chat.sendMessage"> | ||
``` | ||
|
||
These two indicate two methods in a class called `Chat`, which Lift searches | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. "These two" -> "These two changes add data attributes that..." |
||
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: | ||
|
||
``` | ||
package code | ||
package snippet | ||
|
||
import scala.xml._ | ||
|
||
object Chat { | ||
def messages(contents: NodeSeq) = contents | ||
def sendMessage(contents: NodeSeq) = contents | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It would be really cool at some point in the future to take a shot at integrating dexy so all of these have to compile or something. But that's not for now. =) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 100% part of my plan, yeah. |
||
``` | ||
|
||
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. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be worthwhile to include an example of changing the chat page directly by just returning a static I think the addition of that will make the utility of CSS Transforms more obvious, too. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That's a good point… There are definitely some good spots here to suggest exploration, I think. e.g., “try changing I am the walrus! ” and see what happens! |
||
|
||
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]. |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You use |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It could just be how I'm reading it today, but the end of this sentence feels a bit awkward. Would recommend changing There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I went too conversational here, trying to go for a rhythm that shows all the bad things snowballing haha. |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommend the addition of a comma after the word There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Totally rephrased that bit, it was definitely a little rambly. |
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Recommend replacing |
||
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 | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Examples in this form are frequently confusing to me when reading getting started guides. Can we present the entire There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I actually want the compiled output here to produce the whole thing with highlighted changes… I have some more thoughts here. Bit off a little more than I could chew the first time I tried to do this, but I think there are some small things we can do to get to that point quickly. |
||
... | ||
``` | ||
|
||
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We'll need to update this to |
||
[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 [email protected]: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 [email protected]: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`: | ||
|
||
``` | ||
... | ||
<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: | ||
|
||
``` | ||
... | ||
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.]. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's probably also worth adding a footnote here regarding indentation rules, since any Scala developer passing through will likely wonder why we don't represent this as: ClearClearable &
"li *" #> messageEntries There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm going to punt on this… Not sure that note really belongs in a beginner tutorial. I've also been rethinking the general principle, although I like the look of same-indentation. Mostly because if you consider auto-indentation tools your friend, they can actually catch the common error of forgetting an |
||
|
||
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]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This might be easier to parse if it were broken into two sentences right around
in a way
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Excellent point!