Generating forms

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:

  • Mapping form data to user-defined models (such as case classes) or tuples
  • Validating the data entered to see if it meets the required constraints. This can be done for the all of the fields collectively, independently for each field, or both.
  • Filling in default values.

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

Note

The terms email and nonEmptyText, which we used while defining the form using mapping as well as the tuple, are predefined constraints and are also defined in the Form object. The following section discusses them in detail.

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.

Adding constraints on data

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)

text

Mapping[String]

minLength: 0,

maxLength: Int.MaxValue

nonEmptyText

Mapping[String]

minLength: 0,

maxLength: Int.MaxValue

number

Mapping[Int]

min: Int.MinValue,

max: Int.MaxValue,

strict: false

longNumber

Mapping[Long]

min: Long.MinValue,

max: Long.MaxValue, strict: false

bigDecimal

Mapping[BigDecimal]

precision,

scale

date

Mapping[java.util.Date]

pattern,

timeZone: java.util.TimeZone.getDefault

sqlDate

Mapping[java.sql.Date]

pattern,

timeZone: java.util.TimeZone.getDefault

jodaDate

Mapping[org.joda.time.DateTime]

pattern,

timeZone: org.joda.time.DateTimeZone.getDefault

jodaLocalDate

Mapping[org.joda.time.LocalDate]

pattern

email

Mapping[String]

 

boolean

Mapping[Boolean]

 

This table lists the constraints included in the second category:

Constraint

Results in

Required parameters and their type

optional

Mapping[Option[A]]

mapping: Mapping[A]

default

Mapping[A]

mapping: Mapping[A], value: A

list

Mapping[List[A]]

mapping: Mapping[A]

seq

Mapping[Seq[A]]

mapping: Mapping[A]

set

Mapping[Seq[A]]

mapping: Mapping[A]

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.

Handling errors

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

Form-field helpers

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.14.141.115