web-dev-qa-db-fra.com

Comment lier une liste d'objets avec thymeleaf?

J'ai beaucoup de difficulté à poster un formulaire à l’automate, qui doit contenir simplement une liste d’objets que l’utilisateur peut éditer.

Le formulaire se charge correctement, mais lorsqu'il est posté, il ne semble jamais rien publier.

Voici mon formulaire:

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

Ci-dessus fonctionne bien, il charge correctement la liste. Cependant, lorsque je poste, il retourne un objet vide (de taille 0). Je crois que cela est dû au manque de th:field, Mais de toute façon voici le contrôleur POST méthode:

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

J'ai essayé d'ajouter un th:field Mais peu importe ce que je fais, cela provoque une exception.

J'ai essayé:

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

    <td th th:field="*{currentClient.selected}" ></td>
...

Je ne peux pas accéder à currentClient (erreur de compilation), je ne peux même pas sélectionner clientList, cela me donne des options comme get(), add(), clearAll() etc, donc ça va il devrait avoir un tableau, cependant, je ne peux pas passer dans un tableau.

J'ai aussi essayé d'utiliser quelque chose comme th:field=${}, Cela provoque une exception d'exécution

J'ai essayé

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

mais aussi erreur de compilation.

Des idées?


UPDATE 1:

Tobias m'a suggéré de mettre ma liste sous enveloppe. Alors c'est ce que j'ai fait:

ClientWithSelectionWrapper:

public class ClientWithSelectionListWrapper {

private ArrayList<ClientWithSelection> clientList;

public List<ClientWithSelection> getClientList(){
    return clientList;
}

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

Ma page:

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

Au-dessus des charges bien: enter image description here

Puis mon contrôleur:

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

La page se charge correctement, les données sont affichées comme prévu. Si je poste le formulaire sans aucune sélection, je reçois ceci:

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

Je ne sais pas pourquoi il se plaint

(Dans la méthode GET, il a: model.addAttribute("wrapper", wrapper);)

enter image description here

Si je fais ensuite une sélection, c’est-à-dire cochez la première entrée:

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

J'imagine que mon contrôleur POST ne reçoit pas le clientWithSelectionListWrapper. Vous ne savez pas pourquoi, puisque j'ai défini l'objet encapsuleur pour qu'il soit renvoyé via th:object="wrapper" Dans l'en-tête FORM.


UPDATE 2:

J'ai fait des progrès! Enfin, le formulaire soumis est récupéré par la méthode POST dans le contrôleur. Cependant, toutes les propriétés semblent être nulles, sauf si l'élément a été coché ou non. J'ai créé plusieurs changements, voici à quoi cela ressemble:

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

J'ai également ajouté un constructeur sans paramètres par défaut à ma classe wrapper et ajouté une méthode bindingResult param à POST (je ne sais pas si c'est nécessaire).

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

Ainsi, lorsqu'un objet est posté, voici à quoi il ressemble: enter image description here

Bien sûr, l’information système est supposée être nulle (à ce stade), mais le clientID est toujours égal à 0 et ipAddress/Description toujours nul. Le booléen sélectionné est correct pour toutes les propriétés. Je suis sûr que j'ai commis une erreur sur l'une des propriétés quelque part. Retour à l'enquête.


UPDATE 3:

Ok j'ai réussi à remplir toutes les valeurs correctement! Mais je devais changer mon td pour inclure un <input />, Ce qui n’était pas ce que je voulais ... Néanmoins, les valeurs sont renseignées correctement, suggérant que spring cherche une balise d’entrée peut-être pour le mappage des données?

Voici un exemple de la façon dont j'ai modifié les données de la table clientID:

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

Maintenant, je dois comprendre comment l'afficher sous forme de données simples, idéalement sans la présence d'une zone de saisie ...

39
benscabbia

Vous avez besoin d'un objet wrapper pour contenir les données soumises, comme celui-ci:

public class ClientForm {
    private ArrayList<String> clientList;

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

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

et l'utiliser comme @ModelAttribute dans votre méthode processQuery:

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

De plus, l'élément input a besoin d'un name et d'un value. Si vous construisez directement le code HTML, sachez que le nom doit être clientList[i], où i est la position de l’élément dans la liste:

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

Notez que clientList peut contenir null à des positions intermédiaires. Par exemple, si les données enregistrées sont:

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

le résultat ArrayList sera: [null, B, null, D]

UPDATE 1:

Dans l'exemple ci-dessus, ClientForm est un wrapper pour List<String>. Mais dans votre cas, ClientWithSelectionListWrapper contient ArrayList<ClientWithSelection>. Par conséquent clientList[1] devrait être clientList[1].clientID et ainsi de suite avec les autres propriétés que vous souhaitez renvoyer:

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

J'ai construit une petite démo, vous pouvez donc la tester:

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:

Voici le même exemple en utilisant th:field et renvoyer tous les autres attributs en tant que valeurs cachées.

 <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>
47
Tobías

Lorsque vous souhaitez sélectionner des objets dans thymeleaf, vous n'avez pas réellement besoin de créer un wrapper pour stocker un champ de sélection boolean. L'utilisation de dynamic fields Conformément au guide thymeleaf avec la syntaxe th:field="*{rows[__${rowStat.index}__].variety}" Convient lorsque vous souhaitez accéder à un ensemble d'objets existant dans une collection. Ce n'est pas vraiment conçu pour faire des sélections en utilisant des objets wrapper IMO car il crée un code standard inutile et est en quelque sorte un hack.

Prenons cet exemple simple, un Person peut sélectionner Drinks qu’ils aiment. Remarque: Les constructeurs, les Getters et les setters sont omis pour plus de clarté. En outre, ces objets sont normalement stockés dans une base de données, mais j'utilise des tableaux de mémoire pour expliquer le concept.

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

public class Drink {
    private Long id;
    private String name;
}

Contrôleurs de printemps

La chose principale ici est que nous stockons le Person dans le Model afin que nous puissions le lier au formulaire dans th:object. Deuxièmement, les selectableDrinks sont les boissons qu'une personne peut sélectionner sur l'interface utilisateur.

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

Code de modèle

Portez une attention particulière à la boucle li et à la manière dont selectableDrinks est utilisé pour obtenir toutes les boissons pouvant être sélectionnées.

La case à cocher th:field Devient réellement person.drinks Car th:object Est lié à Person et *{drinks} Est simplement le raccourci pour faire référence à une propriété sur l'objet Person. Vous pouvez imaginer que cela signifie simplement que spring/thymeleaf indique que les Drinks sélectionnés vont être placés dans le ArrayList à l'emplacement 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 toute façon ... la sauce secrète utilise th:value=${drinks.id}. Cela repose sur des convertisseurs à ressort. Quand le formulaire sera posté, Spring essaiera de recréer un Person. Pour ce faire, il devra savoir comment convertir les chaînes sélectionnées drink.id En type Drink réel. Remarque: Si vous avez utilisé th:value${drinks}, La clé value dans la case à cocher html serait la représentation toString() d'un Drink qui n'est pas ce que vous voulez, d'où besoin d'utiliser l'identifiant !. Si vous suivez, il vous suffit de créer votre propre convertisseur s'il n'en a pas déjà été créé.

Sans convertisseur, vous recevrez une erreur du genre Failed to convert property value of type 'Java.lang.String' to required type 'Java.util.List' for property 'drinks'

Vous pouvez activer la connexion à application.properties Pour afficher les erreurs en détail. logging.level.org.springframework.web=TRACE

Cela signifie simplement que spring ne sait pas comment convertir un identifiant de chaîne représentant un drink.id En un Drink. Voici un exemple de Converter qui résout ce problème. Normalement, vous devez injecter un référentiel pour avoir accès à la base de données.

@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 une entité a un référentiel de données Spring correspondant, spring crée automatiquement les convertisseurs et se chargera de la récupérer lorsqu'un identifiant est fourni (id de chaîne semble être correct aussi, donc Spring effectue quelques conversions supplémentaires par les apparences). C'est vraiment cool mais ça peut être déroutant de comprendre au début.

2
reversebind