Skip to content

Instantly share code, notes, and snippets.

@kjorg50
Last active July 21, 2017 17:48
Show Gist options
  • Save kjorg50/874704bdd8f80ab12591 to your computer and use it in GitHub Desktop.
Save kjorg50/874704bdd8f80ab12591 to your computer and use it in GitHub Desktop.
Java Play Framework - Dynamic, Custom Forms

Play Framework Dynamic Forms

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

Motivation

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 CSVFields.

Step One - The Controller

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.

Step Two - The View

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 the multi-field-wrapper and multi-fields div elements elsewhere.
  • Since each csvField parameter passed in corresponds to a CSVField Java object, you MUST specify the name and type 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

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.

Javascript

"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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment