Play! Framework: Dynamic forms with repeated fields

Bart's picture
Tue, 07/09/2013 - 10:36 -- Bart

My second post once again concerns the Play! Framework, but who doesn't like to Play!?

For one of our clients I had to develop a webapplication for registering people who will attend an exhibition. On the exhibition they may attend workshops.
Sounds easy enough, make a form with the required data fields and a controller to read the form data and we're done.
However, this exhibition is a recurring thing and the number, dates, hours and description of these workshops will change with every new edition. Since I don't look forward to rewriting that form nor the handler method, I had to think of a way to dynamically import those workshops in my form.

What we need

First of all we need to externalize the data of the workshops. One easy way of doing this is making a JSON file that we can read from a controller, translate to an array of model classes and pass to a HTML template.
Now we want to loop over the array of workshops and display a checkbox to ask whether or not they will attend this workshop and a selection to be able to chose a time of attendence. The easiest way to loop would be with a for each, we won't do this because we need a selector in our array to be able to bind the workshop data to the appropriate workshop entry in our form.

Then we'll need a form definition in our controller, able to hold a variable number of workshops.
Enabling repeated fields is quite easy to do in Play! . (Documentation here)

How we achieve this

Assume we made a Scala case class called Workshop and we passed workshops : Array[Workshop] containing our workshops read from a JSON file to the template. Next we build the form as followed:

@for(i <- 0 to workshops.length - 1){
 @defining(i) {count =>
  @defining(workshops(i)){ ws =>
 
                @checkbox(
                registrationForm("workshops[" + count + "].going"),
                'id ->  box_id,
                'name -> "test",
                '_label -> None, '_text -> ws.display,
                '_showConstraints -> false
                )
 
                @select(
                registrationForm("workshops[" + count + "].hour"),
                options = hours,
                'id -> hour_box,
                '_label -> Messages("uur"),
                '_showConstraints -> false
                )
   }
  }
 }

Note: we need to define the variables we used in the for loop so we can reuse them in the HTML code.
Note: the options for the select are set by the variable hours which has to be of type Seq[(String, String)]

Next stop is the form definition. I'm posting the entire definition to make it a bit more clear how the mappings are nested.

 val registrationForm: Form[Registration] = Form(
 
    mapping(
      "person" ->
        mapping(
          "sexe" -> nonEmptyText,
          "name" -> nonEmptyText .verifying(Messages("error.nonCapital"), test => checkCapital(test)),
          "firstname" -> nonEmptyText .verifying(Messages("error.nonCapital"), test => checkCapital(test)),
          "language" -> nonEmptyText,
          "function" -> nonEmptyText .verifying("Max. 256 characters", test => checkLength(test)),
          "email" -> email
        )(Person.apply)(Person.unapply)
      ,
      "attendences" -> tuple(
        "day1" -> optional(boolean),
        "day2" -> optional(boolean),
        "day3" -> optional(boolean)
      ).verifying(Messages("error.attendences"), test => checkRegistrations(test)),
 
      "workshops" -> list[WorkshopForm] (
          mapping(
            "going" ->  optional(boolean),
            "hour" -> optional(text),
            "description" -> optional(text),
            "date" -> optional(text)
          )(WorkshopForm.apply)(WorkshopForm.unapply)
      )
    )
    {
     (person, attendences, workshops) => Registration(person, attendences, workshops)
    }
    {
     registration => Some(registration.person, registration.attendeces, registration.workshops)
    }
  )

Note: With registrationForm("workshops[" + count + "].going") in the checkbox definition we bind the boolean from the checkbox to the going field from the Workshop in the countth place in the List in the form definition.

And finally we can retrieve all the data input from the user in the submit method as followed:

  def submit() = Action {
    implicit request => {
      registrationForm.bindFromRequest.fold(
        errors => BadRequest(views.html.registration.registrationform(workshops)),
        registration => {
         // do whatever you want with your list of workshops here
         //registration.workshops is a list containing all the workshops with attendence and hour entered by the user
        }
      )
    }
  }

Conclusion

Allthough the Scala documentation is lacking a more indepth example how to use repeated values with our own model types rather than just Strings, it really isn't hard to figure out once we're acquinted with the mappings in a form definition.

See you next time and happy coding!