Search code examples
javahtmlspring-bootthymeleaf

Multiple select in Thymeleaf doesn't store chosen options


I'm trying to learn Spring and other related technologies during summer break by developing a simple web application, however Thymeleaf hinders my progress.

The program is based on two entity classes:

Invoice.java:

@Entity
public class Invoice {
    @Id
    private String invoiceId;

    @NotNull
    @DateTimeFormat(pattern = "yyyy-MM-dd")
    private LocalDate issueDate;

    //getters and setters
}

TrasportOrder.java:

@Entity
public class TransportOrder {

    @Id
    private int number;

    @NotNull
    private BigDecimal value;

    @ManyToOne
    private Invoice invoice;

    //getters and setters
}

I'm getting a form for adding invoices using a method from InvoiceController:

@GetMapping(path = "/add")
public String getForm(Model model) {
     model.addAttribute("unusedOrders", service.getInvoiceOrders(null));
     model.addAttribute("orders", new ArrayList<TransportOrder>());
     model.addAttribute("invoice", new Invoice());

     return "addInvoice";
}

unusedOrders is a list of orders that a user can choose from, orders is a list that is meant to contain orders chosen by user invoice is just an invoice that's being created in the form.

My form contains text and data inputs regarding the invoice, and then comes a multiple select for the orders:

<!-- I used to define th:object here and used th:field in the inputs, however changed it to use th:value everywhere -->
<form action="/invoices/add" method="post">
        <table>
            <tr>
                <th>
                    Invoice ID:
                </th>
                <th>
                    <input type="text" th:value="${invoice.invoiceId}" name="invoiceId"/>
                </th>
            </tr>
            <tr>
                <!-- a few other inputs -->
            </tr>
            <tr>
                <th>
                    Orders:
                </th>
                <th>
                    <!-- problem may lie here -->
                    <select id="orders" th:value="${orders}" multiple="multiple">
                        <option th:each="unusedOrder: ${unusedOrders}"
                                th:value="${unusedOrder.number}"
                                th:text="${unusedOrder}">Unused orders to choose from</option>
                    </select>
                </th>
            </tr>
        </table>
        <button type="submit">Next</button>
    </form>

I've read Thymeleaf docs and forums, as well as several SO questions, but they still leave me confused about how does th:object, th:field, th:value and others work with forms, and especially with multiple select tag.

On submit, the form sends a POST request to a method in the same controller:

@PostMapping(path = "/add")
public String addInvoice(@ModelAttribute Invoice invoice,
                         BindingResult result,
                         @ModelAttribute("orders") ArrayList<TransportOrder> orders,
                         Model model) {

    //invoice and orders saving logic, etc.

    return "viewInvoices";
}

My problem is that invoice is being properly retrieved from the from and persisted in the database, but orders list stays empty. I expect it to be populated with orders chosen in the form. I don't know, if it's because of @ModelAttribute annotation (I also tried @RequestAttribute without success), Thymeleaf tags, or anything else.


Solution

  • Okay, so I decided to fight for the answer once more, and fortunately I stumbled upon an answer.

    Based on this tutorial, I created an wrapper class ChosenOrdersDTO:

    public class ChosenOrdersDTO {
        private List<TransportOrder> chosenOrders;
    
        //constructor, getters, setters...
    }
    

    I added it to the first model (changed the getForm() method as following):

    model.addAttribute("chosenOrders", new ChosenOrdersDTO(new ArrayList<>()));
    

    In the form I used the th:field tag, similarly to previous fields:

    <select id="orders" th:field="${chosenOrders.chosenOrders}" multiple="multiple">
    

    And in the second controller I was able to retrieve the list wrapped in the ChosenOrdersDTO class as a @ModelAttribute:

    @PostMapping(path = "/add")
    public String addInvoice(@ModelAttribute Invoice invoice,
                              BindingResult
                             @ModelAttribute ChosenOrdersDTO chosenOrders,
                              Model model)