How to bind an object list with thymeleaf? How to bind an object list with thymeleaf? java java

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

@SpringBootApplicationpublic 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

@Controllerclass 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>


When you want to select objects in thymeleaf, you dont actually need to create a wrapper for the purpose of storing a boolean select field. Using dynamic fields as per the thymeleaf guide with syntax th:field="*{rows[__${rowStat.index}__].variety}" is good for when you want to access an already existing set of objects in a collection. Its not really designed for doing selections by using wrapper objects IMO as it creates unnecessary boilerplate code and is sort of a hack.

Consider this simple example, a Person can select Drinks they like. Note: Constructors, Getters and setters are omitted for clarity. Also, these objects are normally stored in a database but I am using in memory arrays to explain the concept.

public class Person {    private Long id;    private List<Drink> drinks;}public class Drink {    private Long id;    private String name;}

Spring controllers

The main thing here is that we are storing the Person in the Model so we can bind it to the form within th:object.Secondly, the selectableDrinks are the drinks a person can select on the UI.

   @GetMapping("/drinks")   public String getDrinks(Model model) {        Person person = new Person(30L);        // ud normally get these from the database.        List<Drink> selectableDrinks = Arrays.asList(                new Drink(1L, "coke"),                new Drink(2L, "fanta"),                new Drink(3L, "sprite")        );        model.addAttribute("person", person);        model.addAttribute("selectableDrinks", selectableDrinks);        return "templates/drinks";    }    @PostMapping("/drinks")    public String postDrinks(@ModelAttribute("person") Person person) {                   // person.drinks will contain only the selected drinks        System.out.println(person);        return "templates/drinks";    }

Template code

Pay close attention to the li loop and how selectableDrinks is used to get all possible drinks that can be selected.

The checkbox th:field really expands to person.drinks since th:object is bound to Person and *{drinks} simply is the shortcut to referring to a property on the Person object. You can think of this as just telling spring/thymeleaf that any selected Drinks are going to be put into the ArrayList at location person.drinks.

<!DOCTYPE html><html lang="en" xmlns="http://www.w3.org/1999/xhtml"      xmlns:th="http://www.thymeleaf.org"      xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" ><body><div class="ui top attached segment">    <div class="ui top attached label">Drink demo</div>    <form class="ui form" th:action="@{/drinks}" method="post" th:object="${person}">        <ul>            <li th:each="drink : ${selectableDrinks}">                <div class="ui checkbox">                    <input type="checkbox" th:field="*{drinks}" th:value="${drink.id}">                    <label th:text="${drink.name}"></label>                </div>            </li>        </ul>        <div class="field">            <button class="ui button" type="submit">Submit</button>        </div>    </form></div></body></html>

Any way...the secret sauce is using th:value=${drinks.id}. This relies on spring converters. When the form is posted, spring will try recreate a Person and to do this it needs to know how to convert any selected drink.id strings into the actual Drink type. Note: If you did th:value${drinks} the value key in the checkbox html would be the toString() representation of a Drink which is not what you want, hence need to use the id!. If you are following along, all you need to do is create your own converter if one isn't already created.

Without a converter you will receive an error like Failed to convert property value of type 'java.lang.String' to required type 'java.util.List' for property 'drinks'

You can turn on logging in application.properties to see the errors in detail.logging.level.org.springframework.web=TRACE

This just means spring doesn't know how to convert a string id representing a drink.id into a Drink. The below is an example of a Converter that fixes this issue. Normally you would inject a repository in get access the database.

@Componentpublic class DrinkConverter implements Converter<String, Drink> {    @Override    public Drink convert(String id) {        System.out.println("Trying to convert id=" + id + " into a drink");        int parsedId = Integer.parseInt(id);        List<Drink> selectableDrinks = Arrays.asList(                new Drink(1L, "coke"),                new Drink(2L, "fanta"),                new Drink(3L, "sprite")        );        int index = parsedId - 1;        return selectableDrinks.get(index);    }}

If an entity has a corresponding spring data repository, spring automatically creates the converters and will handle fetching the entity when an id is provided (string id seems to be fine too so spring does some additional conversions there by the looks). This is really cool but can be confusing to understand at first.