how to display validation errors from another form submit - validation

Imagine a form with a single field where the user provides their email. On POST, the controller action method sends a confirmation code to that email and displays a second form in which the user is supposed to enter the confirmation code they received. If the code does not match I then return the original view and I would like to display an error message next to the email ("email was not confirmed").
Sample code below:
1st view (asking the email)
<div>
Please provide the following information to sign-up:
<form:form modelAttribute="newAccountInfo" action="signup-submit.do" method="POST">
<div><form:label path="email">Email:</form:label>
<form:input type="text" path="email"/><form:errors path="email" cssClass="error" element="div"/>
</div>
...
1st view controller method
#RequestMapping(path="/signup-submit", method=RequestMethod.POST)
public String signupSubmit(HttpServletRequest request
, #ModelAttribute("newAccountInfo") #Valid NewAccountInfo newAccountInfo
, BindingResult result
, Model model) {
String confirmCode = generateRandomSecret();
// send confirmCode by email to newAccountInfo.email (omitted)
model.addAttribute("emailConfirmation" , new EmailConfirmation());
request.getSession().setAttribute("newAccountInfo", newAccountInfo);
request.getSession(false).setAttribute("email-code", confirmCode);
return View.SIGNUP_EMAIL_CONFIRMATION.name;
}
2nd view (asking the confirmation code)
<div>
Enter the confirmation code that was sent to your email:
<form:form modelAttribute="emailConfirmation" action="signup-email-confirmation-submit.do" method="POST">
<form:label path="code">Confirmation code:</form:label>
<form:input type="text" path="code"/>
<input type="submit" value="Submit" />
</form:form>
2nd view controller method
#RequestMapping(path="/signup-email-confirmation-submit", method=RequestMethod.POST)
public String signupEmailConfirmationSubmit(
#ModelAttribute("emailConfirmation") EmailConfirmation emailConfirmation
, BindingResult result
, Model model) {
if (emailConfirmation.getCode().equals(request.getSession(false).getAttribute("email-code")))
return View.SIGNUP_SUCCESS.name;
else {
model.addAttribute("newAccountInfo", request.getSession(false).getAttribute("newAccountInfo"));
request.getSession(false).invalidate();
// TODO - what should I do here ?
return View.SIGNUP.name;
}
Assuming the confirmation code was not correctly entered, what should I do in the second view controller method so that when the first view is displayed (for the second time), there is a field validation error message next to the email with description "email was not confirmed" ?
In the line marked with the TODO comment I 've tried the following:
result.rejectValue("email", null, "email was not confirmed");
… but that results in the following exception:
org.springframework.beans.NotReadablePropertyException: Invalid property 'email' of bean class [EmailConfirmation]: Bean property 'email' is not readable or has an invalid getter method: Does the return type of the getter match the parameter type of the setter?
(which makes sense as the email is not a field of EmailConfirmation).
However, the below also fails (silently, without an exception, the 1st view is displayed, I just don't see the validation error message):
result.addError(new FieldError("newAccountInfo", "email", "email could not be confirmed"));
In the end, the only way I could get this to work was to add a custom model property by adding the following (in the TODO line of the second view controller method always):
model.addAttribute("emailConfirmationError", true);
… and then modifying the 1st view as follows:
<div><form:label path="email">Email:</form:label>
<form:input type="text" path="email"/>
<form:errors path="email" cssClass="error" element="div"/>
<c:if test="${not empty emailConfirmationError}">
<span class="error">The email could not be confirmed</span>
</c:if>
</div>
The above succeeds but feels like a hack as I am not using the validation machinery of Spring MVC.
My questions are:
what is the idiomatic way to achieve the above
are there any other flawed mental models or misunderstandings present in the above code?

Related

How to use spring mvc with thymeleaf to iterate over a list?

My goal is to cycle through a list of objects, some to be displayed on the screen, others to be passed into a form as an object of which I can define certain aspects and then return to the controller the object and attribute to be modified.
The problem with the following approach is that the object in the list is not passed correctly to the form and thus gives an error because it is trying to make changes to a non-existent object.
If, on the other hand, I try to pass it as an object via ModelAndView it obviously works but does not have all the characteristics of the object I passed via the list.
Controller
#GetMapping("/")
public ModelAndView home() throws IOException {
ModelAndView mv = new ModelAndView();
mv.setViewName("home");
List<Comics> allComics = cs.getAll();
mv.addObject("comics", allComics);
return mv;
}
#PostMapping("/update")
public ModelAndView update(Comics com, #RequestParam("attr") String attr) throws IOException {
ModelAndView mv = new ModelAndView();
com.setLastRead(attr);
cs.updateAttributes(com);
mv.setViewName("home");
List<Comics> allComics = cs.getAll();
mv.addObject("comics", allComics);
return mv;
}
home.html
<html xmlns:th="http://www.thymeleaf.org">
<tr th:each="comic : ${comics}">
<td th:text="${comic.title}"></td>
<td th:text="${comic.lastChapter}"></td>
<td>
<a th:href="${comic.lastChapterLink}" target="_blank"
role="button" class="btn btn-md btn-block btn-info"> Link
</a>
</td>
<td></td>
<td>
<form th:action="#{/update}" th:object="${comic}" method="post">
<input type="text" name="attr" id="attr"/>
<button type="submit">Sub</button>
</form>
</td>
</tr>
PS: I cut out the head of the html page because it was full of non-relevant CDNs
How can I integrate Spring MVC with Thymeleaf to achieve the result whereby passing a list of objects can be displayed on the screen and used for other purposes within the html page without throwing errors?
Obviously if you know of more efficient methods to achieve the result I'm listening; I only used this method because I didn't know of any others.
Thank you
Answer to #RafaeldaSilva:
I agree, but that does not solve the problem.
Let me explain: the attribute I am going to modify through the form already has its name to allow what you wrote.
But the object iterated through:
tr th:each="comic : ${comics}">
cannot be passed directly as input, as it is a value that is taken from a list and exists individually only in the html page.
One might think of passing it as hidden input, but in this case the result would be the same (I have tried):
<form th:action="#{/update}" th:object="${comic}" method="post">
<input type="hidden" value="${comic}" name="com"/>
<input type="text" name="attr" id="attr"/>
<button type="submit">Sub</button>
</form>
#PostMapping("/update")
public ModelAndView update(#RequestParam("com") Comics com, #RequestParam("attr") String attr) throws IOException {
ModelAndView mv = new ModelAndView();
com.setLastRead(attr);
System.out.println("Comic: " + com);
cs.updateAttributes(com);
mv.setViewName("home");
List<Comics> allComics = cs.getAll();
mv.addObject("comics", allComics);
return mv;
}
Error:
[org.springframework.web.bind.MissingServletRequestParameterException: Required request parameter 'com' for method parameter type Comics is present but converted to null]
try removing the type="hidden" to see what is present in this input, as I understand you are inserting an object by doing value="${comic}", this way the input should not send the value wanted..
change this: <input type="hidden" value="${comic}" name="com"/>
for this: <input type="text" value="${comic}" name="com"/>
so you can see what the form is sending to the controller, I believe it's the object's memory path, and not the data that exists in it.
in the input you must inform the attributes of the object, not the complete object..

Dynamically created elements are not validated on the client

I have a table in my web application, which is populated from the model, where properties have attribute for validation:
[Required(ErrorMessage = "Please enter amount!")]
[DisplayFormat(NullDisplayText = "", ApplyFormatInEditMode = true)]
public decimal? Amount { get; set; }
When I press Submit, the field is properly validated on the client, displaying an error message, if the amount is left empty.
Now the user can add new TRs to the table using jquery. The new record completely imitates the existing records, e.g. when I inspect the Amount field for an existing TR:
<input class="form-control" type="text" data-val="true" data-val-number="The field Amount must be a number." data-val-required="Please enter amount!" id="Financials_1__Amount" name="Financials[1].Amount" value="1834.09"><span class="text-danger field-validation-valid" data-valmsg-for="Financials[1].Amount" data-valmsg-replace="true"></span>
Cf. to the same field of the dynamically added TR:
<input class="form-control" type="text" data-val="true" data-val-number="The amount must be a number." data-val-required="Please enter amount!" id="Financials_77e9f261-010a-4c7c-ae50-e3f6587a8c4e__Amount" name="Financials[77e9f261-010a-4c7c-ae50-e3f6587a8c4e].Amount" value="33"><span class="text-danger field-validation-valid" data-valmsg-for="Financials[77e9f261-010a-4c7c-ae50-e3f6587a8c4e].Amount" data-valmsg-replace="true"></span>
The records look very similar. Yet when I leave the required field empty and press submit, the value is not validated on the client, and the execution comes to the controller's action method. There ModelState.IsValid is false though. This is my first issue.
Here is the action method:
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<ActionResult> Edit(int contractorId, ContractorDetailsDto dto)
{
if (ModelState.IsValid)
{
...
}
return View(_contractorRepository.GetContractorDetailsViewModelByDto(dto));
}
As our ModelState is invalid, the same view is loaded. There the error is displayed in the validation summary, BUT NOT UNDER THE FIELD WITH THE INVALID VALUE. This is the second issue.
I have on my view:
#section Scripts {
<partial name="_ValidationScriptsPartial" />
}
How can I find the cause of my problems?
Answering for your main question. Can you try to execute next javascript code after you add new row
function refreshValidators(formSelector) {
var targetForm = $(formSelector);
targetForm.removeData('validator');
targetForm.removeData('unobtrusiveValidation');
targetForm.removeAttr('novalidate');
$.validator.unobtrusive.parse(targetForm);
}
Regarding your second problem I think you should create separate question for it as it unrelated to your main question. You need to find proper way how to add new rows in ASP.NET Core. For ASP.NET MVC we had next solution how to solve this task http://blog.stevensanderson.com/2010/01/28/editing-a-variable-length-list-aspnet-mvc-2-style/ . After googling BeginCollectionItem I found core analog solution, but it is pretty old and I didnt use it personaly.

Spring form with thymeleaf not binding right, throwing exception: Parameter is null

I have two views, one to book appointments and one to show them in a calendar view. After the booking was successful, there's a confirmation shown. The confirmation then forwards to the calendar view. I want to pass the booking infos as parameters to the calendar view so it can display the new booking accordingly in the calendar, but a null exception is thrown in that step.
I copied the template of a working form. I inspected the web request, all the necessary data is there, I think it's just not binding right.
data class EventAppointmentSearchRequest (val startDateTime: LocalDateTime, val endDateTime: LocalDateTime, val rooms: List<Room>)
/**
* Gets called when confirming a booking to add it to the DB.
*/
#PostMapping("/roomBookingConfirmation")
fun roomBookingConfirmation(model: Model, #ModelAttribute roomBookingRequest: RoomBookingRequest): String {
makeBooking(roomBookingRequest)
val date = roomBookingRequest.datetimeFrom
val start = roomBookingRequest.datetimeFrom.minusDays(date.dayOfWeek.value.toLong())
val end = roomBookingRequest.datetimeFrom.plusDays(7 - date.dayOfWeek.value.toLong())
model.addAttribute("eventAppointmentSearchRequest", EventAppointmentSearchRequest(
startDateTime = start,
endDateTime = end,
rooms = listOf(roomRepository.findByRoomName(roomBookingRequest.roomNr))
))
return "roomBookingConfirmation"
}
/**
* Displays the appointments in the calendar view according to the request
*/
#PostMapping("/calendarView")
fun calendarView(model: Model, #ModelAttribute eventAppointmentSearchRequest: EventAppointmentSearchRequest): String {
// THIS THROWS THE EXCEPTION: java.lang.IllegalArgumentException: Parameter specified as non-null is null: method ...requests.EventAppointmentSearchRequest.<init>, parameter startDateTime
...
}
<!-- /*#thymesVar id="eventAppointmentSearchRequest" type="de.tudarmstadt.pvw.tulpe.soonToBeArtifactory.requests.EventAppointmentSearchRequest"*/ -->
<form th:action="#{/calendarView}" method="post" th:object="${eventAppointmentSearchRequest}" id="forwardToCalendar" style="grid-column: span 4">
<H1 th:text="#{roomBooking.bookingConfirmed}">
Booking confirmed.
</H1>
<div class="links">
<a href="#" th:text="#{roomBooking.nowRedirecting}" onclick="forwardToCalendar()">Redirecting to
calendarView in </a> <b id="secondsLeft">7</b>
<input type="hidden" th:field="${eventAppointmentSearchRequest.startDateTime}" th:name="startDateTime" th:value="${eventAppointmentSearchRequest.startDateTime}">
<input type="hidden" th:field="${eventAppointmentSearchRequest.endDateTime}" th:name="endDateTime" th:value="${eventAppointmentSearchRequest.endDateTime}">
<input type="hidden" th:field="${eventAppointmentSearchRequest.rooms}" name="rooms[]" th:each="room: ${eventAppointmentSearchRequest.rooms}" th:value="${room.RoomId}">
</div>
...
</form>
I expect the form to just be bound correctly, I can see all the necessary data to use the constructor of EventAppointmentSearchRequest in the web inspector of my browser. Actual output is this error message:
java.lang.IllegalArgumentException: Parameter specified as non-null is null: method ...requests.EventAppointmentSearchRequest., parameter startDateTime
Something like this should suffice for the two date hidden inputs:
<input type="hidden" th:field="*{endDateTime}">
As for the third, th:field takes precedence over name and value attributes, if you look closely into the generated HTML, you'll see that the value is identical for each room hidden input, and it's the toString() on the list of Rooms. This is clearly wrong, and you need to specify each attribute of Room that you want to submit, have a look at the following article: https://www.baeldung.com/thymeleaf-list
I don't see the point in transmitting the details of the booking 2 more times between the client and server, I'd just pass a booking ID to the calendar page and have it load all the details...

Why do I lose information after submit a form with Spring MVC?

As I say int the title I loose information in the object that comes back from JSP to Controller.
From my Controller I pass a ModelAndView with an object of class Historic.
In the JSP page I have access to all of the values of this object, but when I submit I just get part of this information, some looses on the way on.
Controller:
#GetMapping("/tt")
public ModelAndView index(Model model) {
HistoricBO historic = new HistoricBO();
// ... I fulfill this object ...
return new ModelAndView("tt", "historic", historic);
}
In JSP I have access to all the information that I passed.
I use the values in two different ways. The first one (information that later I won't be able to recover) is:
<form:form method="POST" action="/addInput" modelAttribute="historic">
....
<form:label path="userHistoric[0].user.name" />
<form:input path="userHistoric[0].user.name" disabled="true" />
Being userHistoric a list inside HistoricBO object.
And the other way that I use the object values is daoing loop to the registers and show them. I can have these values after submit:
c:forEach items="${historic.userHistoric[0].periods[0].registers}" var="reg" varStatus="rog">
...
<td class="tab-odd">
<form:input path="userHistoric[0].periods[0].registers[${rog.index}].hours[0]" class="monin" type="number" />
</td>
The method that catch the submit is as follows:
#PostMapping("/addInput")
public String savePeriod(
#ModelAttribute("historic") HistoricBO inputs,
BindingResult result, ModelMap model) {
if (result.hasErrors()) {
return "error";
}
...
And here the object inputs only has setted the hours values, the rest of the object is empty.
Can you please why is the info loosing and how to solve it?
Thanks
Remove disabled="true" and use readonly="true" or readonly="readonly" instead like below.
<form:input path="userHistoric[0].user.name" readonly="readonly" />
Disabled values will not be submitted with the form.
See this values-of-disabled-inputs-will-not-be-submitted and demo here.

how to do binding in Spring annotated request parameter?

i have a controller that is using annotation for request mapping and requestParam.
the controller is working fine. However when submitting a command object with array, spring will crap out saying array index out of bound. i am guessing there is something wrong with binding but don't know how to fix it.
to be more specific, in eclipse i would set debugger at the beginning of the controller, and when submitting the form (by hitting a input submit button) eclipse debugger will not trigger and i will see array index out of bound error in console.
the controller is something like this:
#RequestMapping(value = {"/internal/pcsearch.dex", "/external/pcsearch.dex"},
method = {RequestMethod.POST, RequestMethod.GET})
public ModelAndView executeProductCatalogSearch(
HttpServletRequest request,
#RequestParam(value = "cat" ,required = false) String cat,
#RequestParam(value = "brand" ,required = false) String brand,
#ModelAttribute("command") ProductCatalogCommand cmd
){
[edit]
and the jsp is like:
<form name="pForm"
id="pForm"
action="<c:url value="psearch.dex"><c:param name="cat" value="${cat}"/></c:url>"
method="POST"
style="display:inline;">
...
...
<c:forEach var="model" items="${models}" varStatus="modelLinkStatus">
<script>
var modelImg<c:out value="${modelLinkStatus.index}"/>Src = '<c:out value="${model.altModelImage}"/>';
</script>
<spring:bind path="command.models[${modelLinkStatus.index}].modelSkusDisplayed">
<input type="hidden" name="<c:out value="${status.expression}"/>" id="<c:out value="${status.expression}"/>" value="<c:out value="${status.value}"/>"/>
</spring:bind>
<spring:bind path="command.updateCartButton">
<input type="submit" value="<spring:message code="orderEntryMessages.ecatalog.button.addToCart" text="Add to Cart" htmlEscape="yes" />" name="<c:out value="${status.expression}"/>" id="<c:out value="${status.expression}"/>" class="sub_buttons"/>
</spring:bind>
...
and the command object declare the model array as:
private List<ModelLink> models = new ArrayList<ModelLink>();
where modelLink is a custom ds.
the first foreach tag handle the the model command object and the 2nd part is the submit button i clicked on.
i think you should use AutoPopulatingList as models to bind list to view and controller. for example please refer link. This might resolve your problem of index.

Resources