tutorial example java spring spring-mvc spring-boot thymeleaf

java - example - ¿Cómo enlazar una lista de objetos con thymeleaf?



thymeleaf spring boot (2)

Tengo muchas dificultades para enviar un formulario al controlador, que debe contener simplemente una lista de objetos que el usuario puede editar.

El formulario se carga correctamente, pero cuando se publica, nunca parece publicar nada.

Aquí está mi forma:

<form action="#" th:action="@{/query/submitQuery}" th:object="${clientList}" 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 : ${clientList}"> <td><input type="checkbox" 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>

Lo anterior funciona bien, carga la lista correctamente. Sin embargo, cuando POST, devuelve un objeto vacío (de tamaño 0). Creo que esto se debe a la falta de th:field , pero de todos modos aquí está el método POST del controlador:

... private List<ClientWithSelection> allClientsWithSelection = new ArrayList<ClientWithSelection>(); //GET method ... model.addAttribute("clientList", allClientsWithSelection) .... //POST method @RequestMapping(value="/submitQuery", method = RequestMethod.POST) public String processQuery(@ModelAttribute(value="clientList") ArrayList clientList, Model model){ //clientList== 0 in size ... }

Intenté agregar un th:field pero independientemente de lo que hago, causa una excepción.

He intentado:

... <tr th:each="currentClient, stat : ${clientList}"> <td><input type="checkbox" th:checked="${currentClient.selected}" th:field="*{}" /></td> <td th th:field="*{currentClient.selected}" ></td> ...

No puedo acceder a currentClient (error de compilación), ni siquiera puedo seleccionar clientList, me da opciones como get() , add() , clearAll() , etc., por lo que debería tener una matriz, sin embargo, no puedo pasar una matriz.

También he intentado usar algo como th:field=${} , esto causa una excepción en tiempo de ejecución

He intentado

th:field = "*{clientList[__currentClient.clientID__]}"

pero también compila error.

¿Algunas ideas?

ACTUALIZACIÓN 1:

Tobias sugirió que necesito envolver mi lista en un wraapper. Entonces eso fue lo que hice:

ClientWithSelectionWrapper:

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

Mi página:

<form action="#" th:action="@{/query/submitQuery}" th:object="${wrapper}" method="post"> .... <tr th:each="currentClient, stat : ${wrapper.clientList}"> <td th:text="${stat}"></td> <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>

Por encima de las cargas bien:

Entonces mi controlador:

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

La página se carga correctamente, los datos se muestran como se esperaba. Si publico el formulario sin ninguna selección, obtengo esto:

org.springframework.expression.spel.SpelEvaluationException: EL1007E:(pos 0): Property or field ''clientList'' cannot be found on null

No estoy seguro de por qué se queja

(En el método GET tiene: model.addAttribute("wrapper", wrapper); )

Si luego hago una selección, es decir, marque la primera entrada:

There was an unexpected error (type=Bad Request, status=400). Validation failed for object=''clientWithSelectionListWrapper''. Error count: 1

Supongo que mi controlador POST no está obteniendo el clientWithSelectionListWrapper. No estoy seguro de por qué, ya que he configurado el objeto contenedor para que se publique de nuevo a través de th:object="wrapper" en el encabezado FORM.

ACTUALIZACIÓN 2:

¡He hecho algunos progresos! Finalmente, el formulario enviado está siendo recogido por el método POST en el controlador. Sin embargo, todas las propiedades parecen ser nulas, excepto si el elemento ha sido marcado o no. He realizado varios cambios, así es como se ve:

<form action="#" th:action="@{/query/submitQuery}" th:object="${wrapper}" method="post"> .... <tr th:each="currentClient, stat : ${clientList}"> <td th:text="${stat}"></td> <td> <input type="checkbox" th:name="|clientList[${stat.index}]|" th:value="${currentClient.getClientID()}" th:checked="${currentClient.selected}" th:field="*{clientList[__${stat.index}__].selected}"> </td> <td th:text="${currentClient.getClientID()}" th:field="*{clientList[__${stat.index}__].clientID}" th:value="${currentClient.getClientID()}" ></td> <td th:text="${currentClient.getIpAddress()}" th:field="*{clientList[__${stat.index}__].ipAddress}" th:value="${currentClient.getIpAddress()}" ></td> <td th:text="${currentClient.getDescription()}" th:field="*{clientList[__${stat.index}__].description}" th:value="${currentClient.getDescription()}" ></td> </tr>

También agregué un constructor predeterminado sin parámetros a mi clase de contenedor y agregué un bindingResult al método POST (no estoy seguro si es necesario).

public String processQuery(@ModelAttribute ClientWithSelectionListWrapper wrapper, BindingResult bindingResult, Model model)

Entonces, cuando se publica un objeto, así es como se ve:

Por supuesto, se supone que systemInfo es nulo (en esta etapa), pero el ID de cliente siempre es 0 y ipAddress / Descripción siempre es nulo. Sin embargo, el booleano seleccionado es correcto para todas las propiedades. Estoy seguro de que he cometido un error en una de las propiedades en alguna parte. De vuelta a la investigación.

ACTUALIZACIÓN 3:

Ok, ¡he logrado completar todos los valores correctamente! Pero tuve que cambiar mi td para incluir un <input /> que no es lo que quería ... Sin embargo, los valores se están rellenando correctamente, lo que sugiere que Spring busque una etiqueta de entrada para el mapeo de datos.

Aquí hay un ejemplo de cómo cambié los datos de la tabla clientID:

<td> <input type="text" readonly="readonly" th:name="|clientList[${stat.index}]|" th:value="${currentClient.getClientID()}" th:field="*{clientList[__${stat.index}__].clientID}" /> </td>

Ahora necesito descubrir cómo mostrarlo como datos simples, idealmente sin la presencia de un cuadro de entrada ...


Cuando desee seleccionar objetos en la hoja de tomillo, en realidad no necesita crear un contenedor con el fin de almacenar un campo de selección boolean . El uso de dynamic fields según la guía de la hoja de tomillo con sintaxis th:field="*{rows[__${rowStat.index}__].variety}" es bueno para cuando desea acceder a un conjunto de objetos ya existente en una colección. No está realmente diseñado para hacer selecciones mediante el uso de objetos de contenedor IMO, ya que crea un código repetitivo innecesario y es una especie de truco.

Considere este ejemplo simple, una Person puede seleccionar las Drinks que le gustan. Nota: Los constructores, captadores y definidores se omiten para mayor claridad. Además, estos objetos normalmente se almacenan en una base de datos, pero estoy usando en matrices de memoria para explicar el concepto.

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

Controladores de primavera

Lo principal aquí es que estamos almacenando la Person en el Model para poder vincularla al formulario dentro de th:object . En segundo lugar, las bebidas selectableDrinks son las bebidas que una persona puede seleccionar en la interfaz de usuario.

@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"; }

Código de plantilla

Presta mucha atención al ciclo li y a cómo se puede selectableDrinks para obtener todas las bebidas posibles que se pueden seleccionar.

La casilla de verificación th:field realmente se expande a person.drinks ya que th:object está vinculado a Person y *{drinks} simplemente es el atajo para referirse a una propiedad en el objeto Person . Puede pensar en esto como simplemente decirle a spring / thymeleaf que cualquier bebida seleccionada se colocará en la lista de ArrayList en la ubicación 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>

De cualquier manera ... la salsa secreta está usando th:value=${drinks.id} . Esto se basa en convertidores de resorte. Cuando se publique el formulario, spring intentará recrear una Person y para hacerlo necesita saber cómo convertir las cadenas de drink.id seleccionadas en el tipo de Drink real. Nota: Si hiciste th:value${drinks} la clave de value en la casilla de verificación html sería la representación toString() de una Drink que no es lo que deseas, por lo tanto, ¡debes usar la identificación !. Si está siguiendo, todo lo que necesita hacer es crear su propio convertidor si aún no lo ha hecho.

Sin un convertidor, recibirá un error como Failed to convert property value of type ''java.lang.String'' to required type ''java.util.List'' for property ''drinks''

Puede activar el inicio de sesión en application.properties para ver los errores en detalle. logging.level.org.springframework.web=TRACE

Esto solo significa que spring no sabe cómo convertir una identificación de cadena que representa un drink.id en una Drink . El siguiente es un ejemplo de un Converter que soluciona este problema. Normalmente inyectaría un repositorio para obtener acceso a la base de datos.

@Component public 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); } }

Si una entidad tiene un repositorio de datos de Spring correspondiente, Spring crea automáticamente los convertidores y manejará la recuperación de la entidad cuando se proporciona una identificación (la identificación de la cadena también parece estar bien, por lo que Spring hace algunas conversiones adicionales allí por las miradas). Esto es realmente genial, pero puede ser confuso de entender al principio.


Necesita un objeto contenedor para contener los datos enviados, como este:

public class ClientForm { private ArrayList<String> clientList; public ArrayList<String> getClientList() { return clientList; } public void setClientList(ArrayList<String> clientList) { this.clientList = clientList; } }

y @ModelAttribute como @ModelAttribute en su método @ModelAttribute :

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

Además, el elemento de input necesita un name y un value . Si clientList[i] directamente el html, tenga en cuenta que el nombre debe ser clientList[i] , donde i es la posición del elemento en la lista:

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

Tenga en cuenta que clientList puede contener null en posiciones intermedias. Por ejemplo, si los datos publicados son:

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

la ArrayList resultante será: [null, B, null, D]

ACTUALIZACIÓN 1:

En mi ejemplo anterior, ClientForm es un contenedor para List<String> . Pero en su caso, ClientWithSelectionListWrapper contiene ArrayList<ClientWithSelection> . Por clientList[1] tanto, clientList[1] debe ser clientList[1].clientID y así sucesivamente con las otras propiedades que desea devolver:

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

He creado una pequeña demostración, para que pueda probarla:

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>

ACTUALIZACIÓN 1.B:

A continuación se muestra el mismo ejemplo usando th:field y enviando todos los demás atributos como valores ocultos.

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