§フォームの送信処理
§概要
フォームの処理と送信は、あらゆるウェブアプリケーションにおいて重要な部分です。Play は、シンプルなフォームの処理を容易にし、複雑なフォームも可能にする機能を提供しています。
Play のフォーム処理アプローチは、データバインディングの概念に基づいています。POST リクエストからデータが入力されると、Play はフォーマットされた値を探して、それらを Form オブジェクトにバインドします。そこから、Play はバインドされたフォームを使用してデータでケースクラスに値を割り当て、カスタム検証を呼び出すなどを行うことができます。
通常、フォームは BaseController インスタンスから直接使用されます。しかし、Form の定義は、ケースクラスやモデルと完全に一致する必要はありません。入力処理のためのものであり、異なる POST に対して異なる Form を使用することは合理的です。
§インポート
フォームを使用するには、次のパッケージをクラスにインポートします。
import play.api.data._
import play.api.data.Forms._
検証と制約を使用するには、次のパッケージをクラスにインポートします。
import play.api.data.validation.Constraints._§フォームの基本
フォーム処理の基本について説明します。
- フォームの定義、
- フォームへの制約の定義、
- アクションでのフォームの検証、
- ビューテンプレートでのフォームの表示、
- 最後に、ビューテンプレートでのフォームの結果(またはエラー)の処理。
最終結果は次のようになります。
§フォームの定義
まず、フォームに含めたい要素を含むケースクラスを定義します。ここでは、ユーザーの名前と年齢を取得したいので、UserData オブジェクトを作成します。
case class UserData(name: String, age: Int)
object UserData {
def unapply(u: UserData): Option[(String, Int)] = Some((u.name, u.age))
}
ケースクラスを作成したので、次のステップは Form 構造を定義することです。`Form` の機能は、フォームデータをケースクラスのバインドされたインスタンスに変換することです。次のように定義します。
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(UserData.apply)(UserData.unapply)
)
Forms オブジェクトは mapping メソッドを定義します。このメソッドは、フォームの名前と制約を受け取ります。また、apply 関数と unapply 関数の 2 つの関数も受け取ります。UserData はケースクラスであるため、その apply メソッドと unapply メソッドをマッピングメソッドに直接接続できます。
**注記:** フォーム処理の実装方法により、単一のタプルまたはマッピングのフィールドの最大数は 22 です。フォームに 22 個を超えるフィールドがある場合は、リストまたはネストされた値を使用してフォームを分割する必要があります。
フォームは、Map が与えられると、バインドされた値を使用して UserData インスタンスを作成します。
val anyData = Map("name" -> "bob", "age" -> "21")
val userData = userForm.bind(anyData).get
しかし、ほとんどの場合、リクエストから提供されたデータを使用して、アクション内からフォームを使用します。Form には bindFromRequest が含まれており、これは暗黙のパラメータとしてリクエストを受け取ります。暗黙のリクエストを定義すると、bindFromRequest はそれを見つけます。
val userData = userForm.bindFromRequest().get
**注記:** ここで
getを使用する際の注意点があります。フォームがデータにバインドできない場合、getは例外をスローします。次のセクションで、入力処理のより安全な方法を示します。
フォームマッピングでは、ケースクラスの使用に限定されません。apply メソッドと unapply メソッドが適切にマッピングされている限り、Forms.tuple マッピングやモデルケースクラスなど、好きなものを渡すことができます。ただし、フォーム用にケースクラスを定義することにはいくつかの利点があります。
- **フォーム固有のケースクラスは便利です。** ケースクラスは、データのシンプルなコンテナとして設計されており、
Formの機能と自然に一致する、すぐに使える機能を提供します。 - **フォーム固有のケースクラスは強力です。** タプルは使いやすいですが、カスタム
applyメソッドまたはunapplyメソッドを使用できず、含まれるデータを参照できるのはアリティ (_1、_2など) のみです。 - **フォーム固有のケースクラスは、フォームに特化しています。** モデルケースクラスの再利用は便利ですが、モデルには多くの場合、緊密な結合につながる可能性のある追加のドメインロジックや永続化の詳細が含まれています。さらに、フォームとモデルの間に直接的な 1 対 1 のマッピングがない場合、パラメータ改ざん攻撃を防ぐために、機密性の高いフィールドを明示的に無視する必要があります。
§フォームへの制約の定義
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"))
boundForm.hasErrors must beTrue
すぐに使える制約は、Forms オブジェクトで定義されています。
text:scala.Stringにマップされ、オプションでminLengthとmaxLengthを使用します。nonEmptyText:scala.Stringにマップされ、オプションでminLengthとmaxLengthを使用します。number:scala.Intにマップされ、オプションでmin、max、strictを使用します。longNumber:scala.Longにマップされ、オプションでmin、max、strictを使用します。bigDecimal:precisionとscaleを使用します。date、sqlDate:java.util.Date、java.sql.Dateにマップされ、オプションでpatternとtimeZoneを使用します。email:メール正規表現を使用してscala.Stringにマップされます。boolean:scala.Booleanにマップされます。checked:scala.Booleanにマップされます。optional:scala.Optionにマップされます。
§アドホック制約の定義
validation パッケージを使用して、ケースクラスに独自のアドホック制約を定義できます。
val userFormConstraints = Form(
mapping(
"name" -> text.verifying(nonEmpty),
"age" -> number.verifying(min(0), max(100))
)(UserData.apply)(UserData.unapply)
)
ケースクラス自体にアドホック制約を定義することもできます。
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
}
)
)
独自の カスタム検証 を作成することもできます。詳細は カスタム検証 セクションを参照してください。
§アクションでのフォームの検証
制約を定義したので、アクション内でフォームを検証し、エラーを含むフォームを処理できます。
これは、fold メソッドを使用して行います。このメソッドは 2 つの関数を取ります。1 つ目はバインディングが失敗した場合に呼び出され、2 つ目はバインディングが成功した場合に呼び出されます。
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 を使用してページをレンダリングし、エラーを含む フォームをページのパラメータとして渡します。ビューヘルパー(後述)を使用する場合、フィールドにバインドされているエラーは、フィールドの横にページにレンダリングされます。
成功の場合、ここではビューテンプレートをレンダリングする代わりに、routes.Application.home へのルートを含む Redirect を送信しています。このパターンは POST 後のリダイレクト と呼ばれ、フォームの重複送信を防ぐ優れた方法です。
**注記:** フラッシュスコープ を使用した
flashingまたはその他のメソッドを使用する場合は、「POST 後のリダイレクト」が**必須**です。新しい cookie は、リダイレクトされた HTTP リクエストの後でのみ使用可能になるためです。
あるいは、リクエストのコンテンツをフォームにバインドする parse.form ボディパーサー を使用することもできます。
val userPost: Action[UserData] = Action(parse.form(userForm)) { implicit request =>
val userData = request.body
val newUser = models.User(userData.name, userData.age)
val id = models.User.create(newUser)
Redirect(routes.Application.home(id))
}
失敗の場合、デフォルトの動作は空の BadRequest レスポンスを返すことです。独自のロジックでこの動作をオーバーライドできます。たとえば、次のコードは、bindFromRequest と fold を使用した前のコードと完全に同等です。
val userPostWithErrors: Action[UserData] = Action(
parse.form(
userForm,
onErrors = (formWithErrors: Form[UserData]) => {
implicit val messages = messagesApi.preferred(Seq(Lang.defaultLang))
BadRequest(views.html.user(formWithErrors))
}
)
) { implicit request =>
val userData = request.body
val newUser = models.User(userData.name, userData.age)
val id = models.User.create(newUser)
Redirect(routes.Application.home(id))
}§ビューテンプレートでのフォームの表示
フォームを作成したら、テンプレートエンジンで使用できるようにする必要があります。これを行うには、フォームをビューテンプレートのパラメータとして含めます。user.scala.html の場合、ページ上部のヘッダーは次のようになります。
@(userForm: Form[UserData])(implicit messages: Messages)
user.scala.html は渡されたフォームを必要とするため、user.scala.html をレンダリングする際には、最初は空の userForm を渡す必要があります。
def index: Action[AnyContent] = Action { implicit request => Ok(views.html.user(userForm)) }
まず最初に、フォームタグを作成できるようになる必要があります。これは、フォームタグを作成し、渡されたリバースルートに従って`action`タグと`method`タグのパラメータを設定するシンプルなビューヘルパーです。
@helper.form(action = routes.Application.userPost) {
@helper.inputText(userForm("name"))
@helper.inputText(userForm("age"))
}
views.html.helperパッケージには、いくつかの入力ヘルパーがあります。これらにフォームフィールドを渡すと、対応するHTML入力が表示され、値、制約が設定され、フォームバインディングが失敗した場合はエラーが表示されます。
注記: テンプレートで
@import helper._を使用すると、ヘルパーに@helper.をプレフィックスする必要がなくなります。
いくつかの入力ヘルパーがありますが、最も役立つのは次のとおりです。
form: form要素をレンダリングします。inputText: テキスト入力要素をレンダリングします。inputPassword: パスワード入力要素をレンダリングします。inputDate: 日付入力要素をレンダリングします。inputFile: ファイル入力要素をレンダリングします。inputRadioGroup: ラジオボタン入力要素をレンダリングします。select: セレクト要素をレンダリングします。textarea: テキストエリア要素をレンダリングします。checkbox: チェックボックス要素をレンダリングします。input: 汎用的な入力要素をレンダリングします(明示的な引数が必要です)。
注記: これらのテンプレートのソースコードは、
views/helperパッケージの下でTwirlテンプレートとして定義されているため、パッケージ化されたバージョンは生成されたScalaソースコードに対応します。参考として、Github上のviews/helperパッケージを参照すると役立つ場合があります。
formヘルパーと同様に、生成されたHTMLに追加される追加のパラメータセットを指定できます。
@helper.inputText(userForm("name"), Symbol("id") -> "name", Symbol("size") -> 30)
上記で説明した汎用的なinputヘルパーを使用すると、目的のHTML結果をコーディングできます。
@helper.input(userForm("name")) { (id, name, value, args) =>
<input type="text" name="@name" id="@id" @toHtmlArgs(args)>
}
注記: **_**文字で始まる場合を除き、すべてに追加パラメータは生成されたHTMLに追加されます。**_**で始まる引数は、フィールドコンストラクタ引数のために予約されています。
複雑なフォーム要素の場合は、独自のカスタムビューヘルパー(viewsパッケージ内のScalaクラスを使用)とカスタムフィールドコンストラクタを作成することもできます。
§MessagesProviderをフォームヘルパーに渡す
上記のフォームヘルパー(input、checkboxなど)はすべて、MessagesProviderを暗黙的なパラメータとして受け取ります。フォームハンドラーは、リクエストで定義された言語にマップされたエラーメッセージを提供する必要があるため、MessagesProviderを受け取る必要があります。Messagesの詳細については、Messagesを使用した国際化ページを参照してください。
必要なMessagesProviderオブジェクトを渡すには、2つの方法があります。
§方法1:リクエストをMessagesに暗黙的に変換する
最初の方法は、コントローラーがplay.api.i18n.I18nSupportを拡張することです。これにより、注入されたMessagesApiが使用され、暗黙的なリクエストが暗黙的なMessagesに暗黙的に変換されます。
class MessagesController @Inject() (cc: ControllerComponents)
extends AbstractController(cc)
with play.api.i18n.I18nSupport {
import play.api.data.Form
import play.api.data.Forms._
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(views.html.UserData.apply)(views.html.UserData.unapply)
)
def index: Action[AnyContent] = Action { implicit request => Ok(views.html.user(userForm)) }
}
つまり、次のフォームテンプレートが解決されます。
@(userForm: Form[UserData])(implicit request: RequestHeader, messagesProvider: MessagesProvider)
@import helper._
@helper.form(action = routes.FormController.post) {
@CSRF.formField @* <- takes a RequestHeader *@
@helper.inputText(userForm("name")) @* <- takes a MessagesProvider *@
@helper.inputText(userForm("age")) @* <- takes a MessagesProvider *@
}§方法2:MessagesRequestを使用する
2番目の方法は、MessagesActionBuilderを依存性注入することです。これにより、MessagesRequestが提供されます。
// Example form injecting a messagesAction
class FormController @Inject() (messagesAction: MessagesActionBuilder, components: ControllerComponents)
extends AbstractController(components) {
import play.api.data.Form
import play.api.data.Forms._
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(views.html.UserData.apply)(views.html.UserData.unapply)
)
def index = messagesAction { implicit request: MessagesRequest[AnyContent] => Ok(views.html.messages(userForm)) }
def post = TODO
}
これは、フォームでCSRFを使用する場合、テンプレートでRequest(技術的にはRequestHeader)とMessagesオブジェクトの両方が使用可能である必要があるため便利です。MessagesRequestはMessagesProviderを拡張するWrappedRequestであるため、テンプレートで使用可能な暗黙的なパラメータは1つだけです。
通常はリクエストの本文は必要ないため、MessagesRequest[_]を入力する代わりに、MessagesRequestHeaderを渡すことができます。
@(userForm: Form[UserData])(implicit request: MessagesRequestHeader)
@import helper._
@helper.form(action = routes.FormController.post) {
@CSRF.formField @* <- takes a RequestHeader *@
@helper.inputText(userForm("name")) @* <- takes a MessagesProvider *@
@helper.inputText(userForm("age")) @* <- takes a MessagesProvider *@
}
MessagesActionBuilderをコントローラーに注入する代わりに、MessagesAbstractControllerを拡張してフォーム処理をコントローラーに組み込むことで、MessagesActionBuilderをデフォルトのActionにすることもできます。
// Form with Action extending MessagesAbstractController
class MessagesFormController @Inject() (components: MessagesControllerComponents)
extends MessagesAbstractController(components) {
import play.api.data.Form
import play.api.data.Forms._
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(views.html.UserData.apply)(views.html.UserData.unapply)
)
def index = Action { implicit request: MessagesRequest[AnyContent] => Ok(views.html.messages(userForm)) }
def post() = TODO
}§ビューテンプレートでのエラーの表示
フォームのエラーは、FormErrorを含むMap[String,FormError]の形式を取ります。
key: フィールドと同じである必要があります。message: メッセージまたはメッセージキー。args: メッセージに対する引数のリスト。
フォームエラーは、バインドされたフォームインスタンスで次のようにアクセスできます。
errors: すべてのエラーをSeq[FormError]として返します。globalErrors: キーのないエラーをSeq[FormError]として返します。error("name"): キーにバインドされた最初のエラーをOption[FormError]として返します。errors("name"): キーにバインドされたすべてのエラーをSeq[FormError]として返します。
フィールドに添付されたエラーは、フォームヘルパーを使用して自動的にレンダリングされるため、エラーのある@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>
フィールドに添付されていないエラーは、暗黙的なplay.api.i18n.Messagesインスタンスを受け取るerror.formatを使用して文字列に変換できます。
キーにバインドされていないグローバルエラーにはヘルパーがないため、ページに明示的に定義する必要があります。
@if(userForm.hasGlobalErrors) {
<ul>
@for(error <- userForm.globalErrors) {
<li>@error.format</li>
}
</ul>
}§タプルを使用したマッピング
フィールドにケースクラスの代わりにタプルを使用できます。
val userFormTuple = Form(
tuple(
"name" -> text,
"age" -> number
) // tuples come with built-in apply/unapply
)
特に低次元のタプルでは、ケースクラスを定義するよりもタプルを使用する方が便利です。
val anyData = Map("name" -> "bob", "age" -> "25")
val (name, age) = userFormTuple.bind(anyData).get§単一値を使用したマッピング
タプルは、複数の値がある場合にのみ可能です。フォームにフィールドが1つしかない場合は、Forms.singleを使用して、ケースクラスまたはタプルを使用することなく単一の値にマッピングします。
val singleForm = Form(
single(
"email" -> email
)
)
val emailValue = singleForm.bind(Map("email" -> "[email protected]")).get§値の入力
既存の値でフォームに入力したい場合があります。これは通常、データの編集に使用されます。
val filledForm = userForm.fill(UserData("Bob", 18))
ビューヘルパーと共にこれを使用すると、要素の値は入力された値で埋められます。
@helper.inputText(filledForm("name")) @* will render value="Bob" *@
selectおよびinputRadioGroupヘルパーなど、値のリストまたはマップを必要とするヘルパーでは、Fillは特に役立ちます。optionsを使用して、これらのヘルパーにリスト、マップ、ペアの値を設定します。
単一値のフォームマッピングでは、セレクトボックスで選択されたオプションを設定できます。
ドロップダウン
val addressSelectForm: Form[HomeAddressData] = Form(
mapping(
"street" -> text,
"city" -> text
)(HomeAddressData.apply)(HomeAddressData.unapply)
)
val selectedFormValues = HomeAddressData(street = "Main St", city = "London")
val filledForm = addressSelectForm.fill(selectedFormValues)
そして、これがペアのリストにオプションを設定するテンプレートで使用されると
@(
homeAddressData: Form[HomeAddressData],
cityOptions: List[(String, String)] = List("New York" -> "U.S. Office", "London" -> "U.K. Office", "Brussels" -> "E.U. Office")
)(implicit messages: Messages)
@helper.select(homeAddressData("city"), options = cityOptions) @* Will render the selected city to be the filled value *@
@helper.inputText(homeAddressData("street"))
入力された値は、ペアの最初の値に基づいてドロップダウンで選択されます。
この場合、「英国オフィス」がセレクトボックスに表示され、オプションの値は
「ロンドン」になります。
§ネストされた値
フォームマッピングでは、既存のマッピング内にForms.mappingを使用することで、ネストされた値を定義できます。
case class HomeAddressData(street: String, city: String)
object HomeAddressData {
def unapply(u: HomeAddressData): Option[(String, String)] = Some((u.street, u.city))
}
case class WorkAddressData(street: String, city: String)
object WorkAddressData {
def unapply(w: WorkAddressData): Option[(String, String)] = Some((w.street, w.city))
}
case class UserAddressData(name: String, homeAddress: HomeAddressData, workAddress: WorkAddressData)
object UserAddressData {
def unapply(u: UserAddressData): Option[(String, HomeAddressData, WorkAddressData)] =
Some(u.name, u.homeAddress, u.workAddress)
}
val userFormNested: Form[UserAddressData] = Form(
mapping(
"name" -> text,
"homeAddress" -> mapping(
"street" -> text,
"city" -> text
)(HomeAddressData.apply)(HomeAddressData.unapply),
"workAddress" -> mapping(
"street" -> text,
"city" -> text
)(WorkAddressData.apply)(WorkAddressData.unapply)
)(UserAddressData.apply)(UserAddressData.unapply)
)
注記: この方法でネストされたデータを使用する場合、ブラウザから送信されるフォーム値は、
homeAddress.street、homeAddress.cityなどのように名前付けする必要があります。
@helper.inputText(userFormNested("name"))
@helper.inputText(userFormNested("homeAddress.street"))
@helper.inputText(userFormNested("homeAddress.city"))
@helper.inputText(userFormNested("workAddress.street"))
@helper.inputText(userFormNested("workAddress.city"))§繰り返し値
フォームマッピングでは、Forms.listまたはForms.seqを使用して繰り返し値を定義できます。
case class UserListData(name: String, emails: List[String])
object UserListData {
def unapply(u: UserListData): Option[(String, List[String])] = Some((u.name, u.emails))
}
val userFormRepeated = Form(
mapping(
"name" -> text,
"emails" -> list(email)
)(UserListData.apply)(UserListData.unapply)
)
このような繰り返しデータを使用する場合、HTTPリクエストでフォーム値を送信するには2つの方法があります。まず、パラメータに空のブラケットペアをサフィックスとして追加します(「emails[]」など)。このパラメータは、標準的な方法で繰り返し使用できます(例:http://foo.com/request?emails[][email protected]&emails[][email protected])。あるいは、クライアントは配列の添字を使用してパラメータに一意の名前を付けることができます(例:emails[0]、emails[1]、emails[2]など)。このアプローチでは、入力シーケンスの順序を維持することもできます。
Playを使用してフォームHTMLを生成する場合、repeatヘルパーを使用して、フォームに含まれるemailsフィールドの入力数を生成できます。
@helper.inputText(myForm("name"))
@helper.repeat(myForm("emails"), min = 1) { emailField =>
@helper.inputText(emailField)
}
minパラメータを使用すると、対応するフォームデータが空の場合でも、最小数のフィールドを表示できます。
フィールドのインデックスにアクセスする場合は、代わりにrepeatWithIndexヘルパーを使用できます。
@helper.repeatWithIndex(myForm("emails"), min = 1) { (emailField, index) =>
@helper.inputText(emailField, Symbol("_label") -> ("email #" + index))
}§オプションの値
フォームマッピングでは、Forms.optionalを使用してオプションの値を定義することもできます。
case class UserOptionalData(name: String, email: Option[String])
object UserOptionalData {
def unapply(u: UserOptionalData): Option[(String, Option[String])] = Some((u.name, u.email))
}
val userFormOptional = Form(
mapping(
"name" -> text,
"email" -> optional(email)
)(UserOptionalData.apply)(UserOptionalData.unapply)
)
これは出力でOption[A]にマッピングされ、フォーム値が見つからない場合はNoneになります。
§デフォルト値
Form#fillを使用して、初期値でフォームに入力できます。
val filledForm = userForm.fill(UserData("Bob", 18))
または、Forms.defaultを使用して、数値のデフォルトマッピングを定義できます。
Form(
mapping(
"name" -> default(text, "Bob"),
"age" -> default(number, 18)
)(UserData.apply)(UserData.unapply)
)
デフォルト値は、次の場合にのみ使用されることに注意してください。
- リクエストなどからデータを使用して
Formに入力する場合 - そして、そのフィールドに対応するデータはありません。
フォーム作成時にデフォルト値は使用されません。
§無視される値
フィールドに静的な値を持つフォームが必要な場合は、Forms.ignored を使用してください。
val userFormStatic = Form(
mapping(
"id" -> ignored(23L),
"name" -> text,
"email" -> optional(email)
)(UserStaticData.apply)(UserStaticData.unapply)
)§フォームマッピングのカスタムバインダー
各フォームマッピングは、暗黙的に提供される Formatter[T] バインダーオブジェクトを使用し、入力された String 型のフォームデータとターゲットデータ型との間の変換を実行します。
case class UserCustomData(name: String, website: java.net.URL)
object UserCustomData {
def unapply(u: UserCustomData): Option[(String, java.net.URL)] = Some((u.name, u.website))
}
上記の例のように、java.net.URLのようなカスタム型にバインドするには、次のようなフォームマッピングを定義します。
val userFormCustom = Form(
mapping(
"name" -> text,
"website" -> of[URL]
)(UserCustomData.apply)(UserCustomData.unapply)
)
これを機能させるには、データのバインド/アンバインドを実行するための暗黙的な Formatter[java.net.URL] を使用可能にする必要があります。
import play.api.data.format.Formats._
import play.api.data.format.Formatter
implicit object UrlFormatter extends Formatter[URL] {
override val format: Option[(String, Seq[Any])] = Some(("format.url", Nil))
override def bind(key: String, data: Map[String, String]) = parsing(new URL(_), "error.url", Nil)(key, data)
override def unbind(key: String, value: URL) = Map(key -> value.toString)
}
String をターゲット型 T に変換する際に発生する例外を捕捉し、フォームフィールドのバインディングに FormError を登録するために、Formats.parsing 関数が使用されていることに注意してください。
§まとめ
エンティティの管理を行うモデルとコントローラーの例を以下に示します。
ケースクラス Contact を想定します。
case class Contact(
firstname: String,
lastname: String,
company: Option[String],
informations: Seq[ContactInformation]
)
object Contact {
def save(contact: Contact): Int = 99
def unapply(c: Contact): Option[(String, String, Option[String], Seq[ContactInformation])] =
Some(c.firstname, c.lastname, c.company, c.informations)
}
case class ContactInformation(label: String, email: Option[String], phones: List[String])
object ContactInformation {
def unapply(c: ContactInformation): Option[(String, Option[String], List[String])] =
Some(c.label, c.email, c.phones)
}
Contact には、ContactInformation 要素を含む Seq と、String の List が含まれています。この場合、ネストされたマッピングと繰り返しマッピング(それぞれ 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)
)
そして、このコードは、入力されたデータを使用して既存の連絡先がフォームにどのように表示されるかを示しています。
def editContact: Action[AnyContent] = Action { implicit request =>
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)))
}
最後に、フォーム送信ハンドラーは次のようになります。
def saveContact: Action[AnyContent] = Action { implicit request =>
contactForm
.bindFromRequest()
.fold(
formWithErrors => {
BadRequest(views.html.contact.form(formWithErrors))
},
contact => {
val contactId = Contact.save(contact)
Redirect(routes.Application.showContact(contactId)).flashing("success" -> "Contact saved!")
}
)
}次へ: CSRFからの保護
このドキュメントに誤りを見つけましたか?このページのソースコードは こちら で確認できます。ドキュメントガイドライン をお読みになった後、プルリクエストを送信していただければ幸いです。ご質問やアドバイスがありましたら、コミュニティフォーラム でコミュニティとの会話を始めてください。
