03 December, 2014

ScalaForms. Form with password confirmation

Problem:

Create register form with password confirmation and validation messages next to field as shown on image below.

Solution:
There are many solutions to this problem. I chose tuple transformation. It provides me error on tuple level, instead of globalErrors.
First step is to create model object: User
case class User(login: String, password: String)
then define form in controller for User case class
  val registrationForm: Form[User] = Form(
    mapping(
      "login" -> nonEmptyText.verifying("user.registration.userExists", User.findByLogin(_) == None),
      "passwordWithConfirmation" -> tuple(
        "password" -> text(minLength = 6, maxLength = 30)
          .verifying("user.registration.passwordOneDigit", name => {
          (name + "A").split("\\d").size > 1
        }),
        "confirmation" -> text
      ).verifying("user.registration.passwordDontMatch", verifyPassword(_)).transform(
      { case (password, confirmation) => password},
      (password: String) => ("", "")
      )
    )(User.apply)(User.unapply)
  )

  def verifyPassword(passwordWithConfirmation: Tuple2[String, String]): Boolean = passwordWithConfirmation match {
    case (password: String, confirmation: String) => password.equals(confirmation)
  }
The form contains passwordWithConfirmation which is tuple mapping for password and confirmation inputs. For this tuple I added verification rule, checking if both fields are equal. Last thing to do is to convert tuple into data that will be stored in User entity. For this purpose transform function is used. The transformation works both ways:
  • from form to object { case (password, confirmation) => password}
  • from object to form (password: String) => ("", "")
Second conversion returns tuple of empty strings, because I don't want password and its confirmation to be loaded into form.
Play template for the Form object may look like this:
@(registrationForm: Form[model.User])(implicit flash: Flash)

@import helper.twitterBootstrap._
@import helper._

@main {
    <h2>@Messages("user.registration.registerAccount")</h2>

    @helper.form(action=routes.UserCtrl.register) {
        <fieldset>
            @inputText(registrationForm("login"))
            @inputPassword(registrationForm("passwordWithConfirmation.password"))
            @inputPassword(registrationForm("passwordWithConfirmation.confirmation"))

        </fieldset>
        <input class="btn btn-primary" type="submit" value="@Messages("user.registration.register")" />
    }
}

Last thing that I need to do is to show error message for unmatched passwords. This will not work automatically, because there is no passwordWithConfirmation element in my form.
            @if(registrationForm.hasErrors) {
                @registrationForm.error("passwordWithConfirmation") match {
                    case Some(err:FormError) => {<span class="help-inline">@Messages(err.message)</span>}
                    case _ => {}
                }
            }