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
, 可選擇性附加minLength
和maxLength
.nonEmptyText
: map 為scala.String
, 可選擇性附加minLength
和maxLength
.number
: map 為scala.Int
, 可選擇性附加min
,max
, 和strict
.longNumber
: map 為scala.Long
, 可選擇性附加min
,max
, 和strict
.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,但是最有用的有:
就表單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)))
}