At the Guardian we build a lot of our software using the Play web framework. For example, we use it for the frontend that powers the Guardian website. Because of this, I spend a lot of time helping new Guardian developers learn how to use Play, and one thing that often seems to stump people is adding forms to an application.
Forms are a fundamental part of many web applications, so you would think Play would make it as easy as possible to build them. But learning how to add a form often involves a perfect storm of advanced Play and Scala concepts, confusing compiler errors and scarce documentation, all of which can leave a beginner reeling.
I don’t like it when tutorials and official documentation only show things going smoothly. So in this post I’ll deliberately make a few mistakes, bump into compiler errors, explain what they mean and show you how to solve them.
I’ve made a sample project so you can refer to the source code and run the application while you read through the rest of the post. It’s available on GitHub. I recommend you read it one commit at a time, starting at the oldest.
Our sample Play application has only one page. It shows a list of widgets, with names and prices. Our goal is to add a form to the bottom of the page, allowing users to type in a name and a price and create a new widget. Without further ado, let’s get started!
Add an action to handle the form post
First let’s add a new action to our Application controller to handle the data that is posted via the form.
class Application extends Controller {
// ...
def createWidget = TODO
}
This action will parse the form data and create the new widget, but we’ll leave it as a “TODO” for now. TODO is a handy Play feature that lets you quickly create a placeholder for an action that you will implement later.
Create a form definition and pass it to the template
Next we’ll create a definition of our form. This specifies the form fields and their types, as well as how to construct a Widget from form data and vice versa. You can also specify extra constraints on the values of fields, for example you could restrict the minimum and maximum length of values that you will accept for a given field.
The reasons for defining forms like this are twofold:
-
to let Play take care of validing user input for you
-
to allow auto-generation of HTML for form fields using form helpers, which we will see in a second
You can put the form definition wherever you like, but I chose to put mine in the controller’s companion object.
object Application {
val createWidgetForm = Form(
mapping(
"name" -> text,
"price" -> number
)(Widget.apply)(Widget.unapply)
)
}
We’ll also add a new parameter to the template:
@(widgets: Seq[Widget], form: Form[Widget])
And pass a blank form from the controller to the template:
class Application extends Controller {
// ...
def listWidgets = Action {
// Pass an unpopulated form to the template
Ok(views.html.listWidgets(widgets.toSeq, Application.createWidgetForm))
}
}
Create a form using plain HTML
We’ve passed the form from the controller to the template, but we won’t use it for anything just yet. Let’s keep things simple for now, writing our form in HTML in the template.
<form method="POST" action="/widgets">
<h2>TODO form fields</h2>
<button type="submit">Create widget</button>
</form>
If you start the Play application and open http://localhost:9000/widgets in your browser, you should see a (very ugly) form.
Introduce a form helper and reverse routing
When we wrote our form in plain HTML, we hardcoded the form’s action to “/widgets”. But we can do better than this. If we use Play’s so-called ‘reverse routing’ functionality, we can avoid hardcoding the URL and thus make our code more resilient to refactoring.
Reverse routing is possible because Play parses the application’s routes file and compiles a Scala object called the reverse router. This allows us to use a controller action (e.g. routes.Application.createWidget) as a key to lookup the corresponding URL. Because the reverse router is automatically generated from the routes file, it gets updated whenever you change any routes, so it is guaranteed not to go stale like a hardcoded URL.
We can use reverse routing if we replace our plain HTML