I'm having an issue where I have a form that will save all updates to a formula. This loads fine and everything works except for when trying to delete an ingredient within a list that is in the updateFormula object.
When I pass my two #RequestParameters in to delete a specific ingredient, I recieve the error:
Required request parameter 'formulaId' for method parameter type String is not present
This has stumped me as the formulaId parameter is for the first #GetMapping method updateFormula, which retrieves the formula information that can be updated. I have tried adding the formulaId as a model object, and pass that into the deleteIngredientInFormula method, but that did not work either.
#GetMapping to get all formula details to display
#GetMapping("/update-formula")
public String updateFormula(#RequestParam("formulaId") String id, Model model) {
//unwraps the optional formula object if present, then adds to the model.
formulaService.getFormulaById(id).ifPresent(f -> model.addAttribute("updatedFormula",f));
return "Update-Formula-Form";
}
#GetMapping to select a specific ingredient in the list to delete
#GetMapping("delete-ingredient")
public String deleteIngredientInFormula(#RequestParam("ingredientId") String inId,
#RequestParam("formId") String formId) {
formulaService.deleteIngredientInFormula(formId, inId);
return "redirect:/update-formula";
}
Thymeleaf Page: Update-Formula-Form
<div class="container">
<h2>Formula Update Form</h2>
<form action="#" th:action="#{/save-updated-formula}" method="post" th:object="${updatedFormula}">
<input type="text" th:readonly="true" th:field="*{formulaId}">
<input type="text" th:field="*{formulaName}">
<input type="text" th:field="*{dosageForm}">
<input type="text" th:readonly="true" th:field="*{unitWeight}">
<input type="text" th:field="*{servingSize}">
<!--FORMULA INGREDIENTS (SELECT ACTION) -->
<div class="container table-responsive">
<table class="table table-striped">
<thead class="table-light">
<tr>
<td>Ingredient ID</td>
<td>Ingredient Name</td>
<td>Type</td>
<td>Potency</td>
<td>Manufacturer</td>
<td>Label Claim (mg)</td>
<td>Delete Ingredient</td>
</tr>
</thead>
<tbody>
<tr th:each="ingredient, holder : *{ingredients}">
<td><input th:readonly="true" th:field="*{ingredients[__${holder.index}__].ingredientId}"></td>
<td><input th:readonly="true" th:field="*{ingredients[__${holder.index}__].ingredientName}"></td>
<td><input th:readonly="true" th:field="*{ingredients[__${holder.index}__].type}"></td>
<td><input th:field="*{ingredients[__${holder.index}__].potency}"></td>
<td><input th:readonly="true" th:field="*{ingredients[__${holder.index}__].manufacturer}"></td>
<td><input th:field="*{ingredients[__${holder.index}__].labelClaim}"></td>
<td>
<a th:href="#{/delete-ingredient(ingredientId=${ingredient.getIngredientId()}, formId=${updatedFormula.getFormulaId()})}"
class="btn btn-info btn-sm">Delete</a>
</td>
</tr>
</tbody>
</table>
</div>
<button type="submit" class="btn btn-info col-2">Save Formula Details</button>
</form>
</div>
When you call the #GetMapping("delete-ingredient") endpoint you are then redirecting to update-formula which requires formulaId. That is why you are getting the error. You are basically redirecting to update-formula without any additional data. You need to add that as follows:
#GetMapping("delete-ingredient")
public String deleteIngredientInFormula(#RequestParam("ingredientId") String inId,
#RequestParam("formId") String formId) {
formulaService.deleteIngredientInFormula(formId, inId);
return "redirect:/update-formula?formulaId=" + formId;
}
Additionally, you might want to use the same parameter names for the same thing. You have #RequestParam("formulaId") String id and #RequestParam("formId") String formId which if I understood this correctly are one and the same thing formulaId.
Finally, you definitely shouldn't use a GET to delete data. That is why the DELETE HTTP method exists.
In my application the user is show a list of exam objects and they select one to edit.
When they click the edit link it brings them to a HTML page where the URL contains that exams ID.
I would like to know how to pass the id of the exam to the controller.
I'm getting this error: "Missing URI template variable 'id' for method parameter of type Long"
I would also like to know how to make existing values appear in the forms text boxses.
HTML allSubjects.html where the user selects a subject to edit: (this all works)
<h4> exams:</h4>
<div th:each="exam : ${subject.exam}">
<h4 th:text="${exam.examTitle}"/>
<a th:href="#{/editExam.html(id=${exam.examId})}">Edit Exam</a>
Edit exam HTML:
<form action="#" th:action="#{/editExam.html{examId}}" th:object="${exam}" method="put">
<table>
<tr>
<td> Exam Title:</td>
<td><input type="text" th:field="*{examTitle}" th:text="${exam.examTitle}"/></td>
<!-- <td th:if="${#fields.hasErrors('examTitlee')}" th:errors="*{examTitle}">error message</td> -->
</tr>
<tr>
<td> Exam grade worth </td>
<td><input th:field="*{examGradeWorth}" /></td>
<!-- <td th:if="${#fields.hasErrors('examGradeWorth')}" th:errors="*{examGradeWorth}">error message</td> -->
</tr>
<tr>
<td>examGradeAchieved</td>
<td><input th:field="*{examGradeAchieved}"/></td>
</tr>
<tr>
<td><button type="submit">Submit post</button></td>
</tr>
</table>
</div>
</form>
My controller:
#RequestMapping(value = "/editExam.html{examId}", method = { RequestMethod.GET, RequestMethod.PUT })
public String editExam(#ModelAttribute("exam") #PathVariable(value = "id")Long examId, #RequestBody Exam exam,Model model, BindingResult result) {
examRepository.findOne(examId);
model.addAttribute("examTitle", exam.getExamTitle());
model.addAttribute("examGradeWorth", exam.getExamGradeWorth());
model.addAttribute("examGradeAchieved", exam.getExamGradeAchieved());
exam.setExamTitle(exam.getExamTitle());
exam.setExamGradeWorth(exam.getExamGradeWorth());
exam.setExamGradeAchieved(exam.getExamGradeAchieved());
examRepository.save(exam);
return "editExam";
}
WHen I try to run all the above code just to see if the form displays I get the following error: Missing URI template variable 'id' for method parameter of type String
In my application the user clicks a link to edit an exam of their choice.
When they click the link it should open the editExam.html and the URL contains the ID of the exam they selected.
When I click the link the URL is correct (it contains the exam ID) however it doesn't display the editExam.html page it just displays my main page (the default localhost page)
allSubjects.html (the page where the user selects which exam to edit)
<h4> exams:</h4>
<div th:each="exam : ${subject.exam}">
<h4 th:text="${exam.examTitle}"/>
<a th:href="#{/editExam.html(id=${exam.examId})}">Edit Exam</a>
editExam.html (this is the page I can't display)
<form action="#"th:action="#{/editExam.html{examId}}" th:object="${exam}" method="put">
<table>
<tr>
<td> Exam Title:</td>
<td><input type="text" th:field="*{examTitle}" th:text="${exam.examTitle}"/></td>
<!-- <td th:if="${#fields.hasErrors('examTitlee')}" th:errors="*{examTitle}">error message</td> -->
</tr>
<tr>
<td> Exam grade worth </td>
<td><input th:field="*{examGradeWorth}" /></td>
<!-- <td th:if="${#fields.hasErrors('examGradeWorth')}" th:errors="*{examGradeWorth}">error message</td> -->
</tr>
<tr>
<td>examGradeAchieved</td>
<td><input th:field="*{examGradeAchieved}"/></td>
</tr>
<tr>
<td><button type="submit">Submit post</button></td>
</tr>
</table>
</div>
</form>
My controller:
//Update an exam
#RequestMapping(value = "/editExam.html{examId}", method = { RequestMethod.POST, RequestMethod.PUT})
public String editExam(#ModelAttribute("exam") #PathVariable(value = "examId")Long examId, #RequestBody Exam exam,Model model, BindingResult result) {
examRepository.findOne(examId);
model.addAttribute("examTitle", exam.getExamTitle());
model.addAttribute("examGradeWorth", exam.getExamGradeWorth());
model.addAttribute("examGradeAchieved", exam.getExamGradeAchieved());
exam.setExamTitle(exam.getExamTitle());
exam.setExamGradeWorth(exam.getExamGradeWorth());
exam.setExamGradeAchieved(exam.getExamGradeAchieved());
examRepository.save(exam);
return "editExam";
}
This code was displaying the editExam page before I added "RequestMethod.POST" to the controller.
Try to change
<a th:href="#{/editExam.html(id=${exam.examId})}">Edit Exam</a>
to
<a th:href="#{/exam/{id}(id=${exam.examId})}">Edit Exam</a>
and in your Controller
#RequestMapping(value = "/editExam.html{examId}", method = { RequestMethod.POST, RequestMethod.PUT})
to
#RequestMapping(value = "/exam/{examId}", method = { RequestMethod.POST, RequestMethod.PUT})
This url does not look correct. It should be /editExam.html?id=1, without a /.
I noticed that you are mixing request params and path variables. In your controller you expect #PathVariable(value = "examId") but in the view you are specifying request parameter instead of path variable.
Check this post: #RequestParam vs #PathVariable
I am having a lot of difficulty with POSTing back a form to the controller, which should contain simply an arraylist of objects that the user may edit.
The form loads up correctly, but when it's posted, it never seems to actually post anything.
Here is my form:
<form action="#" th:action="#{/query/submitQuery}" th:object="${clientList}" method="post">
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Select</th>
<th>Client ID</th>
<th>IP Addresss</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr th:each="currentClient, stat : ${clientList}">
<td><input type="checkbox" th:checked="${currentClient.selected}" /></td>
<td th:text="${currentClient.getClientID()}" ></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}" ></td>
</tr>
</tbody>
</table>
<button type="submit" value="submit" class="btn btn-success">Submit</button>
</form>
Above works fine, it loads up the list correctly. However, when I POST, it returns a empty object (of size 0). I believe this is due to the lack of th:field, but anyway here is controller POST method:
...
private List<ClientWithSelection> allClientsWithSelection = new ArrayList<ClientWithSelection>();
//GET method
...
model.addAttribute("clientList", allClientsWithSelection)
....
//POST method
#RequestMapping(value="/submitQuery", method = RequestMethod.POST)
public String processQuery(#ModelAttribute(value="clientList") ArrayList clientList, Model model){
//clientList== 0 in size
...
}
I have tried adding a th:field but regardless of what I do, it causes an exception.
I've tried:
...
<tr th:each="currentClient, stat : ${clientList}">
<td><input type="checkbox" th:checked="${currentClient.selected}" th:field="*{}" /></td>
<td th th:field="*{currentClient.selected}" ></td>
...
I cannot access currentClient (compile error), I can't even select clientList, it gives me options like get(), add(), clearAll() etc, so it things it should have an array, however, I cannot pass in an array.
I've also tried using something like th:field=${}, this causes runtime exception
I've tried
th:field = "*{clientList[__currentClient.clientID__]}"
but also compile error.
Any ideas?
UPDATE 1:
Tobias suggested that I need to wrap my list in a wraapper. So that's what I did:
ClientWithSelectionWrapper:
public class ClientWithSelectionListWrapper {
private ArrayList<ClientWithSelection> clientList;
public List<ClientWithSelection> getClientList(){
return clientList;
}
public void setClientList(ArrayList<ClientWithSelection> clients){
this.clientList = clients;
}
}
My page:
<form action="#" th:action="#{/query/submitQuery}" th:object="${wrapper}" method="post">
....
<tr th:each="currentClient, stat : ${wrapper.clientList}">
<td th:text="${stat}"></td>
<td>
<input type="checkbox"
th:name="|clientList[${stat.index}]|"
th:value="${currentClient.getClientID()}"
th:checked="${currentClient.selected}" />
</td>
<td th:text="${currentClient.getClientID()}" ></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}" ></td>
</tr>
Above loads fine:
Then my controller:
#RequestMapping(value="/submitQuery", method = RequestMethod.POST)
public String processQuery(#ModelAttribute ClientWithSelectionListWrapper wrapper, Model model){
...
}
The page loads correctly, the data is displayed as expected. If I post the form without any selection I get this:
org.springframework.expression.spel.SpelEvaluationException: EL1007E:(pos 0): Property or field 'clientList' cannot be found on null
Not sure why it's complaining
(In the GET Method it has: model.addAttribute("wrapper", wrapper);)
If I then make a selection, i.e. tick the first entry:
There was an unexpected error (type=Bad Request, status=400).
Validation failed for object='clientWithSelectionListWrapper'. Error count: 1
I'm guessing my POST controller is not getting the clientWithSelectionListWrapper. Not sure why, since I have set the wrapper object to be posted back via the th:object="wrapper" in the FORM header.
UPDATE 2:
I've made some progress! Finally the submitted form is being picked up by the POST method in controller. However, all the properties appear to be null, except for whether the item has been ticked or not. I've made various changes, this is how it is looking:
<form action="#" th:action="#{/query/submitQuery}" th:object="${wrapper}" method="post">
....
<tr th:each="currentClient, stat : ${clientList}">
<td th:text="${stat}"></td>
<td>
<input type="checkbox"
th:name="|clientList[${stat.index}]|"
th:value="${currentClient.getClientID()}"
th:checked="${currentClient.selected}"
th:field="*{clientList[__${stat.index}__].selected}">
</td>
<td th:text="${currentClient.getClientID()}"
th:field="*{clientList[__${stat.index}__].clientID}"
th:value="${currentClient.getClientID()}"
></td>
<td th:text="${currentClient.getIpAddress()}"
th:field="*{clientList[__${stat.index}__].ipAddress}"
th:value="${currentClient.getIpAddress()}"
></td>
<td th:text="${currentClient.getDescription()}"
th:field="*{clientList[__${stat.index}__].description}"
th:value="${currentClient.getDescription()}"
></td>
</tr>
I also added a default param-less constructor to my wrapper class and added a bindingResult param to POST method (not sure if needed).
public String processQuery(#ModelAttribute ClientWithSelectionListWrapper wrapper, BindingResult bindingResult, Model model)
So when an object is being posted, this is how it is looking:
Of course, the systemInfo is supposed to be null (at this stage), but the clientID is always 0, and ipAddress/Description always null. The selected boolean is correct though for all properties. I'm sure I've made a mistake on one of the properties somewhere. Back to investigation.
UPDATE 3:
Ok I've managed to fill up all the values correctly! But I had to change my td to include an <input /> which is not what I wanted... Nonetheless, the values are populating correctly, suggesting spring looks for an input tag perhaps for data mapping?
Here is an example of how I changed the clientID table data:
<td>
<input type="text" readonly="readonly"
th:name="|clientList[${stat.index}]|"
th:value="${currentClient.getClientID()}"
th:field="*{clientList[__${stat.index}__].clientID}"
/>
</td>
Now I need to figure out how to display it as plain data, ideally without any presence of an input box...
You need a wrapper object to hold the submited data, like this one:
public class ClientForm {
private ArrayList<String> clientList;
public ArrayList<String> getClientList() {
return clientList;
}
public void setClientList(ArrayList<String> clientList) {
this.clientList = clientList;
}
}
and use it as the #ModelAttribute in your processQuery method:
#RequestMapping(value="/submitQuery", method = RequestMethod.POST)
public String processQuery(#ModelAttribute ClientForm form, Model model){
System.out.println(form.getClientList());
}
Moreover, the input element needs a name and a value. If you directly build the html, then take into account that the name must be clientList[i], where i is the position of the item in the list:
<tr th:each="currentClient, stat : ${clientList}">
<td><input type="checkbox"
th:name="|clientList[${stat.index}]|"
th:value="${currentClient.getClientID()}"
th:checked="${currentClient.selected}" />
</td>
<td th:text="${currentClient.getClientID()}" ></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}" ></td>
</tr>
Note that clientList can contain null at
intermediate positions. Per example, if posted data is:
clientList[1] = 'B'
clientList[3] = 'D'
the resulting ArrayList will be: [null, B, null, D]
UPDATE 1:
In my exmple above, ClientForm is a wrapper for List<String>. But in your case ClientWithSelectionListWrapper contains ArrayList<ClientWithSelection>. Therefor clientList[1] should be clientList[1].clientID and so on with the other properties you want to sent back:
<tr th:each="currentClient, stat : ${wrapper.clientList}">
<td><input type="checkbox" th:name="|clientList[${stat.index}].clientID|"
th:value="${currentClient.getClientID()}" th:checked="${currentClient.selected}" /></td>
<td th:text="${currentClient.getClientID()}"></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}"></td>
</tr>
I've built a little demo, so you can test it:
Application.java
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
ClientWithSelection.java
public class ClientWithSelection {
private Boolean selected;
private String clientID;
private String ipAddress;
private String description;
public ClientWithSelection() {
}
public ClientWithSelection(Boolean selected, String clientID, String ipAddress, String description) {
super();
this.selected = selected;
this.clientID = clientID;
this.ipAddress = ipAddress;
this.description = description;
}
/* Getters and setters ... */
}
ClientWithSelectionListWrapper.java
public class ClientWithSelectionListWrapper {
private ArrayList<ClientWithSelection> clientList;
public ArrayList<ClientWithSelection> getClientList() {
return clientList;
}
public void setClientList(ArrayList<ClientWithSelection> clients) {
this.clientList = clients;
}
}
TestController.java
#Controller
class TestController {
private ArrayList<ClientWithSelection> allClientsWithSelection = new ArrayList<ClientWithSelection>();
public TestController() {
/* Dummy data */
allClientsWithSelection.add(new ClientWithSelection(false, "1", "192.168.0.10", "Client A"));
allClientsWithSelection.add(new ClientWithSelection(false, "2", "192.168.0.11", "Client B"));
allClientsWithSelection.add(new ClientWithSelection(false, "3", "192.168.0.12", "Client C"));
allClientsWithSelection.add(new ClientWithSelection(false, "4", "192.168.0.13", "Client D"));
}
#RequestMapping("/")
String index(Model model) {
ClientWithSelectionListWrapper wrapper = new ClientWithSelectionListWrapper();
wrapper.setClientList(allClientsWithSelection);
model.addAttribute("wrapper", wrapper);
return "test";
}
#RequestMapping(value = "/query/submitQuery", method = RequestMethod.POST)
public String processQuery(#ModelAttribute ClientWithSelectionListWrapper wrapper, Model model) {
System.out.println(wrapper.getClientList() != null ? wrapper.getClientList().size() : "null list");
System.out.println("--");
model.addAttribute("wrapper", wrapper);
return "test";
}
}
test.html
<!DOCTYPE html>
<html>
<head></head>
<body>
<form action="#" th:action="#{/query/submitQuery}" th:object="${wrapper}" method="post">
<table class="table table-bordered table-hover table-striped">
<thead>
<tr>
<th>Select</th>
<th>Client ID</th>
<th>IP Addresss</th>
<th>Description</th>
</tr>
</thead>
<tbody>
<tr th:each="currentClient, stat : ${wrapper.clientList}">
<td><input type="checkbox" th:name="|clientList[${stat.index}].clientID|"
th:value="${currentClient.getClientID()}" th:checked="${currentClient.selected}" /></td>
<td th:text="${currentClient.getClientID()}"></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}"></td>
</tr>
</tbody>
</table>
<button type="submit" value="submit" class="btn btn-success">Submit</button>
</form>
</body>
</html>
UPDATE 1.B:
Below is the same example using th:field and sending back all other attributes as hidden values.
<tbody>
<tr th:each="currentClient, stat : *{clientList}">
<td>
<input type="checkbox" th:field="*{clientList[__${stat.index}__].selected}" />
<input type="hidden" th:field="*{clientList[__${stat.index}__].clientID}" />
<input type="hidden" th:field="*{clientList[__${stat.index}__].ipAddress}" />
<input type="hidden" th:field="*{clientList[__${stat.index}__].description}" />
</td>
<td th:text="${currentClient.getClientID()}"></td>
<td th:text="${currentClient.getIpAddress()}"></td>
<td th:text="${currentClient.getDescription()}"></td>
</tr>
</tbody>
When you want to select objects in thymeleaf, you dont actually need to create a wrapper for the purpose of storing a boolean select field. Using dynamic fields as per the thymeleaf guide with syntax th:field="*{rows[__${rowStat.index}__].variety}" is good for when you want to access an already existing set of objects in a collection. Its not really designed for doing selections by using wrapper objects IMO as it creates unnecessary boilerplate code and is sort of a hack.
Consider this simple example, a Person can select Drinks they like. Note: Constructors, Getters and setters are omitted for clarity. Also, these objects are normally stored in a database but I am using in memory arrays to explain the concept.
public class Person {
private Long id;
private List<Drink> drinks;
}
public class Drink {
private Long id;
private String name;
}
Spring controllers
The main thing here is that we are storing the Person in the Model so we can bind it to the form within th:object.
Secondly, the selectableDrinks are the drinks a person can select on the UI.
#GetMapping("/drinks")
public String getDrinks(Model model) {
Person person = new Person(30L);
// ud normally get these from the database.
List<Drink> selectableDrinks = Arrays.asList(
new Drink(1L, "coke"),
new Drink(2L, "fanta"),
new Drink(3L, "sprite")
);
model.addAttribute("person", person);
model.addAttribute("selectableDrinks", selectableDrinks);
return "templates/drinks";
}
#PostMapping("/drinks")
public String postDrinks(#ModelAttribute("person") Person person) {
// person.drinks will contain only the selected drinks
System.out.println(person);
return "templates/drinks";
}
Template code
Pay close attention to the li loop and how selectableDrinks is used to get all possible drinks that can be selected.
The checkbox th:field really expands to person.drinks since th:object is bound to Person and *{drinks} simply is the shortcut to referring to a property on the Person object. You can think of this as just telling spring/thymeleaf that any selected Drinks are going to be put into the ArrayList at location person.drinks.
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml"
xmlns:th="http://www.thymeleaf.org"
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" >
<body>
<div class="ui top attached segment">
<div class="ui top attached label">Drink demo</div>
<form class="ui form" th:action="#{/drinks}" method="post" th:object="${person}">
<ul>
<li th:each="drink : ${selectableDrinks}">
<div class="ui checkbox">
<input type="checkbox" th:field="*{drinks}" th:value="${drink.id}">
<label th:text="${drink.name}"></label>
</div>
</li>
</ul>
<div class="field">
<button class="ui button" type="submit">Submit</button>
</div>
</form>
</div>
</body>
</html>
Any way...the secret sauce is using th:value=${drinks.id}. This relies on spring converters. When the form is posted, spring will try recreate a Person and to do this it needs to know how to convert any selected drink.id strings into the actual Drink type. Note: If you did th:value${drinks} the value key in the checkbox html would be the toString() representation of a Drink which is not what you want, hence need to use the id!. If you are following along, all you need to do is create your own converter if one isn't already created.
Without a converter you will receive an error like
Failed to convert property value of type 'java.lang.String' to required type 'java.util.List' for property 'drinks'
You can turn on logging in application.properties to see the errors in detail.
logging.level.org.springframework.web=TRACE
This just means spring doesn't know how to convert a string id representing a drink.id into a Drink. The below is an example of a Converter that fixes this issue. Normally you would inject a repository in get access the database.
#Component
public class DrinkConverter implements Converter<String, Drink> {
#Override
public Drink convert(String id) {
System.out.println("Trying to convert id=" + id + " into a drink");
int parsedId = Integer.parseInt(id);
List<Drink> selectableDrinks = Arrays.asList(
new Drink(1L, "coke"),
new Drink(2L, "fanta"),
new Drink(3L, "sprite")
);
int index = parsedId - 1;
return selectableDrinks.get(index);
}
}
If an entity has a corresponding spring data repository, spring automatically creates the converters and will handle fetching the entity when an id is provided (string id seems to be fine too so spring does some additional conversions there by the looks). This is really cool but can be confusing to understand at first.
I asked a question earlier how to do this using Webflow, but it has proven to be impractical for my situation.
I am trying to have a walk through 3 screens which add to an object information and then require a confirm to add to the database at the end. [To maintain simplicity etc]
The first screen takes in username for example
then the next screen requires contact information
then the third screen shows a summary and asks to confirm
I am having trouble figuring out how to pass the same object through several screens. I understand how to pass information from one screen to next, but for some reason same technique doesn’t work through several screens.
Sample 3 pages:
AddUser.jsp
<div id="form">
<h2 >Step 1</h2>
<form action="AddUserContact" method="post">
<table>
<tr>
<td>User Name:</td>
<td><input type="text" id="username" name="username"/></td>
</tr>
<tr>
<td><input type="submit" value="Next"/></td>
</tr>
</table>
</form>
</div>
AddUserContact.jsp
<div id="form">
<h2 >Step 2</h2>
<form action="UserSummaryConfirm" method="post">
<table>
<tr>
<td>${user.username}</td>
</tr>
<tr>
<td>Address:</td>
<td><input type="text" id="address" name="address"/></td>
</tr>
<tr>
<td><input type="submit" value="Next"/></td>
</tr>
</table>
</form>
</div>
UserSummaryConfirm.jsp
<h2>Step 3</h2>
<form action="home" method="post">
<table>
<tr>
<td>User Name:</td>
<td>${user.username}</td>
</tr>
<tr>
<td>Address:</td>
<td>${address}</td>
</tr>
<tr>
<td>Confirm</td>
</tr>
</table>
</form>
I have a Controller for every page [its for me to understand better what is going on, Ill simplify it later]
AddUserController.java
#Controller
public class AddUserController{
#RequestMapping(value = "AddUser")
public ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
User user = new User();
ModelAndView mav = new ModelAndView("AddUser");
user.setUserName(request.getParameter("username"));
mav.addObject("user", user);
return mav;
}
}
AddUserContactController.java
#Controller
public class AddUserContactController{
#RequestMapping(value = "AddUserContact")
public ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mav = new ModelAndView("AddUserContact");
mav.addObject("user", request.getParameter("user"));
return mav;
}
}
AddUserConfirm.java
#Controller
public class AddUserConfirm{
#RequestMapping(value = "UserSummaryConfirm")
public ModelAndView handleRequestInternal(HttpServletRequest request, HttpServletResponse response) throws Exception {
ModelAndView mav = new ModelAndView("UserSummaryConfirm");
mav.addObject("user", request.getParameter("user"));
mav.addObject("address", request.getParameter("address"));
return mav;
}
}
And then the User class is just a simple class with getters and setters.
The problem I am having is no matter what I have tried to pass the object, I cannot seem to figure out why the same technique doesnt work.
The address on the third screen is beeing displayed no problem, but the username is not displayed on any of the screens.
With the webflow the way I did it was created a UserBean that was global to all webflow screens. It was easy to add to the same object from any screen and display any information. How can I achieve the same result for this?
Thank you.
WORKING CODE:
Using SessionAttributes
AddUser.jsp
<%#taglib uri="http://www.springframework.org/tags/form" prefix="form"%>
<div id="form">
<h2 >Step 1</h2>
<form:form action="AddUserContact" commandName="user" method="POST">
<table>
<tr>
<td>User Name:</td>
<td><form:input type="text" id="username" path="username"/></td>
</tr>
<tr>
<td><input type="submit" value="Next"/></td>
</tr>
</table>
</form:form>
</div>
AssUserContact.jsp
<div id="form">
<h2 >Step 2</h2>
<form:form action="UserSummaryConfirm" commandName="user" method="post">
<table>
<tr>
<td>Address:</td>
<td><input type="text" id="address" path="address"/></td>
</tr>
<tr>
<td><input type="submit" value="Next"/></td>
</tr>
</table>
</form>
</div>
UserSummaryConfirm.jsp
<h2>Step 3</h2>
<form:form action="home" method="get">
<table>
<tr>
<td>User Name:</td>
<td><%=session.getAttribute("username")%></td>
</tr>
<tr>
<td>Address:</td>
<td><%=session.getAttribute("address")%></td>
</tr>
<tr>
<td>Confirm</td>
</tr>
</table>
</form:form>
AddUserController.java
#Controller
#SessionAttributes({ "username", "address" })
public class AddUserController{
User usr = new User();
#RequestMapping(value = "AddUser")
public String loadIndex(Model model, User user) {
model.addAttribute("User", user);
return "AddUser";
}
#RequestMapping(value = "AddUserContact")
public String processUserName(Model model, User user) {
usr.setUsername(user.setUsername());
model.addAttribute("User", user);
return "AddUserContact";
}
#RequestMapping(value = "UserSummaryConfirm")
public String processUserContact(Model model, User user) {
usr.setAddress(user.getAddress());
model.addAttribute("username", usr.getUsername());
model.addAttribute("address", usr.getAddress());
return "UserSummaryConfirm";
}
Because in second screen you are using
<tr>
<td>${user.username}</td>
</tr>
User input is not bind to any input so it will not be passed to controller with form data .
While you are using address as
<tr>
<td>Address:</td>
<td><input type="text" id="address" name="address"/></td>
</tr>
As it is bind to input (as you have assigned name="address" which is same as path="name") so its value will be send to controller.
If you want to pass the same object across 3 screens then it's better use #SessionAttribute instead of hiding it and passing again and again.
EDIT :
As you are using session attribute now, remove input element .(which is bind to username) from second screen. Just use instead
<td>session.getAttribute("username")%></td>
I have done a "wizard-style" form like this before. My solution used a single form bean that we put into session for the entire process, and included only the pieces in each page that needs to be added. Spring will not erase a value in the form bean unless you include an input for the given value. We don't use JSR-303, and I don't think this method would work if you are using it. Our validation uses a combination of BindingErrors and custom validation code, so I just setup the code to only validate portions at a time, corresponding to what page the form just submitted.