本章将介绍如何使用Lift:提交表单处理表单数据,并使用表单元素。表单提交的最终结果可以是在数据库中更新的记录,所以您可能还会发现第7 章或第8章分别讨论关系数据库和MongoDB时有用。
在表单处理将数据传递到服务器的情况下,第5章还有与表单处理相关的配方。
您将在本章中找到许多示例,作为https://github.com/LiftCookbook/cookbook_forms的源代码。
普通旧表格处理
解
例如,我们可以显示表单,处理输入值,并将消息作为通知发回。该模板是一个常规的HTML表单,添加了一个代码段:
<form
data-lift=
"Plain"
action=
"/plain"
method=
"post"
>
<input
type=
"text"
name=
"name"
placeholder=
"What's your name?"
>
<input
type=
"submit"
value=
"Go"
>
</form>
在片段中,我们可以挑选出的字段的值name
有S.param("name")
:
package
code.snippet
import
net.liftweb.common.Full
import
net.liftweb.http.S
import
net.liftweb.util.PassThru
object
Plain
{
def
render
=
S
.
param
(
"name"
)
match
{
case
Full
(
name
)
=>
S
.
notice
(
"Hello "
+
name
)
S
.
redirectTo
(
"/plain"
)
case
_
=>
PassThru
}
}
第一次通过这个代码片段,没有参数,所以我们只是把表单传回给浏览器,这PassThru
是做什么的。然后,您可以在该name
字段中输入一个值并提交表单。这将导致提升处理模板再次,但这次,name
输入的值。结果将是您的浏览器重定向到一个页面,并显示一条消息。
讨论
手动从请求中提取参数并没有充分利用Lift,但有时您需要执行此操作,并且S.param
可以处理请求参数。
结果S.param
是a Box[String]
,在前面的例子中,我们对这个值进行模式匹配。有多个参数,您可能已经看到S.param
以这种方式使用:
def
render
=
{
for
{
name
<-
S
.
param
(
"name"
)
pet
<-
S
.
param
(
"petName"
)
}
{
S
.
notice
(
"Hello %s and %s"
.
format
(
name
,
pet
))
S
.
redirectTo
(
"/plain"
)
}
PassThru
}
如果两个name
和petName
提供,对身体for
进行评估。
-
List[String]
使用给定的名称为所有请求参数 生成一个 - 告诉你请求是GET还是POST
-
给出了
Box[String]
用于在具有给定名称的请求的标头 -
访问
Box[Req]
,它可以访问有关请求的进一步HTTP特定信息
S.params(name)
S.post_?
和 S.get_?
S.getRequestHeader(name)
S.request
作为使用的一个例子S.request
,我们可以List[String]
为参数名称中所有请求参数的值产生一个值name
:
val
names
=
for
{
req
<-
S
.
request
.
toList
paramName
<-
req
.
paramNames
if
paramName
.
toLowerCase
contains
"name"
value
<-
S
.
param
(
paramName
)
}
yield
value
请注意,通过打开,S.request
我们可以通过paramNames
函数访问所有参数名称Req
。
屏幕或向导为表单处理提供了替代方案,但有时您只想从请求中提取值,如本配方所示。
也可以看看
简单提升涵盖了各种处理方式。
Ajax表单处理
解
将表单标记为Ajax表单,data-lift="form.ajax"
并在提交表单时提供在服务器上运行的功能。
这是一个表单的示例,将收集我们的名称并通过Ajax将其提交到服务器:
<form
data-lift=
"form.ajax"
>
<div
data-lift=
"EchoForm"
>
<input
type=
"text"
name=
"name"
placeholder=
"What's your name?"
>
<input
type=
"submit"
>
</div>
</form>
<div
id=
"result"
>
你的名字将在这里回应</div>
以下片段将通过Ajax回显名称:
package
code.snippet
import
net.liftweb.util.Helpers._
import
net.liftweb.http.SHtml.
{
text
,
ajaxSubmit
}
import
net.liftweb.http.js.JsCmd
import
net.liftweb.http.js.JsCmds.SetHtml
import
xml.Text
object
EchoForm
extends
{
def
render
=
{
var
name
=
""
def
process
()
:
JsCmd
=
SetHtml
(
"result"
,
Text
(
name
))
"@name"
#>
text
(
name
,
s
=>
name
=
s
)
&
"type=submit"
#>
ajaxSubmit
(
"Click Me"
,
process
)
}
}
该render
方法将name
输入字段绑定到将分配用户输入到变量的任何函数name
。请注意,您通常会看到s => name = s
以较短的形式写入name = _
。
讨论
data-lift="form.ajax"
该配方的一部分确保Lift将Ajax处理机制添加到表单中。这意味着<form>
输出中的元素将结束如下:
<form
id=
"F2203365740CJME2G"
action=
"javascript://"
onsubmit=
"liftAjax.lift_ajaxHandler(
jQuery('#'+"F2203365740CJME2G").serialize(),
null, null, "javascript");return false;"
>
...</form>
换句话说,当表单被要求提交时,Lift将通过Ajax序列化表单。这意味着你根本不一定需要提交按钮。在这个示例中,单个文本字段,如果省略提交按钮,可以按Return键触发序列化。这将触发s => name = s
在我们的常规data-lift="EchoForm"
代码段中绑定的函数。换句话说,name
即使没有提交按钮,也会分配该值。
添加一个提交按钮可以让我们在所有字段的所有功能执行完成后执行操作。
请注意,Lift的方法是将表单序列化到服务器,执行与字段关联的功能,执行submit函数(如果有的话),然后将JavaScript结果返回给客户端。默认的序列化过程是serialization
在表单上使用jQuery的方法。这个序列化字段除了提交按钮和文件上传。
提交造型
该SHtml.ajaxSubmit
函数生成<input type="submit">
页面的元素。您可能更喜欢使用风格的按钮进行提交。例如,使用Twitter Bootstrap,带有图标的按钮将需要以下标记:
<button
id=
"submit"
class=
"btn btn-primary btn-large"
>
<i
class=
"icon-white icon-ok"
></i>
提交</button>
在窗<button>
体内按一下即可触发提交。但是,如果您绑定了该按钮SHtml.ajaxSubmit
,则内容,因此样式将丢失。
要解决这个问题,您可以为一个隐藏的字段分配一个函数。当与其他任何字段一样提交表单时,将调用此函数。我们的片段中唯一改变的部分是CSS选择器绑定:
import
net.liftweb.http.SHtml.hidden
"@name"
#>
text
(
name
,
s
=>
name
=
s
)
&
"button *+"
#>
hidden
(
process
)
该*+
替换规则是指一个值追加到按钮的子节点。这将包括一个这样的形式的隐藏字段:
<
input
type
=
"hidden"
name
=
"F11202029628285OIEC2"
value
=
"true"
>
提交表单时,隐藏字段被提交,并且像任何字段一样,Lift将调用与其相关的功能:process
在这种情况下。
效果是类似的ajaxSubmit
,但不完全相同。在这种情况下,我们会在后面添加一个隐藏字段<button>
,但您可以将其放置在您找到方便的表单上。然而,有一个并发症:什么时候process
被叫?是否name
已经被分配或之后?这取决于字段的呈现顺序。也就是说,在HTML模板中,将按钮放在文本字段之前(因此在本示例中移动隐藏字段的位置),在process
设置名称之前调用该函数。
有几种方法。或者,确保您以这种方式使用的隐藏字段在您的表单中显示较晚,或者确保该函数被称为迟到与a formGroup
:
import
net.liftweb.http.SHtml.hidden
import
net.liftweb.http.S
"@name"
#>
text
(
name
,
s
=>
name
=
s
)
&
"button *+"
#>
S
.
formGroup
(
1000
)
{
hidden
(
process
)
}
该formGroup
加法操作函数标识符以确保稍后进行排序,导致该函数process
的调用晚于默认组(0)中的字段。
Lift2.6和3.0可能包含ajaxOnSubmit
,这将提供ajaxSubmit
隐藏方法的可靠性和灵活性。如果要在Lift 2.5中尝试,Antonio Salazar Cardozo创建了一个可以包含在您的项目中的帮手。
Ajax JSON表单处理
解
使用Lift的jlift.js JavaScript库和JsonHandler
类。
例如,我们可以创建一个“座右铭服务器”,接受机构名称和机构的座右铭,并对这些价值观进行一些行动。我们只是回到客户的名字和座右铭。
考虑这个HTML,它不是一种形式,而是包含jlift.js:
<html>
<head>
<title>
JSON表单</title>
</head>
<body
data-lift-content-id=
"main"
>
<div
id=
"main"
data-lift=
"surround?with=default;at=content"
>
<h1>
Json表单示例</h1>
<!-- Required for JSON forms processing -->
<script
src=
"/classpath/jlift.js"
data-lift=
"tail"
></script>
<div
data-lift=
"JsonForm"
>
<script
id=
"jsonScript"
data-lift=
"tail"
></script>
<div
id=
"jsonForm"
>
<label
for=
"name"
>
机构
<input
id=
"name"
type=
"text"
name=
"name"
value=
"Royal Society"
/>
</label>
<label
for=
"motto"
>
座右铭
<input
id=
"motto"
type=
"text"
name=
"motto"
value=
"Nullius in verba"
/>
</label>
<input
type=
"submit"
value=
"Send"
/>
</div>
<div
id=
"result"
>
结果将显示在此处。
</div>
</div>
</div>
</body>
</html>
这个HTML向用户呈现包含在一个<div>
被叫中的两个字段,一个名字和一个座右铭jsonForm
。还有一些结果的占位符,您会注意到jsonScript
一些JavaScript代码的占位符。该jsonForm
会被操纵,以确保它通过Ajax发送,jsonScript
将与Lift的代码来执行序列所取代。这在代码段代码中发生:
package
code.snippet
import
scala.xml.
{
Text
,
NodeSeq
}
import
net.liftweb.util.Helpers._
import
net.liftweb.util.JsonCmd
import
net.liftweb.http.SHtml.jsonForm
import
net.liftweb.http.JsonHandler
import
net.liftweb.http.js.JsCmd
import
net.liftweb.http.js.JsCmds.
{
SetHtml
,
Script
}
object
JsonForm
{
def
render
=
"#jsonForm"
#>
((
ns
:
NodeSeq
)
=>
jsonForm
(
MottoServer
,
ns
))
&
"#jsonScript"
#>
Script
(
MottoServer
.
jsCmd
)
object
MottoServer
extends
JsonHandler
{
def
apply
(
in
:
Any
)
:
JsCmd
=
in
match
{
case
JsonCmd
(
"processForm"
,
target
,
params
:
Map
[
String
,String
],
all
)
=>
val
name
=
params
.
getOrElse
(
"name"
,
"No Name"
)
val
motto
=
params
.
getOrElse
(
"motto"
,
"No Motto"
)
SetHtml
(
"result"
,
Text
(
"The motto of %s is %s"
.
format
(
name
,
motto
))
)
}
}
}
像许多片段一样,此Scala代码包含render
绑定到页面上的元素的方法。具体来说,jsonForm
正在被替换SHtml.jsonForm
,这将采取NodeSeq
(这是输入字段),并将其转换为将JSON值提交的形式。提交的内容将是我们的MottoServer
代码。
该jsonScript
元素绑定到JavaScript,将执行值到服务器的传输和编码。
如果您点击“发送”按钮并观察网络流量,您将看到以下内容发送到服务器:
{
"command"
:
"processForm"
,
"params"
:
{
"name"
:
"Royal Society"
,
"motto"
:
"Nullius in verba"
}
}
这是在与之匹配的模式中的all
参数的值。Lift已经照顾了管道来实现这一点。JsonCmd
MottoServer.apply
示例中的模式匹配的结果是选出两个字段值,并发回JavaScript以更新results
<div>
:“皇家学会的座右铭是Nullius in verba”。
讨论
的JsonHandler
类和SHtml.jsonForm
方法一起进行了大量的工作对我们来说。该jsonForm
方法是安排表单字段编码为JSON,并通过Ajax发送到我们MottoServer
的JsonCmd
。其实这是JsonCmd
一个默认的命令名"processForm"
。
我们的MottoServer
课程正在寻找(匹配)JsonCmd
,提取表单域的值,并将它们作为在页面上JsCmd
更新的客户端回显<div>
。
该MottoServer.jsCmd
部分正在生成将表单字段传递到服务器所需的JavaScript。正如我们将在后面看到的,这提供了一个通用功能,我们可以使用它来向服务器发送不同的JSON值和命令。
还要注意,从网络流量来看,发送的表单字段将以页面上给出的名称进行序列化。没有发送该映射的“F ...”值到服务器上的函数调用。这样做的结果是,动态添加到页面的任何字段也将被序列化到服务器,在那里它们可以被拾取MottoServer
。
脚本jlift.js正在提供管道来做这个事情。
在进行之前,请说服自己,在服务器端(MottoServer.jsCmd
)上生成JavaScript,当客户端提交表单时执行该脚本,以将结果传递到服务器。
附加命令
在前面的例子中,我们JsonCmd
使用命令名匹配"processForm"
。您可能想知道可以提供其他命令,还是该target
值的含义。
为了演示如何实现其他命令,我们可以添加两个附加按钮。这些按钮只会将座右铭转换为大写或小写。服务器端render
方法更改如下:
def
render
=
"#jsonForm"
#>
((
ns
:
NodeSeq
)
=>
jsonForm
(
MottoServer
,
ns
))
&
"#jsonScript"
#>
Script
(
MottoServer
.
jsCmd
&
Function
(
"changeCase"
,
List
(
"direction"
),
MottoServer
.
call
(
"processCase"
,
JsVar
(
"direction"
),
JsRaw
(
"$('#motto').val()"
))
)
)
将JsonForm
保持不变。我们仍然包括MottoServer.jsCmd
,我们仍然想包装字段,并像以前一样提交。我们添加的是一个额外的JavaScript函数changeCase
,它调用一个参数,direction
并且身体调用MottoServer
各种参数。当它在页面上呈现时,它将显示为这样:
function
changeCase
(
direction
)
{
F299202CYGIL
({
'command'
:
"processCase"
,
'target'
:
direction
,
'params'
:
$
(
'#motto'
).
val
()
});
}
的F299202CYGIL
功能(或类似名称)由Lift产生作为其一部分MottoServer.jsCmd
,并且它是负责将数据传送到服务器。在这种情况下,它提供的数据是一个由不同的命令(processCase
)组成的JSON结构,它是JavaScript 值direction
评估的目标,也是#motto
表单字段值的jQuery表达式的结果的参数。
什么时候changeCase
调用函数?这取决于我们,调用该函数的一个非常简单的方法是通过添加到HTML中:
<button
onclick=
"javascript:changeCase('upper')"
>
大写字母小提琴</button>
<button
onclick=
"javascript:changeCase('lower')"
>
小写字母</button>
当按下这些按钮之一时,结果将是以指令发送到服务器的JSON值,processCase
并相应地direction
进行params
设置。剩下的就是修改我们在服务器上MottoServer
拿起这个JsonCmd
:
object
MottoServer
extends
JsonHandler
{
def
apply
(
in
:
Any
)
:
JsCmd
=
in
match
{
case
JsonCmd
(
"processForm"
,
target
,
params
:
Map
[
String
,String
],
all
)
=>
val
name
=
params
.
getOrElse
(
"name"
,
"No Name"
)
val
motto
=
params
.
getOrElse
(
"motto"
,
"No Motto"
)
SetHtml
(
"result"
,
Text
(
"The motto of %s is %s"
.
format
(
name
,
motto
))
)
case
JsonCmd
(
"processCase"
,
direction
,
motto
:
String
,
all
)
=>
val
update
=
if
(
direction
==
"upper"
)
motto
.
toUpperCase
else
motto
.
toLowerCase
SetValById
(
"motto"
,
update
)
}
}
使用日期选择器进行输入
解
使用标准提升SHtml.text
输入字段并附加JavaScript日期选择器。在这个例子中,我们将使用jQuery UI日期选择器。
我们的表单将包括一个被称为birthday
日期选择器的输入字段,以及jQuery UI JavaScript和CSS:
<!DOCTYPE html>
<head>
<meta
content=
"text/html; charset=UTF-8"
http-equiv=
"content-type"
/>
<title>
jQuery日期选择器</title>
</head>
<body
data-lift-content-id=
"main"
>
<div
id=
"main"
data-lift=
"surround?with=default;at=content"
>
<h3>
你的生日是什么时候?</h3>
<link
data-lift=
"head"
type=
"text/css"
rel=
"stylesheet"
href=
"//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/css/smoothness
/jquery-ui-1.10.2.custom.min.css"
>
</link>
<script
data-lift=
"tail"
src=
"//cdnjs.cloudflare.com/ajax/libs/jqueryui/1.10.2/jquery-ui.min.js"
>
</script>
<div
data-lift=
"JqDatePicker?form=post"
>
<input
type=
"text"
id=
"birthday"
>
<input
type=
"submit"
value=
"Submit"
>
</div>
</div>
</body>
</html>
这通常会产生一个常规的文本输入字段,但是我们可以通过添加JavaScript来将日期选择器附加到输入字段来进行更改。您可以在模板中执行此操作,但在此示例中,我们将增强文本字段作为代码段代码的一部分:
package
code.snippet
import
java.util.Date
import
java.text.SimpleDateFormat
import
net.liftweb.util.Helpers._
import
net.liftweb.http.
{
S
,
SHtml
}
import
net.liftweb.http.js.JsCmds.Run
import
net.liftweb.common.Loggable
class
JqDatePicker
extends
Loggable
{
val
dateFormat
=
new
SimpleDateFormat
(
"yyyy-MM-dd"
)
val
default
=
(
dateFormat
format
now
)
def
logDate
(
s
:
String
)
:
Unit
=
{
val
date
:
Date
=
tryo
(
dateFormat
parse
s
)
getOrElse
now
logger
.
info
(
"Birthday: "
+
date
)
}
def
render
=
{
S
.
appendJs
(
enhance
)
"#birthday"
#>
SHtml
.
text
(
default
,
logDate
)
}
val
enhance
=
Run
(
"$('#birthday').datepicker({dateFormat: 'yy-mm-dd'});"
)
}
请注意,render
我们将常规SHtml.text
字段绑定到具有ID的元素birthday
,但也将JavaScript附加到页面。JavaScript选择birthday
输入字段并将配置的日期选择器附加到它。
当提交字段时,将logDate
使用文本字段的值调用该方法。我们将文本解析成一个java.util.Date
对象。该tryo
Lift助手将捕获任何ParseException
和返回Box[Date]
如果劣枣供应,这是我们打开,或默认为当前日期。
运行此代码并提交表单将生成一条日志消息,如:
INFO code.snippet.DatePicker - 生日:Sun Apr 21 00:00:00 BST 2013
讨论
该配方中概述的方法可以与其他日期选择器库一起使用。关键是要配置日期选择器,以提交给服务器的值的形式提交日期。这是日期的“有线格式”,并且不一定与用户在浏览器中看到的格式相同,具体取决于所使用的浏览器或JavaScript库。
HTML5日期选择器
HTML5规范包括用于各种日期输入类型的支持:datetime
,datetime-local
,date
,month
,time
,和week
。例如:
<input
type=
"date"
name=
"birthday"
value=
"2013-04-21"
>
这种类型的输入字段将以yyyy-mm-dd格式提交日期,我们的代码段将能够处理。
随着更多的浏览器实现这些类型,它将成为可能依赖于它们。但是,您可以默认使用HTML5浏览器 - 本地日期选择器,并根据需要返回到JavaScript库。差异如图3-1所示。

要检测浏览器是否支持type="date"
输入,我们可以使用Modernizr库。这是我们的模板中的一个附加脚本:
<script
data-lift=
"tail"
src=
"//cdnjs.cloudflare.com/ajax/libs/modernizr/2.6.2/modernizr.min.js"
>
</script>
我们将在我们的代码片段中使用它。实际上,我们需要对代码片段进行两个更改:
- 将
type="date"
属性添加到输入字段。 - 修改JavaScript只将jQuery UI日期选择器附加到不支持
type="date"
输入的浏览器中。
在代码中,
def
render
=
{
S
.
appendJs
(
enhance
)
"#birthday"
#>
SHtml
.
text
(
default
,
logDate
,
(
"type"
->
"date"
))
}
val
enhance
=
Run
(
"""
|if (!Modernizr.inputtypes.date) {
| $('input[type=date]').datepicker({dateFormat: 'yy-mm-dd'});
|}
"""
.
stripMargin
)
该"type" -> "date"
参数上SHtml.text
是设置属性type
的值date
上得到的<input>
场。
当此代码段运行并且页面被渲染时,jQuery UI日期选择器将被附加到type="date"
只有浏览器不支持该类型的输入字段。
自动完成建议
解
使用JavaScript自动完成小部件,例如,通过AutoComplete
Lift窗口小部件模块中的类来进行jQuery UI自动完成。
首先将Lift窗口小部件模块添加到build.sbt中:
libraryDependencies
+=
"net.liftmodules"
%%
"widgets_2.5"
%
"1.3"
要启用自动完成小部件,请在Boot.scala中进行初始化:
import
net.liftmodules.widgets.autocomplete.AutoComplete
AutoComplete
.
init
()
我们可以创建一个常规的表单代码段:
<form
data-lift=
"ProgrammingLanguages?form=post"
>
<input
id=
"autocomplete"
>
<input
type=
"submit"
>
</form>
的连接AutoComplete
类与的ID的元素autocomplete
:
package
code.snippet
import
net.liftweb.util.Helpers._
import
net.liftweb.common.Loggable
import
net.liftmodules.widgets.autocomplete.AutoComplete
class
ProgrammingLanguages
extends
Loggable
{
val
languages
=
List
(
"C"
,
"C++"
,
"Clojure"
,
"CoffeeScript"
,
"Java"
,
"JavaScript"
,
"POP-11"
,
"Prolog"
,
"Python"
,
"Processing"
,
"Scala"
,
"Scheme"
,
"Smalltalk"
,
"SuperCollider"
)
def
render
=
{
val
default
=
""
def
suggest
(
value
:
String
,
limit
:
Int
)
=
languages
.
filter
(
_
.
toLowerCase
.
startsWith
(
value
))
def
submit
(
value
:
String
)
:
Unit
=
logger
.
info
(
"Value submitted: "
+
value
)
"#autocomplete"
#>
AutoComplete
(
default
,
suggest
,
submit
)
}
}
这个代码段的最后一行显示了AutoComplete
该类的绑定,它需要:
- 要显示的默认值
- 一个功能将从输入的文本值产生建议 - 结果是一个
Seq[String]
建议 - 提交表单时调用的函数
运行此代码呈现如图3-2所示。

提交表单时,该submit
函数将被传递给选定的值。该submit
函数只是记录此值:
INFO code.snippet.ProgrammingLanguages - 提交的值:Scala
讨论
自动完成小部件使用jQuery自动完成。NodeSeq
通过检查AutoComplete.apply
方法可以看出这一点:
<span>
<head>
<link
type=
"text/css"
rel=
"stylesheet"
href=
"/classpath/autocomplete/jquery.autocomplete.css"
>
</link>
<script
type=
"text/javascript"
src=
"/classpath/autocomplete/jquery.autocomplete.js"
>
</script>
<script
type=
"text/javascript"
>
// <![CDATA[jQuery
(
document
).
ready
(
function
(){
var
data
=
"/ajax_request?F846528841915S2RBI0=foo"
;
jQuery
(
"#F846528841916S3QZ0V"
).
autocomplete
(
data
,
{
minChars
:
0
,
matchContains
:
true
}).
result
(
function
(
event
,
dt
,
formatted
)
{
jQuery
(
"#F846528841917CF4ZGL"
).
val
(
formatted
);
}
);
});;
// ]]>
</script></head>
<input
type=
"text"
value=
""
id=
"F846528841916S3QZ0V"
></input>
<input
name=
"F846528841917CF4ZGL"
type=
"hidden"
value=
""
id=
"F846528841917CF4ZGL"
></input>
</span>
这个标记块是从AutoComplete(default, suggest, submit)
呼叫生成的。这里发生的情况是,与“Lift”窗口小部件模块捆绑在一起的jQuery UI自动完成JavaScript和CSS正在页面中。从“添加到页面的头部”中,Lift将<head>
将该标记的一部分合并到<head>
最终的HTML页面中。
当页面加载时,jQuery UI autocomplete
功能被绑定到输入字段,并配置了一个URL,它将把用户的输入传递给我们的suggest
函数。所有suggest
需要做的是返回一个Seq[String]
jQuery自动填充代码的值给用户显示。
jQuery 1.9
jQuery 1.9删除了$.browser
自动填充小部件需要的方法。解决这个问题的方法是在你的模板中包含jQuery迁移包:
<script
data-lift=
"head"
src=
"http://code.jquery.com/jquery-migrate-1.2.1.js"
>
</script>
提交新值
AutoComplete
小部件的一个特点是,如果您输入一个新值(一个不建议),然后按提交,则不会将其发送到服务器。也就是说,您需要点击其中一个建议来选择它。如果这不是你想要的行为,你可以调整它。
在该render
方法中,我们可以通过在页面中添加JavaScript来修改行为:
import
net.liftweb.http.S
import
net.liftweb.http.js.JsCmds.Run
S
.
appendJs
(
Run
(
"""
|$('#autocomplete input[type=text]').bind('blur',function() {
| $(this).next().val($(this).val());
|});
"""
.
stripMargin
))
有了这一点,当输入字段失去焦点时 - 例如,当按下提交按钮时,输入字段的值被存储为要发送到服务器的值。
替代自动完成JavaScript
看看小部件模块构建自动完成功能的方式可能会让您深入了解如何将其他JavaScript自动完成库并入Lift应用程序。这个想法是包括JavaScript库,将其连接到页面上的元素,然后安排要调用的服务器来生成建议。当然,如果您只有几个项目供用户选择,您可以只在网页上包含这些项目,而不是往返于服务器。
作为服务器生成的建议的例子,我们可以看一下事先键入的内容包含在Twitter的引导组件。
要结合Typeahead,模板需要更改以包含库,并以Typeahead所期望的方式标记输入字段:
<
link
data
-
lift
=
"head"
rel
=
"stylesheet"
href
=
"//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/css/
bootstrap-combined.min.css"
>
<
script
data
-
lift
=
"tail"
src
=
"//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.1/js/bootstrap.min.js"
>
</
script
>
<
form
data
-
lift
=
"ProgrammingLanguagesTypeAhead"
>
<
script
id
=
"js"
></
script
>
<
input
id
=
"autocomplete"
type
=
"text"
data
-
provide
=
"typeahead"
autocomplete
=
"off"
>
<
input
type
=
"submit"
>
</
form
>
我们已经添加了一个占位符,js
该占位符将会返回给服务器的JavaScript ID 。我们稍等一会。
Typeahead的工作方式是将它附加到我们的输入字段,并在需要提出建议时告诉它调用JavaScript函数。该JavaScript函数将被调用askServer
,并给出两个参数:用户输入的输入到目前为止(query
)和JavaScript函数调用时可用的建议(callback
)。Lift片段需要使用该query
值,然后调用JavaScript callback
函数,并提供任何建议。
实现这一点的代码片段如下:
package
code.snippet
import
net.liftweb.util.Helpers._
import
net.liftweb.common.
{
Full
,
Empty
,
Loggable
}
import
net.liftweb.http._
import
net.liftweb.http.js.JsCmds._
import
net.liftweb.http.js.JsCmds.Run
import
net.liftweb.http.js.JE.JsVar
import
net.liftweb.json.JsonAST._
import
net.liftweb.json.DefaultFormats
class
ProgrammingLanguagesTypeAhead
extends
Loggable
{
val
languages
=
List
(
"C"
,
"C++"
,
"Clojure"
,
"CoffeeScript"
,
"Java"
,
"JavaScript"
,
"POP-11"
,
"Prolog"
,
"Python"
,
"Processing"
,
"Scala"
,
"Scheme"
,
"Smalltalk"
,
"SuperCollider"
)
def
render
=
{
implicit
val
formats
=
DefaultFormats
def
suggest
(
value
:
JValue
)
:
JValue
=
{
logger
.
info
(
"Making suggestion for: "
+
value
)
val
matches
=
for
{
q
<-
value
.
extractOpt
[
String
].
map
(
_
.
toLowerCase
).
toList
lang
<-
languages
.
filter
(
_
.
toLowerCase
startsWith
q
)
}
yield
JString
(
lang
)
JArray
(
matches
)
}
val
callbackContext
=
new
JsonContext
(
Full
(
"callback"
),
Empty
)
val
runSuggestion
=
SHtml
.
jsonCall
(
JsVar
(
"query"
),
callbackContext
,
suggest
_
)
S
.
appendJs
(
Run
(
"""
|$('#autocomplete').typeahead({
| source: askServer
|});
"""
.
stripMargin
))
"#js *"
#>
Function
(
"askServer"
,
"query"
::
"callback"
::
Nil
,
Run
(
runSuggestion
.
toJsCmd
))
&
"#autocomplete"
#>
SHtml
.
text
(
""
,
s
=>
logger
.
info
(
"Submitted: "
+
s
))
}
}
从代码片段的底部开始,我们将常规Lift SHtml.text
输入绑定到自动填充字段。当表单提交时,这将收到所选值。我们还将JavaScript占位符绑定到一个叫做JavaScript函数的定义askServer
。这是Typeahead在需要建议时会调用的功能。
我们定义的JavaScript函数有两个参数:the query
和callback
。askServer
导致它运行的东西被称为runSuggestion
,这是一个jsonCall
到服务器的身体,具有的价值query
。
这些建议是由suggest
函数进行的,该函数期望能够String
在JSON值中找到一个。它使用此值来查找列表中的匹配项languages
。这些被返回作为JArray
的JString
,这将被视为JSON回数据在客户端上。
客户端对数据做了什么?它使用建议调用该callback
功能,从而导致显示更新。我们指定它的callback
via JsonContext
,这是一个类,它允许我们指定一个自定义函数来调用成功的请求到服务器。
通过查看HTML页面中生成的JavaScript可以帮助您了解这一点askServer
:
<script
id=
"js"
>
function
askServer
(
query
,
callback
)
{
liftAjax
.
lift_ajaxHandler
(
'F268944843717UZB5J0='
+
encodeURIComponent
(
JSON
.
stringify
(
query
)),
callback
,
null
,
"json"
)
}
</script>
当用户键入文本字段时,Typeahead将askServer
使用提供的输入进行调用。Lift的Ajax支持安排该值,query
序列化到我们suggest
在服务器上的功能,并将结果传递给callback
。此时,Typeahead再次接管并显示建议。
键入Scala
文本字段并按提交将在服务器上产生如下所示的顺序:
INFO csProgrammingLanguagesTypeAhead - 提出建议:JString(Sc)INFO csProgrammingLanguagesTypeAhead - 提出建议:JString(Sca)INFO csProgrammingLanguagesTypeAhead - 提出建议:JString(Sca)INFO csProgrammingLanguagesTypeAhead - 提出建议:JString(Scal)INFO csProgrammingLanguagesTypeAhead - 提出建议:JString(Scala)INFO csProgrammingLanguagesTypeAhead - 已提交:Scala
也可以看看
“从按钮触发服务器端代码”介绍jsonCall
。
关于新值的小部件模块的行为在模块的GitHub页面的故障单中进行了说明。增强模块是参与Lift的一个途径,第11章介绍了其他的贡献方式。
jQuery UI自动完成文档介绍如何配置窗口小部件。Lift小部件模块附带的版本是1.0.2版。
Typeahead小部件是Twitter Bootstrap的一部分。
提供无线电按钮选择
解
使用SHtml.radioElem
呈现选项的单选按钮。
为了说明这一点,让我们创建一个表单来允许用户指出他的性别:
object
BirthGender
extends
Enumeration
{
type
BirthGender
=
Value
val
Male
=
Value
(
"Male"
)
val
Female
=
Value
(
"Female"
)
val
NotSpecified
=
Value
(
"Rather Not Say"
)
}
我们使用枚举,但它可以是任何类。所述toString
类的将被用作显示给用户的标签。
为了呈现这些选项并处理选项的选择,我们在代码片段中使用这个枚举:
package
code.snippet
import
net.liftweb.common._
import
net.liftweb.util.Helpers._
import
net.liftweb.http.SHtml
import
net.liftweb.http.SHtml.ChoiceHolder
object
Radio
extends
Loggable
{
import
BirthGender._
val
options
:
Seq
[
BirthGender
]
=
BirthGender
.
values
.
toSeq
val
default
:
Box
[
BirthGender
]
=
Empty
val
radio
:
ChoiceHolder
[
BirthGender
]
=
SHtml
.
radioElem
(
options
,
default
)
{
selected
=>
logger
.
info
(
"Choice: "
+
selected
)
}
def
render
=
".options"
#>
radio
.
toForm
}
而不是在render
方法中的一个表达式中生成单选按钮,我们已经拉出了中间值来显示它们的类型。该radio.toForm
调用正在生成单选按钮,我们将它们绑定到以下.option
模板中的CSS选择器:
<div
data-lift=
"Radio?form=post"
>
<span
class=
"options"
>
<input
type=
"radio"
>
选项1</input>
<input
type=
"radio"
>
选项2</input>
</span>
<input
type=
"submit"
value=
"Submit"
>
</div>
该class="options"
范围将被替换为代码中的单选按钮,并且在提交表单时,提供的函数SHtml.radioElem
将被调用,导致所选值被记录。例如,如果没有选择单选按钮:
INFO code.snippet.Radio - 选择:空
或者如果选择了第三个按钮:
INFO code.snippet.Radio - 选择:完全(而不是说)
讨论
Lift的很多SHtml
方法返回一个NodeSeq
,可以直接绑定到我们的HTML中。然而,radioElem
不同之处在于它给了我们一个ChoiceHolder[T]
,并NodeSeq
从中产生一个,我们正在呼唤toForm
。这对于如何定制单选按钮有所影响,我们稍后会看到。
该radioElem
方法需要三个参数:
SHtml
.
radioElem
(
options
,
default
)
{
selected
=>
logger
.
info
(
"Choice: "
+
selected
)
}
第一个是一组选项来显示,作为一个Seq[T]
。第二个是预先选择的价值Box[T]
。在这个例子中,我们没有预先选择的值,它被表示为Empty
。最后,当提交表单时,有类型的函数运行Box[T] => Any
。
请注意,即使用户没有选择任何值,您的函数将被调用,它将被传递给该值Empty
。
要了解一些更多的事情,请查看由radioElem
以下产生的默认HTML :
<span><input
value=
"F317293945993CDMQZ"
type=
"radio"
name=
"F317293946030HYAFP"
>
<input
name=
"F317293946030HYAFP"
type=
"hidden"
value=
"F317293946022HCGEG"
>
男性<br>
</span>
<span><input
value=
"F31729394600RIE253"
type=
"radio"
name=
"F317293946030HYAFP"
>
女性<br>
</span>
<span><input
value=
"F317293946011OMEMM"
type=
"radio"
name=
"F317293946030HYAFP"
>
而不是说<br>
</span>
请注意:
- 所有输入字段都具有相同的随机生成名称。
- 输入字段具有随机生成的值。
- 有一个隐藏的字段添加到第一个项目。
这可能是一个惊喜,如果你只是期待这样的事情:
<input
type=
"radio"
name=
"gender"
value=
"Male"
>
男性<br>
<input
type=
"radio"
name=
"gender"
value=
"Female"
>
女性<br>
<input
type=
"radio"
name=
"gender"
value=
"NotSpecified"
>
而不是说<br>
通过使用随机值,Lift通过防范一系列基于表单的攻击来帮助我们,例如提交我们不期望的值,或者在不希望设置的字段上设置值。
每个随机单选按钮值在服务器上与BirthGender
我们的options
序列中的值相关联。提交表单时,Lift选择所选值(如果有),查找相应的BirthGender
值,并调用我们的功能。
自定义HTML
默认的HTML包装a中的每个单选按钮<span>
,并将它们与a分隔开<br>
。让我们改变一下,使其与Twitter Bootstrap框架一起工作,并将每个选项放在<label>
一个类中。
要自定义HTML,您需要了解这ChoiceHolder
是一系列项目的容器。每个项目都是ChoiceItem
:
final
case
class
ChoiceItem
[
T
](
key
:
T
,
xhtml
:
NodeSeq
)
在key
我们的例子是一个BirthGender
实例,并且xhtml
是单选按钮输入字段(加上第一个选项的隐藏字段)。有了这个知识,我们可以写一个帮手来生成NodeSeq
我们想要的风格:
import
scala.xml.NodeSeq
import
net.liftweb.http.SHtml.ChoiceItem
object
LabelStyle
{
def
htmlize
[
T
](
item
:
ChoiceItem
[
T
])
:
NodeSeq
=
<
label
class
=
"radio"
>{
item
.
xhtml
}
{
item
.
key
.
toString
}</
label
>
def
toForm
[
T
](
holder
:
ChoiceHolder
[
T
])
:
NodeSeq
=
holder
.
items
.
flatMap
(
htmlize
)
}
该htmlize
方法<label>
使用我们想要的类生成一个元素,它包含radio(item.xhtml
)和label(item.key.toString
)的文本。在toForm
正在申请的htmlize
功能,所有的选择。
我们可以在我们的代码段中应用:
def
render
=
".options"
#>
LabelStyle
.
toForm
(
radio
)
结果如下:
<label
class=
"radio"
>
<input
value=
"F234668654428LWW305"
type=
"radio"
name=
"F234668654432WS5LWK"
>
<input
name=
"F234668654432WS5LWK"
type=
"hidden"
value=
"F234668654431KYJB3S"
>
男
</label>
<label
class=
"radio"
>
<input
value=
"F234668654429MB5RF3"
type=
"radio"
name=
"F234668654432WS5LWK"
>
女
</label>
<label
class=
"radio"
>
<input
value=
"F234668654430YULGC1"
type=
"radio"
name=
"F234668654432WS5LWK"
>
而不是说
</label>
该toForm
方法可以将选项包装在某些其他HTML中,例如<ul>
。但在这种情况下,它不是:它只适用htmlize
于每个ChoiceItem
。因此,我们可以LabelStyle
在我们的Lift应用程序中进行默认设置:
ChoiceHolder
.
htmlize
=
c
=>
LabelStyle
.
htmlize
(
c
)
这样做是因为toForm
对ChoiceHolder
推迟到ChoiceHolder.htmlize
,并且ChoiceHolder.htmlize
是可以分配给一个变量。
字符串值
如果要直接使用String
选项的值,可以使用SHtml.radio
。虽然它也产生一个ChoiceHolder
,它不同于radioElem
它使用String
与标签和值相同的。只有当用户选择了一个值时,才会调用与每个选项相关联的功能。
SHtml.radio
我们的示例的一个版本将如下所示:
SHtml
.
radio
(
"Male"
::
"Female"
::
"Rather Not Say"
::
Nil
,
Empty
,
selected
=>
logger
.
info
(
"Choice: "
+
selected
)
)
这是一个类似的结构radioElem
:有一个选项列表,一个默认的,一个调用的函数,它产生一个ChoiceHolder[String]
。提交表单时,我们的函数将传递String
所选项的值。如果没有选择单选按钮,则不调用该功能。
有条件地禁用复选框
解
创建CSS选择器转换以添加禁用的属性,并将其应用于复选框转换。
例如,假设你有一个简单的复选框:
class
Likes
{
var
likeTurtles
=
false
def
render
=
"#like"
#>
SHtml
.
checkbox
(
likeTurtles
,
likeTurtles
=
_
)
}
和相应的模板:
<!DOCTYPE html>
<head>
<meta
content=
"text/html; charset=UTF-8"
http-equiv=
"content-type"
/>
<title>
禁用复选框
</title>
</head>
<body
data-lift-content-id=
"main"
>
<div
id=
"main"
data-lift=
"surround?with=default;at=content"
>
<div>
选择你喜欢的东西:
</div>
<form
data-lift=
"Likes"
>
<label
for=
"like"
>
你喜欢乌龟吗
</label>
<input
id=
"like"
type=
"checkbox"
>
</form>
</div>
</body>
</html>
此外,假设您要在大约50%的时间内禁用它。我们能做到这一点,通过调整NodeSeq
从产生SHtml.checkbox
:
package
code.snippet
import
net.liftweb.util.Helpers._
import
net.liftweb.util.PassThru
import
net.liftweb.http.SHtml
class
Likes
{
var
likesTurtles
=
false
def
disable
=
if
(
math
.
random
>
0.5d
)
"* [disabled]"
#>
"disabled"
else
PassThru
def
render
=
"#like"
#>
disable
(
SHtml
.
checkbox
(
likesTurtles
,
likesTurtles
=
_
)
)
}
当复选框呈现时,大约一半的时间将被禁用。
讨论
该disable
方法返回一个NodeSeq => NodeSeq
函数,意思是当我们应用它时,我们需要给它一个 NodeSeq
,这正是SHtml.checkbox
提供的。
[disabled]
CSS选择器的一部分是选择禁用的属性,并将其替换为右侧的值,#>
在该示例中为“禁用”。
这个组合意味着禁用属性将被设置在复选框的一半时间,而一半的时间NodeSeq
将PassThru
不会改变复选框NodeSeq
。
也可以看看
“返回代码段标记不变”描述了该PassThru
功能。
使用带多个选项的选择框
解
使用SHtml.multiSelect(options, default, selection)
。以下是用户最多可以选择三个选项的示例:
package
code.snippet
import
net.liftweb.util.Helpers._
import
net.liftweb.http.SHtml
import
net.liftweb.common.Loggable
class
MultiSelect
extends
Loggable
{
case
class
Item
(
id
:
String
,
name
:
String
)
val
inventory
=
Item
(
"a"
,
"Coffee"
)
::
Item
(
"b"
,
"Milk"
)
::
Item
(
"c"
,
"Sugar"
)
::
Nil
val
options
:
List
[(
String
,String
)]
=
inventory
.
map
(
i
=>
(
i
.
id
->
i
.
name
))
val
default
=
inventory
.
head
.
id
::
Nil
def
render
=
{
def
selection
(
ids
:
List
[
String
])
:
Unit
=
{
logger
.
info
(
"Selected: "
+
ids
)
}
"#opts *"
#>
SHtml
.
multiSelect
(
options
,
default
,
selection
)
}
}
在这个例子中,用户正在呈现一个三个列表,第一个被选中,如图3-3所示。提交表单时,将selection
调用该函数,并显示所选选项值的列表。

使用该代码段的模板可以是:
<div
data-lift=
"MultiSelect?form=post"
>
<p>
我能得到什么?</p>
<div
id=
"opts"
>
选项到这里</div>
<input
type=
"submit"
value=
"Place Order"
>
</div>
这将呈现如下:
<form
action=
"/"
method=
"post"
><div>
<p>
我能得到什么?</p>
<div
id=
"opts"
>
<select
name=
"F25749422319ALP1BW"
multiple=
"true"
>
<option
value=
"a"
selected=
"selected"
>
咖啡</option>
<option
value=
"b"
>
牛奶</option>
<option
value=
"c"
>
糖</option>
</select>
</div>
<input
value=
"Place Order"
type=
"submit"
>
</form>
讨论
回想一下,HTML选择由一组选项组成,每个选项都有一个值和一个名称。为了反映这一点,前面的例子是我们 inventory
的对象,并把它变成一个被称为的字符串对的列表options
。
给定的函数SHtml.multiSelect
将接收选项的值(ID),而不是名称。也就是说,如果您运行代码,并选择“咖啡”和“牛奶”,该功能将会看到List("a", "b")
。
选择无选项
请注意,如果未选择任何选项,则不会调用处理函数。这在问题1139中有描述。
解决这个问题的一种方法是添加一个隐藏的功能来重置列表。例如,我们可以将以前的代码修改为状态片段,并记住我们选择的值:
package
code.snippet
import
net.liftweb.util.Helpers._
import
net.liftweb.http.
{
StatefulSnippet
,
SHtml
}
import
net.liftweb.common.Loggable
class
MultiSelectStateful
extends
StatefulSnippet
with
Loggable
{
def
dispatch
=
{
case
_
=>
render
}
case
class
Item
(
id
:
String
,
name
:
String
)
val
inventory
=
Item
(
"a"
,
"Coffee"
)
::
Item
(
"b"
,
"Milk"
)
::
Item
(
"c"
,
"Sugar"
)
::
Nil
val
options
:
List
[(
String
,String
)]
=
inventory
.
map
(
i
=>
(
i
.
id
->
i
.
name
))
var
current
=
inventory
.
head
.
id
::
Nil
def
render
=
{
def
logSelected
()
=
logger
.
info
(
"Values selected: "
+
current
)
"#opts *"
#>
(
SHtml
.
hidden
(
()
=>
current
=
Nil
)
++
SHtml
.
multiSelect
(
options
,
current
,
current
=
_
)
)
&
"type=submit"
#>
SHtml
.
onSubmitUnit
(
logSelected
)
}
}
模板未更改,代码段已修改为引入current
值和隐藏函数以重置该值。我们已经绑定提交按钮,以便在提交表单时简单地记录所选值。
每次提交表单时current
,ID列表将设置为您在浏览器中选择的任何内容。但是请注意,我们已经开始使用一个隐藏的功能重置current
到空列表。这意味着如果接收函数multiSelect
从未被调用,因为没有选择任何内容,所以存储的值current
将反映出来Nil
。
这可能是有用的,这取决于您在应用程序中需要哪些行为。
类型安全选项
如果您不想String
使用某个选项的值,可以使用multiSelectObj
。在这个变体中,选项列表仍然提供一个文本名称,但是这个值就是一个类。同样,默认值列表将是一个类实例的列表。
对代码的唯一更改是为List[(Item,String)]
选项生成一个,并使用Item
默认值:
val
options
:
List
[(
Item
,String
)]
=
inventory
.
map
(
i
=>
(
i
->
i
.
name
))
val
default
=
inventory
.
head
::
Nil
从这些数据生成多重选择的调用是类似的,但是注意我们的selection
函数现在收到一个列表Item
:
def
render
=
{
def
selection
(
items
:
List
[
Item
])
:
Unit
=
{
logger
.
info
(
"Selected: "
+
items
)
}
"#opts *"
#>
SHtml
.
multiSelectObj
(
options
,
default
,
selection
)
}
枚举
package
code.snippet
import
net.liftweb.util.Helpers._
import
net.liftweb.http.SHtml
import
net.liftweb.common.Loggable
class
MultiSelectEnum
extends
Loggable
{
object
Item
extends
Enumeration
{
type
Item
=
Value
val
Coffee
,
Milk
,
Sugar
=
Value
}
import
Item._
val
options
:
List
[(
Item
,String
)]
=
Item
.
values
.
toList
.
map
(
i
=>
(
i
->
i
.
toString
))
val
default
=
Item
.
Coffee
::
Nil
def
render
=
{
def
selection
(
items
:
List
[
Item
])
:
Unit
=
{
logger
.
info
(
"Selected: "
+
items
)
}
"#opts *"
#>
SHtml
.
multiSelectObj
(
options
,
default
,
selection
)
}
}
枚举版本的工作方式与类型安全版本相同。
上传文件
解
FileParamHolder
在您的代码段中使用a ,并在提交表单时从中提取文件信息。
从表单开始,标有multipart=true
:
<html>
<head>
<title>
文件上传</title>
<script
id=
"jquery"
src=
"/classpath/jquery.js"
type=
"text/javascript"
>
</script>
<script
id=
"json"
src=
"/classpath/json.js"
type=
"text/javascript"
></script>
</head>
<body>
<form
data-lift=
"FileUploadSnippet?form=post;multipart=true"
>
<label
for=
"file"
>
选择一个文件:<input
id=
"file"
></input>
</label>
<input
type=
"submit"
value=
"Submit"
></input>
</form>
</body>
</html>
我们将文件输入SHtml.fileUpload
和提交按钮绑定到一个功能来处理上传的文件:
package
code.snippet
import
net.liftweb.util.Helpers._
import
net.liftweb.http.SHtml._
import
net.liftweb.http.FileParamHolder
import
net.liftweb.common.
{
Loggable
,
Full
,
Empty
,
Box
}
class
FileUploadSnippet
extends
Loggable
{
def
render
=
{
var
upload
:
Box
[
FileParamHolder
]
=
Empty
def
processForm
()
=
upload
match
{
case
Full
(
FileParamHolder
(
_
,
mimeType
,
fileName
,
file
))
=>
logger
.
info
(
"%s of type %s is %d bytes long"
format
(
fileName
,
mimeType
,
file
.
length
)
)
case
_
=>
logger
.
warn
(
"No file?"
)
}
"#file"
#>
fileUpload
(
f
=>
upload
=
Full
(
f
))
&
"type=submit"
#>
onSubmitUnit
(
processForm
)
}
}
该fileUpload
绑定确保将文件分配给该upload
变量。这允许我们在提交表单时访问方法Array[Byte]
中的文件processForm
。
讨论
HTTP包括一种multipart/form-data
用于支持二进制数据上传的编码类型。该?form=post;multipart=true
模板中的参数标记与此编码形式,并且生成的HTML将看起来像这样:
<form
enctype=
"multipart/form-data"
method=
"post"
action=
"/fileupload"
>
当浏览器提交表单时,Lift会检测到multipart/form-data
编码并从请求中提取任何文件。这些都可以作为uploadedFiles
一个上Req
对象,例如:
val
files
:
List
[
FileParamHolder
]
=
S
.
request
.
map
(
_
.
uploadedFiles
)
openOr
Nil
然而,当我们处理一个带有单个上载字段的表单时SHtml.fileUpload
,将输入绑定到我们的upload
变量上更容易使用。f => upload = Full(f)
当选择文件并通过此字段上传文件时,Lift安排功能被调用。如果文件为零长度,则不调用该函数。
Lift的默认行为是将文件读入内存并将其呈现为FileParamHolder
。在这个食谱中,我们是在字段上的模式匹配,FileParamHolder
并简单地打印出我们对文件的了解。我们忽略第一个参数,它将是Lift为该字段生成的名称,但是捕获mime类型,原始文件名和文件中的原始数据。
您可能不想将此方法用于非常大的文件。其实,LiftRules
提供了一些您可以控制的大小限制:
- 上传的任何单个文件的最大大小(默认为7 MB)
- 多部分上传的最大大小(默认为8 MB)
LiftRules.maxMimeFileSize
LiftRules.maxMimeSize
为什么要两个设置?因为表单提交时,表单上可能会有多个字段。例如,在配方中,提交按钮的值作为其中一个部分发送,并且文件作为另一个发送。因此,您可能希望限制文件大小,但允许提交一些字段值或多个文件。
如果您达到大小限制,将从底层文件上传库中抛出异常。您可以捕获异常,如“Catch Any Exception”中所述:
LiftRules
.
exceptionHandler
.
prepend
{
case
(
_
,
_
,
x
:
FileUploadIOException
)
=>
ResponseWithReason
(
BadResponse
(),
"Unable to process file. Too large?"
)
}
请注意,容器(Jetty,Tomcat)或任何Web服务器(Apache,Nginx)也可能对文件上传大小有限制。
在某些情况下将文件上传到内存中可能会很好,但您可能希望将较大的项目上传到磁盘,然后将其作为流加载到“Lift”中。Lift通过以下设置支持此功能:
LiftRules
.
handleMimeFile
=
OnDiskFileParamHolder
.
apply
该handleMimeFile
变量期望被赋予一个函数,它接收一个字段名称,mime类型,文件名,InputStream
并返回a FileParamHolder
。这个默认的实现是InMemFileParamHolder
,但是改为OnDiskFileParamHolder
意味着Lift会将文件首先写入磁盘。您当然可以实现自己的处理程序,除了使用OnDiskFileParamHolder
或InMemFileParamHolder
。
随着OnDiskFileParamHolder
文件将被写入一个临时位置(System.getProperty("java.io.tmpdir")
),但是当你完成这个文件后,你可以把它删除。例如,我们的代码段可能会更改为:
def
processForm
()
=
upload
match
{
case
Full
(
content
:
OnDiskFileParamHolder
)
=>
logger
.
info
(
"File: "
+
content
.
localFile
.
getAbsolutePath
)
val
in
:
InputStream
=
content
.
fileStream
// ...do something with the stream here...
val
wasDeleted_?
=
content
.
localFile
.
delete
()
case
_
=>
logger
.
warn
(
"No file?"
)
}
注意OnDiskFileParamHolder
实现FileParamHolder
,所以将匹配FileParamHolder
配方中使用的原始模式。但是,如果您访问该file
字段OnDiskFileParamHolder
,则会将该文件导入内存,从而将其存储在磁盘上将其处理为流。
如果要监视服务器端上传的进度,可以。有一个挂钩LiftRules
,被称为上传正在运行:
def
progressPrinter
(
bytesRead
:
Long
,
contentLength
:
Long
,
fieldIndex
:
Int
)
{
println
(
"Read %d of %d for %d"
format
(
bytesRead
,
contentLength
,
fieldIndex
))
}
LiftRules
.
progressListener
=
progressPrinter
这是整个多部分上传的进度,而不仅仅是上传的文件。特别地,contentLength
可能不知道(在这种情况下,它将是-1
),但是如果知道的话,它是完整的多部分上传的大小。在这个食谱的例子中,这将包括文件的大小,还包括提交按钮的值。这也解释了fieldIndex
哪个是正在处理哪个部分的计数器。在这个例子中,这两个部分的值为0和1。
也可以看看
HTTP文件上传机制在RFC 1867 “基于表单的文件上传”中有所描述。
“在REST服务中接受二进制数据”讨论了在REST服务的上下文中的文件上传。
有关通过与JavaScript库集成的Ajax文件上传示例,请参见“Ajax文件上传”,提供进度指示器和拖放支持。
带布局的干燥表单
解
使用CssBoundLiftScreen
字段绑定用于命名展示位置,并可选择覆盖特定单个字段元素的布局。
以下是一个示例代码段:
package
code.snippet
import
scala.xml.NodeSeq
import
net.liftweb.http._
import
net.liftweb.http.FieldBinding.Self
object
AccountInfoEditor
extends
CssBoundLiftScreen
{
val
formName
=
"accountedit"
override
def
allTemplate
=
savedDefaultXml
protected
def
defaultAllTemplate
=
super
.
allTemplate
// Pull the definition of the "normal" field from a template, if it exists
override
def
defaultFieldNodeSeq
:
NodeSeq
=
Templates
(
"accounts"
::
"account_edit_field"
::
Nil
).
openOr
(
<
div
>
<
label
class
=
"label field"
></
label
>
<
span
class
=
"value fieldValue"
></
span
>
<
span
class
=
"help"
></
span
>
<
div
class
=
"errors"
>
<
div
class
=
"error"
></
div
>
</
div
>
</
div
>)
override
def
finish
()
{
println
(
"Account Edited for: "
+
firstName
)
S
.
notice
(
"Done."
)
}
// An example source of an account:
case
class
Account
(
firstName
:
String
,
lastName
:
String
,
address
:
String
)
def
accountToEdit
=
new
Account
(
"Ada"
,
"Lovelace"
,
"Ockham Park, Surrey"
)
// Fields:
val
firstName
=
field
(
"First Name"
,
accountToEdit
.
firstName
,
trim
,
valMinLen
(
1
,
"First name is required"
),
FieldBinding
(
"firstName"
))
val
lastName
=
field
(
"Last Name"
,
accountToEdit
.
lastName
,
trim
,
valMinLen
(
1
,
"Last name is required"
),
FieldBinding
(
"lastName"
))
val
address
=
textarea
(
"Address"
,
accountToEdit
.
address
,
trim
,
valMinLen
(
1
,
"Address is required"
),
valMaxLen
(
255
,
"Address too long"
),
FieldBinding
(
"address"
,
Self
))
}
该片段将呈现(假设)用户帐户记录,允许用户编辑三个字段。这些字段具有验证,并且将被绑定到模板中。
相应的模板可能称为webapp / accountinfo.html:
<!DOCTYPE html>
<head>
<meta
content=
"text/html; charset=UTF-8"
http-equiv=
"content-type"
/>
<title>
自定义CSS绑定屏幕</title>
</head>
<body
data-lift-content-id=
"main"
>
<div
id=
"main"
data-lift=
"surround?with=default;at=content"
>
<div
data-lift=
"AccountInfoEditor"
>
<div>
<!--
Drop regular Lift Screen elements where you want them.
Here we're putting the Next (or Finish) button at the top.
-->
<button
class=
"next"
></button>
</div>
<div
class=
"fields"
>
<h2>
帐户详细资料</h2>
<!--
The template applied to each field is taken from
accounts/account_edit_field.html
-->
<div
id=
"accountedit_firstName_field"
class=
"large"
></div>
<div
id=
"accountedit_lastName_field"
class=
"large"
></div>
<!--
The code binds this field with Self, meaning the
field template is inline in this template and will
be different:
-->
<div
id=
"accountedit_address_field"
>
<div
class=
"large"
>
你喜欢送礼物的地址:</div>
<div
class=
"errors"
>
<div
class=
"error"
></div>
</div>
<span
class=
"value fieldValue"
style=
"width:10em; height:5em"
></span>
</div>
</div>
</div>
</div>
</body>
</html>
运行代码段将导致CssBoundLiftScreen
将defaultFieldNodeSeq
布局应用于每个字段,但我们在模板中定制的“地址”字段除外。
按“完成”按钮可触发验证,显示错误(如果有),或通过该finish
方法完成编辑。
讨论
CssBoundLiftScreen
使用相同的模型(默认情况下)LiftScreen
,但使用CSS类来标识默认模板中的元素(例如, wizard-all.html)。这种强大的机制消除了代码和HTML中的重复; 然而,LiftScreen
牺牲灵活性,因为没有办法制作高度定制的表格。
CssBoundLiftScreen
通过允许您控制表单呈现的每个元素,可以删除此限制。您可以将所有表单字段布局控制到单个字段的布局。您可以在代码段使用网站中嵌入自定义模板:
<div
data-lift=
"AccountInfoEditor"
>
<!-- template for Screen goes here -->
</div>
并提供一些额外的绑定提示。该形式仍然非常小,而完全由设计师控制。
解决方案部分中的示例演示了最重要的方面。特别地,您应该注意,您仍然必须为模板提供源,并且您可以为模板的子部分指定替代方法。在该示例中,我们允许HTML设计器直接访问此屏幕的字段模板,方法是将布局放在自定义模板文件accounts / account_edit_field.html中。
其余的很容易。您必须formName
为Scala 提供声明 CssBoundLiftScreen
,并FieldBinding
为每个字段提供一个参数。结合内部函数(可以覆盖),将生成可以在模板中使用的唯一(但是已知的)名称。默认模式为:“formName_fieldName_field”。所以,如果你将表单命名为“myform”,并将一个字段绑定到“address”,那么你的HTML模板应该包含一个ID为“myform_address_field”的div。这就是使用您的字段模板的正常绑定所需要的。
要调整特定字段的字段布局,可以Self
在Scala字段绑定中指定。这表明该特定字段的模板应来自字段的div。
如果你想获得更多的爱好者,还有其他字段绑定,例如 Dynamic(() => NodeSeq)
,使用提供的函数在每次渲染时为该字段生成一个模板。
也可以看看
更多的例子可以在:
- CSSBoundLiftScreen上 的Lift Wiki条目。
- Peter Brant的Lift屏幕CSS绑定GitHub存储库。
LiftScreen
描述在Lift Wiki和Simply Lift。