1. 程式人生 > >Handling form submission(處理表單提交)

Handling form submission(處理表單提交)

一、概述

表單的處理和提交是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, 可選擇性附加minLengthmaxLength.
  • nonEmptyText: map 為scala.String, 可選擇性附加 minLengthmaxLength.
  • number: map 為scala.Int, 可選擇性附加min,max, 和 strict.
  • longNumber: map 為scala.Long, 可選擇性附加 min, max, 和 strict.
  • date: map 為java.util.Date, 可選擇性附加patterntimeZone.
  • 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,但是最有用的有:

就表單helper而言,你可以為生成的Html確定一個額外的引數集合:
@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", "[email protected]")).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("[email protected]"), List("01.23.45.67.89", "98.76.54.32.10")
      ),
      ContactInformation(
        "Professional", Some("[email protected]"), List("01.23.45.67.89")
      ),
      ContactInformation(
        "Previous", Some("[email protected]"), List()
      )
    )
  )
  Ok(views.html.contact.form(contactForm.fill(existingContact)))
}