I'm skilling in form validation with spring boot and thymeleaf and i have problem: i can't do validation in form with two #ModelAttribute fields. The example like form validation om spring official site works correctly, but when I added two #model Attribute in post i get only error at webpage and no hints at form like in spring example.
Controller class:
#Controller
public class MyController {
#Autowired
InstructorRepository instructorRepository;
#Autowired
DetailRepository detailRepository;
#GetMapping("/index")
public String mainController(){
return "index";
}
#GetMapping("/add")
public String addInstructorForm(Model model){
model.addAttribute("instructor", new Instructor());
model.addAttribute("detail", new InstructorDetail());
return "addInstructor";
}
#PostMapping("/add")
public String submitForm(#Valid #ModelAttribute Instructor instructor, #ModelAttribute InstructorDetail instructorDetail, BindingResult bindingResult1){
/* if (bindingResult.hasErrors()) {
return "instructorsList";
}
instructor.setInstructorDetail(instructorDetail);
instructorRepository.save(instructor);*/
if (bindingResult1.hasErrors()) {
return "addInstructor";
}
return "redirect:/instructorsList";
}
#GetMapping("/instructorsList")
public String getList(Model model){
Map map = new HashMap<>();
List list = new ArrayList<Instructor>();
list = instructorRepository.findAll();
List resultList = new ArrayList();
for (int i = 0; i < list.size(); i++) {
Instructor instructor = (Instructor)list.get(i);
InstructorDetail detail = detailRepository.getInstructorDetailById(instructor.getId());
InstructorAndDetail iid = new InstructorAndDetail(instructor, detail);
resultList.add(iid);
}
model.addAttribute("instructors", resultList);
return "instructorsList";
}
}
html form snippet:
<form action="#" data-th-action="#{/add}" data-th-object="${instructor}" method="post">
<div class="form-group">
<label for="1">First name</label>
<input class="form-control" id="1" type="text" data-th-field="${instructor.firstName}" placeholder="John"/>
<div data-th-if="${#fields.hasErrors('firstName')}" data-th-errors="${instructor.firstName}">name error</div>
</div>
There was next problem: then I add entity to thymeleaf form I passed only 2 fields (or one field after), but there was 3 fields with
#NotNull
#Size(min=2, max=30)
So when I commented them in my code the single field validation begin to work :).
If you stucked at the same problem check that all you fields in class that marked #Valid annotation are mirrored in your form.
(or have default valid values? UPD: dont work with valid defaults if they have no form mirroring)
Related
I have a form with a date-time input, like this:
<form th:object="${appointment}"
method="post"
th:action="#{/appointments/{id}/book (id=*{customerId})}">
<input type="datetime-local" th:field="*{dateTime}">
<button type="submit">Book appointment</button>
</form>
The form is accessible via a URL like:
http://localhost:8080/appointments/453ef24c-f11e-4f33-ae0a-d6cbde4c4d45/book
Basically I need to pass the ID via the URL to create the appointment for the Customer with that ID.
I added a validation in the Model responsible to get the data from the form:
#Data
#NoArgsConstructor
public class AppointmentViewModel {
#Future(message = "appointment date and time must not be in the past")
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private LocalDateTime dateTime;
private UUID customerId;
}
The validation is added because I want to display in Thymeleaf the validation error with something like:
<form th:object="${appointment}"
method="post"
th:action="#{/appointments/{id}/book (id=*{customerId})}">
<input type="datetime-local" th:field="*{dateTime}">
<!-- New code -->
<span th:if="${#fields.hasErrors('dateTime')}" th:errors="*{dateTime}"></span>
<button type="submit">Book appointment</button>
</form>
My controller would be something like this:
#Controller
#RequestMapping("/appointments")
#RequiredArgsConstructor
class AppointmentsController {
// ...
#GetMapping("/{customerId}/book")
String viewBookAppointment(#PathVariable("customerId") UUID customerId, Model model) {
AppointmentViewModel appointmentViewModel = new AppointmentViewModel();
appointmentViewModel.setCustomerId(customerId);
// ...
return "appointments/creation";
}
#PostMapping("/{customerId}/book")
String bookAppointment(#PathVariable("customerId") UUID customerId,
#Valid AppointmentViewModel appointmentViewModel,
Errors errors) {
if (errors.hasErrors())
return "appointments/creation";
// ...
}
}
This way, I thought, I would be able to display the error in the view appointments/creation, but the solution above doesn't work because the view, in order to work, needs the path variable. So I used a redirect:
if (errors.hasErrors())
return String.format("/appointments/%s/book", customerId);
but in this way the error is not passed to the appointments/creation.
How can I redirect to a view in case of validation errors, passing a path variable and keeping the errors field in the Thymeleaf rendered page?
You can try as below with BindingResult
#PostMapping("/{customerId}/book")
String bookAppointment(#PathVariable("customerId") UUID
customerId, #Valid AppointmentViewModel
appointmentViewModel, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
FieldError error = new FieldError("appointment",
"CustomerId", customerId+"has error");
bindingResult.addError(error);
return "appointments/creation";
}
return "redirect:/results";
}
The solution was this:
#PostMapping("/{customerId}/book")
String bookAppointment(#PathVariable("customerId") UUID customerId,
#ModelAttribute("appointment") #Valid AppointmentViewModel appointmentViewModel,
Errors errors) {
if (errors.hasErrors()) {
return "appointments/creation";
}
Instant appointmentDateTime = timeConverter.toInstant(appointmentViewModel.getDateTime());
appointmentsService.book(customerId, appointmentDateTime);
return String.format("redirect:/appointments/%s/list", customerId);
}
The difference is the #ModelAttribute("appointment") which binds the model to the attribute named appointment.
This was necessary as the name of the class I was using as DTO was not Appointment but AppointmentViewModel.
If I had used Appointment it would have worked from the beginning.
Validating the overlapping of date intervals, in the implementation of the Validator class. I get a list from a Posgresql database, which I would like to show, next to the error message.
I've tried to insert it, without succeeding, with this line of code:
model.addAttribute("dateOverlaps", pricesValidator.getDBIntervals());
That's the complete code:
#Controller
public class PricesController {
#Autowired
private RateRepository rateRepository;
#Autowired
private PricesValidator pricesValidator;
#InitBinder
protected void initBinder(WebDataBinder binder) {
binder.addValidators(pricesValidator);
}
//Enter new price form
#GetMapping("/admin/rates/priceform")
public String priceForm(Model model){
model.addAttribute("rate", new Rate());
model.addAttribute("dateOverlaps", pricesValidator.getDBIntervals());
return "/admin/rates/priceform";
}
#PostMapping("/admin/rates/priceform")
public String priceSubmit(#ModelAttribute #Valid Rate price, BindingResult bindingResult){
if(bindingResult.hasErrors()){
return "/admin/rates/priceform";
}
rateRepository.addRate(price);
return "redirect:/admin/rates/prices";
}
}
I use Thymeleaf, but I take for granted that the problem is not with the viewer.
This is the html view:
<!--Global validation results-->
<div th:if="${#fields.hasErrors('global')}">
<div class="alert alert-danger" role="alert"
th:each="err : ${#fields.errors('global')}">
<div th:switch="${err}">
<p th:case="error.fromAfterTo" th:text="#error.fromAfterTo}"></p>
<p th:case="error.overlaps" th:text="#{error.overlaps}"></p>
<ul>
<li th:text="#{from} + ' - ' + #{to}"></li>
<li th:each="interval : ${dateOverlaps}"
th:text="${#temporals.format(interval.datefrom, 'dd/MM/yyyy')} + '-' +
${#temporals.format(interval.dateto, 'dd/MM/yyyy')}">Intervals</li>
</ul>
</div>
</div>
</div><!--Global validation results-->
Thank you in advance for your help.
The problem is that pricesValidator.getDBIntervals() at #GetMapping:
model.addAttribute("dateOverlaps", pricesValidator.getDBIntervals());
is calling the list that I want to show with the message error, which is empty until the form has been submitted. Because its values, are picked up from the database, with the validation process.
That raises a new question:
How can I set this model attribute in the #PostMapping section?
The answer to this question is simple, in the #PostMapping section, you can set a Model, as well as in the #GetMapping one. In my case, this was the solution:
#PostMapping("/admin/rates/priceform")
public String priceSubmit(#ModelAttribute #Valid Rate price, BindingResult bindingResult, Model model){
if(bindingResult.hasErrors()){
model.addAttribute("dateOverlaps", pricesValidator.getDBIntervals());
return "/admin/rates/priceform";
}
rateRepository.addRate(price);
return "redirect:/admin/rates/prices";
}
How to bind input elements to an arraylist element in Spring MVC?
The view model:
public class AssigneesViewModel {
private int evaluatorId;
private int evaluatedId;
private String evaluatorName;
private String evalueatedName;
//getters and setters
}
The model attribute:
public class AssignEvaluationForm{
private ArrayList<AssigneesViewModel> options;
public ArrayList<AssigneesViewModel> getOptions() {
return options;
}
public void setOptions(ArrayList<AssigneesViewModel> options) {
this.options = options;
}
}
Controller
#RequestMapping(value="addAssignment", method = RequestMethod.GET)
public String addAssignment(Model model){
model.addAttribute("addAssignment", new AssignEvaluationForm());
return "addAssignment";
}
Then in the jsp i have 4 hidden inputs which represent the fields for the evaluatedId, evaluatorId, evaluatorName, evaluatedName -> options[0].
How i am going to write the jsp code to map those inputs with an element of the arrayList?
Update:
<form:form commandName="addAssignment" modelAttribute="addAssignment" id="addAssignment" method="POST">
//..........
<c:forEach items="${addAssignment.options}" var="option" varStatus="vs">
<div id="assigneesOptions" >
<form:input path="addAssignment.options[${vs.index}].evaluatedId" value="1"></form:input>
</div>
</c:forEach>
//..............
</form:form>
With this update i get the following error:
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'options[]' available as request attribute
<form:input path="addAssignment.options[${vs.index}].evaluatedId" value="1"></form:input>
Instead of this addAssignment.options[${vs.index}].evaluatedId
Use this -> option.evaluatedId
Or you might reach value with arraylist get -> ${addAssignment.options.get(vs.index).evaluatedId} , try to turn out that ${} jstl's call curly brackets. BTW i'm not sure this last example work on path="" attribute.
I am using Spring 3 and Tiles 3. Below is just a simplified example I made. I have a test controller where I list all the SimpleEntity objects. And there is an input field on the JSP to add a new entity via a POST. Here is the controller.
#Controller
#RequestMapping(value="/admin/test")
public class TestAdminController {
private String TEST_PAGE = "admin/test";
#Autowired
private SimpleEntityRepository simpleEntityRepository;
#ModelAttribute("pageName")
public String pageName() {
return "Test Administration Page";
}
#ModelAttribute("simpleEntities")
public List<SimpleEntity> simpleEntities() {
return simpleEntityRepository.getAll();
}
#RequestMapping(method=RequestMethod.GET)
public String loadPage() {
return TEST_PAGE;
}
#RequestMapping(method=RequestMethod.POST)
public String addEntity(#RequestParam String name) {
SimpleEntity simpleEntity = new SimpleEntity();
simpleEntity.setName(name);
simpleEntityRepository.save(simpleEntity);
return "redirect:/" + TEST_PAGE;
}
}
Everything works fine. However, when I submit the form, the URL adds the pageName parameter, so it goes from /admin/test to /admin/test?pageName=Test+Administration+Page. Is there anyway to prevent this from happening when the page reloads?
UPDATE
Here is the JSP form.
<form:form action="/admin/test" method="POST">
<input type="text" name="name" />
<input type="submit" name="Save" />
</form:form>
I am using Spring MVC with Annotations. Here's a quick outline of my problem.
My Domain:
public class Restaurant {
private String name;
private Address address = new Address();
//Get and set....
}
public class Address{
private String street;
//Get and set....
}
My Controller:
//Configure and show restaurant form.
public ModelAndView showAction() {
ModelAndView mav = new ModelAndView("/restaurant/showRestaurant");
restaurant = new Restaurant();
mav.addObject("restaurant", restaurant);
return mav;
}
//Save restaurant
public ModelAndView saveAction(#ModelAttribute(value="restaurant") Restaurant restaurant,BindingResult result) {
restaurant.getName();//<- Not is null
restaurant.getAddress().getStreet(); //<- is null
}
My View:
<form>
<span class="full addr1">
<label for="Nome">Name<span class="req">*</span></label>
<h:inputText class="field text large" value="#{restaurant.name}"
id="name" forceId="true" styleClass="field text addr"/>
</span>
<span class="full addr1">
<label for="Nome">Street <span class="req">*</span></label>
<h:inputText class="field text large" value="#{restaurant.address.street}"
id="street" forceId="true" styleClass="field text addr"/>
</span>
</form>
My problem is, when I fill the name and the street to call the method "saveAction" when I try to get the restaurant filled happens that the name comes from the street but did not.
I'm not all that familliar with jsf, but for binding in spring you generally need the full path, i.e. name="address.street", in order to get the street name bound properly
Try binding using the spring form tags http://static.springsource.org/spring/docs/2.0.x/reference/spring-form.tld.html. Its pretty easy.