Hello Darwin

From Lift

Jump to: navigation, search

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 show of the class HelloForm2
  • <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 attribute who with the incoming value _. text(...) is a method from S imported via import 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

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/

Personal tools