Monday, 7 September 2009

Writing a View Page in Lift

The CRUDify trait adds a view url for a item. In this post we will turn off this view and build our own. To turn off the CRUDify view override the viewMenuLoc method in the Item object

 override def viewMenuLoc = Empty

The view item the url is http://localhost:8080/item/view/1 where final part of the url is the primary key (PK) of the item. To write our own version of the view functionality the url needs to rewritten to convert the 1 into a parameter.

Lift handles url rewriting using LiftRules rewrite. The rewite is a RulesSeq where the rules take a RewriteRequest and return a RewriteResponse. The RewriteRequests are a case class used in a case statement on the incoming request.

The RewriteRequest takes a ParsePath which represents the incoming url that needs rewriting. The path is the first argument in the ParsePath constructor, the second is the suffix to match on, the third is a boolean that indicates if the path is root relative i.e. starts with the context path of the app and the fourth indicates that the url ends with a /.

The instance we are interested in is the path item/view/1 so the parse path is constructed as shown below where the _ means match anything in the case statement.

ParsePath(List("item", "view", id),_,_,_)

The RewriteRequest takes as its second argument a RequestType that represents a HTTP method. The final argument it the HttpServletRequest.

For the item view rewrite there is no need to match the http request type or match on the HttpServletRequest .

RewriteRequest(
        ParsePath(List("item", "view", id),_,_,_),_,_)

The RewriteResponse takes a ParsePath and a map of parameters.

RewriteResponse("item" :: "view" :: Nil, Map("id" -> id))

Put both theRewriteRequest and RewriteResponse together in the Boot class. Notice that id from the RewriteRequest ParsePath is mapped to the id variable id which is used in the RewriteResponse.

LiftRules.rewrite.append {
    case RewriteRequest(
    ParsePath(List("item", "view", id),_,_,_),_,_) =>
    RewriteResponse("item" :: "view" :: Nil, Map("id" -> id))
}

To test the new rewrite add a new html file under /src/main/webapp/item called view.html.

Create view html file

Add some test content to the view.html file

<lift:surround with="default" at="content">
<p>My view template</p>
</lift:surround>

The view item does not need to appear in the left hand menu but unless the location is added to the sitemap access will not be allowed to the page. To allow access to all files in the item folder add a Menu to the sitemap which allow access to sub paths but is not shown.

Menu(Loc("Item", List("item") -> true, "Item", Hidden))

Run the application mvn jetty:run and view this page in your browser http://localhost:8080/item/view/1.

To display the Item in the page update the view.html.

<lift:surround with="default" at="content">
<lift:ViewItem.view >
    <table>
        <tr>
            <td>Name</td>
            <td><item:name/></td>
        </tr>
        <tr>
            <td>Amonut</td>
            <td><item:amount/></td>
        </tr>
    </table>
</lift:ViewItem.view>
</lift:surround>

Create ViewItem.scala in the snippets directory.

View item snippet

The view method on the ViewItem class should find the Item associated with the passed in id request parameter. The Lift S object provides the param method to retrieve the http parameter from the request. The MetaMapper findByKey method returns a item that has the value of the id parameter passed in. If the item is found a Full Box is returned and the values bound to the incomming NodeSequence. If the item is not found a Empty is returned and a Text node containing "Invalid Item" is returned.

package com.shopping.snippet

import com.shopping._
import model._
import net.liftweb._

import util._
import Helpers._
import http._

import scala.xml._

class ViewItem {

    var id = S.param("id") openOr ""

    var item = try {
        Item.findByKey(id.toLong)
    } catch {
        case e:NumberFormatException => Empty
    }

    def view(html : NodeSeq): NodeSeq = {
        item map ({ i =>
            bind("item", html,
                 "name" -> i.name,
                 "amount" -> i.amount
            )
        }) openOr Text("Invalid Item")

    }
}

Run the app using mvn jetty:run create some items and then go to the list view and click on the view link.

The code for this app is available on git hub http://github.com/oliverdaff/Lift-Shopping/tree/master and is tagged with viewItem.

3 comments:

  1. Is there a way to customize the view and delete pages with less boilerplate ?

    Compare with edit: there you only need a single line in the html file and a call to item.toForm in the snippet, while here we need to enumerate all fields both in the html file and the snippet.

    ReplyDelete
  2. Hi Remyremy,

    The Lift Record framework has more flexibility in controlling the output of the toForm than Mapper. See section 6.2.2 'Form Generation' in the Exploring Lift book.

    If you are using a lot of views on with the Mapper framework and want to get rid of all boiler plate with out using CRUDify take a look a the code in the CRUDify (1.0) source around line 115. This code iterates over the entries fields and binds the displayHtml and asHTML to the _viewTemplate wrapped in the pageWrapper.

    Ollie

    ReplyDelete
  3. D'oh ! I had seen this piece of code in CRUDify, but somehow managed to forget about it...

    Regarding Record, the message found at http://www.mail-archive.com/liftweb@googlegroups.com/msg13708.html suggests that it may not be ready for use with a RDBMS yet.

    Thanks for your help !

    ReplyDelete