Search code examples
javaspringthymeleaf

Pass dynamic list from View to Controller in Spring


I am trying to pass a List of "Experience" objects which size is unknown beforehand. The user can add as many experiences as he wants on the FE, so I don't know the number of objects in advance.

This is part of the Controller:

 @PostMapping("/buildcv")
 public ResponseEntity<byte[]> buildcv(@ModelAttribute("experience") ExperienceDTO experience{
   ...
}

The DTO:

public class ExperienceDTO {
    private List<Experience> experienceList;

    //getters and setters
}

The Experience Entity:

@Entity
public class Experience {
    @Id
    @GeneratedValue
    private int id;
    private String company;
    private String position;
    private String responsibilities;
    private Date startDate;
    private Date endDate;

    //getters and setters

The View:

 <div class="work-expirience-fields" id="work-expirience">
                            <a class="remove-image" id="close-button">&#215;</a>
        
                            <div class="col-one-row-one">
                                <label>Job title</label>
                                <input th:field="${experience.position}" id="jobTitle">
                            </div>

                            <div class="col-two-row-one">
                                <label>Employer</label>
                                <input th:field="${experience.company}" id="employer">
                            </div>

                            ....

                        </div>
                     </div>

This examples works if there is only one single Experience object passed to the controller instead of the DTO with the list in it. However there is button which copies this most outer div with all of the tags in it.

I tried using spring:bind with setting a path for each field like this but the list was not populated to the controller and I couldn't find an example which quite describes what I want to do:

 <spring:bind path="experience.experienceList[0]">
                            <a class="remove-image" id="close-button">&#215;</a>

                            <div class="col-one-row-one">
                                <label>Job title</label>
                                <input th:field="${experience.position}" id="jobTitle">
                            </div>

.......

Solution

  • The input names must be indexed, for them to bind to List of objects in the model object (that is ExperienceDTO).

    One important point is, it is not good practice to use @Entity objects as the View objects. You should have a separate POJO and map it from Entity. But let us ignore this point for this discussion.

    Your thymeleaf template should look like this,

    <form th:action="@{/buildcv}" th:object="${experience}" method="post">
        <div th:each="experienceItem, expState : *{experienceList}">
            <div>
                <label>Job Title</label>
                <input th:field="*{experienceList[__${expState.index}__].position}"/>
            </div>
            <div>
                <label>Employer</label>
                <input th:field="*{experienceList[__${expState.index}__].company}"/>
            </div>
            
            ......
            
        </div>
    </form>
    

    That results in, the first div on the HTML page as,

    <div>
        <div>
            <label>Job Title</label>
            <input id="experienceList0.position" name="experienceList[0].position" value="">
        </div>
        <div>
            <label>Employer</label>
            <input id="experienceList0.company" name="experienceList[0].company" value="">
        </div>
    
        ...
    
    </div>
    

    and the second row will be

    <div>
        <div>
            <label>Job Title</label>
            <input id="experienceList1.position" name="experienceList[1].position" value="">
        </div>
        <div>
            <label>Employer</label>
            <input id="experienceList1.company" name="experienceList[1].company" value="">
        </div>
    
        ...
    
    </div>
    

    As you can see the name attribute values are index accordingly,

    experienceList[0].position, experienceList[0].company

    and

    experienceList[1].position, experienceList[1].company

    The th:object="expereince" in the form element is the model attribute name that refers to ExperienceDTO object. Ideally (but not necessary, it depends on how you get to the form page) there should be a separate GetMapping controller method, that loads the form, there the model object is set before redirecting to view.

    .....
    model.addAttribute("experience", experienceDTO);
    ....
    return "buildCvForm";
    

    Assuming buildCvForm.html is your thymeleaf template name.

    You mentioned that the rows can be added on the page in Front-End, I assume it might be by using some Front-End framework like JQuery, Angular or something else. Irrespective of what is used in Front-End, when new rows are added, they have to adhere to the indexing of names. Let us say a user already has 2 Experiences that the page is loaded with and on Front-End when a 3rd row is added, then the input names must be

    experienceList[2].position, experienceList[2].company

    As long as the names are correctly indexed for every set of Experience fields, the form data is sent correctly for your controller to bind them into the List inside the ExperienceDTO model attribute.

    Refer to Thymeleaf documentation here