Recommended way to handle Thymeleaf Spring MVC AJAX Forms and their error messages
What I like to do is replace the entire form when an error occurs. The following is a super primitive example. I'm not going to use a ton of fragments for rendering a form... just keeping it simple.
This was written in a Spring 4.2.1 and Thymeleaf 2.1.4
A basic class representing a user info form: UserInfo.java
package myapp.forms;
import org.hibernate.validator.constraints.Email;
import javax.validation.constraints.Size;
import lombok.Data;
@Data
public class UserInfo {
@Email
private String email;
@Size(min = 1, message = "First name cannot be blank")
private String firstName;
}
The controller: UsersAjaxController.java
import myapp.forms.UserInfo;
import myapp.services.UserServices;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import javax.transaction.Transactional;
@Controller
@Transactional
@RequestMapping("/async/users")
public class UsersAjaxController {
@Autowired
private UserServices userServices;
@RequestMapping(value = "/saveUserInfo", method = RequestMethod.POST)
public String saveUserInfo(@Valid @ModelAttribute("userInfo") UserInfo userInfo,
BindingResult result,
Model model) {
// if any errors, re-render the user info edit form
if (result.hasErrors()) {
return "fragments/user :: info-form";
}
// let the service layer handle the saving of the validated form fields
userServices.saveUserInfo(userInfo);
return "fragments/user :: info-success";
}
}
The file used to render the form and the success message: fragments/user.html
<div th:fragment="info-form" xmlns:th="http://www.thymeleaf.org" th:remove="tag">
<form id="userInfo" name="userInfo" th:action="@{/async/users/saveUserInfo}" th:object="${userInfo}" method="post">
<div th:classappend="${#fields.hasErrors('firstName')}?has-error">
<label class="control-label">First Name</label>
<input th:field="*{firstName}" type="text" />
</div>
<div th:classappend="${#fields.hasErrors('first')}?has-error">
<label class="control-label">Email</label>
<input th:field="*{email}" ftype="text" />
</div>
<input type="submit" value="Save" />
</form>
</div>
<div th:fragment="info-success" xmlns:th="http://www.thymeleaf.org" th:remove="tag">
<p>Form successfully submitted</p>
</div>
The JS code will simply submit the form to the URL provided in the form action attribute. When the response is returned to the JS callback, check for any errors. If there are errors, replace the form with the one from the response.
(function($){
var $form = $('#userInfo');
$form.on('submit', function(e) {
e.preventDefault();
$.ajax({
url: $form.attr('action'),
type: 'post',
data: $form.serialize(),
success: function(response) {
// if the response contains any errors, replace the form
if ($(response).find('.has-error').length) {
$form.replaceWith(response);
} else {
// in this case we can actually replace the form
// with the response as well, unless we want to
// show the success message a different way
}
}
});
})
}(jQuery));
Again, this is just a basic example. As mentioned in the comments above, there is no right or wrong way to go about this. This isn't exactly my preferred solution either, there are definitely a few tweaks to this I would do, but the general idea is there.
NOTE: There's a flaw in my JS code as well. If you replace the form with the one from the response, the form submit handler will not be applied to the newly replaced form. You would need to ensure you reinitialize the form handler properly after replacing the form, if going this route.
Don't know if anybody's still interested, but I'll leave this here for the sake of anyone looking for an example solution.
I implemented yorgo's solution and got around the "flaw" by including the script in the fragment. There was also another small flaw in that if there was more than one submit button with an extra parameter (which is often used to implement dynamic forms in spring + thymeleaf) the information is lost when the form is serialised. I got around this by manually appending this information onto the serialised form.
You can view my implementation of yorgo's solution with the fixes here: https://github.com/Yepadee/spring-ajax/blob/master/src/main/resources/templates/project_form.html?fbclid=IwAR0kr_-t05_vFrPJxvbsG1Et7aLGir5ayXyPi2EKI6OHbEALgDfmHZ7HaKI
Whats nice about it is that you can implement it on any existing thymeleaf form simply by doing the following:
- put the script in the form and make it reference the form's id
- put the form into a fragment
- change the post methods in the controller to return the form fragment rather than the entire template