Spring Thymeleaf form POST not sending updated collection model attribute

Viewed 425

I have a Spring Boot application that server side renders the web pages. One of the forms the application has isn't working correctly. The form will render the page correctly, and will bind the value to the input. When a value is changed, and the form submitted the value received by controller is the original one.

I have a CardDTO object, that has the following:

@Data
@NoArgsConstructor
public class CardDTO {
    private int cardId;
    // Other fields
    private List<CustomValueDTO> customValues;
}

That is added to the model as a card attribute.

The form is then looks like:

<form method="post" action="#" th:action="${#httpServletRequest.requestURI}" th:object="${card}">
    <div class="row" th:if="${editType == 'notes'}">
        <div class="col-12 text-right">
            <button class="btn btn-primary ml-4" type="submit" th:text="#{app.savebtn.label}">SAVE</button>
        </div>
    </div>
    <div th:switch="${editType}">
        <!-- other cases -->
        <div th:case="'notes'" th:insert="card-form-section-notes :: notes-form"></div>
    </div>
</form>
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<body>
    <div th:fragment="notes-form" id="notes-edit-form" class="pr-3">
        <div class="row text-left" th:each="i: *{#numbers.sequence(0, customValues.size() - 1)}" th:if="*{customValues}">
            <div th:switch="*{customValues[__${i}__].type.name()}">
                <div th:case="LETTER" class="form-group md-form">
                    <textarea rows="1" type="text"
                        class="form-control custom-textarea"
                        th:field="*{customValues[__${i}__].value}" placeholder="Enter letters only"
                        th:errorclass="invalid"></textarea>
                    <label th:for="*{customValues[__${i}__].value}" th:text="*{customValues[__${i}__].label}"></label>
                    <small class="text-danger" th:if="${#fields.hasErrors('customValues[__${i}__].value')}"
                        th:errors="*{customValues[__${i}__].value}">Error</small>
                </div>

                <!-- more inputs -->
            </div>
        </div>
    </div>
</body>
</html>

With the controller method looking like:


@Controller
@SessionAttributes({"card"})
public class CardCreateEditController {

    @PostMapping(value = {"/cards/{cardUUID}/{updateType}/edit","/locations/{locationUUID}/cards/{cardUUID}/{updateType}/edit"})
    public String submitChildCardEdit(@AuthenticationPrincipal OidcUser currentUser, @PathVariable String updateType, @PathVariable String cardUUID, @PathVariable Optional<String> locationUUID, @ModelAttribute("card") @Valid final CardDTO editCardDTO, final BindingResult bindingResult, final Model model, final SessionStatus status) {
    // logic here
    }
}

So the issue is that when the form is updated, the @ModelAttribute("card") @Valid final CardDTO editCardDTO parameter that is sent with it still has the original values in editCardDTO.customValues(i) from when the page loaded.

Looking at the network request in Chrome it shows that it's submitting the form being submitted with the update values, and these form parameters:

customValues[0].value: database1234
customValues[1].value: test
...

I have tried adding all fields from the CustomValueDTO as hidden inputs, which are also submitted as form params but still not changes.

This is pretty much the exact same to a different form I have, and that one works perfectly. I can't seem to figure out why this one doesn't.

UPDATE - with full HTML:

card-edit-form.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<body>

    <div th:fragment="card-form-content">

        <div class="card">

            <!-- Card body -->
            <div class="card-body">

                <p class="h4 mb-3" th:text="#{${'myCompany.header.edit.'+editType}}">EDIT</p>
                <form method="post" action="#" th:action="${#httpServletRequest.requestURI}" th:object="${card}"
                    autocomplete="off" enctype="application/x-www-form-urlencoded">
                    <div class="row" th:if="${editType == 'notes'}">
                        <div class="col-12 text-right">
                            <a th:if="${locationObj==null}" href="#"
                                th:href="@{/cards/{cardUUID}/cancel(cardUUID=${{cardUUID}})}"
                                th:text="#{myCompany.cancelbtn.label}" tabindex="-1">CANCEL</a>
                            <a th:if="${locationObj!=null}" href="#"
                                th:href="@{/locations/{locationUUID}/cards/{cardUUID}/cancel(cardUUID=${{cardUUID}},locationUUID=${{locationObj.locationUUID}})}"
                                th:text="#{myCompany.cancelbtn.label}" tabindex="-1">CANCEL</a>
                            <button class="btn btn-primary ml-4" type="submit" th:text="#{myCompany.savebtn.label}">SAVE</button>
                        </div>
                    </div>
                    <h6 class="mb-4" th:if="${#messages.msgOrNull('myCompany.subheader.edit.'+editType)!=null}"
                        th:text="#{${'myCompany.subheader.edit.'+editType}}">SUBTITLE</h6>
                    <hr>
                    <div th:switch="${editType}">
                        <div th:case="'ico'" th:insert="card-form-section-ico :: ico-form"></div>
                        <div th:case="'info'" th:insert="card-form-section-info :: info-form"></div>
                        <!--/* <div th:case="'address'" th:insert="card-form-section-address :: address-form"></div> */-->
                        <div th:case="'health'" th:insert="card-form-section-health :: health-form"></div>
                        <div th:case="'resideswith'" th:insert="card-form-section-resideswith :: resideswith-form">
                        </div>
                        <div th:case="'notes'" th:insert="card-form-section-notes :: notes-form"></div>
                    </div>

                    <div th:if="${editType != 'notes'}">
                        <div class="row mt-5 align-bottom">
                            <div class="col-12 text-right">
                                <a th:if="${locationObj==null}" href="#"
                                    th:href="@{/cards/{cardUUID}/cancel(cardUUID=${{cardUUID}})}"
                                    th:text="#{myCompany.cancelbtn.label}" tabindex="-1">CANCEL</a>
                                <a th:if="${locationObj!=null}" href="#"
                                    th:href="@{/locations/{locationUUID}/cards/{cardUUID}/cancel(cardUUID=${{cardUUID}},locationUUID=${{locationObj.locationUUID}})}"
                                    th:text="#{myCompany.cancelbtn.label}" tabindex="-1">CANCEL</a>
                                <button class="btn btn-primary ml-4" type="submit"
                                    th:text="#{myCompany.savebtn.label}">SAVE</button>
                            </div>
                        </div>
                    </div>

                </form>

            </div> <!-- card body -->
        </div> <!-- card -->

<div th:if="${editType != 'notes'}">
        <div class="mt-4 text-center">
            <small><a href="#" th:text="#{myCompany.learnmore.part1}" data-toggle="modal"
                    data-target="#whyWeAskModal">LEARN MORE</a> <span th:text="#{myCompany.learnmore.part2}">ABOUT WHY WE
                    ASK FOR THIS INFO.</span></small>
        </div>
        </div>
        <div th:insert="components/card-why-we-ask-for-info :: why-we-ask-modal"></div>

    </div> <!-- fragment -->

</body>

</html>

card-form-section-notes.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">

<body>
    <div th:fragment="notes-form" id="notes-edit-form" class="pr-3">
        <h4 class="mt-3" th:if="*{customValues == null}" th:text="#{myCompany.section.cards.customfields.edit.noneexistmsg}">NONE EXIST YET.</h4>
        <div class="row text-left" th:each="customValue, itemStat : *{customValues}" th:if="*{customValues}">
            <div th:switch="*{customValues[__${itemStat.index}__].type.name()}">
<!--                <input type="hidden" th:field="*{customValues[__${i}__].id}">-->
<!--                <input type="hidden" th:field="*{customValues[__${i}__].uuid}">-->
<!--                <input type="hidden" th:field="*{customValues[__${i}__].type}">-->
<!--                <input type="hidden" th:field="*{customValues[__${i}__].label}">-->
<!--                <input type="hidden" th:field="*{customValues[__${i}__].customField}">-->

                <div th:case="LETTER" class="form-group md-form">
                    <textarea rows="1" type="text"
                        class="form-control custom-textarea"
                        th:field="*{customValues[__${itemStat.index}__].value}" placeholder="Enter letters only"
                        th:errorclass="invalid"></textarea>
<!--                    <label th:for="*{customValues[__${itemStat.index}__].value}" th:text="*{customValues[__${itemStat.index}__].label}"></label>-->
<!--                    <small class="text-danger" th:if="${#fields.hasErrors('customValues[__${itemStat.index}__].value')}"-->
<!--                        th:errors="*{customValues[__${itemStat.index}__].value}">Error</small>-->
                </div>

<!--                <div th:case="NUMBER" class="md-form form-group">-->
<!--                    <input type="number" th:id="'customValue' + ${i}" th:field="*{customValues[__${i}__].value}"-->
<!--                        class="form-control" th:errorclass="invalid" placeholder="Enter numbers only">-->
<!--                    <label th:for="'customValue' + ${i}" th:text="*{customValues[__${i}__].label}">NAME</label>-->
<!--                    <small class="text-danger" th:if="${#fields.hasErrors('*{customValues[__${i}__].value}')}"-->
<!--                        th:errors="*{customValues[__${i}__].value}">Error</small>-->
<!--                </div>-->

<!--                <div th:case="YESNO" class="form-group md-form yes-no-input">-->
<!--                    <span class="label-multi" th:text="*{customValues[__${i}__].label}">YES/NO</span>-->
<!--                    <div class="yes-no-check pb-2">-->
<!--                        <div class="form-check form-check-inline" th:each="b : ${allBooleanTypes}">-->
<!--                            <input class="form-check-input" type="radio"-->
<!--                                   th:field="*{customValues[__${i}__].value}" th:value="${b}" />-->
<!--                            <label class="form-check-label" th:for="*{customValues[__${i}__].value}"-->
<!--                                   th:text="#{${b==true ? 'myCompany.yes' : 'myCompany.no'}}">Wireframe</label>-->
<!--                        </div>-->
<!--                        <small class="text-danger" th:if="${#fields.hasErrors('*{customValues[__${i}__].value}')}"-->
<!--                               th:errors="*{customValues[__${i}__].value}">Error</small>-->
<!--                    </div>-->
<!--                </div>-->

<!--                <div th:case="PHONE" class="md-form form-group">-->
<!--                    <input type="text" th:id="'customValue' + ${i}" th:field="*{customValues[__${i}__].value}"-->
<!--                        class="form-control phone-text" th:errorclass="invalid" placeholder="###-###-####">-->
<!--                    <label th:for="'customValue' + ${i}" th:text="*{customValues[__${i}__].label}">NAME</label>-->
<!--                    <small class="text-danger" th:if="${#fields.hasErrors('*{customValues[__${i}__].value}')}"-->
<!--                        th:errors="*{customValues[__${i}__].value}">Error</small>-->
<!--                </div>-->

<!--                <div th:case="DATE" class="md-form form-group">-->
<!--                    <input type="text" th:id="'customValue' + ${i}" th:field="*{customValues[__${i}__].value}"-->
<!--                        class="form-control child_dob_input_date" th:errorclass="invalid" placeholder="YYYY-MM-DD">-->
<!--                    <label th:for="'customValue' + ${i}" th:text="*{customValues[__${i}__].label}">NAME</label>-->
<!--                    <small class="text-danger" th:if="${#fields.hasErrors('*{customValues[__${i}__].value}')}"-->
<!--                           th:errors="*{customValues[__${i}__].value}">Error</small>-->
<!--                </div>-->

<!--                <div th:case="YEARMONTH" class="md-form form-group">-->
<!--                    <input type="text" th:id="'customValue' + ${i}" th:field="*{customValues[__${i}__].value}"-->
<!--                        class="form-control child_dob_input_YM" th:errorclass="invalid" placeholder="YYYY-MM">-->
<!--                    <label th:for="'customValue' + ${i}" th:text="*{customValues[__${i}__].label}">NAME</label>-->
<!--                    <small class="text-danger" th:if="${#fields.hasErrors('*{customValues[__${i}__].value}')}"-->
<!--                           th:errors="*{customValues[__${i}__].value}">Error</small>-->
<!--                </div>-->

<!--                <div th:case="ANY" class="form-group md-form">-->
<!--                    <textarea rows="1" type="text" th:name="'customFields.customField' + ${i}"-->
<!--                        th:id="'customValue' + ${i}" class="form-control custom-textarea"-->
<!--                        th:field="*{customValues[__${i}__].value}" placeholder="Enter any letters"-->
<!--                        th:errorclass="invalid"></textarea>-->
<!--                    <label th:for="'customValue' + ${i}" th:text="*{customValues[__${i}__].label}">NAME</label>-->
<!--                    <small class="text-danger" th:if="${#fields.hasErrors('*{customValues[__${i}__].value}')}"-->
<!--                           th:errors="*{customValues[__${i}__].value}">Error</small>-->
<!--                </div>-->

            </div>
            <!--</div>-->
        </div>

    </div> <!-- fragment -->

</body>

</html>
1 Answers

The problem can be with the HTTP POST request. Specifically its encoding.
See this answer please: https://stackoverflow.com/a/26503292/3461793

Assuming you do not have @SessionAttributes on your controller with the "card" and mixing your conversation id.

Related