Spring 3 MVC: One-To-Many Within a Dynamic Form (Add/Remove on Create/Update)

Spring 3 MVC: one-to-many within a dynamic form (add/remove on create/update)

This point is still quite confusing and unclear on the web, so here is the way I solved my problem. This solution is probably not the most optimized one, but it works when creating and updating a master entity.

Theory

  1. Use a List instead of a Set for your one-to-many relations which should be dynamically managed.

  2. Initialize your List as an AutoPopulatingList. It's a lazy list which allows to add dynamically elements.

  3. Add an attribute remove of int to your child entity. This will play the part of a boolean flag and will be usefull when removing dynamically an element.

  4. When posting the form, persist only the elements that have the flag remove on 0 (i.e. false).

Practice

A working full-example: an employer has many employees, an employee has one employer.

Entities:

Employer.java

@Entity
@Table(name = "employer")
public class Employer

private Integer id;

private String firstname;
private String lastname;
private String company;

private List<Employee> employees; // one-to-many

/* getters & setters */

}

Employee.java

@Entity
@Table(name = "employee")
public class Employee {

private Integer id;

@Transient // means "not a DB field"
private Integer remove; // boolean flag

private String firstname;
private String lastname;

private Employer employer; // many-to-one

/* getters & setters */

}

Controller:

EmployerController.java

@Controller
@RequestMapping("employer")
public class EmployerController {

// Manage dynamically added or removed employees
private List<Employee> manageEmployees(Employer employer) {
// Store the employees which shouldn't be persisted
List<Employee> employees2remove = new ArrayList<Employee>();
if (employer.getEmployees() != null) {
for (Iterator<Employee> i = employer.getEmployees().iterator(); i.hasNext();) {
Employee employee = i.next();
// If the remove flag is true, remove the employee from the list
if (employee.getRemove() == 1) {
employees2remove.add(employee);
i.remove();
// Otherwise, perform the links
} else {
employee.setEmployer(employer);
}
}
}
return employees2remove;
}

// -- Creating a new employer ----------

@RequestMapping(value = "create", method = RequestMethod.GET)
public String create(@ModelAttribute Employer employer, Model model) {
// Should init the AutoPopulatingList
return create(employer, model, true);
}

private String create(Employer employer, Model model, boolean init) {
if (init) {
// Init the AutoPopulatingList
employer.setEmployees(new AutoPopulatingList<Employee>(Employee.class));
}
model.addAttribute("type", "create");
return "employer/edit";
}

@RequestMapping(value = "create", method = RequestMethod.POST)
public String create(@Valid @ModelAttribute Employer employer, BindingResult bindingResult, Model model) {
if (bindingResult.hasErrors()) {
// Should not re-init the AutoPopulatingList
return create(employer, model, false);
}
// Call the private method
manageEmployees(employer);
// Persist the employer
employerService.save(employer);
return "redirect:employer/show/" + employer.getId();
}

// -- Updating an existing employer ----------

@RequestMapping(value = "update/{pk}", method = RequestMethod.GET)
public String update(@PathVariable Integer pk, @ModelAttribute Employer employer, Model model) {
// Add your own getEmployerById(pk)
model.addAttribute("type", "update");
return "employer/edit";
}

@RequestMapping(value = "update/{pk}", method = RequestMethod.POST)
public String update(@PathVariable Integer pk, @Valid @ModelAttribute Employer employer, BindingResult bindingResult, Model model) {
// Add your own getEmployerById(pk)
if (bindingResult.hasErrors()) {
return update(pk, employer, model);
}
List<Employee> employees2remove = manageEmployees(employer);
// First, save the employer
employerService.update(employer);
// Then, delete the previously linked employees which should be now removed
for (Employee employee : employees2remove) {
if (employee.getId() != null) {
employeeService.delete(employee);
}
}
return "redirect:employer/show/" + employer.getId();
}

// -- Show an existing employer ----------

@RequestMapping(value = "show/{pk}", method = RequestMethod.GET)
public String show(@PathVariable Integer pk, @ModelAttribute Employer employer) {
// Add your own getEmployerById(pk)
return "employer/show";
}

}

View:

employer/edit.jsp

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"
%><%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"
%><%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"
%><%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"
%><!DOCTYPE HTML>
<html>
<head>

<title>Edit</title>
<style type="text/css">.hidden {display: none;}</style>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script>
<script type="text/javascript">
$(function() {

// Start indexing at the size of the current list
var index = ${fn:length(employer.employees)};

// Add a new Employee
$("#add").off("click").on("click", function() {
$(this).before(function() {
var html = '<div id="employees' + index + '.wrapper" class="hidden">';
html += '<input type="text" id="employees' + index + '.firstname" name="employees[' + index + '].firstname" />';
html += '<input type="text" id="employees' + index + '.lastname" name="employees[' + index + '].lastname" />';
html += '<input type="hidden" id="employees' + index + '.remove" name="employees[' + index + '].remove" value="0" />';
html += '<a href="#" class="employees.remove" data-index="' + index + '">remove</a>';
html += "</div>";
return html;
});
$("#employees" + index + "\\.wrapper").show();
index++;
return false;
});

// Remove an Employee
$("a.employees\\.remove").off("click").on("click", function() {
var index2remove = $(this).data("index");
$("#employees" + index2remove + "\\.wrapper").hide();
$("#employees" + index2remove + "\\.remove").val("1");
return false;
});

});
</script>

</head>
<body>

<c:choose>
<c:when test="${type eq 'create'}"><c:set var="actionUrl" value="employer/create" /></c:when>
<c:otherwise><c:set var="actionUrl" value="employer/update/${employer.id}" /></c:otherwise>
</c:choose>

<form:form action="${actionUrl}" modelAttribute="employer" method="POST" name="employer">
<form:hidden path="id" />
<table>
<tr>
<td><form:label path="firstname">Firstname</form:label></td>
<td><form:input path="firstname" /><form:errors path="firstname" /></td>
</tr>
<tr>
<td><form:label path="lastname">Lastname</form:label></td>
<td><form:input path="lastname" /><form:errors path="lastname" /></td>
</tr>
<tr>
<td><form:label path="company">company</form:label></td>
<td><form:input path="company" /><form:errors path="company" /></td>
</tr>
<tr>
<td>Employees</td>
<td>
<c:forEach items="${employer.employees}" varStatus="loop">
<!-- Add a wrapping div -->
<c:choose>
<c:when test="${employer.employees[loop.index].remove eq 1}">
<div id="employees${loop.index}.wrapper" class="hidden">
</c:when>
<c:otherwise>
<div id="employees${loop.index}.wrapper">
</c:otherwise>
</c:choose>
<!-- Generate the fields -->
<form:input path="employees[${loop.index}].firstname" />
<form:input path="employees[${loop.index}].lastname" />
<!-- Add the remove flag -->
<c:choose>
<c:when test="${employees[loop.index].remove eq 1}"><c:set var="hiddenValue" value="1" /></c:when>
<c:otherwise><c:set var="hiddenValue" value="0" /></c:otherwise>
</c:choose>
<form:hidden path="employees[${loop.index}].remove" value="${hiddenValue}" />
<!-- Add a link to remove the Employee -->
<a href="#" class="employees.remove" data-index="${loop.index}">remove</a>
</div>
</c:forEach>
<button id="add" type="button">add</button>
</td>
</tr>
</table>
<button type="submit">OK</button>
</form:form>

</body>
</html>

Hope that could help :)

Dynamic Form in Spring 3

My problem was a lack of experience.

I ended up with this (much simplified for explanation):

My bean has the following fields: String questionType, String questionText, String answer.
My list of beans is List questions.

When I generate the HTML from the list of beans, I just need to make the id/name of each form element match up with the name of the list, and the position of the bean within the list.

So, if my list of questions looks like this:

[0] {questionType="TEXT", questionText="What is your name?", answerText=null}
[1] {questionType="TEXT", questionText="What is your quest?", answerText=null}
[2] {questionType="TEXT", questionText="What is your favorite color?", answerText=null}

Then I need to generate the following HTML when I loop through the list:

<div class="question">
<p class="questionText">What is your name?</p>
<input type="text" id="questions[0].answerText" name="questions[0].answerText" />
</div>
<div class="question">
<p class="questionText">What is your quest?</p>
<input type="text" id="questions[1].answerText" name="questions[1].answerText" />
</div>
<div class="question">
<p class="questionText">What is your favorite color?</p>
<input type="text" id="questions[2].answerText" name="questions[2].answerText" />
</div>

When the form is submitted, Spring will find those beans and call setAnswerText(String value) on them with the form data.

I hope this helps someone else stumbling at the beginning of their Spring MVC journey.

Dynamic Forms in Spring

You can't use <form:input> within the javascript because is a jsp tag that runs on the server-side.

However, there's nothing magical about how an HTML input gets bound to a field in the Spring command object; it's just based on the name. So in your javascript, add a new
<input type="text" name="activity[1].activity">
(for example -- obviously you'll increment the index).

Another option I've used for more complicated controls is to grab the HTML of the existing control (which was created using the Spring form:input tag) and clone it, replacing the indexes with the incremented number. This gets a lot easier if you use jQuery.

EDITED TO ADD:
One issue that may cause you problems: you're appending your new input box to the end of your outer div ("text"), which means it's not inside the form tags. That won't work.

Spring MVC - Child entity id lost when submitting a form

I found what was wrong.

Basically the idserv wasn't getting the default value of 0 so that
spring can handle the request to the server and hibernate can insert the ServeurApplicatif entity alongside the Environnment entity when saveOrUpdate is invoked, so i added a value="0" to the javascript code responsible for creating form inputs for ServeurApplicatifs on the fly and now it works as expected.

Handling parameters from dynamic form for one-to-many relationships in grails

disclaimer: i do not know if the following approach works when using grails. Let me know later.

See better way for dynamic forms. The author says:

To add LineItems I have some js that calculates the new index and adds that to the DOM. When deleting a LineItem i have to renumber all the indexes and it is what i would like to avoid

So what i do

I have a variable which stores the next index

var nextIndex = 0;

When the page is loaded, i perform a JavaScript function which calculates how many child The collection has and configure nextIndex variable. You can use JQuery or YUI, feel free.

Adding a child statically

I create a variable which store the template (Notice {index})

var child   = "<div>"
+= "<div>"
+= "<label>Name</label>"
+= "<input type="text" name=\"childList[{index}].name\"/>"
+= "</div>"
+= "</div>"

When the user click on the Add child button, i replace {index} - by using regex - by the value stored in the nextIndex variable and increment by one. Then i add to the DOM

See also Add and Remove HTML elements dynamically with Javascript

Adding a child dinamically

Here you can see The Paolo Bergantino solution

By removing

But i think it is the issue grow up when deleting. No matter how many child you remove, does not touch on the nextIndex variable. See here

/**
* var nextIndex = 3;
*/

<input type="text" name="childList[0].name"/>
<input type="text" name="childList[1].name"/> // It will be removed
<input type="text" name="childList[2].name"/>

Suppose i remove childList1 What i do ??? Should i renumber all the indexes ???

On the server side i use AutoPopulatingList. Because childList1 has been removed, AutoPopulatingList handles it as null. So on the initialization i do

List<Child> childList = new AutoPopulatingList(new ElementFactory() {
public Object createElement(int index) throws ElementInstantiationException {
/**
* remove any null value added
*/
childList.removeAll(Collections.singletonList(null));

return new Child();
}
});

This way, my collection just contains two child (without any null value) and i do not need to renumber all the indexes on the client side

About adding/removing you can see this link where i show a scenario wich can gives you some insight.

See also Grails UI plugin



Related Topics



Leave a reply



Submit