
Learn through the super-clean Baeldung Pro experience:
>> Membership and Baeldung Pro.
No ads, dark-mode and 6 months free of IntelliJ Idea Ultimate to start with.
Last updated: December 25, 2024
The Play framework comes with Twirl as the default template engine. Twirl is Scala-based and easy to learn for developers who have worked with Scala and HTML.
In this tutorial, we’ll demonstrate how to create and submit forms using Twirl within a Play application.
Forms are Scala objects defined in the application code and passed through template parameters to the Twirl templates to be compiled and rendered. The apply methods of the Form helper object are a convenient way to create a Form object:
val form: Form[SimpleForm] = Form(
mapping(
"i" -> number,
"active" -> boolean,
"msg" -> text
)(SimpleForm.apply)(SimpleForm.unapply)
)
Now that we’ve created a form with three fields, including a number, a boolean, and a text field, it’s time to use it within a template:
@(simpleForm: Form[SimpleForm])(implicit messages: Messages)
@form(action = routes.FormController.formPost()) {
@inputText(simpleForm("i"))
@inputText(simpleForm("active"))
@inputText(simpleForm("msg"))
}
Subsequently, a controller method will expose the template:
def simpleForm: Action[AnyContent] = Action { implicit request =>
Ok(views.html.Baeldung.FormTemplate(SimpleForm.form))
}
Finally, the controller’s formPost function handles the form submission:
def formPost(): Action[AnyContent] = Action { implicit request =>
val form = SimpleForm.form.bindFromRequest().get
Ok(form.toString)
}
Now that all the building blocks are in place, let’s fetch the form to inspect the HTML code:
$ curl localhost:9000/simple-form | awk '!/^[[:space:]]*$/'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 732 100 732 0 0 75046 0 --:--:-- --:--:-- --:--:-- 81333
<form action="/simple-form" method="POST" >
<dl class=" " id="i_field">
<dt><label for="i">i</label></dt>
<dd>
<input type="text" id="i" name="i" value="" />
</dd>
<dd class="info">Numeric</dd>
</dl>
<dl class=" " id="active_field">
<dt><label for="active">active</label></dt>
<dd>
<input type="text" id="active" name="active" value="" />
</dd>
<dd class="info">format.boolean</dd>
</dl>
<dl class=" " id="msg_field">
<dt><label for="msg">msg</label></dt>
<dd>
<input type="text" id="msg" name="msg" value="" />
</dd>
</dl>
<input type="submit" value="Submit">
</form>
It’s important to note that the awk post-processing we’ve added removes unnecessary empty lines added by the template engine and it’s used to shorten the curl command’s output. In other words, the awk part of the command is completely optional and can be omitted.
Out of the box, Twirl forms support various field types such as number, boolean, date, sqldate, localdate, uuid, and more. Specifically, the Forms helper object contains the mapping code for these types.
To demonstrate some of the various supported field types, we’ve created the MultipleFieldsForm:
val form: Form[MultipleFieldsForm] = Form(
mapping(
"i" -> number,
"pwd" -> text,
"active" -> boolean,
"msg" -> text,
"date"-> date,
"uuid" -> uuid,
"favMovie" -> text,
"favDrink" -> text
)(MultipleFieldsForm.apply)(MultipleFieldsForm.unapply)
)
object Movies {
val list = List(
"pulpFiction" -> "Pulp Fiction",
"inception" -> "Inception",
"theMatrix" -> "The Matrix",
"titanic" -> "Titanic",
)
}
object Drinks {
val list = List(
"vodka" -> "Vodka",
"tequila" -> "Tequila",
"whisky" -> "Whisky",
"wine" -> "Wine",
"beer" -> "Beer"
)
}
By using template input helpers, the generated form prevents user input errors by adding type validations:
@(multipleFieldsForm: Form[MultipleFieldsForm])(implicit messages: Messages)
@form(action = routes.FormController.multipleFieldsFormPost()) {
@inputText(multipleFieldsForm("i"))
@inputPassword(multipleFieldsForm("pwd"))
@checkbox(multipleFieldsForm("active"))
@inputText(multipleFieldsForm("msg"))
@inputDate(multipleFieldsForm("date"))
@inputText(multipleFieldsForm("uuid"))
@inputRadioGroup(multipleFieldsForm("favMovie"), MultipleFieldsForm.Movies.list)
@select(multipleFieldsForm("favDrink"), MultipleFieldsForm.Drinks.list)
<input type="submit" value="Submit">
}
Twirl also supports custom fields, as long as a Mapping implementation is present. For this purpose, we’ll introduce the UnitMeasurement case class:
case class UnitMeasurement(quantity: Int, unit: String)
In other words, we’ll add support for fields like “1 pound” and “3 meters”. Initially, a Mapping of this custom type is required, namely the unitMeasurementMapping:
def unitMeasurementMapping: Mapping[UnitMeasurement] =
Forms.of[UnitMeasurement]
Another addition we have to include to make our custom type work is the implicit Formatter since the Forms.of method requires it:
implicit def binder: Formatter[UnitMeasurement] =
new Formatter[UnitMeasurement] {
override def bind(
key: String,
data: Map[String, String]
): Either[Seq[FormError], UnitMeasurement] = Formats.parsing(
d => UnitMeasurement.fromString(d),
"The format is (\\d*)(\\s)(\\D*)- example: \"1 pound\"",
Nil
)(key, data)
override def unbind(
key: String,
value: UnitMeasurement
): Map[String, String] = Map(key -> s"${value.quantity} ${value.unit}")
}
Essentially, the Formatter[UnitMeasurement] trait defines the “UnitMeasurement to String” and “String to UnitMeasurement” transformations. Another important building block is the helper method Formats.parsing that provides every Form-related functionality, requiring only a “String to T” arrow function.
The UnitMeasurement.fromString method implements the actual transformation:
object UnitMeasurement {
implicit val unitMeasurementFormWrites: Writes[UnitMeasurement] =
Json.writes[UnitMeasurement]
private val pattern = "(\\d*)(\\s)(\\D*)".r
def fromString(str: String): UnitMeasurement = {
val matches = pattern.findAllIn(str)
if (matches.hasNext) {
val List(number, space, quantity) = matches.subgroups
UnitMeasurement(number.toInt, quantity)
} else {
throw new RuntimeException(s"Incorrect data: $str")
}
}
}
Now, we’re ready to use this new custom Mapping in a form definition:
val form: Form[ComplexFormCustomField] = Form(
mapping(
"i" -> number,
"active" -> boolean,
"msg" -> text,
"measurement" -> Measure.unitMeasurementMapping
)(ComplexFormCustomField.apply)(ComplexFormCustomField.unapply)
)
While Twirl provides field-type validation out of the box, additional constraints can be applied to the form fields. The Forms helper object provides a variety of helper functions that return constrained mappings:
val form: Form[InputFormWithConstraints] = Form(
mapping(
"i" -> number(min = 10, max = 20),
"msg" -> text(minLength = 3, maxLength = 12),
"msgOpt" -> optional(text),
"email" -> email,
"birthday" -> date
)(InputFormWithConstraints.apply)(InputFormWithConstraints.unapply)
)
Another neat feature is that we’re able to define custom constraints by leveraging the verifying method of the Mapping trait:
val form: Form[InputFormWithConstraints] = Form(
mapping(
"email" -> email.verifying(minLength(15)),
"birthday" -> date.verifying(
"Not 18 years old",
d => d.toInstant.isBefore(Instant.now().minus(18, ChronoUnit.YEARS))
)
)(InputFormWithCustomConstraints.apply)(
InputFormWithCustomConstraints.unapply
)
)
In the above example, an email is valid only if it’s at least 15 characters long. Also, a birthday date is considered valid if the date is older than 18 years.
Field constraints enable us to provide meaningful feedback to the users regarding unsuccessful form submissions. To do so, the controller methods that handle form submission requests need to be written differently:
def formWithConstraintsPost(): Action[AnyContent] = Action { implicit request =>
InputFormWithConstraints.form
.bindFromRequest()
.fold(
{ formWithError =>
BadRequest(views.html.Baeldung.FormTemplateWithConstraints(formWithError))
},
{ data => Ok(Json.toJson(data)) }
)
}
In case of erroneous form submissions, the formWithConstraintsPost method wraps the template initialized with the rejected form in a BadRequest. Let’s post an empty form to inspect the server response:
$ curl localhost:9000/form-with-constraints -d "" | awk '!/^[[:space:]]*$/'
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 1655 100 1655 0 0 183k 0 --:--:-- --:--:-- --:--:-- 202k
<form action="/form-with-constraints" method="POST" >
<dl class=" error" id="i_field">
<dt><label for="i">i</label></dt>
<dd>
<input type="text" id="i" name="i" value="" />
</dd>
<dd class="error">This field is required</dd>
<dd class="info">Minimum value: 10</dd>
<dd class="info">Maximum value: 20</dd>
<dd class="info">Numeric</dd>
</dl>
<dl class=" error" id="msg_field">
<dt><label for="msg">msg</label></dt>
<dd>
<input type="text" id="msg" name="msg" value="" />
</dd>
<dd class="error">This field is required</dd>
<dd class="info">Minimum length: 3</dd>
<dd class="info">Maximum length: 12</dd>
</dl>
<dl class=" " id="msgOpt_field">
<dt><label for="msgOpt">msgOpt</label></dt>
<dd>
<input type="text" id="msgOpt" name="msgOpt" value="" />
</dd>
</dl>
<dl class=" error" id="email_field">
<dt><label for="email">email</label></dt>
<dd>
<input type="text" id="email" name="email" value="" />
</dd>
<dd class="error">This field is required</dd>
<dd class="info">Email</dd>
</dl>
<dl class=" error" id="birthday_field">
<dt><label for="birthday">birthday</label></dt>
<dd>
<input type="date" id="birthday" name="birthday" value="" />
</dd>
<dd class="error">This field is required</dd>
<dd class="info">Date ('yyyy-MM-dd')</dd>
</dl>
<input type="submit" value="Submit">
</form>
As expected, the HTML response is our form with error element additions based on the field constraints we defined earlier.
In this article, we’ve described how to create, render, submit, and validate HTML forms with Play framework. The Twirl engine is a solid choice for projects using Scala and Play if there’s no alternative way of handling forms.