Hello Darwin
From Lift
In this "tutorial", I'll show you the evolution of a "hello world" sample.
Contents |
prerequisites
For this tutorial, I'll use:
- JDK 1.5, Maven 2.0.7 (later versions of both seem to work as well)
- An editor (jEdit), but you could use an IDE
- 2 terminal windows (I'm under linux)
- One for running jetty (without restart), using Maven
- One for compiling, also using Maven
Now create the project:
#terminal 1 cd work/_sandbox mvn archetype:generate -U \ -DarchetypeGroupId=net.liftweb \ -DarchetypeArtifactId=lift-archetype-blank \ -DarchetypeVersion=0.9 \ -DremoteRepositories=http://scala-tools.org/repo-releases \ -DgroupId=sandbox.lift.hellodarwin -DartifactId=hellodarwin cd hellodarwin mvn jetty:run -U
Open a browser to http://localhost:8080/
static content
my first static html
I create a static page src/main/webapp/helloStatic.html
<html xmlns="http://www.w3.org/1999/xhtml">
<body>
<h1>Static HTML</h1>
Hello world
</body>
</html>
If you point your browser to http://localhost:8080/helloStatic.html or http://localhost:8080/helloStatic , you get "Invalid URL" or 404. Why? because the project template uses a SiteMap. So every html page must be declared in the SiteMap.
So edit src/main/scala/bootstrap/liftweb/Boot.scala to have :
package bootstrap.liftweb
import net.liftweb.util._
import net.liftweb.http._
import net.liftweb.sitemap._
import net.liftweb.sitemap.Loc._
import Helpers._
/**
* A class that's instantiated early and run. It allows the application
* to modify lift's environment
*/
class Boot {
def boot {
// where to search snippet
LiftRules.addToPackages("sandbox.lift.hellodarwin")
// Build SiteMap
val entries = Menu(Loc("Home", "/", "Home")) ::
Menu(Loc("Hello1.1", "/helloStatic", "Hello Static")) ::
Nil
LiftRules.setSiteMap(SiteMap(entries:_*))
}
}
}
The change is an additional line in the definition of "entries", which defines the SiteMap. See the API of Loc for details (the 2nd parameter is the URL, the 3rd parameter is the text to display).
Now compile in terminal 2, without stopping the server :
#terminal 2 cd work/_sandbox/hellodarwin mvn compile
Alternatively you can also start the scala compiler in continuous compilation mode with:
#terminal 2 cd work/_sandbox/hellodarwin mvn scala:cc
If you do that, the compiler will check for filesystem changes and compile them automatically, so you don't need to worry about the compiler nor jetty, it just takes a couple of seconds for both applications to notice the change and act on it... the compiler sometimes doesn't tell you what's going on. just restart with a 'mvn clean scala:cc' and it should report everything properly. If you enter 'cc'-mode, you can ignore the manual compile steps below.
After compilation in terminal 2, you should see a line in terminal 1 (the Jetty console) starting
[INFO] Restart completed at ...
This indicates that the Jetty server has been restarted with new compiled class :)
Now try pointing your browser to http://localhost:8080/. "Hello Static" is added to the menu. Click on it, and the page http://localhost:8080/helloStatic is displayed :).
add header
create src/main/webapp/helloStatic2.html
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="description" content="" />
<meta name="keywords" content="" />
<title>Hello Darwin</title>
</head>
<body>
<h1>Static HTML 2</h1>
Hello world
</body>
</html>
And add another new entry into the sitemap :
val entries = Menu(Loc("Home", "/", "Home")) ::
Menu(Loc("Hello1.1", "/helloStatic", "Hello Static")) ::
Menu(Loc("Hello1.2", "/helloStatic2", "Hello Static2")) ::
Nil
Recompile
#terminal 2 mvn compile
Go to http://localhost:8080/helloStatic2 ... Nice! The right title, everything works!
Note: changes in html files do not need a recompile; changes in Scala files do need a recompile.
adding sitemap to page with snippets
Static html is great, but I can do that without Lift. In the home page, there is a sitemap and an error message that are displayed. We want to do the same in a new page: src/main/webapp/helloStatic3.html
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="description" content="" />
<meta name="keywords" content="" />
<title>Hello Darwin</title>
<lift:CSS.blueprint />
<lift:CSS.fancyType />
<script id="jquery" src="/classpath/jquery.js" type="text/javascript"/>
<script id="json" src="/classpath/json.js" type="text/javascript"/>
</head>
<body>
<h1>+/- Static HTML 3</h1>
Hello world
<lift:Menu.builder />
<lift:msgs />
</body>
</html>
We add:
- <lift:CSS.blueprint /> and <lift:CSS.fancyType /> to use the blueprint css (packaged into lift's jar), (It's not required here, but nicer look ;) )
- jquery and json, because Lift uses it as the base javascript framework (It's not required here, but ...). jquery and json are part of the lift's jar so you don't need to download it, create a classpath dir,... In fact when the url starts with "/classpath/" lift search if resource is available in the classpath and explicitly allowed to be serve (details later or see net.liftweb.http.ResourceServer)
- <lift:Menu.builder /> will be replaced by the sitemap menu (note: previous/deprecated notation was <lift:snippet type="Menu:builder" />)
- <lift:msgs /> will be replaced by messages (if they exist)
Then add another new entry into the sitemap :
val entries = Menu(Loc("Home", "/", "Home")) ::
Menu(Loc("Hello1.1", "/helloStatic", "Hello Static")) ::
Menu(Loc("Hello1.2", "/helloStatic2", "Hello Static2")) ::
Menu(Loc("Hello1.3", "/helloStatic3", "Hello Static3")) ::
Nil
Now recompile
#terminal 2 mvn compile
Point your browser to http://localhost:8080/helloStatic3 ... Nice! We get the right title, and the menu is part of the page.
shared template
Copy/paste-ing the html header and other common stuff is a pain, and a lot of work when we want to change a common thing.
Lift allows us to write that common stuff once, and reference it wherever we want using templates.
Because we use the archetype, a default "template" exists : src/main/webapp/templates-hidden/default.html. Edit the template to change the title :
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8" />
<meta name="description" content="" />
<meta name="keywords" content="" />
<title>Hello Darwin</title>
<lift:CSS.blueprint />
<lift:CSS.fancyType />
<script id="jquery" src="/classpath/jquery.js" type="text/javascript"/>
<script id="json" src="/classpath/json.js" type="text/javascript"/>
</head>
<body>
<lift:bind name="content" />
<lift:Menu.builder/>
<lift:msgs/>
</body>
</html>
Create src/main/webapp/helloStatic4.html
<lift:surround with="default" at="content">
<h1>+/- Static HTML 4</h1>
Hello world
</lift:surround>
<lift:surround with="default" at="content"> :
- with="default" => using the template templates-hidden/default.html
- at="content" => replace <lift:bind name="content" /> in the template
Another way of interpreting this is "start with what's in the 'default' template and first replace take the part called 'content' and replace it with what's in the 'surround' tag.
Next add yet another entry into the sitemap :
val entries = Menu(Loc("Home", "/", "Home")) ::
Menu(Loc("Hello1.1", "/helloStatic", "Hello Static")) ::
Menu(Loc("Hello1.2", "/helloStatic2", "Hello Static2")) ::
Menu(Loc("Hello1.3", "/helloStatic3", "Hello Static3")) ::
Menu(Loc("Hello1.4", "/helloStatic4", "Hello Static4")) ::
Nil
Recompile
#terminal 2 mvn compile
Now point your browser to http://localhost:8080/helloStatic4 ... Nice! The right title, and menu is shown. Go to http://localhost:8080/ Oh! The title is the same (because index.html uses the same template).
add snippet
constant snippet
To add some dynamic content, create a snippet, src/main/scala/sandbox/lift/hellodarwin/snippet/HelloSnippet.scala:
package sandbox.lift.hellodarwin.snippet
class HelloSnippet {
def show = <tt>Friends</tt>
}
Create a page calling the snippet, src/main/webapp/helloSnippet.html :
<lift:surround with="default" at="content">
<h1>Hello Snippet</h1>
Hello <lift:snippet type="HelloSnippet:show" />
</lift:surround>
type="HelloSnippet:show" => replace the snippet tag by the return of the method show of HelloSnippet. the method must return a NodeSeq, it's the scala native format for xml. Scala does automatic conversion of xml in the code into NodeSeq.
We need to update the Boot class again. Lift searches for snippet classes in packages identified in the Boot class :
// where to search for snippets
LiftServlet.addToPackages("sandbox.lift.hellodarwin")
And add entry into the sitemap declared in Boot:
val entries = Menu(Loc("Home", "/", "Home")) ::
Menu(Loc("Hello1.1", "/helloStatic", "Hello Static")) ::
Menu(Loc("Hello1.2", "/helloStatic2", "Hello Static2")) ::
Menu(Loc("Hello1.3", "/helloStatic3", "Hello Static3")) ::
Menu(Loc("Hello1.4", "/helloStatic4", "Hello Static4")) ::
Menu(Loc("Hello2.1", "/helloSnippet", "Hello Snippet")) ::
Nil
Recompile
#terminal 2 mvn compile
Point your browser to http://localhost:8080/helloSnippet ... :)
dynamic snippet
Instead of displaying the static string Friends, I'll display my user name : create a snippet src/main/scala/sandbox/lift/hellodarwin/snippet:
package sandbox.lift.hellodarwin.snippet
class HelloSnippet {
def show = <tt>Friends</tt>
def username = <tt>{System.getProperty("user.name")}</tt>
}
Explications:
- code between { and } inside a NodeSeq sequence is Scala code and its last expression is inserted in the NodeSeq
Create a page calling the snippet, src/main/webapp/helloSnippet2.html :
<lift:surround with="default" at="content">
<h1>Hello Snippet2</h1>
Hello <lift:snippet type="HelloSnippet:username" />
</lift:surround>
And add entry into sitemap :
val entries = Menu(Loc("Home", "/", "Home")) ::
Menu(Loc("Hello1.1", "/helloStatic", "Hello Static")) ::
Menu(Loc("Hello1.2", "/helloStatic2", "Hello Static2")) ::
Menu(Loc("Hello1.3", "/helloStatic3", "Hello Static3")) ::
Menu(Loc("Hello1.4", "/helloStatic4", "Hello Static4")) ::
Menu(Loc("Hello2.1", "/helloSnippet", "Hello Snippet")) ::
Menu(Loc("Hello2.2", "/helloSnippet2", "Hello Snippet2")) ::
Nil
Recompile
#terminal 2 mvn compile
Go to http://localhost:8080/helloSnippet2 ... :)
add form
my first form
create src/main/webapp/helloForm.html
<lift:surround with="default" at="content">
<h1>Hello Form</h1>
Hello <lift:HelloForm.who />
<br/>
<form>
<label for="whoField">Who :</label>
<input type="text" name="whoField"/>
<input type="submit" value="send"/>
</form>
</lift:surround>
create the snippet that retreive who from the request, src/main/scala/sandbox/lift/hellodarwin/snippet/HelloForm.scala :
package sandbox.lift.hellodarwin.snippet
import net.liftweb.http.S
class HelloForm {
def who = <tt>{S.param("whoField").openOr("")}</tt>
}
And add entry into sitemap :
val entries = Menu(Loc("Home", "/", "Home")) ::
Menu(Loc("Hello1.1", "/helloStatic", "Hello Static")) ::
Menu(Loc("Hello1.2", "/helloStatic2", "Hello Static2")) ::
Menu(Loc("Hello1.3", "/helloStatic3", "Hello Static3")) ::
Menu(Loc("Hello1.4", "/helloStatic4", "Hello Static4")) ::
Menu(Loc("Hello2.1", "/helloSnippet", "Hello Snippet")) ::
Menu(Loc("Hello3.1", "/helloForm", "Hello Form")) ::
Nil
Recompile
#terminal 2 mvn compile
Go to http://localhost:8080/helloForm and play ... :)
a more liftway version
The previous version is very basic (data in the form are reset at each request). So try a more 'liftway' version : create src/main/webapp/helloForm2.html
<lift:surround with="default" at="content">
<h1>Hello Form2</h1>
<lift:HelloForm2.show form="POST">
Hello <hello:who/>
<br/>
<label for="whoField">Who :</label>
<hello:whoField/>
<hello:submit/>
</lift:HelloForm2.show>
</lift:surround>
src/main/scala/sandbox/lift/hellodarwin/snippet/HelloForm2.scala :
package sandbox.lift.hellodarwin.snippet
import scala.xml.NodeSeq
import net.liftweb.http.S._
import net.liftweb.http.SHtml._
import net.liftweb.util.Helpers._
class HelloForm2 {
var who = "world"
def show(xhtml: NodeSeq): NodeSeq = {
bind("hello", xhtml,
"whoField" --> text(who, who = _) % ("size" -> "10") % ("id" -> "whoField"),
"submit" --> submit(?("Send"), ignore => {println("value:" + who + " :: " + param("whoField"))}),
"who" --> who
)
}
}
Explication :
- <lift:HelloForm2:show form="POST"> ... </lift:HelloForm2.show> => create a form, send result to the method
showof the classHelloForm2 - <hello:who/>, <hello:whoField/>, <hello:submit/> are replace by binded content in the method show (define in parent node). In the method show, we call bind (imported from Helpers by
import net.liftweb.util.Helpers._) :- first arg "hello" the namespace use to bing
- xhtml the input fragment
- the list of element to bind/replace (name --> NodeSeq)
- "whoField" replace <hello:whoField> with
-
text(who, who = _): an input tag of type text, initial value: who, function to execute on submit : set the attributewhowith the incoming value _.text(...)is a method from S imported viaimport net.liftweb.http.S._ -
% ("size" -> "10")modify the xml node, add an xml attribute "size" with value "10" -
% ("id" -> "whoField")modify the xml node, add an xml attribute "id" with value "whoField" (lift auto generate name attribute)
-
- "submit" replace <hello:submit>
- submit(...) from S, generate a submit button for the surround form
-
?("Send")set the label of the button to "Send", using ?(...) method from S is to retreive a localized string if exists (In your case it doesn't exist, but it's a good practice to find/identify displayable and localizable string). -
ignore => {println("value:" + who + " :: " + param("whoField"))}define a function to execute on submit : print to the console, the value of who and of the param("whoField").
- "who" replace with the value of attribue
who
- "whoField" replace <hello:whoField> with
And add entry into sitemap :
val entries = Menu(Loc("Home", "/", "Home")) ::
Menu(Loc("Hello1.1", "/helloStatic", "Hello Static")) ::
Menu(Loc("Hello1.2", "/helloStatic2", "Hello Static2")) ::
Menu(Loc("Hello1.3", "/helloStatic3", "Hello Static3")) ::
Menu(Loc("Hello1.4", "/helloStatic4", "Hello Static4")) ::
Menu(Loc("Hello2.1", "/helloSnippet", "Hello Snippet")) ::
Menu(Loc("Hello2.2", "/helloSnippet2", "Hello Snippet2")) ::
Menu(Loc("Hello3.1", "/helloForm", "Hello Form")) ::
Menu(Loc("Hello3.2", "/helloForm2", "Hello Form2")) ::
Nil
Recompile
#terminal 2 mvn compile
Go to http://localhost:8080/helloForm2 and play ... :(
WRONG the form doesn't work, and the console of terminal 1 print correct value for who and nil for whoField ???
Explication : The parameters the get passed to text() and submit() are functions that refer to "who" in the scope of the previous instance of the HelloForm2 class.
A way you can pass values around as part of the current request is as follows:
package sandbox.lift.hellodarwin.snippet
import scala.xml.NodeSeq
import net.liftweb.http.S._
import net.liftweb.http.SHtml._
import net.liftweb.http.RequestVar
import net.liftweb.util.Helpers._
import net.liftweb.util.Full
class HelloForm2 {
object who extends RequestVar(Full("world"))
def show(xhtml: NodeSeq): NodeSeq = {
bind("hello", xhtml,
"whoField" --> text(who.openOr(""), v => who(Full(v))) % ("size" -> "10") % ("id" -> "whoField"),
"submit" --> submit(?("Send"), ignore => {println("value:" + who.openOr("") + " :: " + param("whoField"))}),
"who" --> who.openOr("")
)
}
}
In this case, 'who' is a RequestVar... it's valid for the life of the request. It's a way to type-safely pass values from the functions to other instances and other code.
Recompile
#terminal 2 mvn compile
Go to http://localhost:8080/helloForm2 and play ... :)
embedded html
As you see in simple snippet, it is possible to embedded html in your code. It is also possible for form. There is pro and con (matter of taste) : create src/main/webapp/helloForm3.html
<lift:surround with="default" at="content">
<h1>Hello Form3</h1>
<lift:HelloForm3.show form="POST"/>
</lift:surround>
src/main/scala/sandbox/lift/hellodarwin/snippet/HelloForm3.scala :
package sandbox.lift.hellodarwin.snippet
import scala.xml.NodeSeq
import net.liftweb.http.S._
import net.liftweb.http.SHtml._
import net.liftweb.http.RequestVar
import net.liftweb.util.Helpers._
import net.liftweb.util.Full
class HelloForm3 {
object who extends RequestVar(Full("world"))
def show(xhtml: NodeSeq): NodeSeq = {
<xml:group>
Hello {who.openOr("")}
<br/>
<label for="whoField">Who :</label>
{ text(who.openOr(""), v => who(Full(v))) % ("size" -> "10") % ("id" -> "whoField") }
{ submit(?("Send"), ignore => {println("value:" + who.openOr(""))}) }
</xml:group>
}
}
And add entry into sitemap :
...
Menu(Loc("Hello3.1", "/helloForm", "Hello Form")) ::
Menu(Loc("Hello3.2", "/helloForm2", "Hello Form2")) ::
Menu(Loc("Hello3.3", "/helloForm3", "Hello Form3")) ::
...
#terminal 2 mvn compile
Go to http://localhost:8080/helloForm3 and play ... :)
stateful form
Instead of using RequestVar, you could define your snippet as Stateful : extends the trait StatefulSnippet.
create src/main/webapp/helloForm4.html
<lift:surround with="default" at="content">
<h1>Hello Form4</h1>
<lift:HelloForm4.show form="POST"/>
</lift:surround>
src/main/scala/sandbox/lift/hellodarwin/snippet/HelloForm4.scala :
package sandbox.lift.hellodarwin.snippet
import scala.xml.NodeSeq
import net.liftweb.http.S._
import net.liftweb.http.SHtml._
import net.liftweb.http.StatefulSnippet
import net.liftweb.util.Helpers._
import net.liftweb.util.Full
class HelloForm4 extends StatefulSnippet{
val dispatch: DispatchIt = {
case "show" => show _
}
var who = "world"
def show(xhtml: NodeSeq): NodeSeq = {
<xml:group>
Hello {who}
<br/>
<label for="whoField">Who :</label>
{ text(who, v => who = v) % ("size" -> "10") % ("id" -> "whoField") }
{ submit(?("Send"), ignore => {println("value:" + who)}) }
</xml:group>
}
}
And add entry into sitemap :
...
Menu(Loc("Hello3.1", "/helloForm", "Hello Form")) ::
Menu(Loc("Hello3.2", "/helloForm2", "Hello Form2")) ::
Menu(Loc("Hello3.3", "/helloForm3", "Hello Form3")) ::
Menu(Loc("Hello3.4", "/helloForm4", "Hello Form4")) ::
...
#terminal 2 mvn compile
Go to http://localhost:8080/helloForm4 and play ... :)
add ajax
my first ajax form
submit form and refresh data, without reload the page.
create src/main/webapp/helloFormAjax.html:
<lift:surround with="default" at="content">
<h1>Hello FormAjax</h1>
<lift:HelloFormAjax.show>
Hello <hello:who/><br/>
<label for="whoField">Who :</label><hello:whoField/>
<hello:submit/>
</lift:HelloFormAjax.show>
</lift:surround>
create src/main/scala/sandbox/lift/hellodarwin/snippet/HelloFormAjax
package sandbox.lift.hellodarwin.snippet
import scala.xml.NodeSeq
import net.liftweb.http.S._
import net.liftweb.http.SHtml._
import net.liftweb.util.Helpers._
import net.liftweb.http.js.{JsCmd, JsCmds}
class HelloFormAjax {
def whoNode(str: String) = <span id="who">{str}</span>
def updateWho(str: String): JsCmd = {
println("updateWho on " + str)
JsCmds.Run("$('#who').text('"+str+"')")
}
def show(xhtml: NodeSeq): NodeSeq = {
bind("hello", xhtml,
"whoField" --> text("world", null) % ("size" -> "10") % ("id" -> "whoField"),
"submit" --> <button type="button">{?("Send")}</button> % ("onclick" -> ajaxCall("$('#whoField').attr('value')", s => updateWho(s))),
"who" --> whoNode("world")
)
}
}
Explications:
-
JsCmds.Run("$('#who').text('"+str+"')"): use Run to execute arbitrary JavaScript code. In this case replace the text in the node with id "who" with the content of "str" -
<button type="button">{?("Send")}</button>: create a button with localized label "Send" -
% ("onclick" -> ajaxCall("$('#whoField').attr('value')", s => updateWho(s))): add the "onclick" attribute to button, the value of on click is a ajaxCall to the server: calling the function updateWho with the result/value of the javascript code$('#whoField').attr('value'). -
$('#whoField').attr('value'): it's a JQuery (jquery is include in lift's archetypes and sample) code, that could be translate as find node with whoField (CSS selector) and get the value of the attribute value.
And add entry into sitemap :
val entries = Menu(Loc("Home", "/", "Home")) ::
Menu(Loc("Hello1.1", "/helloStatic", "Hello Static")) ::
Menu(Loc("Hello1.2", "/helloStatic2", "Hello Static2")) ::
Menu(Loc("Hello1.3", "/helloStatic3", "Hello Static3")) ::
Menu(Loc("Hello1.4", "/helloStatic4", "Hello Static4")) ::
Menu(Loc("Hello2.1", "/helloSnippet", "Hello Snippet")) ::
Menu(Loc("Hello2.2", "/helloSnippet2", "Hello Snippet2")) ::
Menu(Loc("Hello3.1", "/helloForm", "Hello Form")) ::
Menu(Loc("Hello3.2", "/helloForm2", "Hello Form2")) ::
Menu(Loc("Hello4.1", "/helloFormAjax", "Hello FormAjax")) ::
Nil
Recompile
#terminal 2 mvn compile
Go to http://localhost:8080/helloAjax and play ... :)
...TO BE CONTINUED... (add explications, and evolutions with rewrite url, statefull snippet, serving resource, redirectTo, localization, comet, validation,...)
Notes : you could browse the (full) code at http://code.google.com/p/liftweb/source/browse/trunk/liftweb/sites/hellodarwin/

