This post is intended to summarize my efforts in creating a dynamic form with custom data objects in the Java Play Framework. The version I am working with is 2.3.7. This example proved to be very useful
I have a CSVData
class in my app, and I want to create instances of this class via form input. Here are the basic attributes of this class, with annotations removed for brevity:
public class CSVData extends VirtualSensor implements CSVWrappable {
// used by play to save it in the DB
public Long id;
private String dataFilePath = "";
private List<CSVField> fields;
private int linesToSkip;
private String separator=",";
The basic attributes like name
,description
, latitude
, longitude
, etc. are in the VirtualSensor
base class. One thing to note is the fields
attribute with an arbitraty list of CSVField
objects. Here are the attributes of the CSVField
class, again with annotations removed:
public class CSVField extends CSVAbstractField implements Comparable {
// used for storing in DB
public Long id;
public CSVData owner;
// attributes inherited from CSVAbStractField
// private String name;
// private String type;
I needed some way to store a name and an associated type, and consider this combination as one data item. I considered using a Map or Tuple object in the CSVData
class, but ultimately I chose to abstract it into it's own subclass. (Side note - since I ended up needing to make an APIField
class eventually, I made the common abstract class CSVAbstractField
, which is basic OOP design). The tricky part of this form is the fact that is should be able to accept an arbitray number of CSVField
s.
Nothing too special, just follow the basic format for creating a form for a specific class
Form<CSVData> formData = Form.form(CSVData.class).bindFromRequest();
CSVData data = formData.get();
With the appropriate setup, the nested, custom CSVField
data items will get binded to the fields
list.
First, I needed to create the view template for a single CSVField
. You can do this in its own file, or within the view template for the rest of the form, it doesn't matter. For the name
attribute I could use a simple text field, but for the type
attribute I wanted to limit the user to a few specific options -- thus I used a select box. Here's the scala view template code for those items. I also kept the "Remove" button at the end of the line. (some bootstrap classes removed for brevity)
@fieldGroup(csvField: Field, optionMap: List[String]) = {
<div class="multi-field">
<div class="gsn-data-input @if(csvField("name").hasErrors) {has-error}">
<input type="text"
class="form-control"
name="@csvField("name").name"
value="@csvField("name").value.getOrElse("")"/>
</div>
<div class="@if(csvField("type").hasErrors) {has-error}">
<select
class="form-control"
name="@csvField("type").name">
<option class="blank" value="">Select a type</option>
@for((optionName) <- optionMap) {
<option id="@optionName" value="@optionName">
@optionName
</option>
}
</select>
</div>
<button type="button" class="btn-danger remove-field">Delete</button>
<div class="@if(csvField("name").hasErrors || csvField("type").hasErrors) {has-error}">
<span class="help-block">@{csvField("name").error.map { error => error.message }}</span>
<span class="help-block">@{csvField("type").error.map { error => error.message }}</span>
</div>
</div>
}
Some things to note:
- The outer-most div of this template is the
multi-field
div from this example. I use themulti-field-wrapper
andmulti-fields
div elements elsewhere. - Since each
csvField
parameter passed in corresponds to aCSVField
Java object, you MUST specify thename
andtype
as@csvField("name")
and@csvField("type")
respectively.- Thus, to bind the appropriate HTML attributes to the
CSVField
attributes you must reference them like@csvField("name").name
or@csvField("name").type
. This took me forever to figure out, so maybe this will help others figure out similar issues - See my question on stackoverflow for the answer that helped me acheive this
- Thus, to bind the appropriate HTML attributes to the
Once I had a simple template containing the two data items I needed, I could use this in a @repeat
helper block. See this page for a brief example. Thus, I could write my helper like this
<fieldset>
<legend>CSV Fields</legend>
<div class="form-group">
<label class="control-label" for="fields">Column Name and Type</label>
<div class="multi-field-wrapper">
<div class="multi-fields">
@helper.repeat(csvForm("fields"), min=1) { csvField =>
@fieldGroup(csvField,
optionMap = gsnTypes)
}
</div>
<span class="help-block">
Enter the column names and respective types for the data items in the file
</span>
<button type="button" class="btn btn-success add-field">Add field</button>
</div>
</div>
</fieldset>
I had to surround the section with the appropriate divs, but the @helper.repeat
section ends up being quite simple, since we already defined the @fieldGroup
template. Thus, the play framework expects that there might be repeated items, and it makes sure to bind them to CSVField
objects in the list of fields
in CSVData
.
"The pain and suffering of javascript" - Rich Wolski.
Yes, you will need some javascript to have a dynamic form. A simple web search allowed me to find this example with some basic jQuery code and associated HTML. I showed some of this in the previous section. I chose to place this code in it's own file in the /public/javascripts/
folder of the application, and then included it in my main.scala.html
<script type="text/javascript" src="@routes.Assets.at("javascripts/dynamicFields.js")"></script>
Another key piece of getting this all to work correctly and have the data itms bind properly to the CSVData
object was what to do when there are some dynamically added elements that get deleted. From the play framework Forms example I found a renumber()
function. Here's what I ended up with for my form:
var renumber = function() {
$('.multi-field').each(function(i) {
$('input', this).each(function() {
$(this).attr('name', $(this).attr('name').replace(/fields\[.+?\]/g, 'fields[' + i + ']'));
//console.log('renumbered label ' +i);
})
$('select', this).each(function() {
$(this).attr('name', $(this).attr('name').replace(/fields\[.+?\]/g, 'fields[' + i + ']'));
// console.log('renumbered select box ' +i);
})
})
}
In my dynamicFields.js file I made sure to call this function whenever new fields are added or deleted. Thus, even if you added some new elements and then deleted some, the remaining ones would still get correctly added to the list as fields[0]
,fields[1]
,...,etc.
With this added, I now have all the pieces in place for a dynamic form! Here's what the user interaction looks like