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:
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
You may need to use the double-underscore notation when looping over
${records}
to get eachRecord
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.
Double-check that you're handling the resulting list correctly in your Spring
@Controller
by accepting aList<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
No Exception While Type Casting with a Null in Java
Compare Two Objects in Java with Possible Null Values
In Java, What Are the Boolean "Order of Operations"
Comparing Strings by Their Alphabetical Order
Make a File/Folder Hidden on Windows with Java
Counting the Number of Files in a Directory Using Java
How to Prevent Java.Lang.String.Split() from Creating a Leading Empty String
Hibernate Sessionfactory VS. JPA Entitymanagerfactory
Get First Date of Current Month in Java
Scanning Classpath/Modulepath in Runtime in Java 9
How to Stop a Scheduled Task That Was Started Using @Scheduled Annotation
Java's Bigdecimal.Power(Bigdecimal Exponent): Is There a Java Library That Does It