How to make custom form binders in play!

Back

How to make custom type binders for Play! Forms

If you've used play! then you know that it comes with a number of form helpers that help define the types of data in a form, such as nonEmptyText, boolean, email, and a number of others. These, as far as the type goes, map to normal primitives like String, Long, Int, and in the case of the date helpers, to Date, sql.Date, and DateTime.

There are Mappings, and then there are Formats. They perform similar methods within play: binding values to forms and form fields. So what's the difference between the two? A Formatter is what's looked for when one calls of[T] when setting the type of a form element. Like so:

import java.util.UUID

val myForm = new Form[(String, UUID)](
    tuple(
        "str" -> text,
        "uuid" -> of[UUID]
    )
)

of will look for an implicit Format for the type given to use when trying to bind and unbind the field uuid. This is as simple as looking at the trait documentation for Formatters and implementing it for the type:

import play.api.data.FormError
import play.api.data.format.Formatter

implicit val UUIDFormat = new Formatter[UUID] {
    def bind(key: String, data: Map[String, String]): Either[Seq[FormError], UUID] = {
        data.get(key).map(UUID.fromString(_)).toRight(Seq(FormError(key, "forms.invalid.uuid", data.get(key).getOrElse(key))))
    }
    def unbind(key: String, value: UUID): Map[String, String] = Map(key -> value.toString)
}

The bind method is used to transform text data from the submitted form into the required type. The result of the bind method is an Either, with the failed left projection indicating a FormError has occured. The arguments to the FormError are similar to the arguments to defining a custom Constraint in play, the forms.invalid.uuid indicates what message from the Message's API will be loaded if it's in scope, and the arguments after the hard-coded string correspond to any number of arguments that will be interpolated by the messages parameter substitution.*

The unbind method, unsurprisingly, does the opposte of the bind statement in that we convert from our type to a string so that we can pass the form field to any templates requiring us to.

*In a messages file, if you set something like, forms.invalid.uuid={0} is invalid, then you're going to see the first argument given to the FormError where that {0} is.

A Mapping had a bit more methods than just bind and unbind, however they're very easily composable, allowing the creation of custom type mappings be leveraging existing ones. For example, to create a UUID Mapping we can leverage the existing text mapping:

def uuid: Mapping[UUID] = {
    text.transform(UUID.fromString _, _.toString)
}

Though, this isn't as safe as it could be, we could be safer if we verified that the text was a valid UUID first via a constraint:

val validUUID = Constraint[String]("forms.constraint.uuid") { str =>
    Try(UUID.fromString(str)) match {
        case Success(uuid) => Valid
        case Failure(e) => Invalid(ValidationError("forms.invalid.uuid", str))
    }
}

def uuid: Mapping[UUID] = {
    text.verifying(validUUID).transform(UUID.fromString _, _.toString)
}

Once we have the mapping defined, we can use it in a form like:

val myForm = new Form[(String, UUID)](
    tuple(
        ...,
        "uuid" -> uuid,
        ...
    )
)

And we'll get a useful error message if we can't bind the UUID for any reason. All without implicits because we've explicitly provided a mapping. From my understanding, this is the main difference between the two. As, calling of will actually result in a FieldMapping being created.

So, which one should you prefer? Creating a Mapping, or a Formatter? I think that this is a matter of preference and your concern over compilation times. It's a given that when the compiler has to resolve an implicit type during it's "proofing" of the code that this will take longer than if it doesn't. So if you're working on a very large project, and compilation time is an issue, I'd suggest favoring Mappings rather than using of. I also find defining a Constraint and then using verifying to be more understandable, just from a semantic point of view. After all, when I think about binding values to and from a form, I don't think "format", I think "mapping".

Other Posts

comments powered by Disqus