What You'll Learn

There are many trustees and we'd like to show them all on a form and enable the user to update them all at the same time.

There are a number of changes in the repository. Review the changes using a "diff" viewer. You can do this in IntelliJ by going to the Git view, clicking on the commit that you are interested in, and then double-clicking the amended file in the right-hand pane.

Some of the changes will be referred to, but not shown in this tutorial since the focus on this tutorial are the changes in the MVC layer.

The key concept to understand is that the mapping between Spring and Thymeleaf require a wrapper class. In the HTML world, the submission is of one form. We can't have nested forms. This means that we are sending all of the trustee data back with the form post. The Spring model is that we map that single submission into a single object. Therefore, we are going to have two classes; a TrusteeForm that will capture the data for a single trustee and a TrusteesForm (note the plural) that will wrap a list of TrusteeForm objects.

Let's have a look at the (bulk of that) code.

TrusteeForm.java

@Data
@AllArgsConstructor
@NoArgsConstructor
public class TrusteeForm {
    private UUID id;

    @Size(min = 1, max = 50, message = "Name of trustee must be between 1 and 50 characters.")
    private String name;
    private String role;
}

TrusteesForm.java

public class TrusteesForm {
    @Valid
    private List<TrusteeForm> trusteeFormList;

    public TrusteesForm(List<TrusteeForm> trusteeForm) {
        trusteeFormList = new ArrayList<>();
        trusteeFormList.addAll(trusteeForm);
    }

    public List<TrusteeForm> getTrustees() {
        return trusteeFormList;
    }

    public void setTrustees(List<TrusteeForm> trusteeForm) {
        this.trusteeFormList = trusteeForm;
    }

    public void addTrustee(TrusteeDTO trusteeDTO) {
        TrusteeForm trusteeForm = new TrusteeForm(trusteeDTO.getId(), trusteeDTO.getName(), trusteeDTO.getRole().name());
        trusteeFormList.add(trusteeForm);
    }
}

There's nothing really special here, except we have to put @Valid on the list so that the validation will cascade. The wrapper contains a List of TrusteeForm objects and there are methods for accessing and adding to that list.

We need to add a method handle the request for the trustee form. This is take the charity acronym as a parameter (from a path variable) and return a new template.

In CharityAdminController.java

@GetMapping("/update-charity/{furl}/trustees")
    public String trusteeForm(
            @PathVariable(value = "furl", required = true) String furl
            , Model model) {

        // TODO should do error handling here...
        CharityDTO charityDTO = charitySearch.findByAcronym(furl).get();

        Set<TrusteeDTO> trusteeDTOList = charitySearch.findTrusteesByCharity(charityDTO);

        List<TrusteeForm> trusteeFormList = new ArrayList<>();

        trusteeFormList.addAll(
                trusteeDTOList
                        .stream()
                        .map(t -> new TrusteeForm(t.getId(), t.getName(), t.getRole().name()))
                        .collect(Collectors.toList()))
        ;

        TrusteesForm trusteesForm = new TrusteesForm(trusteeFormList);

        model.addAttribute("charityDTO", charityDTO);
        model.addAttribute("trusteesForm", trusteesForm);

        return "admin/trustees-form.html";

    }

The controller method gets the charity DTO and the trustee DTOs from the service layer. It then transforms the trustee DTOs into trustee forms and then creates a new wrapper object passing in this list.

It puts the charity DTO and the wrapper object onto the model, and then redirects to the new template. Notice that the name of the wrapper object (the key in the map) is "trusteesForm".

We now add a new template admin/trustees-form.html. We're showing the main div element.

<div class="container">

    <h1>You are updating the trustees for:</h1>
    <div>
        <h2 th:text="${charityDTO.name}"></h2>
        <div class="mb-3" th:text="'Registration: ' + ${charityDTO.registration}"></div>
    </div>


    <div th:if="${#lists.isEmpty(trusteesForm.trustees)}">
        There are no trustees.
    </div>


    <div id="trustees-div" th:if="${!#lists.isEmpty(trusteesForm.trustees)}">


        <form method="post" th:action="@{/update-charity/{furl}/trustee (furl=${charityDTO.acronym.toLowerCase()})}"
              th:object="${trusteesForm}">

            <div th:each="trustee, itemStat : *{trustees}">

                <div class="mb-3">
                    <input th:field="*{trustees[__${itemStat.index}__].id}" type="hidden"/>
                    <input th:field="*{trustees[__${itemStat.index}__].name}"/>
                    <span th:errors="*{trustees[__${itemStat.index}__].name}"
                          th:if="${#fields.hasErrors('trustees[__${itemStat.index}__].name')}">Trustee Name Error</span>


                    <select th:field="*{trustees[__${itemStat.index}__].role}">
                        <option th:text="CHAIR" th:value="CHAIR"></option>
                        <option th:text="SECRETARY" th:value="SECRETARY"></option>
                        <option th:text="TREASURER" th:value="TREASURER"></option>
                        <option th:text="UNSPECIFIED" th:value="UNSPECIFIED"></option>
                    </select>
                </div>


            </div>
            <div class="mb-3">

                <button class="btn btn-primary">Submit</button>
            </div>

            <div>
                <div class="mb-3">
                    <a class="btn btn-primary" href="/">Cancel</a>
                </div>
            </div>

        </form>
    </div>

</div>

The form plays some reference data about the charity (from the DTO) and checks if there are trustees. Things get interesting when we examine the form for the trustees.

Let's zoom in on the form element for trustees.

    <div id="trustees-div" th:if="${!#lists.isEmpty(trusteesForm.trustees)}">


        <form method="post" th:action="@{/update-charity/{furl}/trustee (furl=${charityDTO.acronym.toLowerCase()})}"
              th:object="${trusteesForm}">

            <div th:each="trustee, itemStat : *{trustees}">

                <div class="mb-3">
                    <input th:field="*{trustees[__${itemStat.index}__].id}" type="hidden"/>
                    <input th:field="*{trustees[__${itemStat.index}__].name}"/>
                    <span th:errors="*{trustees[__${itemStat.index}__].name}"
                          th:if="${#fields.hasErrors('trustees[__${itemStat.index}__].name')}">Trustee Name Error</span>


                    <select th:field="*{trustees[__${itemStat.index}__].role}">
                        <option th:text="CHAIR" th:value="CHAIR"></option>
                        <option th:text="SECRETARY" th:value="SECRETARY"></option>
                        <option th:text="TREASURER" th:value="TREASURER"></option>
                        <option th:text="UNSPECIFIED" th:value="UNSPECIFIED"></option>
                    </select>
                </div>


            </div>
            <div class="mb-3">

                <button class="btn btn-primary">Submit</button>
            </div>

            <div>
                <div class="mb-3">
                    <a class="btn btn-primary" href="/">Cancel</a>
                </div>
            </div>

        </form>

The form action

First, let's examine the form element itself.

<form method="post" th:action="@{/update-charity/{furl}/trustee (furl=${charityDTO.acronym.toLowerCase()})}"
      th:object="${trusteesForm}">

We can now see the th:action attribute being used to create a dynamic URL since we want to send the trustee data back, but we need to know which charity this is for. We could have put the charity acronym in the form as a hidden field as an alternative.

Also, notice that the wrapper is mapped to the th:object attribute.

The iteration statement

We now have a slightly different use of the th:each attribute.

<div th:each="trustee, itemStat : *{trustees}">

The first field (trustee) provides a reference to the current object in the loop. Notice that the collection is referenced by *{trustees}. The ‘*' syntax tells us that this is a field of the form object, so we get the ‘trustees' collection from the wrapper.

The second field (itemStat) is a Thymeleaf "iteration status" object that provides useful data such as the index in the list, whether it's an odd or even row, etc.

For more, see this Baeldung article.

We now need to create indexed form elements within this loop. We do this using some rather odd syntax.

<input th:field="*{trustees[__${itemStat.index}__].name}"/>

Here we create a form field and we're binding it to the nth trustee's name.

The "" syntax is actually pre-processing and the reason for it is well-explained here (search for "").

We can support other HTML elements as well. Here we provide an example of a drop-down list, and there are multiple examples/tutorials online for radio buttons, checkboxes, etc.

<select th:field="*{trustees[__${itemStat.index}__].role}">
    <option th:text="CHAIR" th:value="CHAIR"></option>
    <option th:text="SECRETARY" th:value="SECRETARY"></option>
    <option th:text="TREASURER" th:value="TREASURER"></option>
    <option th:text="UNSPECIFIED" th:value="UNSPECIFIED"></option>
</select>

The select tag has the same "__" syntax to identify the field and then th:value is used within the option tag.

We now need to provide a method with a @PostMapping annotation to handle the submission of the form.

In CharityAdminController.java

@PostMapping("/update-charity/{furl}/trustees")
    public String trusteesFormPost(@PathVariable(value = "furl", required = true) String furl,
                                   @Valid TrusteesForm trusteesForm,
                                   BindingResult bindingResult,
                                   Model model) {

        CharityDTO charityDTO = charitySearch.findByAcronym(furl).get();

        if (bindingResult.hasErrors()) {
            model.addAttribute("charityDTO", charityDTO);
            return "admin/trustees-form";
        }

        List<TrusteeDTO> trusteeDTOList = trusteesForm
                .getTrusteeForms()
                .stream()
                .map(t -> new TrusteeDTO(t.getId(), t.getName(), Trustee.Role.valueOf(t.getRole())))
                .collect(Collectors.toList());


        charitySearch.updateTrusteesOfCharity(charityDTO, trusteeDTOList);

        return "redirect:/charities";
    }

The mapping of the path variable is standard and notice that the wrapper object is the parameter. Again, if we get the mappings correct in our code and follow the conventions, the framework gets us straight to OO programming.

From the form, a List of Trustee DTO object is created. Notice that the role is converted from the String to the TrusteeRole enum.

The service layer

There is then a new method on the service to update the trustees. This code is left to the reader to examine on the Gitlab repository.

You can now test updating the trustees.

The key part of this tutorial is to introduce the management of collections on a form.

What it also shows is the value of splitting the problem into tiers/modules/components. With the addition of some logging code, it would be possible to trace the what's happening in the controller and once it is confirmed that the controller has, in its control, a set of correct DTO objects, the mind can shift to processing that set of objects in the service layer.

Go to tag 025-link-update-pages to see the changes that integrate this into the page flow.