Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
tree: 78870d6b33
Fetching contributors…

Cannot retrieve contributors at this time

2844 lines (1854 sloc) 48.773 kb
#LyX 1.6.7 created this file. For more info see http://www.lyx.org/
\lyxformat 345
\begin_document
\begin_header
\textclass book
\use_default_options false
\language english
\inputencoding auto
\font_roman default
\font_sans default
\font_typewriter default
\font_default_family default
\font_sc false
\font_osf false
\font_sf_scale 100
\font_tt_scale 100
\graphics default
\paperfontsize default
\spacing single
\use_hyperref false
\papersize default
\use_geometry false
\use_amsmath 1
\use_esint 1
\cite_engine basic
\use_bibtopic false
\paperorientation portrait
\secnumdepth 3
\tocdepth 3
\paragraph_separation indent
\defskip medskip
\quotes_language english
\papercolumns 1
\papersides 1
\paperpagestyle default
\tracking_changes false
\output_changes false
\author ""
\author ""
\end_header
\begin_body
\begin_layout Chapter
PocketChange
\begin_inset CommandInset label
LatexCommand label
name "cha:PocketChange"
\end_inset
\end_layout
\begin_layout Standard
As a way to demonstrate the concepts in the book, we're going to build a
basic application and then build on it as we go along.
As it evolves, so will your understanding of Lift.
The application we've picked is an Expense Tracker.
We call it
\shape italic
PocketChange.
\end_layout
\begin_layout Standard
\begin_inset Float figure
placement h
wide false
sideways false
status open
\begin_layout Plain Layout
\align center
\begin_inset Graphics
filename images/pocketchange.png
width 6in
\end_inset
\end_layout
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
The PocketChange App
\end_layout
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout Standard
PocketChange will track your expenses, keep a running total of what you've
spent, allow you to organize your data using tags, and help you to visualize
the data.
During the later chapters of the book we'll add a few fun features, such
as AJAX charting and allowing multiple people per account (with Comet update
of entries).
Above all, we want to keep the interface lean and clean.
\end_layout
\begin_layout Standard
We're going to be using the
\emph on
View First
\emph default
pattern for the design of our app, because Lift's separation of presentation
and logic via templating, views, and snippets lends itself to the
\emph on
View First
\emph default
pattern so well.
For an excellent article on the design decisions behind Lift's approach
to templating and logic, read David Pollak's
\emph on
Lift View First
\emph default
article on the Wiki
\begin_inset Foot
status open
\begin_layout Plain Layout
\begin_inset Flex URL
status open
\begin_layout Plain Layout
http://www.assembla.com/wiki/show/liftweb/View_First
\end_layout
\end_inset
\end_layout
\end_inset
.
\end_layout
\begin_layout Standard
Another important thing to note is that we're going to breeze through the
app and touch on a lot of details.
We'll provide plenty of references to the chapters where things are covered.
This chapter is really intended just to give you a taste of Lift, so feel
free to read ahead if you want more information on how something works.
The full source for the entire PocketChange application is available at
GitHub
\begin_inset Foot
status open
\begin_layout Plain Layout
\begin_inset Flex URL
status open
\begin_layout Plain Layout
http://github.com/tjweir/pocketchangeapp
\end_layout
\end_inset
\end_layout
\end_inset
.
Enough chatter, let's go!
\end_layout
\begin_layout Section
Defining the Model
\end_layout
\begin_layout Standard
The first step we'll take is to define the database entities that we're
going to use for our app.
The base functionality of a categorized expense tracker is covered by the
following items:
\end_layout
\begin_layout Itemize
User: A user of the application
\end_layout
\begin_layout Itemize
Account: A specific expense account - we want to support more than one per
user
\end_layout
\begin_layout Itemize
Expense: A specific expense transaction tied to a particular account
\end_layout
\begin_layout Itemize
Tag: A word or phrase that permits us a to categorize each expense for later
searching and reporting
\end_layout
\begin_layout Standard
We'll start out with the User, as shown in listing
\begin_inset CommandInset ref
LatexCommand ref
reference "lst:PocketChange-User-entity"
\end_inset
.
We leverage Lift's MegaProtoUser (Section
\begin_inset CommandInset ref
LatexCommand vref
reference "sub:ProtoUser-and-MegaProtoUser"
\end_inset
) class to handle pretty much everything we need for user management.
For example, with just the code you see, we define an entire user management
function for our site, including a signup page, a lost password page, and
a login page.
The accompanying SiteMap (Section
\begin_inset CommandInset ref
LatexCommand vref
reference "cha:SiteMap"
\end_inset
) menus are generated with a single call to
\family typewriter
User.siteMap
\family default
.
As you can see, we can customize the XHTML that's generated for the user
management pages with a few simple defs.
The opportunities for customization provided by MetaMegaProtoUser are extensive.
\end_layout
\begin_layout Standard
\begin_inset listings
inline false
status open
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
The PocketChange User Entity
\begin_inset CommandInset label
LatexCommand label
name "lst:PocketChange-User-entity"
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout Plain Layout
package com.pocketchangeapp.model
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Import all of the mapper classes
\end_layout
\begin_layout Plain Layout
import _root_.net.liftweb.mapper._
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Create a User class extending the Mapper base class
\end_layout
\begin_layout Plain Layout
// MegaProtoUser, which provides default fields and methods
\end_layout
\begin_layout Plain Layout
// for a site user.
\end_layout
\begin_layout Plain Layout
class User extends MegaProtoUser[User] {
\end_layout
\begin_layout Plain Layout
def getSingleton = User // reference to the companion object below
\end_layout
\begin_layout Plain Layout
def allAccounts : List[Account] =
\end_layout
\begin_layout Plain Layout
Account.findAll(By(Account.owner, this.id))
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Create a "companion object" to the User class (above).
\end_layout
\begin_layout Plain Layout
// The companion object is a "singleton" object that shares the same
\end_layout
\begin_layout Plain Layout
// name as its companion class.
It provides global (i.e.
non-instance)
\end_layout
\begin_layout Plain Layout
// methods and fields, such as find, dbTableName, dbIndexes, etc.
\end_layout
\begin_layout Plain Layout
// For more, see the Scala documentation on singleton objects
\end_layout
\begin_layout Plain Layout
object User extends User with MetaMegaProtoUser[User] {
\end_layout
\begin_layout Plain Layout
override def dbTableName = "users" // define the DB table name
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Provide our own login page template.
\end_layout
\begin_layout Plain Layout
override def loginXhtml =
\end_layout
\begin_layout Plain Layout
<lift:surround with="default" at="content">
\end_layout
\begin_layout Plain Layout
{ super.loginXhtml }
\end_layout
\begin_layout Plain Layout
</lift:surround>
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Provide our own signup page template.
\end_layout
\begin_layout Plain Layout
override def signupXhtml(user: User) =
\end_layout
\begin_layout Plain Layout
<lift:surround with="default" at="content">
\end_layout
\begin_layout Plain Layout
{ super.signupXhtml(user) }
\end_layout
\begin_layout Plain Layout
</lift:surround>
\end_layout
\begin_layout Plain Layout
}
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Note that we've also added a utility method,
\family typewriter
allAccounts
\family default
, to the User class to retrieve all of the accounts for a given user.
We use the MetaMapper.findAll method to do a query by owner ID (Section
\begin_inset CommandInset ref
LatexCommand vref
reference "sub:Querying-for-Entities"
\end_inset
) supplying this user's ID as the owner ID.
\end_layout
\begin_layout Standard
Defining the Account entity is a little more involved, as shown in Listing
\begin_inset CommandInset ref
LatexCommand ref
reference "lst:PocketChange-Account-entity"
\end_inset
.
Here we define a class with a Long primary key and some fields associated
with the accounts.
We also define some helper methods for object relationship joins (Section
\begin_inset CommandInset ref
LatexCommand vref
reference "sub:helper-joins"
\end_inset
).
The Expense and Tag entities (along with some ancillary entities) follow
suit, so we won't cover them here.
\end_layout
\begin_layout Standard
\begin_inset listings
inline false
status open
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
The PocketChange Account Entity
\begin_inset CommandInset label
LatexCommand label
name "lst:PocketChange-Account-entity"
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout Plain Layout
package com.pocketchangeapp.model
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
import _root_.java.math.MathContext
\end_layout
\begin_layout Plain Layout
import _root_.net.liftweb.mapper._
\end_layout
\begin_layout Plain Layout
import _root_.net.liftweb.util.Empty
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Create an Account class extending the LongKeyedMapper superclass
\end_layout
\begin_layout Plain Layout
// (which is a "mapped" (to the database) trait that uses a Long primary
key)
\end_layout
\begin_layout Plain Layout
// and mixes in trait IdPK, which adds a primary key called "id".
\end_layout
\begin_layout Plain Layout
class Account extends LongKeyedMapper[Account] with IdPK {
\end_layout
\begin_layout Plain Layout
// Define the singleton, as in the "User" class
\end_layout
\begin_layout Plain Layout
def getSingleton = Account
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Define a many-to-one (foreign key) relationship to the User class
\end_layout
\begin_layout Plain Layout
object owner extends MappedLongForeignKey(this, User) {
\end_layout
\begin_layout Plain Layout
// Change the default behavior to add a database index
\end_layout
\begin_layout Plain Layout
// for this column.
\end_layout
\begin_layout Plain Layout
override def dbIndexed_? = true
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Define an "access control" field that defaults to false.
We'll
\end_layout
\begin_layout Plain Layout
// use this in the SiteMap chapter to allow the Account owner to
\end_layout
\begin_layout Plain Layout
// share out an account view.
\end_layout
\begin_layout Plain Layout
object is_public extends MappedBoolean(this) {
\end_layout
\begin_layout Plain Layout
override def defaultValue = false
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Define the field to hold the actual account balance with up to 16
\end_layout
\begin_layout Plain Layout
// digits (DECIMAL64) and 2 decimal places
\end_layout
\begin_layout Plain Layout
object balance extends MappedDecimal(this, MathContext.DECIMAL64, 2)
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
object name extends MappedString(this,100)
\end_layout
\begin_layout Plain Layout
object description extends MappedString(this, 300)
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Define utility methods for simplifying access to related classes.
We'll
\end_layout
\begin_layout Plain Layout
// cover how these methods work in the Mapper chapter
\end_layout
\begin_layout Plain Layout
def admins = AccountAdmin.findAll(By(AccountAdmin.account, this.id))
\end_layout
\begin_layout Plain Layout
def addAdmin (user : User) =
\end_layout
\begin_layout Plain Layout
AccountAdmin.create.account(this).administrator(user).save
\end_layout
\begin_layout Plain Layout
def viewers = AccountViewer.findAll(By(AccountViewer.account, this.id))
\end_layout
\begin_layout Plain Layout
def entries = Expense.getByAcct(this, Empty, Empty, Empty)
\end_layout
\begin_layout Plain Layout
def tags = Tag.findAll(By(Tag.account, this.id))
\end_layout
\begin_layout Plain Layout
def notes = AccountNote.findAll(By(AccountNote.account, this.id))
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// The companion object to the above Class
\end_layout
\begin_layout Plain Layout
object Account extends Account with LongKeyedMetaMapper[Account] {
\end_layout
\begin_layout Plain Layout
// Define a utility method for locating an account by owner and name
\end_layout
\begin_layout Plain Layout
def findByName (owner : User, name : String) : List[Account] =
\end_layout
\begin_layout Plain Layout
Account.findAll(By(Account.owner, owner.id.is), By(Account.name, name))
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
...
more utility methods ...
\end_layout
\begin_layout Plain Layout
}
\end_layout
\end_inset
\end_layout
\begin_layout Section
Our First Template
\end_layout
\begin_layout Standard
Our next step is to figure out how we'll present this data to the user.
We'd like to have a home page on the site that shows, depending on whether
the user is logged in, either a welcome message or a summary of account
balances with a place to enter new expenses.
Listing
\begin_inset CommandInset ref
LatexCommand ref
reference "lst:The-welcome-template"
\end_inset
shows a basic template to handle this.
We'll save this as
\family typewriter
index.html
\family default
.
The astute reader will notice that we have a head element but no body.
This is XHTML, so how does that work? This template uses the
\family typewriter
<lift:surround>
\family default
tag (Section
\begin_inset CommandInset ref
LatexCommand vref
reference "sub:surround-tag"
\end_inset
) to embed itself into a master template (
\family typewriter
/templates_hidden/default
\family default
).
Lift actually does what's called a
\begin_inset Quotes eld
\end_inset
head merge
\begin_inset Quotes erd
\end_inset
(Section
\begin_inset CommandInset ref
LatexCommand vref
reference "sec:Head-Merge"
\end_inset
) to merge the contents of the
\family typewriter
head
\family default
tag in our template below with the
\family typewriter
head
\family default
element of the master template.
The
\family typewriter
<lift:HomePage.summary>
\family default
and
\family typewriter
<lift:AddEntry.addentry>
\family default
tags are calls to snippet methods.
Snippets are the backing Scala code that provides the actual page logic.
We'll be covering them in the next section.
\end_layout
\begin_layout Standard
\begin_inset listings
lstparams "language=XML"
inline false
status open
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
The Welcome Template
\begin_inset CommandInset label
LatexCommand label
name "lst:The-welcome-template"
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout Plain Layout
<lift:surround with="default" at="content">
\end_layout
\begin_layout Plain Layout
<head>
\end_layout
\begin_layout Plain Layout
<!-- include the required plugins -->
\end_layout
\begin_layout Plain Layout
<script type="text/javascript" src="/scripts/date.js"></script>
\end_layout
\begin_layout Plain Layout
<!--[if IE]>
\end_layout
\begin_layout Plain Layout
<script type="text/javascript" src="/scripts/jquery.bgiframe.js">
\end_layout
\begin_layout Plain Layout
</script>
\end_layout
\begin_layout Plain Layout
<![endif]-->
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
<!-- include the jQuery DatePicker JavaScript and CSS -->
\end_layout
\begin_layout Plain Layout
<script type="text/javascript" src="/scripts/jquery.datePicker.js">
\end_layout
\begin_layout Plain Layout
</script>
\end_layout
\begin_layout Plain Layout
<link rel="stylesheet" type="text/css" href="/style/datePicker.css" />
\end_layout
\begin_layout Plain Layout
</head>
\end_layout
\begin_layout Plain Layout
<!-- The contents of this element will be passed to the summary method
\end_layout
\begin_layout Plain Layout
in the HomePage snippet.
The call to bind in that method will
\end_layout
\begin_layout Plain Layout
replace the XML tags below (e.g.
account:name) with the account
\end_layout
\begin_layout Plain Layout
data and return a NodeSeq to replace the lift:HomePage.summary
\end_layout
\begin_layout Plain Layout
element.
-->
\end_layout
\begin_layout Plain Layout
<lift:HomePage.summary>
\end_layout
\begin_layout Plain Layout
<div class="column span-24 bordered">
\end_layout
\begin_layout Plain Layout
<h2>Summary of accounts:</h2>
\end_layout
\begin_layout Plain Layout
<account:entry>
\end_layout
\begin_layout Plain Layout
<acct:name /> : <acct:balance /> <br/>
\end_layout
\begin_layout Plain Layout
</account:entry>
\end_layout
\begin_layout Plain Layout
</div>
\end_layout
\begin_layout Plain Layout
<hr />
\end_layout
\begin_layout Plain Layout
</lift:HomePage.summary>
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
<div class="column span-24">
\end_layout
\begin_layout Plain Layout
<!-- The contents of this element will be passed into the add method
\end_layout
\begin_layout Plain Layout
in the AddEntry snippet.
A form element with method "POST" will
\end_layout
\begin_layout Plain Layout
be created and the XML tags (e.g.
e:account) below will be
\end_layout
\begin_layout Plain Layout
replaced with form elements via the call to bind in the add
\end_layout
\begin_layout Plain Layout
method.
This form will replace the lift:AddEntry.addentry element
\end_layout
\begin_layout Plain Layout
below.
-->
\end_layout
\begin_layout Plain Layout
<lift:AddEntry.addentry form="POST">
\end_layout
\begin_layout Plain Layout
<div id="entryform">
\end_layout
\begin_layout Plain Layout
<div class="column span-24"><h3>Entry Form</h3>
\end_layout
\begin_layout Plain Layout
<e:account /> <e:dateOf /> <e:desc /> <e:value />
\end_layout
\begin_layout Plain Layout
<e:tags/><button>Add $</button>
\end_layout
\begin_layout Plain Layout
</div>
\end_layout
\begin_layout Plain Layout
</div>
\end_layout
\begin_layout Plain Layout
</lift:AddEntry.addentry>
\end_layout
\begin_layout Plain Layout
</div>
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
<script type="text/javascript">
\end_layout
\begin_layout Plain Layout
Date.format = 'yyyy/mm/dd';
\end_layout
\begin_layout Plain Layout
jQuery(function () {
\end_layout
\begin_layout Plain Layout
jQuery('#entrydate').datePicker({startDate:'00010101',
\end_layout
\begin_layout Plain Layout
clickInput:true});
\end_layout
\begin_layout Plain Layout
})
\end_layout
\begin_layout Plain Layout
</script>
\end_layout
\begin_layout Plain Layout
</lift:surround>
\end_layout
\end_inset
\end_layout
\begin_layout Standard
As you can see, there's no control logic at all in our template, just well-forme
d XML and some JavaScript to activate the jQuery datePicker functionality.
\end_layout
\begin_layout Section
Writing Snippets
\end_layout
\begin_layout Standard
Now that we have a template, we need to write the HomePage and AddEntry
snippets so that we can actually do something with the site.
First, let's look at the HomePage snippet, shown in Listing
\begin_inset CommandInset ref
LatexCommand ref
reference "lst:Defining-the-summary-snippet"
\end_inset
.
We've skipped the standard Lift imports (Listing
\begin_inset CommandInset ref
LatexCommand ref
reference "lst:Standard-import-statements"
\end_inset
) to save space, but we've specifically imported
\family typewriter
java.util.Date
\family default
and all of our Model classes.
\end_layout
\begin_layout Standard
\begin_inset listings
inline false
status open
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
Defining the Summary Snippet
\begin_inset CommandInset label
LatexCommand label
name "lst:Defining-the-summary-snippet"
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout Plain Layout
package com.pocketchangeapp.snippet
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
import ...
standard imports ...
\end_layout
\begin_layout Plain Layout
import _root_.com.pocketchangeapp.model._
\end_layout
\begin_layout Plain Layout
import _root_.java.util.Date
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
class HomePage {
\end_layout
\begin_layout Plain Layout
// User.currentUser returns a "Box" object, which is either Full
\end_layout
\begin_layout Plain Layout
// (i.e.
contains a User), Failure (contains error data), or Empty.
\end_layout
\begin_layout Plain Layout
// The Scala match method is used to select an action to take based
\end_layout
\begin_layout Plain Layout
// on whether the Box is Full, or not ("case _" catches anything
\end_layout
\begin_layout Plain Layout
// not caught by "case Full(user)".
See Box in the Lift API.
We also
\end_layout
\begin_layout Plain Layout
// briefly discuss Box in Appendix C.
\end_layout
\begin_layout Plain Layout
def summary (xhtml : NodeSeq) : NodeSeq = User.currentUser match {
\end_layout
\begin_layout Plain Layout
case Full(user) => {
\end_layout
\begin_layout Plain Layout
val entries : NodeSeq = user.allAccounts match {
\end_layout
\begin_layout Plain Layout
case Nil => Text("You have no accounts set up")
\end_layout
\begin_layout Plain Layout
case accounts => accounts.flatMap({account =>
\end_layout
\begin_layout Plain Layout
bind("acct", chooseTemplate("account", "entry", xhtml),
\end_layout
\begin_layout Plain Layout
"name" -> <a href={"/account/" + account.name.is}>
\end_layout
\begin_layout Plain Layout
{account.name.is}</a>,
\end_layout
\begin_layout Plain Layout
"balance" -> Text(account.balance.toString))
\end_layout
\begin_layout Plain Layout
})
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
bind("account", xhtml, "entry" -> entries)
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
case _ => <lift:embed what="welcome_msg" />
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
}
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Our first step is to use the
\family typewriter
User.currentUser
\family default
method (this method is provided by the
\family typewriter
MetaMegaProtoUser
\family default
trait) to determine if someone is logged in.
This method returns a
\begin_inset Quotes eld
\end_inset
Box,
\begin_inset Quotes erd
\end_inset
which is either Full (with a User) or Empty.
(A third possibility is a Failure, but we'll ignore that for now.) If it
is full, then a user is logged in and we use the
\family typewriter
User.allAccounts
\family default
method to retrieve a
\family typewriter
List
\family default
of all of the user's accounts.
If the user doesn't have accounts, we return an XML text node saying so
that will be bound where our tag was placed in the template.
If the user does have accounts, then we map the accounts into XHTML using
the bind function.
For each account, we bind the name of the account where we've defined the
\family typewriter
<acct:name>
\family default
tag in the template, and the balance where we defined
\family typewriter
<acct:balance>
\family default
.
The resulting List of XML NodeSeq entities is used to replace the
\family typewriter
<lift:HomePage.summary>
\family default
element in the template.
Finally, we match the case where a user isn't logged in by embedding the
contents of a welcome template (which may be further processed).
Note that we can nest Lift tags in this manner and they will be recursively
parsed.
\end_layout
\begin_layout Standard
Of course, it doesn't do us any good to display account balances if we can't
add expenses, so let's define the
\family typewriter
AddEntry
\family default
snippet.
The code is shown in Listing
\begin_inset CommandInset ref
LatexCommand ref
reference "lst:The-addEntry-snippet"
\end_inset
.
This looks different from the HomePage snippet primarily because we're
using a
\family typewriter
StatefulSnippet
\family default
(Section
\begin_inset CommandInset ref
LatexCommand vref
reference "sub:Stateless-versus-Stateful"
\end_inset
).
The primary difference is that with a
\family typewriter
StatefulSnippet
\family default
the same
\begin_inset Quotes eld
\end_inset
instance
\begin_inset Quotes erd
\end_inset
of the snippet is used for each page request in a given session, so we
can keep the variables around in case we need the user to fix something
in the form.
The basic structure of the snippet is the same as for our summary: we do
some work (we'll cover the
\family typewriter
doTagsAndSubmit
\family default
function in a moment) and then bind values back into the template.
In this snippet, however, we use the
\family typewriter
SHtml.select
\family default
and
\family typewriter
SHtml.text
\family default
methods to generate form fields.
The
\family typewriter
text
\family default
fields simply take an initial value and a function (closure) to process
the value on submission.
The
\family typewriter
select
\family default
field is a little more complex because we give it a list of options, but
otherwise it is the same concept.
\end_layout
\begin_layout Standard
\begin_inset listings
lstparams "tabsize=4"
inline false
status open
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
The AddEntry Snippet
\begin_inset CommandInset label
LatexCommand label
name "lst:The-addEntry-snippet"
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout Plain Layout
package com.pocketchangeapp.snippet
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
import ...
standard imports ...
\end_layout
\begin_layout Plain Layout
import com.pocketchangeapp.model._
\end_layout
\begin_layout Plain Layout
import com.pocketchangeapp.util.Util
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
import java.util.Date
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
/* date | desc | tags | value */
\end_layout
\begin_layout Plain Layout
class AddEntry extends StatefulSnippet {
\end_layout
\begin_layout Plain Layout
// This maps the "addentry" XML element to the "add" method below
\end_layout
\begin_layout Plain Layout
def dispatch = {
\end_layout
\begin_layout Plain Layout
case "addentry" => add _
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
var account : Long = _
\end_layout
\begin_layout Plain Layout
var date = ""
\end_layout
\begin_layout Plain Layout
var desc = ""
\end_layout
\begin_layout Plain Layout
var value = ""
\end_layout
\begin_layout Plain Layout
// S.param("tag") returns a "Box" and the "openOr" method returns
\end_layout
\begin_layout Plain Layout
// either the contents of that box (if it is "Full"), or the empty
\end_layout
\begin_layout Plain Layout
// String passed to it, if the Box is "Empty".
The S.param method
\end_layout
\begin_layout Plain Layout
// returns parameters passed by the browser.
In this instance, the
\end_layout
\begin_layout Plain Layout
// name of the parameter is "tag".
\end_layout
\begin_layout Plain Layout
var tags = S.param("tag") openOr ""
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
def add(in: NodeSeq): NodeSeq = User.currentUser match {
\end_layout
\begin_layout Plain Layout
case Full(user) if user.editable.size > 0 => {
\end_layout
\begin_layout Plain Layout
def doTagsAndSubmit(t: String) {
\end_layout
\begin_layout Plain Layout
tags = t
\end_layout
\begin_layout Plain Layout
if (tags.trim.length == 0)
\end_layout
\begin_layout Plain Layout
S.error("We're going to need at least one tag.")
\end_layout
\begin_layout Plain Layout
else {
\end_layout
\begin_layout Plain Layout
// Get the date correctly, comes in as yyyy/mm/dd
\end_layout
\begin_layout Plain Layout
val entryDate = Util.slashDate.parse(date)
\end_layout
\begin_layout Plain Layout
val amount = BigDecimal(value)
\end_layout
\begin_layout Plain Layout
val currentAccount = Account.find(account).open_!
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// We need to determine the last serial number and balance
\end_layout
\begin_layout Plain Layout
// for the date in question.
This method returns two values
\end_layout
\begin_layout Plain Layout
// which are placed in entrySerial and entryBalance
\end_layout
\begin_layout Plain Layout
// respectively
\end_layout
\begin_layout Plain Layout
val (entrySerial, entryBalance) =
\end_layout
\begin_layout Plain Layout
Expense.getLastExpenseData(currentAccount, entryDate)
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
val e = Expense.create.account(account)
\end_layout
\begin_layout Plain Layout
.dateOf(entryDate)
\end_layout
\begin_layout Plain Layout
.serialNumber(entrySerial + 1)
\end_layout
\begin_layout Plain Layout
.description(desc)
\end_layout
\begin_layout Plain Layout
.amount(BigDecimal(value)).tags(tags)
\end_layout
\begin_layout Plain Layout
.currentBalance(entryBalance + amount)
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// The validate method returns Nil if there are no errors,
\end_layout
\begin_layout Plain Layout
// or an error message if errors are found.
\end_layout
\begin_layout Plain Layout
e.validate match {
\end_layout
\begin_layout Plain Layout
case Nil => {
\end_layout
\begin_layout Plain Layout
Expense.updateEntries(entrySerial + 1, amount)
\end_layout
\begin_layout Plain Layout
e.save
\end_layout
\begin_layout Plain Layout
val acct = Account.find(account).open_!
\end_layout
\begin_layout Plain Layout
val newBalance = acct.balance.is + e.amount.is
\end_layout
\begin_layout Plain Layout
acct.balance(newBalance).save
\end_layout
\begin_layout Plain Layout
S.notice("Entry added!")
\end_layout
\begin_layout Plain Layout
// remove the statefullness of this snippet
\end_layout
\begin_layout Plain Layout
unregisterThisSnippet()
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
case x => error(x)
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
val allAccounts =
\end_layout
\begin_layout Plain Layout
user.allAccounts.map(acct => (acct.id.toString, acct.name))
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Parse through the NodeSeq passed as "in" looking for tags
\end_layout
\begin_layout Plain Layout
// prefixed with "e".
When found, replace the tag with a NodeSeq
\end_layout
\begin_layout Plain Layout
// according to the map below (name -> NodeSeq)
\end_layout
\begin_layout Plain Layout
bind("e", in,
\end_layout
\begin_layout Plain Layout
"account" -> select(allAccounts, Empty,
\end_layout
\begin_layout Plain Layout
id => account = id.toLong),
\end_layout
\begin_layout Plain Layout
"dateOf" -> text(Util.slashDate.format(new Date()).toString,
\end_layout
\begin_layout Plain Layout
date = _,
\end_layout
\begin_layout Plain Layout
"id" -> "entrydate"),
\end_layout
\begin_layout Plain Layout
"desc" -> text("Item Description", desc = _),
\end_layout
\begin_layout Plain Layout
"value" -> text("Value", value = _),
\end_layout
\begin_layout Plain Layout
"tags" -> text(tags, doTagsAndSubmit))
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
// If no user logged in, return a blank Text node
\end_layout
\begin_layout Plain Layout
case _ => Text("")
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
}
\end_layout
\end_inset
\end_layout
\begin_layout Standard
The
\family typewriter
doTagsAndSubmit
\family default
function is a new addition.
Its primary purpose is to process all of the submitted data, create and
validate an
\family typewriter
Expense
\family default
entry, and then return to the user.
This pattern of defining a local function to handle form submission is
quite common as opposed to defining a method on your class.
The main reason is that by defining the function locally, it becomes a
closure on any variables defined in the scope of your snippet function.
\end_layout
\begin_layout Section
A Little AJAX Spice
\end_layout
\begin_layout Standard
So far this is all pretty standard fare, so let's push things a bit and
show you some more advanced functionality.
Listing
\begin_inset CommandInset ref
LatexCommand ref
reference "lst:Displaying-an-Expense-table"
\end_inset
shows a template for displaying a table of Expenses for the user with an
optional start and end date.
The
\family typewriter
Accounts.detail
\family default
snippet will be defined later in this section.
\end_layout
\begin_layout Standard
\begin_inset listings
lstparams "language=XML,tabsize=4"
inline false
status open
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
Displaying an Expense Table
\begin_inset CommandInset label
LatexCommand label
name "lst:Displaying-an-Expense-table"
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout Plain Layout
<lift:surround with="default" at="content">
\end_layout
\begin_layout Plain Layout
<lift:Accounts.detail eager_eval="true">
\end_layout
\begin_layout Plain Layout
<div class="column span-24">
\end_layout
\begin_layout Plain Layout
<h2>Summary</h2>
\end_layout
\begin_layout Plain Layout
<table><tr><th>Name</th><th>Balance</th></tr>
\end_layout
\begin_layout Plain Layout
<tr><td><acct:name /></td><td><acct:balance /></td></tr>
\end_layout
\begin_layout Plain Layout
</table>
\end_layout
\begin_layout Plain Layout
<div>
\end_layout
\begin_layout Plain Layout
<h3>Filters:</h3>
\end_layout
\begin_layout Plain Layout
<table><tr><th>Start Date</th><td><acct:startDate /></td>
\end_layout
\begin_layout Plain Layout
<th>End Date</th><td><acct:endDate /></td></tr>
\end_layout
\begin_layout Plain Layout
</table>
\end_layout
\begin_layout Plain Layout
</div>
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
<div class="column span-24" >
\end_layout
\begin_layout Plain Layout
<h2>Transactions</h2>
\end_layout
\begin_layout Plain Layout
<lift:embed what="entry_table" />
\end_layout
\begin_layout Plain Layout
</div>
\end_layout
\begin_layout Plain Layout
</lift:Accounts.detail>
\end_layout
\begin_layout Plain Layout
</lift:surround>
\end_layout
\end_inset
\end_layout
\begin_layout Standard
The <lift:embed> tag (Section
\begin_inset CommandInset ref
LatexCommand vref
reference "sub:embed-tag"
\end_inset
) allows you to include another template at that point.
In our case, the entry_table template is shown in Listing
\begin_inset CommandInset ref
LatexCommand ref
reference "lst:Embedded-Expense-table"
\end_inset
.
This is really just a fragment and is not intended to be used alone, since
it's not a full XHTML document and it doesn't surround itself with a master
template.
It does, however, provide binding sites that we can fill in.
\end_layout
\begin_layout Standard
\begin_inset listings
lstparams "language=XML,tabsize=4"
inline false
status open
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
The Embedded Expense Table
\begin_inset CommandInset label
LatexCommand label
name "lst:Embedded-Expense-table"
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout Plain Layout
<table class="" border="0" cellpadding="0" cellspacing="1"
\end_layout
\begin_layout Plain Layout
width="100%">
\end_layout
\begin_layout Plain Layout
<thead>
\end_layout
\begin_layout Plain Layout
<tr>
\end_layout
\begin_layout Plain Layout
<th>Date</th><th>Description</th><th>Tags</th><th>Value</th>
\end_layout
\begin_layout Plain Layout
<th>Balance</th>
\end_layout
\begin_layout Plain Layout
</tr>
\end_layout
\begin_layout Plain Layout
</thead>
\end_layout
\begin_layout Plain Layout
<tbody id="entry_table">
\end_layout
\begin_layout Plain Layout
<acct:table>
\end_layout
\begin_layout Plain Layout
<acct:tableEntry>
\end_layout
\begin_layout Plain Layout
<tr><td><entry:date /></td><td><entry:desc /></td>
\end_layout
\begin_layout Plain Layout
<td><entry:tags /></td><td><entry:amt /></td>
\end_layout
\begin_layout Plain Layout
<td><entry:balance /></td>
\end_layout
\begin_layout Plain Layout
</tr>
\end_layout
\begin_layout Plain Layout
</acct:tableEntry>
\end_layout
\begin_layout Plain Layout
</acct:table>
\end_layout
\begin_layout Plain Layout
</tbody>
\end_layout
\begin_layout Plain Layout
</table>
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Before we get into the AJAX portion of the code, let's define a helper method
in our Accounts snippet class, shown in Listing
\begin_inset CommandInset ref
LatexCommand ref
reference "lst:The-table-helper-function"
\end_inset
, to generate the XHTML table entries that we'll be displaying (assuming
normal imports).
Essentially, this function pulls the contents of the
\family typewriter
<acct:tableEntry>
\family default
tag (via the
\family typewriter
Helpers.chooseTemplate
\family default
method, Section
\begin_inset CommandInset ref
LatexCommand vref
reference "sec:BindHelpers"
\end_inset
) and binds each
\family typewriter
Expense
\family default
from the provided list into it.
As you can see in the
\family typewriter
entry_table
\family default
template, that corresponds to one table row for each entry.
\end_layout
\begin_layout Standard
\begin_inset listings
inline false
status open
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
The Table Helper Function
\begin_inset CommandInset label
LatexCommand label
name "lst:The-table-helper-function"
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout Plain Layout
package com.pocketchangeapp.snippet
\end_layout
\begin_layout Plain Layout
...
imports ...
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
class Accounts {
\end_layout
\begin_layout Plain Layout
...
\end_layout
\begin_layout Plain Layout
def buildExpenseTable(entries : List[Expense], template : NodeSeq) = {
\end_layout
\begin_layout Plain Layout
// Calls bind repeatedly, once for each Entry in entries
\end_layout
\begin_layout Plain Layout
entries.flatMap({ entry =>
\end_layout
\begin_layout Plain Layout
bind("entry", chooseTemplate("acct", "tableEntry", template),
\end_layout
\begin_layout Plain Layout
"date" -> Text(Util.slashDate.format(entry.dateOf.is)),
\end_layout
\begin_layout Plain Layout
"desc" -> Text(entry.description.is),
\end_layout
\begin_layout Plain Layout
"tags" -> Text(entry.tags.map(_.tag.is).mkString(", ")),
\end_layout
\begin_layout Plain Layout
"amt" -> Text(entry.amount.toString),
\end_layout
\begin_layout Plain Layout
"balance" -> Text(entry.currentBalance.toString))
\end_layout
\begin_layout Plain Layout
})
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
...
\end_layout
\begin_layout Plain Layout
}
\end_layout
\end_inset
\end_layout
\begin_layout Standard
The final piece is our
\family typewriter
Accounts.detail
\family default
snippet, shown in Listing
\begin_inset CommandInset ref
LatexCommand ref
reference "lst:Our-Ajax-snippet"
\end_inset
.
We start off with some boilerplate calls to match to locate the
\family typewriter
Account
\family default
to be viewed, then we define some vars to hold state.
It's important that they're vars so that they can be captured by the
\family typewriter
entryTable
\family default
,
\family typewriter
updateStartDate
\family default
, and
\family typewriter
updateEndDate
\family default
closures, as well as the AJAX form fields that we define.
The only magic we have to use is the
\family typewriter
SHtml.ajaxText
\family default
form field generator (Chapter
\begin_inset CommandInset ref
LatexCommand vref
reference "cha:AJAX-and-COMET"
\end_inset
), which will turn our update closures into AJAX callbacks.
The values returned from these callbacks are JavaScript code that will
be run on the client side.
You can see that in a few lines of code we now have a page that will automatica
lly update our Expense table when you set the start or end dates!
\end_layout
\begin_layout Standard
\begin_inset listings
lstparams "tabsize=4"
inline false
status open
\begin_layout Plain Layout
\begin_inset Caption
\begin_layout Plain Layout
Our AJAX Snippet
\begin_inset CommandInset label
LatexCommand label
name "lst:Our-Ajax-snippet"
\end_inset
\end_layout
\end_inset
\end_layout
\begin_layout Plain Layout
package com.pocketchangeapp.snippet
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
import ...
standard imports ...
\end_layout
\begin_layout Plain Layout
import com.pocketchangeapp.model._
\end_layout
\begin_layout Plain Layout
import com.pocketchangeapp.util.Util
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
class Accounts {
\end_layout
\begin_layout Plain Layout
def detail (xhtml: NodeSeq) : NodeSeq = S.param("name") match {
\end_layout
\begin_layout Plain Layout
// If the "name" param was passed by the browser...
\end_layout
\begin_layout Plain Layout
case Full(acctName) => {
\end_layout
\begin_layout Plain Layout
// Look for an account by that name for the logged in user
\end_layout
\begin_layout Plain Layout
Account.findByName(User.currentUser.open_!, acctName) match {
\end_layout
\begin_layout Plain Layout
// If an account is returned (as a List)
\end_layout
\begin_layout Plain Layout
case acct :: Nil => {
\end_layout
\begin_layout Plain Layout
// Some closure state for the AJAX calls
\end_layout
\begin_layout Plain Layout
// Here is Lift's "Box" in action: we are creating
\end_layout
\begin_layout Plain Layout
// variables to hold Date Boxes and initializing them
\end_layout
\begin_layout Plain Layout
// to "Empty" (Empty is a subclass of Box)
\end_layout
\begin_layout Plain Layout
var startDate : Box[Date] = Empty
\end_layout
\begin_layout Plain Layout
var endDate : Box[Date] = Empty
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// AJAX utility methods.
Defined here to capture the closure
\end_layout
\begin_layout Plain Layout
// vars defined above
\end_layout
\begin_layout Plain Layout
def entryTable = buildExpenseTable(
\end_layout
\begin_layout Plain Layout
Expense.getByAcct(acct, startDate, endDate, Empty),
\end_layout
\begin_layout Plain Layout
xhtml)
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
def updateStartDate (date : String) = {
\end_layout
\begin_layout Plain Layout
startDate = Util.parseDate(date, Util.slashDate.parse)
\end_layout
\begin_layout Plain Layout
JsCmds.SetHtml("entry_table", entryTable)
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
def updateEndDate (date : String) = {
\end_layout
\begin_layout Plain Layout
endDate = Util.parseDate(date, Util.slashDate.parse)
\end_layout
\begin_layout Plain Layout
JsCmds.SetHtml("entry_table", entryTable)
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
\end_layout
\begin_layout Plain Layout
// Bind the data to the passed in XML elements with
\end_layout
\begin_layout Plain Layout
// prefix "acct" according to the map below.
\end_layout
\begin_layout Plain Layout
bind("acct", xhtml,
\end_layout
\begin_layout Plain Layout
"name" -> acct.name.asHtml,
\end_layout
\begin_layout Plain Layout
"balance" -> acct.balance.asHtml,
\end_layout
\begin_layout Plain Layout
"startDate" -> SHtml.ajaxText("", updateStartDate),
\end_layout
\begin_layout Plain Layout
"endDate" -> SHtml.ajaxText("", updateEndDate),
\end_layout
\begin_layout Plain Layout
"table" -> entryTable)
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
// An account name was provided but did not match any of
\end_layout
\begin_layout Plain Layout
// the logged in user's accounts
\end_layout
\begin_layout Plain Layout
case _ => Text("Could not locate account " + acctName)
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
// The S.param "name" was empty
\end_layout
\begin_layout Plain Layout
case _ => Text("No account name provided")
\end_layout
\begin_layout Plain Layout
}
\end_layout
\begin_layout Plain Layout
}
\end_layout
\end_inset
\end_layout
\begin_layout Section
Conclusion
\end_layout
\begin_layout Standard
We hope that this chapter has demonstrated how powerful Lift can be while
remaining concise and easy to use.
Don't worry if there's something you didn't understand, we'll be explaining
in more detail as we go along.
We'll continue to expand on this example app throughout the book, so feel
free to make this chapter a base reference, or pull your own version of
PocketChange from the git repository with the following command (assuming
you have git installed):
\end_layout
\begin_layout LyX-Code
git clone
\begin_inset Flex URL
status open
\begin_layout Plain Layout
git://github.com/tjweir/pocketchangeapp.git
\end_layout
\end_inset
\end_layout
\begin_layout Standard
Now let's dive in!
\end_layout
\end_body
\end_document
Jump to Line
Something went wrong with that request. Please try again.