How to Bind an Object List with Thymeleaf

How to bind an object list with thymeleaf?

You need a wrapper object to hold the submited data, like this one:

public class ClientForm {
private ArrayList<String> clientList;

public ArrayList<String> getClientList() {
return clientList;
}

public void setClientList(ArrayList<String> clientList) {
this.clientList = clientList;
}
}

and use it as the @ModelAttribute in your processQuery method:

@RequestMapping(value="/submitQuery", method = RequestMethod.POST)
public String processQuery(@ModelAttribute ClientForm form, Model model){
System.out.println(form.getClientList());
}

Moreover, the input element needs a name and a value. If you directly build the html, then take into account that the name must be clientList[i], where i is the position of the item in the list:

<tr th:each="currentClient, stat : ${clientList}">         
<td><input type="checkbox"
th:name="|clientList[${stat.index}]|"
th:value="${currentClient.getClientID()}"
th:checked="${currentClient.selected}" />
</td>
<td th:text="${currentClient.getClientID()}" ></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}" ></td>
</tr>

Note that clientList can contain null at
intermediate positions. Per example, if posted data is:

clientList[1] = 'B'
clientList[3] = 'D'

the resulting ArrayList will be: [null, B, null, D]

UPDATE 1:

In my exmple above, ClientForm is a wrapper for List<String>. But in your case ClientWithSelectionListWrapper contains ArrayList<ClientWithSelection>. Therefor clientList[1] should be clientList[1].clientID and so on with the other properties you want to sent back:

<tr th:each="currentClient, stat : ${wrapper.clientList}">
<td><input type="checkbox" th:name="|clientList[${stat.index}].clientID|"
th:value="${currentClient.getClientID()}" th:checked="${currentClient.selected}" /></td>
<td th:text="${currentClient.getClientID()}"></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}"></td>
</tr>

I've built a little demo, so you can test it:

Application.java

@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

ClientWithSelection.java

public class ClientWithSelection {
private Boolean selected;
private String clientID;
private String ipAddress;
private String description;

public ClientWithSelection() {
}

public ClientWithSelection(Boolean selected, String clientID, String ipAddress, String description) {
super();
this.selected = selected;
this.clientID = clientID;
this.ipAddress = ipAddress;
this.description = description;
}

/* Getters and setters ... */
}

ClientWithSelectionListWrapper.java

public class ClientWithSelectionListWrapper {

private ArrayList<ClientWithSelection> clientList;

public ArrayList<ClientWithSelection> getClientList() {
return clientList;
}
public void setClientList(ArrayList<ClientWithSelection> clients) {
this.clientList = clients;
}
}

TestController.java

@Controller
class TestController {

private ArrayList<ClientWithSelection> allClientsWithSelection = new ArrayList<ClientWithSelection>();

public TestController() {
/* Dummy data */
allClientsWithSelection.add(new ClientWithSelection(false, "1", "192.168.0.10", "Client A"));
allClientsWithSelection.add(new ClientWithSelection(false, "2", "192.168.0.11", "Client B"));
allClientsWithSelection.add(new ClientWithSelection(false, "3", "192.168.0.12", "Client C"));
allClientsWithSelection.add(new ClientWithSelection(false, "4", "192.168.0.13", "Client D"));
}

@RequestMapping("/")
String index(Model model) {

ClientWithSelectionListWrapper wrapper = new ClientWithSelectionListWrapper();
wrapper.setClientList(allClientsWithSelection);
model.addAttribute("wrapper", wrapper);

return "test";
}

@RequestMapping(value = "/query/submitQuery", method = RequestMethod.POST)
public String processQuery(@ModelAttribute ClientWithSelectionListWrapper wrapper, Model model) {

System.out.println(wrapper.getClientList() != null ? wrapper.getClientList().size() : "null list");
System.out.println("--");

model.addAttribute("wrapper", wrapper);

return "test";
}
}

test.html

<!DOCTYPE html>
<html>
<head></head>
<body>
<form action="#" th:action="@{/query/submitQuery}" th:object="${wrapper}" method="post">

<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Select</th>
<th>Client ID</th>
<th>IP Addresss</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr th:each="currentClient, stat : ${wrapper.clientList}">
<td><input type="checkbox" th:name="|clientList[${stat.index}].clientID|"
th:value="${currentClient.getClientID()}" th:checked="${currentClient.selected}" /></td>
<td th:text="${currentClient.getClientID()}"></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}"></td>
</tr>
</tbody>
</table>
<button type="submit" value="submit" class="btn btn-success">Submit</button>
</form>

</body>
</html>

UPDATE 1.B:

Below is the same example using th:field and sending back all other attributes as hidden values.

 <tbody>
<tr th:each="currentClient, stat : *{clientList}">
<td>
<input type="checkbox" th:field="*{clientList[__${stat.index}__].selected}" />
<input type="hidden" th:field="*{clientList[__${stat.index}__].clientID}" />
<input type="hidden" th:field="*{clientList[__${stat.index}__].ipAddress}" />
<input type="hidden" th:field="*{clientList[__${stat.index}__].description}" />
</td>
<td th:text="${currentClient.getClientID()}"></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}"></td>
</tr>
</tbody>

how we can bind a list of a list of object using thymeleaf

Your approach is okay. But you need to fix a few things.

In the getTable method, you are setting empty lists for tables and columns. So there is nothing to iterate over in the view layer to show the form. Change to:

@ModelAttribute("page")
public Page getTable() {
Column column = new Column();
List<Column> columns = new ArrayList<>();
columns.add(column);

Table table = new Table();
table.setColumns(columns);

List<Table> tables = new ArrayList<>();
tables.add(table);

Page page = new Page();
page.setTables(tables);

return page;
}

And

Add missing } for th:field="*{tables[__${i.index}__].name" and close this input tag.



NOTE:
I am not sure how you wanted to handle the three select inputs. I tested omitting them, meaning, keeping only Column id and name in the form, data bind without any issue in that case.

Also I didn't check your JS, as you have mentioned that you haven't tested it yet.

Suggestions:


I see you are returning a view name from your POST handler. Take a look at the following article on Wikipedia.

Post/Redirect/Get

Binding a List in Thymeleaf

I had a similar problem not too long ago. I solved it by wrapping the List<> object in a Data Transfer Object then modifying the List<> as a field instead of the object itself. Here is your solution:
DTO class

public class FileDTO {

private List<File> files;

public FileDTO() {
files = new ArrayList<>();
}

public List<File> getFiles() {
return files;
}

public void setFiles(List<File> files) {
this.files = files;
}
}

thymeleaf view:

<form action="/example" th:object="${dto}">
<input type="file" th:each="file, itemStat : *{files}" th:id="${'file' + ${itemStat.index}}"
th:field="*{files[__${itemStat.index}__]}" name="files" accept=".gif, .jpg, .png, .jpeg">
<input type="submit" value="Submit">
</form>

Sorry if that was a little much to sort through. But the idea is that the List<> has to be a field not the form backing object itself then to iterate over the List<> field you can use the loop shown.

Spring Boot : How to bind list of objects on POST in thymleaf

There are a number of potential causes of your problem. The three items listed below should help you get the form mapped correctly:

  1. You should build the form correctly, including using the * notation to reduce repetition, for example:

    <th:block th:each = "record : ${records}">
    <tr>
    <td><input type="checkbox" th:field="*{selected}"/></td>
    <td><input type="text" th:field="*{id}"/></td>
    <td><input type="text" th:field="*{name}"/></td>
    <td><input type="text" th:field="*{phone}"/></td>
    </tr>
    </th:block>

    As shown in the Spring + Thymeleaf tutorial

  2. You may need to use the double-underscore notation when looping over ${records} to get each Record filled correctly in your form. As per the Thymeleaf + Spring tutorial:

    ..__${...}__ syntax is a preprocessing expression, which is an inner expression that is evaluated before actually evaluating the whole expression.

    See for example this question.

  3. Double-check that you're handling the resulting list correctly in your Spring @Controller by accepting a List<Record> annotated with @ModelAttribute or @RequestParam. (It looks like you're doing this already.)

    See for example this question.

Thymeleaf Binding list of objects

Add the "__" notation like this

<form class="form-horizontal">
<div th:each="catFeatGroup,status : ${catFeatGroupList}" class="form-group">
<label>Position</label><input th:field="*{catFeatGroupList[__${status.index}__].position}" th:value="${catFeatGroup.position}" />
<label>Name</label> <input data-th-name="*{catFeatGroupList[__${status.index}__].name}" th:value="${catFeatGroup.name}" />
</div>
<button type="submit" class="btn btn-default">Submit</button>
</form>


Related Topics



Leave a reply



Submit