2

I have a form for adding products written in HTML and thymeleaf.

<form th:action="@{/products/get}" th:object="${form}" method="post">
            <div id="fields">
                <label for="name"></label><input type="text" id="name" name="name" autofocus="autofocus" placeholder="NAME" required/><br>
                <label for="label"></label><input type="text" id="label" name="label" autofocus="autofocus" placeholder="LABEL" required/><br>

Below the form, there is a button that adds two input fields to the form every time it's pressed. New input fields are the same as those two input fields above. The idea is that the user can enter data for as many products as he wants using the same form. For example, after pressing the button once the form will look like that:

<form th:action="@{/products/get}" th:object="${form}" method="post">
            <div id="fields">
                <label for="name"></label><input type="text" id="name" name="name" autofocus="autofocus" placeholder="NAME" required/><br>
                <label for="label"></label><input type="text" id="label" name="label" autofocus="autofocus" placeholder="LABEL" required/><br>
                <label for="name"></label><input type="text" id="name" name="name" autofocus="autofocus" placeholder="NAME" required/><br>
                <label for="label"></label><input type="text" id="label" name="label" autofocus="autofocus" placeholder="LABEL" required/><br>

The thing is I'd like to create ArrayList of class ProductForm using the values from input fields and then pass it to my controller using @ModelAttribute.

public class ProductForm{

    private String name;
    private String label;

//getters and setters
}

Then created a class that wraps ProductForm into ArrayList

public class ProductFormArray {

ArrayList<ProductForm> forms;
    //getters and setters
}

And a Controller

@Controller
@RequestMapping(value = "/products")
public class CreateAccountControllerTemporary {


    @RequestMapping(value = "/get", method = RequestMethod.POST)
    public String createAccount(@ModelAttribute(name = "form")ProductFormArray form){
//some code
}}

My problem is that I can't figure out how to add objects to form ArrayList using values from input fields? Is that even possible? How should I change my HTML file?

1 Answer 1

2

It is certainly possible, I explain this on pages 361 to 389 in my book Taming Thymeleaf.

You can check out the sources of the book for free at https://github.com/wimdeblauwe/taming-thymeleaf-sources/tree/main/chapter16

It is hard to summarize 30 pages into a stackoverflow answer, but briefly, check out:

CreateTeamFormData.java: This is similar to your ProductFormArray class. I do use an array instead of an ArrayList.

public class CreateTeamFormData {
    @NotBlank
    @Size(max = 100)
    private String name;
    @NotNull
    private UserId coachId;

    @NotNull
    @Size(min = 1)
    @Valid
    private TeamPlayerFormData[] players;

TeamPlayerFormData.java: This is similar to your ProductForm class.

public class TeamPlayerFormData {
    @NotNull
    private UserId playerId;
    @NotNull
    private PlayerPosition position;

TeamController.java: This the controller that uses the CreateTeamFormData.

    @GetMapping("/create")
    @Secured("ROLE_ADMIN")
    public String createTeamForm(Model model) {
        model.addAttribute("team", new CreateTeamFormData());
        model.addAttribute("users", userService.getAllUsersNameAndId());
        model.addAttribute("positions", PlayerPosition.values()); //<.>
        return "teams/edit";
    }

    @PostMapping("/create")
    @Secured("ROLE_ADMIN")
    public String doCreateTeam(@Valid @ModelAttribute("team") CreateTeamFormData formData,
                               BindingResult bindingResult, Model model) {
        if (bindingResult.hasErrors()) {
            model.addAttribute("editMode", EditMode.CREATE);
            model.addAttribute("users", userService.getAllUsersNameAndId());
            model.addAttribute("positions", PlayerPosition.values());
            return "teams/edit";
        }

        service.createTeam(formData.toParameters());

        return "redirect:/teams";
    }

edit.html -> This is the Thymeleaf template. Note that I am using a Thymeleaf fragment edit-teamplayer-fragment for the part of the form that repeats itself (So the name and label fields in your case)

<h3>Players</h3>
                           <div class="col-span-6 ml-4">
                               <div id="teamplayer-forms"
                                    th:data-teamplayers-count="${team.players.length}"> <!--.-->
                                   <th:block th:each="player, iter : ${team.players}">
                                       <div th:replace="teams/edit-teamplayer-fragment :: teamplayer-form(index=${iter.index}, teamObjectName='team')"></div>
                                       <!--.-->
                                   </th:block>
                               </div>
                               <div class="mt-4">
                                   <a href="#"
                                      class="py-2 px-4 border border-gray-300 rounded-md text-sm leading-5 font-medium text-gray-700 hover:text-gray-500 focus:outline-none focus:border-blue-300 focus:shadow-outline-blue active:bg-gray-50 active:text-gray-800"
                                      id="add-extra-teamplayer-form-button"
                                      th:text="#{team.player.add.extra}"
                                      @click="addExtraTeamPlayerForm()"
                                   ></a> <!--.-->
                               </div>
                           </div>

edit-teamplayer-fragment.html: Here is the important part where you need to keep track of the index for each fragment:

<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org"
      lang="en">
<!-- tag::main[] -->
<div th:fragment="teamplayer-form"
     class="col-span-6 flex items-stretch"
     th:id="${'teamplayer-form-section-' + __${index}__}"
     th:object="${__${teamObjectName}__}"> <!--.-->
    <!-- end::main[] -->
    <div class="grid grid-cols-1 row-gap-6 col-gap-4 sm:grid-cols-6">
        <div class="sm:col-span-2">
            <div class="mt-1 rounded-md shadow-sm">
                <select class="form-select block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"
                        th:field="*{players[__${index}__].playerId}">
                    <option th:each="user : ${users}"
                            th:text="${user.userName.fullName}"
                            th:value="${user.id.asString()}">
                </select>
            </div>
        </div>
        <div class="sm:col-span-2">
            <div class="mt-1 rounded-md shadow-sm">
                <select class="form-select block w-full transition duration-150 ease-in-out sm:text-sm sm:leading-5"
                        th:field="*{players[__${index}__].position}">
                    <option th:each="position : ${positions}"
                            th:text="#{'PlayerPosition.' + ${position}}"
                            th:value="${position}">
                </select>
            </div>
        </div>
        <!-- tag::delete[] -->
        <div class="ml-1 sm:col-span-2 flex items-center text-green-600 hover:text-green-900">
            <div class="h-6 w-6">
                <svg th:replace="trash"></svg>
            </div>
            <a href="#"
               class="ml-1"
               th:text="#{team.player.remove}"
               x-data
               th:attr="data-formindex=__${index}__"
               @click="removeTeamPlayerForm($el.dataset.formindex)"> <!--.-->
            </a>
        </div>
        <!-- end::delete[] -->
    </div>
</div>
</html>
Sign up to request clarification or add additional context in comments.

2 Comments

Thank you for your answer! I tried to follow your solution, but It seems like you're referring to the array that already exists using "${team.players.length}" and then player, iter : ${team.players}. In my case, I wanted to create a new array out of user input. Am I right or maybe I misunderstood your solution?
Check the constructor of CreateTeamFormData. I add indeed already a single TeamPlayerFormData instance there to have the first "row" of data in the form. This is not a problem because I remove "empty rows" in the custom validator anyway. See github.com/wimdeblauwe/taming-thymeleaf-sources/blob/…

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.