§处理表单提交
§概述
表单处理和提交是任何 Web 应用程序的重要组成部分。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))
}
现在我们有了 case 类,下一步是定义一个 Form
结构。Form
的功能是将表单数据转换为 case 类的绑定实例,我们定义如下
val userForm = Form(
mapping(
"name" -> text,
"age" -> number
)(UserData.apply)(UserData.unapply)
)
The Forms 对象定义了 mapping
方法。此方法接受表单的名称和约束,以及两个函数:apply
函数和 unapply
函数。因为 UserData 是一个 case 类,所以我们可以直接将它的 apply
和 unapply
方法插入到 mapping 方法中。
注意:由于表单处理的实现方式,单个元组或映射的最大字段数为 22。如果您的表单中包含超过 22 个字段,则应使用列表或嵌套值来分解表单。
当给定一个 Map 时,表单将创建一个包含绑定值的 UserData
实例
val anyData = Map("name" -> "bob", "age" -> "21")
val userData = userForm.bind(anyData).get
但大多数情况下,您将在 Action 中使用表单,并使用请求提供的 data。 Form
包含 bindFromRequest
,它将接受一个请求作为隐式参数。如果您定义了一个隐式请求,那么 bindFromRequest
将找到它。
val userData = userForm.bindFromRequest().get
注意:这里使用
get
有一个问题。如果表单无法绑定到数据,那么get
将抛出异常。我们将在接下来的几节中展示一种更安全的处理输入的方法。
您不限于在表单映射中使用 case 类。只要 apply
和 unapply
方法正确映射,您就可以传入任何您喜欢的,例如使用 Forms.tuple
映射的元组或模型 case 类。但是,专门为表单定义一个 case 类有几个优点
- 特定于表单的 case 类很方便。Case 类被设计为简单的数据容器,并提供开箱即用的功能,这些功能与
Form
功能自然匹配。 - 特定于表单的 case 类功能强大。元组使用起来很方便,但不允许自定义
apply
或unapply
方法,并且只能通过元数 (_1
、_2
等) 来引用包含的数据。 - 特定于表单的 case 类专门针对表单。重用模型 case 类可能很方便,但模型通常包含额外的领域逻辑,甚至持久化细节,这会导致紧耦合。此外,如果表单和模型之间没有直接的 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 包 在 case 类上定义自己的临时约束。
val userFormConstraints = Form(
mapping(
"name" -> text.verifying(nonEmpty),
"age" -> number.verifying(min(0), max(100))
)(UserData.apply)(UserData.unapply)
)
您也可以在 case 类本身定义临时约束。
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
}
)
)
您还可以选择构建自己的自定义验证。有关更多详细信息,请参阅 自定义验证 部分。
§在 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 渲染页面,并将带有错误的表单作为参数传递给页面。如果我们使用视图助手(下面会讨论),那么绑定到字段的任何错误都将在页面中字段旁边渲染。
在成功的情况下,我们在这里发送一个带有 routes.Application.home
路由的 Redirect
,而不是渲染视图模板。这种模式被称为 POST 重定向,是防止重复提交表单的绝佳方法。
注意:使用
flashing
或其他 flash 范围 方法时,必须使用“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
: 渲染一个 表单 元素。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
对象。
§选项一:隐式地将请求转换为 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 *@
}
§选项二:使用 MessagesRequest
第二种方法是依赖注入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
),只需要向模板提供一个隐式参数。
因为通常不需要请求主体,所以可以传递 MessagesRequestHeader
,而不是键入 MessagesRequest[_]
@(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
}
§在视图模板中显示错误
表单中的错误采用 Map[String,FormError]
的形式,其中 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>
未附加到字段的错误可以使用 error.format
转换为字符串,它需要一个隐式的 play.api.i18n.Messages 实例。
未绑定到键的全局错误没有助手,必须在页面中显式定义
@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
§使用单个进行映射
元组只有在有多个值时才有可能。如果表单中只有一个字段,请使用 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
助手。使用 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 请求中发送表单值有两种选择。首先,您可以用空方括号对作为后缀添加参数,例如“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
,例如,从请求中填充 - 并且该字段没有相应的 data。
创建表单时不会使用默认值。
§忽略的值
如果您希望表单对某个字段使用静态值,请使用 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)
}
请注意,Formats.parsing
函数用于捕获将 String
转换为目标类型 T
时抛出的任何异常,并在表单字段绑定上注册一个 FormError
。
§综合示例
以下是一个用于管理实体的模型和控制器的示例。
给定 case 类 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 攻击
发现此文档中的错误?此页面的源代码可以在 此处 找到。阅读完 文档指南 后,请随时贡献拉取请求。有疑问或建议要分享?请访问 我们的社区论坛,与社区进行交流。