Using the @repeat helper in Play!

Piezo is a system to operate and manage a Quartz Scheduler cluster. It is made up of the Worker and the Admin.

One of the main features needed before an official public release of Piezo was the ability to add, edit, and delete jobs from the admin interface. The bulk of this work was nearly identical to what was done to add, edit, and delete triggers. The main difference was that jobs have a “Job Data Map” field that allows zero or more key/value pairs. The end result looks like this:

The add and delete links should work how you probably expect. When you click add, a new pair of key/value fields is added to the form. When you click delete, the associated existing pair is removed.

To get this kind of functionality in a Play! template it was necessary to use the repeat helper. For the purposes of this blog post, we’ll build a form that has the Job Data Map field shown above as the only field. Links to the complete code on github will be provided.

Controller

I’m assuming a controller already exists (Piezo’s Job controller is a class called Jobs) and that this controller has an action to create a new entry (getNewJobForm)and an action to edit an existing job ([getEditJob][getEditJob]).

With that, the first thing we need to do is set up a helper to build our form. In Piezo it is part of the JobFormHelper class.

def buildJobForm() = Form[JobDetail](
mapping("job-data-map" -> optional(list(mapping(
"key" -> text,
"value" -> text
)(DataMap.apply)(DataMap.unapply)))
)(jobFormApply)(jobFormUnapply)
)

In order to make “job-data-map” an optional list of pairs, a call to optional, list, and mapping is necessary. These allow for zero entries, more than one entry, and entry as pairs, respectively. The call to mapping needs an apply and unapply method given. Creating a case class to store the data gives you these functions automatically.

 

case class DataMap(key: String, value: String)

 

Now DataMap.apply and DataMap.unapply can be given as the second and third parameter to mapping. To use buildJobForm for a new entry simply call it and pass the returned form to your view. If you want to edit an entry, you need to create the form you’re going to pass to the view with a call that looks like this (assuming formData has been retrieved previously):

 

val form = buildJobForm.fill(formData)

View

In the view, we’ll assume the form is available as jobForm. This is where the Play! repeat helper comes in. The code to display the Job Data Map field looks like this:

 

<div>
@helper.repeat(jobForm(“job-data-map”), min = jobForm(“job-data-map”).indexes.length + 1) { dataMap =>
@dataMap(“key”).value.map { _ =>
@dataMap(“value”).value.map { _ =>
<div><a href=”#”>delete</a></div>
}
}

@helper.inputText(dataMap(“key”), ‘_label -> “Key”, ‘labelClass -> “col-sm-3 text-right”, ‘inputDivClass -> “col-sm-4”, ‘placeholder -> “Key”, ‘class -> “job-data-key form-control form-inline-control”)
@helper.inputText(dataMap(“value”), ‘_label -> “Value”, ‘labelClass -> “col-sm-3 text-right”, ‘inputDivClass -> “col-sm-4”, ‘placeholder -> “Value”, ‘class -> “job-data-value form-control form-inline-control”)

}

<div><a href=”#”>add</a></div>
</div>

 

The repeat helper is being used with two parameters here. The first is simply the data in the field. The second specifies the minimum number of key/value pairs to show. Above, min is set to:

 

jobForm(“job-data-map”).indexes.length + 1

 

This tells the form to always show one more than the total number of existing entries. Without the + 1, there wouldn’t be a blank field to entry new entries into.

The delete link shown in the screenshot above should only show up on pairs that have data. That is, we only want to be able to delete a pair if it existed previously. That’s what the next few lines do.

 

@dataMap(“key”).value.map { _ =>
@dataMap(“value”).value.map { _ =>
<div><a href=”#”>delete</a></div>
}
}

 

Since @dataMap(“key”).value is an Option, calling map on it is a simply way to conditionally execute code if the value is not None. So this block of code will only show the delete link if both @dataMap(“key”).value and @dataMap(“value”).value are not None.

The rest of the code in the repeat is simply outputting the two HTML fields for the current pair. After the repeat, we also put in an add link.

The delete and add links are then handled with JavaScript. The code here is relatively simple. On the respective events, you either insert a new key/value pair into the form, or remove an existing one. However, there is one gotcha. You have to make sure the names and ids of all the fields still in the form remain consistent and unique so that play can properly process the data in the form. A function called renumberDataMap was created for this purpose. It should be called after key/value pairs are added to or deleted from the form. When submitting the form, it is also important to first remove any empty key/value pairs and then call renumberDataMap one last time before you let the default submit action take place. In total, all of this work ended up being only 56 lines.

You can see a full example of the code in the Piezo project. The relevant files include:

 

1 Comment

  1. The javascript examples are missing, please can you add them back in? what are you using atm?

Your email address will not be published.