Spring Dynamic (Expandable) List form

2019-03-29 12:58发布

问题:

I am having problems with dynamic forms in spring. In our form we want to specify a title, and add a number of questions. We have an "add" button to add question input form using jquery.

Our form has one question field when it is requested. Extra fields are added every time the "add" button is pressed. When submitting it seems that the extra fields are not being submitted (the first one is received by the controller). Why are the extra fields not received being sent?

I roughly based my code on this dynamic binding list example.

My model consists of a class "Report" which has a "title" and a list of "Researchquestion"s.
A short version of the two model classes is below. Roo takes care of all the getters and setters

@Entity
@RooJavaBean
@RooEntity
public class Report{
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;

    @NotEmpty
    private String title;

    @OneToMany(mappedBy="report")
    private List<Researchquestion> researchquestions;
}



@Entity
@RooJavaBean
@RooEntity
public class Researchquestion {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id")
    private Long id;    
    @NotEmpty
    private String question;
}

Here the jspx for the form

<div xmlns:c="http://java.sun.com/jsp/jstl/core"
    xmlns:form="http://www.springframework.org/tags/form"
    xmlns:jsp="http://java.sun.com/JSP/Page"
    xmlns:fn="http://java.sun.com/jsp/jstl/functions"
    xmlns:spring="http://www.springframework.org/tags"
    version="2.0">
    <jsp:output omit-xml-declaration="yes"/>

    <spring:url value="/admin/report/appendquestion" var="insert_url"/>

    <script type="text/javascript">
    $(document).ready(function() {
        var questionPosition = 0;
        $("#addQuestionButton").click(function() {
            questionPosition++;

            $.get("${insert_url}", { fieldId: questionPosition},
                function(data){
                    $("#insertAbove").before($(data));
            });
        });
    });
    </script>

    <div class="list_overview_box span-19">
        <spring:url value="/admin/report/" var="form_url"/>
        <div class="list_overview_content">
            <table>
                <form:form action="${form_url}" method="post" modelAttribute="report">
                    <tr>
                        <th class="span-3">Veld</th>
                        <th>Waarde</th>
                        <th class="span-5">Errors</th>
                    </tr>

        <!-- Title -->
                    <tr class="row">
                        <td class="vmiddle aleft">Title</td>
                        <td><form:input path="title" /></td>
                        <td></td>                           
                    </tr>

           <!-- the "add" button -->
                    <tr class="row">
                        <td class="vmiddle aleft">Researchquestions</td>
                        <td colspan="2"><input type="button" id="addQuestionButton" value="Add question" /></td>
                    </tr>
           <!-- First Researchquestion -->
                    <spring:bind path="researchquestions[0].question">
                        <tr class="row">
                            <td class="vmiddle aleft">Question 1</td>
                            <td><form:input path="${status.expression}" /></td>
                            <td></td>                           
                        </tr>
                    </spring:bind>

        <!--  Save button, extra question's are added here -->
                    <tr id="insertAbove" class="row">
                        <spring:message code="button.save" var="form_submit"/>
                        <td colspan="3"><input id="proceed" type="submit" value="${form_submit}" /></td>
                    </tr>   
                </form:form> 
            </table>
        </div>
    </div>
</div>

Below is the page the controller returns after the jquery .get request I have the idea that I need to use <spring:bind> just like in the form above. When I do this however I get an error:

java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'researchquestions[1]' available as request attribute

appendquestion.jspx

<jsp:root version="2.0"
    xmlns:jsp="http://java.sun.com/JSP/Page"
    xmlns:c="http://java.sun.com/jsp/jstl/core"
    xmlns:tiles="http://tiles.apache.org/tags-tiles"
    xmlns:form="http://www.springframework.org/tags/form"
    xmlns:spring="http://www.springframework.org/tags"
    xmlns:roo="urn:jsptagdir:/WEB-INF/tags" >

    <tr class="row">
        <jsp:directive.page contentType="text/html;charset=UTF-8" />   

        <td class="vmiddle aleft">Question ${questionNumber +1}</td>
        <td>
            <form:input path="report.researchquestions[${questionNumber}].question" size="40" />
        </td>
        <td></td>
    </tr>

</jsp:root>

Here the relevant @ModelAttribute and @requestmapping methods in our controller The @ModelAttribute method makes sure that the List in an instance of AutoPopulatingList, I am not really sure if this is required though.
If I add @RequestParam Map formdata to the create() (POST) method then the formdata does contain researchquestions[0].question but not researchquestions\[1\].question or any other question fields that have been added after pressing the "add" button

@ModelAttribute("report")
public Report getReport(Long id) {
    Report result;
    if(id != null){
        result = Report.findReport(id);
    } else{
        result = new Report();
    }

    //Make sure the List in result is an AutoPopulatingList
    List<Researchquestion> vragen = result.getResearchquestions();
    if(vragen == null){
        result.setResearchquestions(new AutoPopulatingList<Researchquestion>(Researchquestion.class));
    } else if(!(vragen instanceof AutoPopulatingList)){
        result.setResearchquestions(new AutoPopulatingList<Researchquestion>(
                vragen, Researchquestion.class));
    }

    return result;
}

/**
 * Aanmaken Report
 * @param report
 * @param result
 * @param modelMap
 * @return
 */
@RequestMapping(method = RequestMethod.POST)
public String create(@Valid @ModelAttribute("report") Report report,
        BindingResult result, ModelMap modelMap) {

    if (report == null) throw new InvalidBeanException("A report is required");

    if (result.hasErrors()) {
        modelMap.addAttribute("report", report);
        return "admin/report/create";
    }

    report.persist();

    //create questions
    for(Researchquestion question : report.getResearchquestions()){
        question.setProfielwerkstuk(report);
        question.persist();
    }

    report.merge();
    return "redirect:/admin/report";
}

@RequestMapping(value = "/appendquestion", method = RequestMethod.GET)
public String appendResearchquestionField(@RequestParam Integer fieldId, ModelMap modelMap){
    modelMap.addAttribute("questionNumber", fieldId);
    return "admin/report/appendquestion";
}

Additional info (as requested by Ralph)

Below the HTML that Spring generates, researchquestions[0].question is in the form by default, researchquestions[1].question is added after pressing the "add" button

 <tr class="row">
    <td class="vmiddle aleft">Question 1</td>
    <td>
        <input id="researchquestions0.question" type="text" value=""
             name="researchquestions[0].question">
    </td>

    <td></td>
</tr>

<tr class="row">
    <td class="vmiddle aleft">Question 2</td>
    <td>
        <input id="researchquestions1.question" type="text" size="40" value="" name="researchquestions[1].question">
    </td>
    <td></td>
</tr>

Below the relevant info from Live HTTP Headers
I intered "This is the title" in the "title" field, "This is the first question" in the "Question 1" field, and "This is the second question" in the "Question 2" field (which has been added by pressing the "add" button.

It is clear that researchquestions[0].question is being submitted, but researchquestions[1].question is not submitted at all in the POST request.

Content-Type: application/x-www-form-urlencoded
Content-Length: 73
   title=This+is+the+title&researchquestions%5B0%5D.question=This+is+the+first+question

My suspicions The difference between the first question (that is in the form by default) and the subsequent questions is that the first question uses <spring:bind> and the subsequent ones do not. When I remove the <spring:bind> tag for the first question, the researchquestions[0] is also not submitted.

As I explained above, I get an IllegalStateException when adding the <spring:bind> to the appendquestion.jspx. It seems that spring searches for the object researchquestions[1] instead of report.researchquestions[1]

java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'researchquestions[1]' available as request attribute

回答1:

I found the reason why the form was not being submitted correctly. I noticed the following HTML in firebug:

<form id="researchquestion" method="post" action="/site/admin/researchquestion/"></form>

The form tag is closed immediately, so the HTML generated by spring was not correct. It appears that this was because the form was inside the table switching the <table> and <form:form> tags fixed the issue.

Original code

<table>
    <form:form action="${form_url}" method="post" modelAttribute="report">
         <!--  Code here -->  
    </form:form> 
</table>

Working version

<form:form action="${form_url}" method="post" modelAttribute="report">
    <table>
         <!--  Code here -->  
    </table>
</form:form>

In a <table> it is only allowed to use table related tags such as <tr> <th> and <td>. Which is probably why Spring immediately closed the <form> tag.