Search code examples
javaplayframeworkplayframework-2.2

How to name ManyToMany checkboxes on a Play 2.2.1 Form?


I have the following two Java classes:

Person.java

@Entity
public class Person extends Model {
@Id
public Long id;
@ManyToOne
public Category category = new Category();
@ManyToMany
@JoinTable(joinColumns = {@JoinColumn(name = "person_id")}, inverseJoinColumns = {@JoinColumn(name = "category_id")}, name = "category_person")
public List<Category> categories = new ArrayList<>();


}

Category.java

@Entity
public class Category extends Model {
@Id
public Long id;
public String description;
@OneToMany
public Person person;
@ManyToMany(mappedBy = "categories")
public List<Person> persons = new ArrayList<>();
}

In the controller I have

public static Result blank() {
    Form<Person> personForm = Form.form(Person.class);
    return ok(blank.render(personForm));
}

How do I structure the form to reflect the ManyToMany relationship between Person and Category via checkboxes?

I've tried

@Category.find.order("description asc").findList().map { c =>
                <input type="checkbox" name="categories[@c.id].id" value="@c.id"/> @c.description <br />
        }

But when I try to save the Person it tells me

[PersistenceException: ManyToMany bean models.Category@11ac3fd does not have an Id value.]

If I try

@Category.find.order("description asc").findList().map { c =>
                <input type="checkbox" name="categories[]" value="@c.id"/> @c.description <br />
        }

The validation fails and it tells me that categories[0] has an Invalid Value.

How do I fix this?


Solution

  • I've found the solution to the problem: I need to register a custom DataBinder. This will tell Play! (specifically the Form object) how to bind values entered into a form to an instance of a particular model.

    First start by registering the custom DataBinder in a Global.java file created in the app folder. Mine contains this code:

    public class Global extends GlobalSettings {
    
    public void onStart(Application app) {
        Formatters.register(Category.class, new Formatters.SimpleFormatter<Category>() {
            @Override
            public Category parse(String text, Locale locale) throws ParseException {
                return Category.find.byId(Long.parseLong(text));
            }
    
            @Override
            public String print(Category category, Locale locale) {
                return category.id+"";
            }
        });
    }
    

    Next, in my form I name the ManyToMany Categories checkboxes as follows:

    <input type="checkbox" name="categories[]" value="@category.id" /> @category.description <br />
    

    However, that's not all. I had to cater for the instance where the form had to be re-rendered with filled information e.g. User editing his profile. This means that I need to pre-check the checkboxes that the user selected when signing up. This is the code I used:

    @defining(personForm("categories").indexes.map { i => personForm("categories")("[" + i + "]").value }) { values =>
        @Category.find.all().map { category =>
            <input type="checkbox"
            name="categories[]"
            value="@category.id"
        @if(values.contains(Some(category.id.toString()))) { checked } /> @category.description <br />
        }
    }
    

    A bit complicated if you ask me, but this is the route I had to take as I wanted to go with passing a Form<> to the template and not a POJO.

    I hope this helps someone.