Forms are important in situations where the application requires input from users, for example, in the case of registration, login, search, and so on.
Play provides helpers to generate a form and wrapper classes to translate the form data into a Scala object.
Now, we'll build a user registration form using the form helper provided by Play:
@helper.form(action = routes.Application.newUser) { <label>Email Id <input type="email" name="email" tabindex="1" required="required"> </label> <label>Password <input type="password" name="password" tabindex="2" required="required"> </label> <input type="submit" value="Register" type="button"> }
Here, @helper.form
is a template provided by Play, which is defined as follows:
@(action: play.api.mvc.Call, args: (Symbol,String)*)(body: => Html) <form action="@action.url" method="@action.method" @toHtmlArgs(args.toMap)> @body </form>
We can also provide other parameters for the form
element as a tuple of Symbol
and String
. The Symbol
component will become the parameter and its corresponding String
component will be set as its value in the following way:
@helper.form(action = routes.Application.newUser, 'enctype -> "multipart/form-data")
The resulting HTML will now be as follows:
<form action="/register" method="POST" enctype="multipart/form-data">...</form>
This is possible due to the toHtmlArgs
helper method, defined as follows:
def toHtmlArgs(args: Map[Symbol, Any]) = play.twirl.api.Html(args.map({ case (s, None) => s.name case (s, v) => s.name + "="" + play.twirl.api.HtmlFormat.escape(v.toString).body + """ }).mkString(" "))
Now, when we try to register a user, the request body within the action will be:
AnyContentAsFormUrlEncoded(Map(email -> ArrayBuffer([email protected]), password -> ArrayBuffer(password)))
If the enctype
parameter is specified, and the request is parsed as multipartformdata
, the request body will be as follows:
MultipartFormData(Map(password -> List(password), email -> List([email protected])),List(),List(),List())
Instead of defining custom methods to take a map so that it results in a corresponding model, we can use the play.api.data.Form
form data helper object.
The form object aids in the following:
We might need to have the form data translated into credentials; in this case, the class is defined as follows:
case class Credentials(loginId: String, password: String)
We can update the registration view to use the form object in the following way:
@import models.Credentials @(registerForm: Form[Credentials])(implicit flash: Flash) @main("Register") { <div id="signup" class="form"> @helper.form(action = routes.Application.newUser, 'enctype -> "multipart/form-data") { <hr/> <div> <label>Email Id <input type="email" name="loginId" tabindex="1" required="required"> </label> <label>Password <input type="password" name="password" tabindex="2" required="required"> </label> </div> <input type="submit" value="Register"> <hr/> Existing User?<a href="@routes.Application.login()">Login</a> <hr/> } </div> }
Now we define a form that creates a credentials object from a form with the loginId
and password
field:
val signupForm = Form( mapping( "loginId" -> email, "password" -> nonEmptyText )(Credentials.apply)(Credentials.unapply)
We now define the following actions:
def register = Action { implicit request => Ok(views.html.register(signupForm)).withNewSession } def newUser = Action(parse.multipartFormData) { implicit request => signupForm.bindFromRequest().fold( formWithErrors => BadRequest(views.html.register(formWithErrors)), credentials => Ok ) }
The register
and newUser
methods are mapped to GET /register
and POST /register
, respectively. We pass the form in the view so that when there are errors in form validation, they are shown in the view along with the form fields. We will see this in detail in the following section.
Let us now see how this works. When we fill the form and submit, the call goes to the newUser
action. The signupForm
is a form and is defined as follows:
case class Form[T](mapping: Mapping[T], data: Map[String, String], errors: Seq[FormError], value: Option[T]) { … }
We used the constructor, which is defined in its companion object:
def apply[T](mapping: Mapping[T]): Form[T] = Form(mapping, Map.empty, Nil, None)
The mapping
method can accept a maximum of 18 arguments. Forms can also be defined using the tuple
method, which will in turn call the mapping
method:
def tuple[A1, A2](a1: (String, Mapping[A1]), a2: (String, Mapping[A2])): Mapping[(A1, A2)] = mapping(a1, a2)((a1: A1, a2: A2) => (a1, a2))((t: (A1, A2)) => Some(t))
Using this, instead of mapping for signupForm
, you will get this code:
val signupForm = Form( tuple( "loginId" -> email, "password" -> nonEmptyText ) )
When defining forms that have a single field, we can use the single
method since the tuple is not defined for a single field, as shown here:
def single[A1](a1: (String, Mapping[A1])): Mapping[(A1)] = mapping(a1)((a1: A1) => (a1))((t: (A1)) => Some(t))
The method called in our action is signupForm.bindRequestFrom
. The bindRequestFrom
method takes an implicit request and fills the form with the form data in the request.
Once we have filled the form, we need to check if it has any errors or not. This is where the fold
method comes in handy, as defined here:
def fold[R](hasErrors: Form[T] => R, success: T => R): R = value match { case Some(v) if errors.isEmpty => success(v) case _ => hasErrors(this) }
The variable errors and value are from the form constructor. The type of error is Seq[FormError]
, whereas that of the value is Option[T]
.
We then map the result from fold
to BadRequest(formWithErrors)
if the form has errors. If it doesn't, we can continue with the handled data submitted through the form.
It is a common requirement to restrict the form data entered by users with one rule or another. For example, checking to ensure that the name field data does not contain digits, the age is less than 18 years, if an expired card is being used to complete the transaction, and so on. Play provides default constraints, which can be used to validate the field data. Using these constraints, we can define a form easily as well as restrict the field data in some ways, as shown here:
mapping( "userName" -> nonEmptyText, "emailId" -> email, "password" -> nonEmptyText(minLength=8,maxLength=15) )
The default constraints can be broadly classified into two categories: the ones that define a simple Mapping[T]
, and the ones that consume Mapping[T]
and result in Mapping[KT]
, as shown here:
mapping( "userName" -> nonEmptyText, "interests" -> list(nonEmptyText) )
In this example, Mapping[String]
is transformed into Mapping[List[String]]
.
There are two other constraints that do not fall into either category. They are ignored
and checked
.
The ignored
constraint can be used when we do need mapping from the user data for that field. For example, fields such as login time or logout time should be filled in by an application and not the user. We could use mapping
in this way:
mapping( "loginId" -> email, "password" -> nonEmptyText, "loginTime" -> ignored(System.currentTimeMillis()) )
The checked
constraint can be used when we need to ensure that a particular checkbox has been selected by the user. For example, accepting terms and conditions of the organization, and so on, in signupForm
:
mapping( "loginId" -> email, "password" -> nonEmptyText, "agree" -> checked("agreeTerms") )
The constraints of the first category are listed in this table:
Constraint |
Results in |
Additional properties and their default values (if any) |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
precision, scale |
|
|
pattern,
|
|
|
pattern,
|
|
|
pattern,
|
|
|
pattern |
|
| |
|
|
This table lists the constraints included in the second category:
Constraint |
Results in |
Required parameters and their type |
---|---|---|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
In addition to these field constraints, we can also define ad hoc and/or custom constraints on a field using the verifying
method.
An instance might arise where an application lets users choose their userName
, which can only consist of numbers and alphabet. To ensure that this rule is not broken, we can define an ad hoc constraint:
mapping( "userName" -> nonEmptyText(minLength=5) verifying pattern("""[A-Za-z0-9]*""".r, error = "only digits and alphabet are allowed in userName" )
Or, we can define a custom constraint using the Constraint
case class:
val validUserName = """[A-Za-z0-9]*""".r val userNameCheckConstraint: Constraint[String] = Constraint("contraints.userName")({ text => val error = text match { case validUserName() => Nil case _ => Seq(ValidationError("only digits and alphabet are allowed in userName")) } if (error.isEmpty) Valid else Invalid(error) }) val userNameCheck: Mapping[String] = nonEmptyText(minLength = 5).verifying(passwordCheckConstraint)
We can use this in a form definition:
mapping( "userName" -> userNameCheck )
Note that nonEmpty
, minLength
, maxLength
, min
, max
, pattern
, and email
are predefined constraints. They are defined in the play.api.data.validation
trait. The available constraints can be used as references when defining custom constraints.
What happens when one or more constraints has been broken in the form that has been submitted? The bindFromRequest
method creates a form with errors, which we earlier referred to as formWithErrors
.
For each violated constraint, an error is saved. An error is represented by FormError
, defined as follows:
case class FormError(key: String, messages: Seq[String], args: Seq[Any] = Nil)
The key
is the name of the field where a constraint was broken, message
is its corresponding error message and args
are the arguments, if any, used in the message. In the case of constraints defined in multiple fields, the key is an empty string and such errors are termed globalErrors
.
The errors in a form for a specific field can be accessed through the errors
method, defined as:
def errors(key: String): Seq[FormError] = errors.filter(_.key == key)
For example:
registerForm.errors("userName")
Alternatively, to access only the first error, we can use the error
method instead. It is defined as follows:
def error(key: String): Option[FormError] = errors.find(_.key == key)
Now, how do we access globalErrors
(that is, an error from a constraint defined in multiple fields together)?
We can use the form's globalErrors
method, which is defined as follows:
def globalErrors: Seq[FormError] = errors.filter(_.key.isEmpty)
If we want just the first globalError
method, we can use the globalError
method. It is defined as follows:
def globalError: Option[FormError] = globalErrors.headOption
When we use the form-field helpers, field-specific errors are mapped to the field and displayed if they're present. However, if we are not using the form helpers, we will need to display the errors, as shown here:
<label>Password <input type="password" name="password" tabindex="2" required="required"> </label> @registerForm.errors("password").map{ er => <p>@er.message</p>}
The globalErrors
method needs to be added to the view explicitly, as shown here:
@registerForm.globalErrors.map{ er => <p>@er.message</p>}
In the previous example, we used the HTML code for the form
fields, but we can also do this using the form
field helpers provided by Play. We can update our view,@import models.Credentials
, as shown here:
@(registerForm: Form[Credentials])(implicit flash: Flash) @main("Register") { @helper.form(action = routes.Application.newUser, 'enctype -> "multipart/form-data") { @registerForm.globalErrors.map { error => <p class="error"> @error.message </p> } @helper.inputText(registerForm("loginId"), 'tabindex -> "1", '_label -> "Email ID", 'type -> "email", 'required -> "required", '_help -> "A valid email Id") @helper.inputPassword(registerForm("password"), 'tabindex -> "2", 'required -> "required", '_help -> "preferable min.length=8") <input type="submit" value="Register"> <hr/> Existing User?<a href="@routes.Application.login()">Login</a> } }
Let's see how this works. The helper inputText
is a view defined as follows:
@(field: play.api.data.Field, args: (Symbol,Any)*)(implicit handler: FieldConstructor, lang: play.api.i18n.Lang) @inputType = @{ args.toMap.get('type).map(_.toString).getOrElse("text") } @input(field, args.filter(_._1 != 'type):_*) { (id, name, value, htmlArgs) => <input type="@inputType" id="@id" name="@name" value="@value" @toHtmlArgs(htmlArgs)/> }
It uses the input helper internally, which is also a view and can be defined as follows:
@(field: play.api.data.Field, args: (Symbol, Any)* )(inputDef: (String, String, Option[String], Map[Symbol,Any]) => Html)(implicit handler: FieldConstructor, lang: play.api.i18n.Lang) @id = @{ args.toMap.get('id).map(_.toString).getOrElse(field.id) } @handler( FieldElements( id, field, inputDef(id, field.name, field.value, args.filter(arg => !arg._1.name.startsWith("_") && arg._1 != 'id).toMap), args.toMap, lang ) )
Both the form
field helpers use an implicit FieldConstructor
. This field constructor is responsible for the HTML rendered. By default, defaultFieldConstructor
is forwarded. It is defined as follows:
@(elements: FieldElements) <dl class="@elements.args.get('_class) @if(elements.hasErrors) {error}" id="@elements.args.get('_id).getOrElse(elements.id + "_field")"> @if(elements.hasName) { <dt>@elements.name(elements.lang)</dt> } else { <dt><label for="@elements.id">@elements.label(elements.lang)</label></dt> } <dd>@elements.input</dd> @elements.errors(elements.lang).map { error => <dd class="error">@error</dd> } @elements.infos(elements.lang).map { info => <dd class="info">@info</dd> } </dl>
So, if we wish to change the layouts for our form
fields, we can define a custom FieldConstructor
and pass it to the form
field helpers, as shown here:
@input(contactForm("name"), '_label -> "Name", '_class -> "form-group", '_size -> "100") { (id, name, value, htmlArgs) => <input class="form-control" type="text" id="@id" name="@name" value="@value" @toHtmlArgs(htmlArgs)/> }
This section attempts to explain how the form helper works; for more examples, refer to the Play Framework documentation at http://www.playframework.com/documentation/2.3.x/ScalaForms.
3.14.141.115