In cases when a form backing object contains a list or a map, you can build the JSP in such a way that Spring can bind the collection automatically for you. The collection can be static or dynamic. A static collection is when the size of the collection does not change between displaying the form page and processing the form page submit. A dynamic collection is when the size of the collection does change.
Sometimes your form backing object will use a list or a map. The following shows how to construct the jsp so that it'll bind to the collection object automatically.
The following guide is for binding a static list where the size of the list does not change. For example, a page that allows user to change the name for a list of six colleges here at UCSD.
A domain object that contains the data for id and name of a college.
public class College { private int id; private String name; public int getId() { return id; } public String getName() { return name; } public void setId(int id) { this.id = id; } public void setName(String name) { this.name = name; }
Contains a list of college objects.
public class CollegeCmd { private List<College> collegeList; public CollegeListCmd() { // initialize collegeList with six colleges collegeList = ... } public List<College> getCollegeList() { return collegeList; } public void setCollegeList(List<College> collegeList) { this.collegeList = collegeList; } }
Two methods here, one to setup the form, another one to process the form.
@Controller public class CollegeController { @RequestMapping(value = "/college-list-form.htm") public @ModelAttribute("collegeListCmd") CollegeListCmd setupForm() { return new CollegeListCmd(); } @RequestMapping(value = "/college-list.htm", method = RequestMethod.POST) public @ModelAttribute("collegeListCmd") CollegeListCmd save(HttpServletRequest req, @ModelAttribute("collegeListCmd") CollegeListCmd collegeListCmd, BindingResult bindResult, ModelMap model) { // validation logic here... ... // collegeList should have the modified name in it List<College> collegeList = collegeListCmd.getCollegeList(); ... return collegeListCmd; }
The form page.
<form:form modelAttribute="collegeListCmd" action="college-list.htm" method="post"> <form:errors path="*"> <div class="msg error"> <h4>ATTENTION!</h4> <p>Please make the following correction(s) before proceeding.</p> </div> </form:errors> <fieldset><legend>College Information</legend> <div class="help icon astrisk">required</div> <c:forEach items="${collegeListCmd.collegeList}" varStatus="vs"> <div class="field"> <div class="label required"><form:label path="collegeList[${vs.index}].name" cssErrorClass="invalid">College Name</form:label></div> <div class="input"><form:input path="collegeList[${vs.index}].name" cssErrorClass="invalid " /><form:label path="collegeList[${vs.index}].name" cssErrorClass="icon invalid" /><form:errors path="collegeList[${vs.index}].name" cssClass="inline_invalid" /></div> </div> <form:hidden path="collegeList[${vs.index}].id" /> <hr /> </c:forEach> <div class="field"> <div class="input"><input type="submit" class="button primary" value="Save" /></div> </div> </fieldset> </form:form>
Notice that the list is using item[#]
where # is the integer index of the list. ${vs}
is the variable status and the index of ${vs}
gives you the index of the current for each loop.
The result page.
<fieldset><legend>College Information</legend> <div class="help icon astrisk">required</div> <c:forEach items="${collegeListCmd.collegeList}" var="college" varStatus="vs"> <div class="field"> <div class="label required">College Name</div> <div class="output"><input type="text" value="<c:out value='${college.name}' />" /></div> </div> <hr /> </c:forEach> </fieldset>
The following guide is for binding a static list where the size of the list does not change. For example, a page that allows user to add comment to a list of six colleges here at UCSD.
Contains a list of college objects.
public class CollegeCommentListCmd { private Map<College, String> collegeCommentMap; public CollegeCommentListCmd() { // initialize with empty comment collegeCommentMap = new HashMap<College, String>(); for (College college : College.values()) collegeCommentMap.put(college, ""); } public Map<College, String> getCollegeCommentMap() { return collegeCommentMap; } public void setCollegeCommentMap(Map<College, String> collegeCommentMap) { this.collegeCommentMap = collegeCommentMap; } }
Two methods here, one to setup the form, another one to process the form.
@Controller public class CollegeCommentController { @RequestMapping(value = "/college-comment-list-form.htm") public @ModelAttribute("collegeCommentListCmd") CollegeCommentListCmd setupForm() { return new CollegeCommentListCmd(); } @RequestMapping(value = "/college-comment-list.htm", method = RequestMethod.POST) public @ModelAttribute("collegeCommentListCmd") CollegeCommentListCmd save( HttpServletRequest req, @ModelAttribute("collegeCommentListCmd") CollegeCommentListCmd collegeCommentListCmd, BindingResult bindResult, ModelMap model) { // validation // have latest comment in it Map<College, String> collegeCommentMap = collegeCommentListCmd .getCollegeCommentMap(); collegeCommentMap.size(); return collegeCommentListCmd; }
The form page.
<form:form modelAttribute="collegeCommentListCmd" action="college-comment-list.htm" method="post"> <form:errors path="*"> <div class="msg error"> <h4>ATTENTION!</h4> <p>Please make the following correction(s) before proceeding.</p> </div> </form:errors> <fieldset><legend>College CommentInformation</legend> <div class="help icon astrisk">required</div> <c:forEach items="${collegeCommentListCmd.collegeCommentMap}" var="mapEntry"> <div class="field"> <div class="label"><label>College Name</label></div> <div class="input"><c:out value="${mapEntry.key.name}" /></div> </div> <div class="field"> <div class="label required"><form:label path="collegeCommentMap[${mapEntry.key}]" cssErrorClass="invalid">Comment</form:label></div> <div class="input"><form:input path="collegeCommentMap[${mapEntry.key}]" cssErrorClass="invalid" /><form:label path="collegeCommentMap[${mapEntry.key}]" cssErrorClass="icon invalid" /><form:errors path="collegeCommentMap[${mapEntry.key}]" cssClass="inline_invalid" /></div> </div> <hr /> </c:forEach> <div class="field"> <div class="input"><input type="submit" class="button primary" value="Save" /></div> </div> </fieldset> </form:form>
Notice that the list is using item[#]
where # is the integer index of the list. ${vs}
is the variable status and the index of ${vs} gives you the index of the current for each loop.
The result page.
<fieldset><legend>College Comment Information</legend> <c:forEach items="${collegeCommentListCmd.collegeCommentMap}" var="mapEntry"> <div class="field"> <div class="label"><label>College Name</label></div> <div class="input"><c:out value="${mapEntry.key.name}" /></div> </div> <div class="field"> <div class="label"><label>Comment</label></div> <div class="input"><c:out value="${mapEntry.value}" /></div> </div> <hr /> </c:forEach> <div class="field"> <div class="input"><input type="submit" class="button primary" value="Save" /></div> </div> </fieldset>
Spring MVC's form:checkboxes allows user to select zero or many items in a list and will be automatically binded to a collection of items in the form backing object. However, the list of items are bounded (the size of the collection in form backing object is pre-determined). For example, the toppings on a pizza is limited to a maximum of 5 in this case:
But in some cases, the list of the items are not bounded. The size of the collection in form backing object is not pre-determined. For example, if a charge is to be allocated to different funds or items adding to a shopping cart. One can have as many funds or items as possible. If the size of the collection submitted by the user does not match the size of the collection in the form backing object an ArrayIndexOutOfBoundsException will occure during the binding process. To resolve it, we use a lazy collection in the form backing object on the server side and we dynamically generate normalized items in the collection on the client side.
Johnny won a $100 lottery ticket and he wants to share his winnings. He'll provide a list of name and amount.
The initial jsp with just one allocation looks like the following:
<c:forEach varStatus="vs" items="${allocateCmd.allocateList}"> <div class="field"> <div class="label icon astrisk"><form:label path="allocateList[${vs.index}].name" cssErrorClass="invalid">Name</form:label></div> <div class="input"><form:input path="allocateList[${vs.index}].name" cssErrorClass="invalid " /><form:label path="allocateList[${vs.index}].name" cssErrorClass="icon invalid" /><form:errors path="allocateList[${vs.index}].name" cssClass="inline_invalid" /></div> </div> <div class="field"> <div class="label required"><form:label path="allocateList[${vs.index}].amount" cssErrorClass="invalid">Amount</form:label></div> <div class="input"><form:input path="allocateList[${vs.index}].amount" cssErrorClass="invalid" itemLabel="name" itemValue="id" /><form:label path="allocateList[${vs.index}].amount" cssErrorClass="icon invalid" /><form:errors path="allocateList[${vs.index}].amount" cssClass="inline_invalid" /></div> </div>
The generated html code with just one allocation looks like the following:
<div class="field"> <div class="label required"><label for="allocateList0.name">Name</label></div> <div class="input"><input id="allocateList0.name" name="allocateList[0].name" type="text" value=""/><label for="allocateList0.name"></label></div> </div> <div class="field"> <div class="label required"><label for="allocateList0.amount">Name</label></div> <div class="input"><input id="allocateList0.amount" name="allocateList[0].amount" type="text" value=""/><label for="allocateList0.amount"></label></div> </div>
If two new names are added, the generated html with three allocation should look like the following:
<div class="field"> <div class="label required"><label for="allocateList0.name">Name</label></div> <div class="input"><input id="allocateList0.name" name="allocateList[0].name" type="text" value=""/><label for="allocateList0.name"></label></div> </div> <div class="field"> <div class="label required"><label for="allocateList0.amount">Name</label></div> <div class="input"><input id="allocateList0.amount" name="allocateList[0].amount" type="text" value=""/><label for="allocateList0.amount"></label></div> </div> <div class="field"> <div class="label required"><label for="allocateList1.name">Name</label></div> <div class="input"><input id="allocateList1.name" name="allocateList[1].name" type="text" value=""/><label for="allocateList1.name"></label></div> </div> <div class="field"> <div class="label required"><label for="allocateList1.amount">Name</label></div> <div class="input"><input id="allocateList1.amount" name="allocateList[1].amount" type="text" value=""/><label for="allocateList1.amount"></label></div> </div> <div class="field"> <div class="label required"><label for="allocateList2.name">Name</label></div> <div class="input"><input id="allocateList1.name" name="allocateList[2].name" type="text" value=""/><label for="allocateList2.name"></label></div> </div> <div class="field"> <div class="label required"><label for="allocateList2.amount">Name</label></div> <div class="input"><input id="allocateList2.amount" name="allocateList[2].amount" type="text" value=""/><label for="allocateList2.amount"></label></div> </div>
Noticed the second set have the index 0 changed to 1 for the second allocation and changed to 2 for the third allocation. When the second one is removed, the third allocation should be normalized such that it's index should be changed to 1 and the generated html should look like the following:
<div class="field"> <div class="label required"><label for="allocateList0.name">Name</label></div> <div class="input"><input id="allocateList0.name" name="allocateList[0].name" type="text" value=""/><label for="allocateList0.name"></label></div> </div> <div class="field"> <div class="label required"><label for="allocateList0.amount">Name</label></div> <div class="input"><input id="allocateList0.amount" name="allocateList[0].amount" type="text" value=""/><label for="allocateList0.amount"></label></div> </div> <div class="field"> <div class="label required"><label for="allocateList1.name">Name</label></div> <div class="input"><input id="allocateList1.name" name="allocateList[1].name" type="text" value=""/><label for="allocateList1.name"></label></div> </div> <div class="field"> <div class="label required"><label for="allocateList1.amount">Name</label></div> <div class="input"><input id="allocateList1.amount" name="allocateList[1].amount" type="text" value=""/><label for="allocateList1.amount"></label></div> </div>
As you can see, it's quite a bit of work creating a new allocation while normalizing the index if one allocation gets removed. Luckly, we have a dynamic list widget to help you with the interaction and normalization on the client side. (For more on the dyanmic list widget, please refer to the Table Form section in the sample web application) You just to wrap each allocation list with a div tag with id id_list_#
and each allocation item with a div tag with class dl_item_#
. The # are the index that ranges from from 0 to n. So the jsp that supports dynamic list in this example will look like the following:
<div id="dl_list_0"> <c:forEach varStatus="vs" items="${allocateCmd.allocateList}"> <div class="dl_item_<c:out value='${vs.index}' />"></div> </c:forEach> </div>
And an add item icon:
<li><a href="" class="icon_plus">add</a></li>
And finally, a remove item icon for each allocation:
<li><a href="" class="icon_minus">remove</a></li>
And the server side form backing object will use Spring's AutoPopulatingList for the allocation list:
public class AllocateCmd { private List<Allocate> allocateList = new AutoPopulatingList( new AllocateElementFactory()); ...
Here's a another example with the complete jsp form:
<form:form modelAttribute="allocateCmd" action="allocate.htm" method="post"> <form:errors path="*"> <div class="msg error"> <h4>ATTENTION!</h4> <p>Please make the following correction(s) before proceeding.</p> </div> </form:errors> <fieldset><legend>Allocate Information</legend> <div class="help icon astrisk">required</div> <div id="dl_list_0"> <c:forEach varStatus="vs" items="${allocateCmd.allocateList}"> <div class="dl_item_<c:out value='${vs.index}' />"> <div class="field"> <div class="label required"><form:label path="allocateList[${vs.index}].name" cssErrorClass="invalid">Name</form:label></div> <div class="input"><form:input path="allocateList[${vs.index}].name" cssErrorClass="invalid " /><form:label path="allocateList[${vs.index}].name" cssErrorClass="icon invalid" /><form:errors path="allocateList[${vs.index}].name" cssClass="inline_invalid" /></div> </div> <div class="field"> <div class="label required"><form:label path="allocateList[${vs.index}].gender" cssErrorClass="invalid">Gender</form:label></div> <div class="input"><form:radiobuttons items="${genderList}" path="allocateList[${vs.index}].gender" cssErrorClass="invalid" itemLabel="name" itemValue="id" /><form:label path="allocateList[${vs.index}].gender" cssErrorClass="icon invalid" /><form:errors path="allocateList[${vs.index}].gender" cssClass="inline_invalid" /></div> </div> <div class="field"> <div class="label required"><form:label path="allocateList[${vs.index}].college" cssErrorClass="invalid">College</form:label></div> <div class="input"><form:select path="allocateList[${vs.index}].college" cssErrorClass="invalid "> <form:options items="${collegeList}" itemLabel="name" itemValue="id" /> </form:select><form:label path="allocateList[${vs.index}].college" cssErrorClass="icon invalid" /><form:errors path="allocateList[${vs.index}].college" cssClass="inline_invalid" /></div> </div> <div class="field"> <div class="label required"><form:label path="allocateList[${vs.index}].comment" cssErrorClass="invalid">Comment</form:label></div> <div class="input"><form:textarea path="allocateList[${vs.index}].comment" cssErrorClass="invalid " /><form:label path="allocateList[${vs.index}].comment" cssErrorClass="icon invalid" /> <a href="#" class="remove_item icon minus">remove</a> <form:errors path="allocateList[${vs.index}].comment" cssClass="invalid" /></div> </div> <hr /> </div> </c:forEach> <div class="field"> <div class="label"><a class="add_item icon plus" href="#">add</div> </div> </div> <div class="field"> <div class="input"><input type="submit" class="button primary" value="Save" /></div> </div> </fieldset> </form:form>