【原文地址】:https://www.playframework.com/documentation/2.2.x/ScalaForms
一、概述
表单的处理和提交是web应用中非常重要的一块。Play自带功能让处理简单表单变得更容易,并且使得处理复杂表单成为可能。
Play的表单处理方法基于数据绑定的概念。当数据来自POST请求时,Play将会查找格式化的值,并且把它们和一个表单的对象绑定。Play可以用这些绑定的表单为一个case类赋值,也可以调用自定义的验证等。
通常形式的表单是被一个Controller实例直接使用的。但是,表单定义不必精确匹配case类或者模型,因为它们纯粹是为了处理输入,而且为了一个独立的POST而单独使用一个表单也是合理的。
二、导入
为了使用表单,要在你的类中导入以下的包:
import play.api.data._
import play.api.data.Forms._
三、表单基础
我们通过以下步骤处理表单:
- 定义一个表单
- 在表单中定义约束条件
- 在一个action中验证表单
- 在一个视图模板中现实表单
- 最后,在视图模板中处理结果(或者错误)
最后的结果会类似于这样:
1、定义一个表单
首先,定义一个包含你表单中需要元素的case类。这儿我们想要获得一个用户(User)的姓名和年龄,所以我们先创建一个UserData的对象:
case class UserData(name: String, age: Int)
现在我们拥有了一个case类,接下来我们要定义一个表单结构。
Form的功能就是把表单数据转化成为一个case类的一个绑定的实例,我们如下定义:
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(UserData.apply)(UserData.unapply)
)
表单对象定义了mapping方法。这种方法包含了表单的名称和约束,同时也包含了两个函数:一个apply函数和一个unapply函数。因为UserData是一个case类,我们可以把它的apply和unapply方法直接插入到mapping方法中。
注意:case类至多只能map22种不同的field,根据编译限制 。如果你在表单中的field数目大于22的话,你应该使用list或者嵌套数据拆开你的表单。
一个表单当被给予一个Map时,将会创建一个带有绑定数值的UserData实例:
val anyData = Map("name" -> "bob", "age" -> "21")
val userData = userForm.bind(anyData).get
但是,大多数时间你会在一个带有请求数据的Action中使用表单。Form中包含bindFromRequest方法,该方法拥有一个作为隐式参数的请求。如果你定义一个隐式请求,那么bindFromRequest将会找到它。
val userData = userForm.bindFromRequest.get
注意:有一种使用get的情况,就是当表单无法绑定到数据的时候,get就会抛出一个异常。我们在将在接下来的几段展示一种更安全的处理输入的方法。
你在表单mapping中使用case类不会受到限制。只要apply和unapply方法被正确地map,你就可以传递你喜欢的任何东西,比如使用Forms.tuple mapping或者模板case类的元组。但是,对一个表单明确地定义一个case类还是有很多优点:
- 方便。case类被设计成为简单的数据容器,已经提供了一些与Form功能匹配的特性。
- 强大。元组便于使用,但是不允许被传统的apply或unapply方法使用,而且只能引用包含数字的数据(_1,_2等)。
- 专门针对表单。模板case类的重用会非常方便,但通常模板会包含一些附加的域逻辑,甚至会有一些能导致紧耦合的持久性的细节。另外,如果在表单和模型之间没有一个直接的1:1mapping的话,那么一些敏感的field必须被显式忽略从而避免一次参数篡改攻击。
2、在表单中定义约束条件
text的约束条件为简单的字符串。这意味着name为空也不会报错,但这并不是我们想要的。一种保证name取得正确值的方法就是使用nonEmptyText约束条件。
val userFormConstraints2 = Form(
mapping(
"name" -> nonEmptyText,
"age" -> number(min = 0, max = 100)
)(UserData.apply)(UserData.unapply)
)
使用这个表单,如果输入不匹配约束条件的话将会报错:
val boundForm = userFormConstraints2.bind(Map("bob" -> "", "age" -> "25"))<span style="color:#FF0000;">
</span>
boundForm.hasErrors must beTrue
表单对象上定义的一些已有的约束条件:
text
: map 为scala.String
, 可选择性附加minLength
和maxLength
.nonEmptyText
: map 为scala.String
, 可选择性附加minLength
和maxLength
.number
: map 为scala.Int
, 可选择性附加min
,max
, 和strict
.longNumber
: map 为scala.Long
, 可选择性附加min
,max
, 和strict
.bigDecimal
:增加精度和规模
.date
: map 为java.util.Date
, 可选择性附加pattern
和timeZone
.email
: map 为scala.String
, 使用一个email的正则表达.boolean
: map 为scala.Boolean
.checked
: map 为scala.Boolean
.optional
: map 为scala.Option
.
3、定义ad-hoc约束条件
你可以通过在case类中使用validation包来定义你自己的ad-hoc条件。
val userFormConstraints = Form(
mapping(
"name" -> text.verifying(nonEmpty),
"age" -> number.verifying(min(0), max(100))
)(UserData.apply)(UserData.unapply)
)
你也可以用case类自身定义ad-hoc约束条件:
def validate(name: String, age: Int) = {
name match {
case "bob" if age >= 18 =>
Some(UserData(name, age))
case "admin" =>
Some(UserData(name, age))
case _ =>
None
}
}
val userFormConstraintsAdHoc = Form(
mapping(
"name" -> text,
"age" -> number
)(UserData.apply)(UserData.unapply) verifying("Failed form constraints!", fields => fields match {
case userData => validate(userData.name, userData.age).isDefined
})
)
你也可以选择创建你自己的验证方式。请参照普通验证部分,获取更多细节。
4、在Action中验证表单
现在我们已经有了约束条件了,我们可以在一个action中验证表单,处理错误。
我们使用fold方法来完成上述功能,该方法带有两个函数:第一个是在绑定失败的时候调用,第二个是在绑定成功的时候调用。
userForm.bindFromRequest.fold(
formWithErrors => {
// binding failure, you retrieve the form containing errors:
BadRequest(views.html.user(formWithErrors))
},
userData => {
/* binding success, you get the actual value. */
val newUser = models.User(userData.name, userData.age)
val id = models.User.create(newUser)
Redirect(routes.Application.home(id))
}
)
在失败的情况下,我们提交带有BadRequest的页面,同时将错误作为页面参数传入表单。如果我们使用视图helper(下面会有讨论),那么任何绑定到一个field的错误都会在页面中紧邻该field被提交。
在成功的情况下,我们将发送一个路由到routes.Application.home
的Redirect,而不是发送一个视图模板。这种模式叫做POST之后重定向,是一种非常棒的防止表单重复提交的方法。
注意:在使用flashing或者其他使用闪存区域的方法时,“POST之后重定向”是必须的,因为新的cookies只有在重定向的HTTP请求之后才可用。
5、在视图模板中显示表单
一旦你有一个表单,那么你需要让它对于模板引擎是可用的。你可以通过把表单作为视图模板的一个参数来实现。对于user.scala.html
,它页面顶部的header将会看起来像这样:
@(userForm: Form[UserData])
因为
user.scala.html需要被传入一个表单,你可以在最开始在提交user.scala.html
的时候传入一个空的userForm:
def index = Action {
Ok(views.html.user(userForm))
}
第一件事就是要创建一个表单标签。它是一个用来创建表单标签
和根据你传入的反向路由设置action和方法标签参数的简单的视图helper
@helper.form(action = routes.Application.userPost()) {
@helper.inputText(userForm("name"))
@helper.inputText(userForm("age"))
}
你可以在views.html.helper
包里面找到许多输入的helper。你用表单的field填充它们,它们就会显示相应的HTML输入、设置、值、约束条件和绑定失败时报的错误。
注意:你可以在模板中使用@import helper._
来避免在helper之前加@helper.
有许多输入helper,但是最有用的有:
form
: 提交一个form 元素.inputText
:提交一个text input 元素.inputPassword
:提交一个password input 元素.inputDate
:提交一个date input 元素.inputFile
:提交一个file input 元素.inputRadioGroup
:提交一个radio input 元素.select
:提交一个select 元素.textarea
:提交一个textarea 元素.checkbox
: 提交一个checkbox元素.input
:提交一个一般的 input元素 (需要显式参数).
@helper.inputText(userForm("name"), 'id -> "name", 'size -> 30)
上文提到的一般的输入helper允许你为期望得到的HTML结果编码:
@helper.input(userForm("name")) { (id, name, value, args) =>
<input type="text" name="@name" id="@id" @toHtmlArgs(args)>
}
注意:除非你使用_字符开始,否则所有的额外参数都会被附加在生成的Html中。以_开始的参数是为field构造参数保留的。对于复杂的表单元素,你也可以创建你自己的传统的视图helper(在views包里面使用scala类)和field构造器。
6、在视图模板中显示错误
表单中的错误表现为Map[String,FormError]
,其中FormError有:
key
: 应该与field相同.message
: 一个消息或者消息主键.args
: 消息的参数列表.
表单错误在绑定的表单实例中被如下使用:
errors
:作为Seq[FormError]返回所有错误
.globalErrors
:返回没有任何主键作为Seq[FormError]的错误
.error("name")
:返回第一个作为Option[FormError]
绑定到主键的错误.errors("name")
:返回所有作为Seq[FormError]
绑定到主键的错误.
被关联到field的错误将会通过表单helper自动提交,因此,有错误的@helper.inputText将会显示如下
<dl class="error" id="age_field">
<dt><label for="age">Age:</label></dt>
<dd><input type="text" name="age" id="age" value=""></dd>
<dd class="error">This field is required!</dd>
<dd class="error">Another error</dd>
<dd class="info">Required</dd>
<dd class="info">Another constraint</dd>
</dl>
没有被绑定到主键的全局错误(global errors)没有一个helper,而且必须在页面上显式定义:
@if(userForm.hasGlobalErrors) {
<ul>
@userForm.globalErrors.foreach { error =>
<li>error.message</li>
}
</ul>
}
7、使用元组(tuples)Mapping
在你的field中,你可以使用元组代替case类:
val userFormTuple = Form(
tuple(
"name" -> text,
"age" -> number
) // tuples come with built-in apply/unapply
)
使用元组比定义case类更加方便,尤其是对于数量较少的元组:
val anyData = Map("name" -> "bob", "age" -> "25")
val (name, age) = userFormTuple.bind(anyData).get
8、使用单个元素(single)Mapping
只有值比较多的时候才使用元组。如果在表单中只有一个field,使用Forms.single来map一个值,而不用额外开销一个case类或者元组:
val singleForm = Form(
single(
"email" -> email
)
)
val email = singleForm.bind(Map("email", "bob@example.com")).get
9、填写值
有时候你会想着用存在的值去填充一个表单,典型的情形就是编辑数据:
val filledForm = userForm.fill(UserData("Bob", 18))
当你通过视图helper使用它时,元素的值将会被填充为:
@helper.inputText(filledForm("name")) @* will render value="Bob" *@
填充对于那些需要值的map列表的helper尤其有用,比如select和inputRadioGroup的helper。可以选择list,map和pair为这些helper赋值。
10、嵌套值
一个表单mapping可以通过在已有的mapping中使用Forms.mapping来定义嵌套值:
case class AddressData(street: String, city: String)
case class UserAddressData(name: String, address: AddressData)
val userFormNested: Form[UserAddressData] = Form(
mapping(
"name" -> text,
"address" -> mapping(
"street" -> text,
"city" -> text
)(AddressData.apply)(AddressData.unapply)
)(UserAddressData.apply)(UserAddressData.unapply)
)
注意:当你通过这种方式使用嵌套值时,由浏览器发送的表单值必须被命名为类似address.street
,address.city
等。
@helper.inputText(userFormNested("name"))
@helper.inputText(userFormNested("address.street"))
@helper.inputText(userFormNested("address.city"))
11、重复值
一个表单mapping可以通过使用Forms.list或者Forms.seq来定义重复值:
case class UserListData(name: String, emails: List[String])
val userFormRepeated = Form(
mapping(
"name" -> text,
"emails" -> list(email)
)(UserListData.apply)(UserListData.unapply)
)
当你这样使用重复值时,被浏览器发送的重复值必须被命名为emails[0]
,emails[1]
,emails[2]
等。现在你必须使用repeat helper生成和emails field一样多的输入:
@helper.inputText(myForm("name"))
@helper.repeat(myForm("emails"), min = 1) { emailField =>
@helper.inputText(emailField)
}
min参数允许你显示一个fileld的最小数量,即使相应的表单数据为空。
12、可选值
一个表单mapping也可以通过使用Forms.optional来定义可选值:
case class UserOptionalData(name: String, email: Option[String])
val userFormOptional = Form(
mapping(
"name" -> text,
"email" -> optional(email)
)(UserOptionalData.apply)(UserOptionalData.unapply)
)
这个的mapping在输出中可以map到一个Option[A],如果没有发现表单值的话该选项为None。
13、默认值
你可以使用Form#fill通过初始值来验证表单:
val filledForm = userForm.fill(User("Bob", 18))
或者你可以使用Forms.default为数字定义一个默认的mapping:
Form(
mapping(
"name" -> default(text, "Bob")
"age" -> default(number, 18)
)(User.apply)(User.unapply)
)
14、忽略值
如果你想让一个表单的一个field拥有一个静态值,那就使用Forms.ignored:
val userFormStatic = Form(
mapping(
"id" -> ignored(23L),
"name" -> text,
"email" -> optional(email)
)(UserStaticData.apply)(UserStaticData.unapply)
)
四、归总
Play有一些表单示例程序在/samples/scala/forms
下,其中有一些非常有用的例子讲的是怎样生成复杂的表单。作为例子,这是Contacts的controller。
得到了一个case类Contact:
case class Contact(firstname: String,
lastname: String,
company: Option[String],
informations: Seq[ContactInformation])
case class ContactInformation(label: String,
email: Option[String],
phones: List[String])
注意到Contact包含一个拥有ContactInformation
元素的Seq和一个String的List。在这种情况下,我们可以把嵌套mapping和重复mapping(分别通过Forms.seq和Forms.list定义)结合起来。
val contactForm: Form[Contact] = Form(
// Defines a mapping that will handle Contact values
mapping(
"firstname" -> nonEmptyText,
"lastname" -> nonEmptyText,
"company" -> optional(text),
// Defines a repeated mapping
"informations" -> seq(
mapping(
"label" -> nonEmptyText,
"email" -> optional(email),
"phones" -> list(
text verifying pattern("""[0-9.+]+""".r, error="A valid phone number is required")
)
)(ContactInformation.apply)(ContactInformation.unapply)
)
)(Contact.apply)(Contact.unapply)
)
这段代码展示了一个已经存在的contact怎样使用被填充的数据在表单中显示:
def editForm = Action {
val existingContact = Contact(
"Fake", "Contact", Some("Fake company"), informations = List(
ContactInformation(
"Personal", Some("fakecontact@gmail.com"), List("01.23.45.67.89", "98.76.54.32.10")
),
ContactInformation(
"Professional", Some("fakecontact@company.com"), List("01.23.45.67.89")
),
ContactInformation(
"Previous", Some("fakecontact@oldcompany.com"), List()
)
)
)
Ok(views.html.contact.form(contactForm.fill(existingContact)))
}
下一篇:Protecting against Cross Site Request Forgery