Documentation

You are viewing the documentation for the 2.8.17 release in the 2.8.x series of releases. The latest stable release series is 3.0.x.

§Handling form submission

§Overview

Form handling and submission is an important part of any web application. Play comes with features that make handling simple forms easy and complex forms possible.

Play’s form handling approach is based around the concept of binding data. When data comes in from a POST request, Play will look for formatted values and bind them to a Form object. From there, Play can use the bound form to value a case class with data, call custom validations, and so on.

Typically forms are used directly from a BaseController instance. However, Form definitions do not have to match up exactly with case classes or models: they are purely for handling input and it is reasonable to use a distinct Form for a distinct POST.

§Imports

To use forms, import the following packages into your class:

import play.api.data._
import play.api.data.Forms._

To make use of validation and constraints, import the following packages into your class:

import play.api.data.validation.Constraints._

§Form Basics

We’ll go through the basics of form handling:

The end result will look something like this:

§Defining a form

First, define a case class which contains the elements you want in the form. Here we want to capture the name and age of a user, so we create a UserData object:

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

Now that we have a case class, the next step is to define a Form structure. The function of a `Form is to transform form data into a bound instance of a case class, and we define it like follows:

val userForm = Form(
  mapping(
    "name" -> text,
    "age"  -> number
  )(UserData.apply)(UserData.unapply)
)

The Forms object defines the mapping method. This method takes the names and constraints of the form, and also takes two functions: an apply function and an unapply function. Because UserData is a case class, we can plug its apply and unapply methods directly into the mapping method.

Note: Maximum number of fields for a single tuple or mapping is 22 due to the way form handling is implemented. If you have more than 22 fields in your form, you should break down your forms using lists or nested values.

A form will create UserData instance with the bound values when given a Map:

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

But most of the time you’ll use forms from within an Action, with data provided from the request. Form contains bindFromRequest, which will take a request as an implicit parameter. If you define an implicit request, then bindFromRequest will find it.

val userData = userForm.bindFromRequest.get

Note: There is a catch to using get here. If the form cannot bind to the data, then get will throw an exception. We’ll show a safer way of dealing with input in the next few sections.

You are not limited to using case classes in your form mapping. As long as the apply and unapply methods are properly mapped, you can pass in anything you like, such as tuples using the Forms.tuple mapping or model case classes. However, there are several advantages to defining a case class specifically for a form:

§Defining constraints on the form

The text constraint considers empty strings to be valid. This means that name could be empty here without an error, which is not what we want. A way to ensure that name has the appropriate value is to use the nonEmptyText constraint.

val userFormConstraints2 = Form(
  mapping(
    "name" -> nonEmptyText,
    "age"  -> number(min = 0, max = 100)
  )(UserData.apply)(UserData.unapply)
)

Using this form will result in a form with errors if the input to the form does not match the constraints:

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

The out of the box constraints are defined on the Forms object:

§Defining ad-hoc constraints

You can define your own ad-hoc constraints on the case classes using the validation package.

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

You can also define ad-hoc constraints on the case classes themselves:

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
      }
  )
)

You also have the option of constructing your own custom validations. Please see the custom validations section for more details.

§Validating a form in an Action

Now that we have constraints, we can validate the form inside an action, and process the form with errors.

We do this using the fold method, which takes two functions: the first is called if the binding fails, and the second is called if the binding succeeds.

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))
  }
)

In the failure case, we render the page with BadRequest, and pass in the form with errors as a parameter to the page. If we use the view helpers (discussed below), then any errors that are bound to a field will be rendered in the page next to the field.

In the success case, we’re sending a Redirect with a route to routes.Application.home here instead of rendering a view template. This pattern is called Redirect after POST, and is an excellent way to prevent duplicate form submissions.

Note: “Redirect after POST” is required when using flashing or other methods with flash scope, as new cookies will only be available after the redirected HTTP request.

Alternatively, you can use the parse.form body parser that binds the content of the request to your form.

val userPost = 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))
}

In the failure case, the default behaviour is to return an empty BadRequest response. You can override this behaviour with your own logic. For instance, the following code is completely equivalent to the preceding one using bindFromRequest and fold.

val userPostWithErrors = 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))
}

§Showing forms in a view template

Once you have a form, then you need to make it available to the template engine. You do this by including the form as a parameter to the view template. For user.scala.html, the header at the top of the page will look like this:

@(userForm: Form[UserData])(implicit messages: Messages)

Because user.scala.html needs a form passed in, you should pass the empty userForm initially when rendering user.scala.html:

def index = Action { implicit request =>
  Ok(views.html.user(userForm))
}

The first thing is to be able to create the form tag. It is a simple view helper that creates a form tag and sets the action and method tag parameters according to the reverse route you pass in:

@helper.form(action = routes.Application.userPost) {
  @helper.inputText(userForm("name"))
  @helper.inputText(userForm("age"))
}

You can find several input helpers in the views.html.helper package. You feed them with a form field, and they display the corresponding HTML input, setting the value, constraints and displaying errors when a form binding fails.

Note: You can use @import helper._ in the template to avoid prefixing helpers with @helper.

There are several input helpers, but the most helpful are:

Note: The source code for each of these templates is defined as Twirl templates under views/helper package, and so the packaged version corresponds to the generated Scala source code. For reference, it can be useful to see the views/helper package on Github.

As with the form helper, you can specify an extra set of parameters that will be added to the generated Html:

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

The generic input helper mentioned above will let you code the desired HTML result:

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

Note: All extra parameters will be added to the generated Html, unless they start with the _ character. Arguments starting with _ are reserved for field constructor arguments.

For complex form elements, you can also create your own custom view helpers (using scala classes in the views package) and custom field constructors.

§Passing MessagesProvider to Form Helpers

The form helpers above – input, checkbox, and so on – all take MessagesProvider as an implicit parameter. The form handlers need to take MessagesProvider because they need to provide error messages mapped to the language defined in the request. You can see more about Messages in the Internationalization with Messages page.

There are two ways to pass in the MessagesProvider object required.

§Option One: Implicitly Convert Request to Messages

The first way is to make the controller extend play.api.i18n.I18nSupport, which makes use of an injected MessagesApi, and will implicitly convert an implicit request to an implicit 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 { implicit request =>
    Ok(views.html.user(userForm))
  }
}

This means that the following form template will be resolved:

@(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 *@
}

§Option Two: Use MessagesRequest

The second way is to dependency inject a MessagesActionBuilder, which provides a 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
    }

This is useful because to use CSRF with forms, both a Request (technically a RequestHeader) and a Messages object must be available to the template. By using a MessagesRequest, which is a WrappedRequest that extends MessagesProvider, only a single implicit parameter needs to be made available to templates.

Because you typically don’t need the body of the request, you can pass MessagesRequestHeader, rather than typing 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 *@
}

Rather than inject MessagesActionBuilder into your controller, you can also make MessagesActionBuilder be the default Action by extending MessagesAbstractController to incorporate form processing into your controllers.

// 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
}

§Displaying errors in a view template

The errors in a form take the form of Map[String,FormError] where FormError has:

The form errors are accessed on the bound form instance as follows:

Errors attached to a field will render automatically using the form helpers, so @helper.inputText with errors can display as follows:

<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>

Errors that are not attached to a field can be converted to a string with error.format, which takes an implicit play.api.i18n.Messages instance.

Global errors that are not bound to a key do not have a helper and must be defined explicitly in the page:

@if(userForm.hasGlobalErrors) {
  <ul>
  @for(error <- userForm.globalErrors) {
    <li>@error.format</li>
  }
  </ul>
}

§Mapping with tuples

You can use tuples instead of case classes in your fields:

val userFormTuple = Form(
  tuple(
    "name" -> text,
    "age"  -> number
  ) // tuples come with built-in apply/unapply
)

Using a tuple can be more convenient than defining a case class, especially for low arity tuples:

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

§Mapping with single

Tuples are only possible when there are multiple values. If there is only one field in the form, use Forms.single to map to a single value without the overhead of a case class or tuple:

val singleForm = Form(
  single(
    "email" -> email
  )
)

val emailValue = singleForm.bind(Map("email" -> "[email protected]")).get

§Fill values

Sometimes you’ll want to populate a form with existing values, typically for editing data:

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

When you use this with a view helper, the value of the element will be filled with the value:

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

Fill is especially helpful for helpers that need lists or maps of values, such as the select and inputRadioGroup helpers. Use options to value these helpers with lists, maps and pairs:

A single valued form mapping can set the selected options in a select
dropdown:

val addressSelectForm: Form[AddressData] = Form(
  mapping(
    "street" -> text,
    "city"   -> text
  )(AddressData.apply)(AddressData.unapply)
)
val selectedFormValues = AddressData(street = "Main St", city = "London")
val filledForm         = addressSelectForm.fill(selectedFormValues)

And when this is used in a template that sets the options to a list of pairs

@(
addressData: Form[AddressData], 
cityOptions: List[(String, String)] = List("New York" -> "U.S. Office", "London" -> "U.K. Office", "Brussels" -> "E.U. Office")
)(implicit messages: Messages)
@helper.select(addressData("city"), options = cityOptions) @* Will render the selected city to be the filled value *@
@helper.inputText(addressData("street"))

The filled value will be selected in the dropdown based on the first value of the pair.
In this case, the U.K. Office will be displayed in the select and the option’s value
will be London.

§Nested values

A form mapping can define nested values by using Forms.mapping inside an existing 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)
)

Note: When you are using nested data this way, the form values sent by the browser must be named like address.street, address.city, etc.

@helper.inputText(userFormNested("name"))
@helper.inputText(userFormNested("address.street"))
@helper.inputText(userFormNested("address.city"))

§Repeated values

A form mapping can define repeated values using Forms.list or Forms.seq:

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

When you are using repeated data like this, there are two alternatives for sending the form values in the HTTP request. First, you can suffix the parameter with an empty bracket pair, as in “emails[]”. This parameter can then be repeated in the standard way, as in http://foo.com/request?emails[][email protected]&emails[][email protected]. Alternatively, the client can explicitly name the parameters uniquely with array subscripts, as in emails[0], emails[1], emails[2], and so on. This approach also allows you to maintain the order of a sequence of inputs.

If you are using Play to generate your form HTML, you can generate as many inputs for the emails field as the form contains, using the repeat helper:

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

The min parameter allows you to display a minimum number of fields even if the corresponding form data are empty.

If you want to access the index of the fields you can use the repeatWithIndex helper instead:

@helper.repeatWithIndex(myForm("emails"), min = 1) { (emailField, index) =>
    @helper.inputText(emailField, Symbol("_label") -> ("email #" + index))
}

§Optional values

A form mapping can also define optional values using Forms.optional:

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

This maps to an Option[A] in output, which is None if no form value is found.

§Default values

You can populate a form with initial values using Form#fill:

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

Or you can define a default mapping on the number using Forms.default:

Form(
  mapping(
    "name" -> default(text, "Bob"),
    "age"  -> default(number, 18)
  )(UserData.apply)(UserData.unapply)
)

Keep in mind that default values are used only when:

  1. Populating the Form from data, for example, from the request
  2. And there is no corresponding data for the field.

The default value is not used when creating the form.

§Ignored values

If you want a form to have a static value for a field, use Forms.ignored:

val userFormStatic = Form(
  mapping(
    "id"    -> ignored(23L),
    "name"  -> text,
    "email" -> optional(email)
  )(UserStaticData.apply)(UserStaticData.unapply)
)

§Custom binders for form mappings

Each form mapping uses an implicitly provided Formatter[T] binder object that performs the conversion of incoming String form data to/from the target data type.

case class UserCustomData(name: String, website: java.net.URL)

To bind to a custom type like java.net.URL in the example above, define a form mapping like this:

val userFormCustom = Form(
  mapping(
    "name"    -> text,
    "website" -> of[URL]
  )(UserCustomData.apply)(UserCustomData.unapply)
)

For this to work you will need to make an implicit Formatter[java.net.URL] available to perform the data binding/unbinding.

import play.api.data.format.Formatter
import play.api.data.format.Formats._
implicit object UrlFormatter extends Formatter[URL] {
  override val format                                       = 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)
}

Note the Formats.parsing function is used to capture any exceptions thrown in the act of converting a String to target type T and registers a FormError on the form field binding.

§Putting it all together

Here’s an example of what a model and controller would look like for managing an entity.

Given the case class Contact:

case class Contact(
    firstname: String,
    lastname: String,
    company: Option[String],
    informations: Seq[ContactInformation]
)

object Contact {
  def save(contact: Contact): Int = 99
}

case class ContactInformation(label: String, email: Option[String], phones: List[String])

Note that Contact contains a Seq with ContactInformation elements and a List of String. In this case, we can combine the nested mapping with repeated mappings (defined with Forms.seq and Forms.list, respectively).

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)
)

And this code shows how an existing contact is displayed in the form using filled data:

def editContact = 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)))
}

Finally, this is what a form submission handler would look like:

def saveContact = 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!")
    }
  )
}

Next: Protecting against CSRF