I have a Spring Data form binding interface like this:
interface GuestbookForm {
/**
* Returns the value bound to the {@code name} attribute of the request.
*
* @return
*/
@NotBlank
String getName();
/**
* Returns the value bound to the {@code text} attribute of the request.
*
* @return
*/
@NotBlank
String getText();
…
}
and a Spring MVC controller method using that interface to bind the form as well as an Errors instance to collect binding errors:
@PostMapping(path = "/guestbook")
String addEntry(@Valid GuestbookForm form, Errors errors, Model model) {
if (errors.hasErrors()) {
return guestBook(model, form);
}
guestbook.save(form.toNewEntry());
return "redirect:/guestbook";
}
While the Errors object that gets handed into the controller carries the errors, in the form below, they're inaccessible, i.e. #fields.hasErrors('*') returns false, error CSS classes are not set etc:
<form method="post" role="form" class="gb-form" id="form" th:action="@{/guestbook}" th:object="${form}">
<div th:if="${#fields.hasErrors('*')}">Entry has errors!</div>
<div class="form-group">
<label for="name" th:text="#{guestbook.form.name}">Name</label><br />
<input class="form-control" type="text" th:field="*{name}" th:errorclass="fieldError" /><br />
</div>
<div class="form-group">
<label for="text" th:text="#{guestbook.form.text}">Text</label><br />
<textarea th:field="*{text}" th:errorclass="fieldError" class="form-control"></textarea><br />
</div>
<input type="submit" class="btn btn-default" th:value="#{guestbook.form.submit}" value="Senden" />
</form>
Here's what I've noticed and debugged already:
- The controller method argument not carrying an
@ModelAttribute is not an oversight. It needs to be that way so that the standard Spring form binding doesn't get active but Spring Data's special interface binding.
- I've debugged into
FieldUtils.getBindStatusFromParsedExpression(…) and can see that the requestContext that's obtained in the first step doesn not contain the Errors instance that the controller sees but an empty one.
- This leads me to the assumption that Thymeleaf sort of loses the connection between the model object
form and the associated Errors instance.
- I failed to find the code that makes Thymeleaf aware of the binding result produced by Spring MVC in the first place.
You can find a reproducing sample project here. Steps to reproduce:
$ git clone https://github.com/st-tu-dresden/guestbook
$ cd guestbook
$ git co thymeleaf
$ mvn spring-boot:run
Browse to http://localhost:8080, submit the empty form. See the page redisplayed without any errors. Put a breakpoint in GuestbookContoller.addEntry(…) and see the Errors instance actually carrying errors.
I have a Spring Data form binding interface like this:
and a Spring MVC controller method using that interface to bind the form as well as an
Errorsinstance to collect binding errors:While the
Errorsobject that gets handed into the controller carries the errors, in the form below, they're inaccessible, i.e.#fields.hasErrors('*')returns false, error CSS classes are not set etc:Here's what I've noticed and debugged already:
@ModelAttributeis not an oversight. It needs to be that way so that the standard Spring form binding doesn't get active but Spring Data's special interface binding.FieldUtils.getBindStatusFromParsedExpression(…)and can see that therequestContextthat's obtained in the first step doesn not contain theErrorsinstance that the controller sees but an empty one.formand the associatedErrorsinstance.You can find a reproducing sample project here. Steps to reproduce:
Browse to http://localhost:8080, submit the empty form. See the page redisplayed without any errors. Put a breakpoint in
GuestbookContoller.addEntry(…)and see theErrorsinstance actually carrying errors.