Handling form submission

    Play 的表单处理方式基于数据绑定的概念。当数据来自于一个 POST 请求时,Play 会查找格式化的值并将其绑定到 对象上。Play 可以根据绑定的表单及数据构建一个样本类(case class),调用自定义的验证器,等等。

    通常,表单会在 Controller 实例中直接使用。不过 Form 的定义并不需要和样本类(case class)或是模型一致匹配:他们纯粹都是为了处理输入,不同的 POST 使用不同的 Form 非常合理。

    为了使用表单,必须在类中先导入以下几个包:

    让我们先来看一下基本的表单处理:

    • 定义一个表单,
    • 定义表单中的约束,
    • 在一个 action 中验证该表单,
    • 在视图模板中显示表单,
    • 最后,在视图模板中处理表单的结果(或是错误)

    最终的结果看起来是这样的:

    首先,定义一个包含了表单中所有元素的样本类(case class)。这里我们想要获取一个用户的名字和年龄,因此我们创建了一个 UserData 对象:

    1. case class UserData(name: String, age: Int)

    现在我们有了一个样本类(case class),接着需要定义一个 Form 结构。定义表单的方法是将表单的数据绑定到样本类(case class)的实例中,定义如下:

    1. val userForm = Form(
    2. mapping(
    3. "name" -> text,
    4. "age" -> number
    5. )(UserData.apply)(UserData.unapply)
    6. )

    表单对象定义了 mapping 方法。这个方法接收表单的名字和约束作为参数,并接收另外两个方法:一个 apply 方法和一个 unapply 方法。因为 UserData 是一个样本类(case class),我们可以将 applyunapply 直接插入到 mapping 方法中。

    注意:由于表单处理的实现问题,一个单一元组(tuple)或是 mapping 最多只能有18个元素。如果你的表单有超过18个元素,你需要将他们通过列表或是嵌套值分开来。

    当你使用了 Map ,表单会创建一个带有绑定值的 UserData 实例:

    1. val anyData = Map("name" -> "bob", "age" -> "21")
    2. val userData = userForm.bind(anyData).get

    但大多数的时候会在 Action 中使用表单,其中的数据由请求提供。Form 包含了 bindFormRequest,接收一个请求作为隐式(implicit)参数。如果你定义了一个隐式(implicit)请求,那么 bindFormRequest 就能够找到他。

    1. val userData = userForm.bindFromRequest.get

    注意:这里使用 get 其实是有问题的。如果表单没能绑定数据,那么 get 会抛出异常。在接下来的几章中我们会介绍一种更安全的方法来处理输入。

    Play 并没有限制说只能用样本类(case class) 来映射你的表单。只要正确设置了 applyunapply 方法,就能传入你想要的任何东西,比如元组可以使用 Forms.tuple 来映射,或是模型样本类(model case class)。但是,为表单指定一个样本类(case class)有以下这些好处:

    • 表单指定的样本类(case class)简单易用。样本类(case class)原本便被设计为存储数据的简易容器,即写即用,和表单功能天然匹配。
    • 表单指定的样本类(case class)功能强大。元组易于使用,但元组并不允许你自定义 apply 或是 unapply 方法,且只能通过元数(arity,如 _1, _2 等)来引用其中的数据。
    • 表单指定的样本类(case class)专为表单设计。重用模型样本类(model case class)确实很方便,但模型通常都含有一些额外的领域逻辑,甚至于一些数据持久化的细节,这些都会导致耦合过于紧密。另外,如果表单和模型并不是1:1严格映射的话,敏感数据必须被显示的忽略,以此来抵御攻击。

    定义表单的约束

    text 约束认定空字符串依然有效。也就是说 name 可以为空且不会报错,这显然不是我们想要的。一个保证 name 有值的方法是使用 nonEmptyText 约束。

    1. val userFormConstraints2 = Form(
    2. mapping(
    3. "name" -> nonEmptyText,
    4. "age" -> number(min = 0, max = 100)
    5. )(UserData.apply)(UserData.unapply)
    6. )

    如果表单的输入没有满足该表单的约束条件,则会报错:

    1. val boundForm = userFormConstraints2.bind(Map("bob" -> "", "age" -> "25"))
    2. boundForm.hasErrors must beTrue

    预置的约束定义在了中:

    • text:对应于 scala.String,可选参数为 minLengthmaxLength
    • nonEmptyText:对应于 scala.String,可选参数为 minLengthmaxLength
    • number:对应于 scala.Int,可选参数为 min, maxstrict
    • longNumber:对应于 scala.Long,可选参数为 minmax,和 strict
    • bigDecimal:参数为 precisionscale
    • datesqlDatejodaDate:对应于 java.util.Datejava.sql.Dateorg.joda.time.DateTime,可选参数为 patterntimeZone
    • jodaLocalDate:对应于 org.joda.time.LocalDate,可选参数为 pattern
    • email:对应于 scala.String,使用邮件正则表达式.
    • boolean:对应于 scala.Boolean
    • checked:对应于 scala.Boolean
    • optional:对应于 scala.Option

    定义特殊约束

    你可以使用在样本类(case class)中定义你自己的特殊约束。

    1. val userFormConstraints = Form(
    2. mapping(
    3. "name" -> text.verifying(nonEmpty),
    4. "age" -> number.verifying(min(0), max(100))
    5. )(UserData.apply)(UserData.unapply)
    6. )

    你也可以直接在样本类(case class)中定义特殊约束:

    1. def validate(name: String, age: Int) = {
    2. name match {
    3. case "bob" if age >= 18 =>
    4. Some(UserData(name, age))
    5. case "admin" =>
    6. Some(UserData(name, age))
    7. case _ =>
    8. None
    9. }
    10. }
    11. val userFormConstraintsAdHoc = Form(
    12. mapping(
    13. "name" -> text,
    14. )(UserData.apply)(UserData.unapply) verifying("Failed form constraints!", fields => fields match {
    15. case userData => validate(userData.name, userData.age).isDefined
    16. })
    17. )

    在 Action 中验证表单

    现在已经有了约束,我们需要在 action 中验证表单,并处理表单错误。

    我们使用 fold 方法来实现,它接收了两个函数:绑定失败时第一个会被调用,绑定成功则会调用第二个。

    1. userForm.bindFromRequest.fold(
    2. formWithErrors => {
    3. // binding failure, you retrieve the form containing errors:
    4. BadRequest(views.html.user(formWithErrors))
    5. },
    6. userData => {
    7. /* binding success, you get the actual value. */
    8. val newUser = models.User(userData.name, userData.age)
    9. val id = models.User.create(newUser)
    10. Redirect(routes.Application.home(id))
    11. }
    12. )

    绑定失败时,我们用了 BadRequest 来渲染页面,并将错误作为参数传入该页。如果使用了视图 helper (在下面讨论),那么任何绑定于一个元素的错误都会被渲染在该元素边上。

    绑定成功时,我们发出了一个 Redirect ,路由到 routes.Application.home,而不是渲染一个视图模板。这种模式称为 ,这是一种很好的防止重复提交的方式。

    注意:当使用了 flashing 或是在其他方法中用到了 flash 域,使用 “POST 后重定向” 是必需的,因为新的 cookie 只能在重定向 HTTP 请求后获取。

    有了表单之后,我们就能在中调用它。做法是在视图模板中将表单作为参数引入。对于 user.scala.html来说,该页的开头是这样的:

    1. @(userForm: Form[UserData])

    由于 user.scala.html 需要接收一个表单,在渲染 user.scala.html 时应先传入一个空的 userForm

    1. def index = Action {
    2. Ok(views.html.user(userForm))
    3. }

    首先是要创建表单标签。这是一个简单的视图 helper, 创建了一个,并根据你所传入的逆向路由设置了 actionmethod 的标签参数。

    你可以在 views.html.helper 包中找到多个输入 helper。传入一个表单域,他们就能显示出相应的 HTML 输入,设置值和约束,并在表单绑定失败时报错。

    注意:你可以在模板中使用 @import heler._ 来避免在调用 helper 时前置

    Play 有多个输入 helper,其中最有用的是:

    • form: 渲染表单
    • inputText: 渲染。
    • inputPassword: 渲染密码输入
    • inputDate: 渲染。
    • inputFile: 渲染文件输入
    • inputRadioGroup: 渲染。
    • select: 渲染下拉列表
    • textarea: 渲染。
    • checkbox: 渲染复选框
    • input: 渲染通用输入(需要显示声明参数)。

    form helper 中,你可以通过指定额外的参数,将其添加到生成的 HTML中:

    1. @helper.inputText(userForm("name"), 'id -> "name", 'size -> 30)

    上面这个通用的 input helper 能让你编写你想要的 HTML 代码:

    1. @helper.input(userForm("name")) { (id, name, value, args) =>
    2. <input type="text" name="@name" id="@id" @toHtmlArgs(args)>
    3. }

    注意:所有额外的参数都会被添加到生成的 HTML 中,除非他们以 _ 字符开头。以 _ 开头的参数是。

    对于复杂表单元素,你同样可以自定义视图 helper (在 views 页面中使用 scala 类)并自定义域构造器

    在视图模板中显示错误

    表单中的错误类型为 Map[String, FormError],其中 FormError中有:

    • key:应和表单域相同。
    • message:一段错误提示信息或是对应于该信息的键。
    • args:错误提示信息中用到的一组参数。

    绑定的表单实例中可以获得如下错误:

    • errors:返回所有错误,类型为 Seq[FormError]
    • globalErrors:返回所有错误,类型为没有键的 Seq[FormError]
    • error("name"):返回第一个绑定了该键的错误,类型为 Option[FormError]
    • errors("name"):返回所有绑定了该键的错误,类型为 Option[FormError]

    使用表单 helper 可以自动渲染绑定于某表单域的错误,如 @helper.inputText 的错误显示如下:

    1. <dl class="error" id="age_field">
    2. <dt><label for="age">Age:</label></dt>
    3. <dd><input type="text" name="age" id="age" value=""></dd>
    4. <dd class="error">This field is required!</dd>
    5. <dd class="error">Another error</dd>
    6. <dd class="info">Required</dd>
    7. <dd class="info">Another constraint</dd>
    8. </dl>

    没有绑定于任一键的全局错误(Global errors)不会有相应的 helper,且必须显示的定义在页面中:

    1. @if(userForm.hasGlobalErrors) {
    2. <ul>
    3. @for(error <- userForm.globalErrors) {
    4. <li>@error.message</li>
    5. }
    6. </ul>
    7. }

    使用元组做映射

    1. val userFormTuple = Form(
    2. tuple(
    3. "name" -> text,
    4. "age" -> number
    5. ) // tuples come with built-in apply/unapply
    6. )

    有时候使用元组会比定义样本类(case class)更方便,尤其在元组的元数(arity)很小时:

    1. val anyData = Map("name" -> "bob", "age" -> "25")
    2. val (name, age) = userFormTuple.bind(anyData).get

    使用 single 做映射

    元组只有在有多个值的时候才有用。如果只有一个表单域,可以用 Forms.single 来映射单一值,避免了使用样本类(case class)或是元组的额外开销:

    1. val singleForm = Form(
    2. single(
    3. "email" -> email
    4. )
    5. )
    1. val email = singleForm.bind(Map("email", "bob@example.com")).get

    有时候,你想要在表单中预置一些值,通常用于编辑数据:

    1. val filledForm = userForm.fill(UserData("Bob", 18))

    当配合视图 helper 使用时,该元素会填上预置的值:

    1. @helper.inputText(filledForm("name")) @* will render value="Bob" *@

    填值在 helper 需要一列值或是键值对时尤其有用,比如 selectinputRadioGroup helper 。使用 options 可以为 helper 填入列表,键值对和対值(pair)。

    嵌套值

    表单映射同样可以在已有的映射中使用 Forms.mapping 来实现嵌套值:

    1. case class AddressData(street: String, city: String)
    2. case class UserAddressData(name: String, address: AddressData)

    注意:当你使用这种方法来嵌套数据时,浏览器传来的表单值必须命名为 address.streetaddress.city

    1. @helper.inputText(userFormNested("address.street"))
    2. @helper.inputText(userFormNested("address.city"))

    重复值

    表单映射可以使用 Forms.listForms.seq 来定义重复值:

    1. case class UserListData(name: String, emails: List[String])
    1. val userFormRepeated = Form(
    2. mapping(
    3. "name" -> text,
    4. "emails" -> list(email)
    5. )(UserListData.apply)(UserListData.unapply)
    6. )

    当你这么使用重复数据时,浏览器传入的表单值必须被命名为 emails[0]emails[1]emails[2]

    使用 repeat helper 来生成和表单 emails 域相同数目的输入:

    1. @helper.inputText(myForm("name"))
    2. @helper.repeat(myForm("emails"), min = 1) { emailField =>
    3. @helper.inputText(emailField)
    4. }

    min 参数允许你在表单数据为空时,定义最少显示多少个表单域。

    可选值

    使用 Forms.optional 来定义可选值:

    1. case class UserOptionalData(name: String, email: Option[String])
    1. val userFormOptional = Form(
    2. mapping(
    3. "name" -> text,
    4. "email" -> optional(email)
    5. )(UserOptionalData.apply)(UserOptionalData.unapply)
    6. )

    这样做会输出一个 Option[A],在没有找到任何表单值的情况下则返回 None

    你可以使用 Form#fill 来初始化值:

    1. val filledForm = userForm.fill(User("Bob", 18))

    你也可以通过 Forms.default 来定义默认值:

    1. Form(
    2. mapping(
    3. "name" -> default(text, "Bob")
    4. "age" -> default(number, 18)
    5. )(User.apply)(User.unapply)
    6. )

    忽略值

    如果你想定义某个表单域为一个静态值,使用 Forms.ignored

    1. val userFormStatic = Form(
    2. mapping(
    3. "id" -> ignored(23L),
    4. "name" -> text,
    5. "email" -> optional(email)
    6. )(UserStaticData.apply)(UserStaticData.unapply)
    7. )

    这个例子演示了使用模型和控制器来处理一个实体:

    样本类 Contact

    1. case class Contact(firstname: String,
    2. lastname: String,
    3. company: Option[String],
    4. informations: Seq[ContactInformation])
    5. object Contact {
    6. def save(contact: Contact): Int = 99
    7. }
    8. case class ContactInformation(label: String,
    9. email: Option[String],
    10. phones: List[String])

    需要注意的是,Contact 里有一个包含了 ContactInformation 元素的 Seq 和一个含有 StringList。在这个例子中,我们组合了嵌套映射和重复映射(分别由 Forms.seqForms.list 定义)。

    1. val contactForm: Form[Contact] = Form(
    2. // Defines a mapping that will handle Contact values
    3. mapping(
    4. "firstname" -> nonEmptyText,
    5. "lastname" -> nonEmptyText,
    6. "company" -> optional(text),
    7. // Defines a repeated mapping
    8. "informations" -> seq(
    9. mapping(
    10. "label" -> nonEmptyText,
    11. "email" -> optional(email),
    12. "phones" -> list(
    13. text verifying pattern("""[0-9.+]+""".r, error="A valid phone number is required")
    14. )
    15. )(ContactInformation.apply)(ContactInformation.unapply)
    16. )
    17. )(Contact.apply)(Contact.unapply)
    18. )

    最后是表单提交:

    1. def saveContact = Action { implicit request =>
    2. contactForm.bindFromRequest.fold(
    3. formWithErrors => {
    4. BadRequest(views.html.contact.form(formWithErrors))
    5. },
    6. contact => {
    7. val contactId = Contact.save(contact)
    8. Redirect(routes.Application.showContact(contactId)).flashing("success" -> "Contact saved!")
    9. }
    10. }