I have a jsp table with dynamically created rows - each row is binded to DAO. Each row also has a button that is supposed to provide availability to delete that DAO from DB and of course from the table itself.
My question is, how can I inform controller which exactly DAO was called to be deleted?
My idea was to extract varStatus index to controller and find it with #ModelAttribute ("games") somehow, but honestly I don't know how to pass this index up to controller. I know model is generated by controller and passed to JSP which extracts the data from the model and renders it, but what I'm doing here is obviously a POST/DELETE method.
My Controller mapping for that jsp:
#RequestMapping(value="/deleteGame", method=RequestMethod.GET)
public String getDeleteGame (ModelMap model) {
List<Game> myGames = gamesService.fetchById();
model.addAttribute("games", myGames);
return "deleteGame";
}
#RequestMapping(value="/deleteGame", method=RequestMethod.POST)
public String postDeleteGame (ModelMap model) {
return "deleteGame";
}
And here is my ref JSP table:
<div class="table-responsive" id="tableWithBg">
<table class = "table-hover">
<thead>
<tr>
<th class="col-md-2">TITLE</th>
<th class="col-md-2">TYPE</th>
<th class="col-md-2">MODE</th>
<th class="col-md-2">PRODUCER</th>
<th class="col-md-3">OPINION</th>
<th class="col-md-1">DELETE</th>
</tr>
</thead>
<tbody>
<c:forEach items="${games}" var="game" varStatus="loopIndex">
<tr>
<td>${game.title}</td>
<td>${game.type}</td>
<td>${game.mode}</td>
<td>${game.producent}</td>
<td>${game.opinion}</td>
<td>
<button type="button" action="delete" onclick="getCategoryIndex(${loopIndex.index})" class="btn btn-danger">
<strong>X</strong>
</button>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
Firstly, add common class(like class="js-delete-game") to all your delete button to check which button is triggered.
data-game-id="${game.id}" is game's unique id (supposing that your unique id is id) to remove appropriate game.
<button data-game-id="${game.id}" type="button" class="js-delete-game" id="js-delete-game${game.id}">
<strong>X</strong>
</button>
An event handler to check which button is clicked and ajax method to remove your game.
//<table id="game-list">
$("#game-list").on('click','.js-delete-game',function(event){
var id = $(event.target).data('gameId'); //data-game-id Get your unique game ID from the clicked button.
var data = JSON.stringify({"id":id});
var rowId= "#"+event.target.id; //get button id from clicked button and create selector
$.ajax({
type : "DELETE",
url : "${pageContext.request.contextPath}/deleteGame",
contentType: "application/json",
data : data,
success: function(data){
//remove row deleted on page without refreshing.
$(rowId).closest('tr').remove();
}
});
});
Finally change your mapping method as follows:
#RequestMapping(value = "/deleteGame" , method = RequestMethod.DELETE)
#ResponseBody
public void deleteGameById(#RequestBody Map<String, String> id) {
//your logic maybe different
gamesService.remove(id.get("id"));
}
Briefly, if you click the delete button, the game's unique id will send to server. Now you can get the id on server-side and delete the game by id.
Related
I have a web application that uses Spring MVC and Hibernate. I follow this document https://getbootstrap.com/docs/4.3/components/navs/#tabs to create tabs.
In jsp the code for the tab is
<div id="memberDiv" class="container">
<div id="memberAccountManagement" class="jumbotron">
<h3>Member List</h3>
<hr class="mb-4">
<ul class="nav nav-tabs" id="myTab" role="tablist">
<li class="nav-item"><a class="nav-link active" id="other-tab"
data-toggle="tab" href="#other" role="tab" aria-controls="other"
aria-selected="true">Other</a></li>
<li class="nav-item"><a class="nav-link" id="regular-tab"
data-toggle="tab" href="#regular" role="tab"
aria-controls="regular" aria-selected="false">Regular</a></li>
</ul>
<div class="tab-content" id="myTabContent">
<div class="tab-pane fade show active" id="other" role="tabpanel"
aria-labelledby="other-tab">Other member content here
<br />
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>ID</th>
<th>NAME</th>
<th>ROLE</th>
</tr>
</thead>
<tbody>
<c:forEach var="member" items="${members}">
<tr>
<td>${member.id}</td>
<td>${member.username}</td>
<td>${member.memberRole.role}</td>
<td>${member.enabled}</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
<div class="tab-pane fade" id="regular" role="tabpanel"
aria-labelledby="regular-tab">Regular member content here
<br />
<table class="table table-striped table-bordered table-hover">
<thead>
<tr>
<th>ID</th>
<th>NAME</th>
<th>ROLE</th>
<th>ENABLED</th>
<th> </th>
</tr>
</thead>
<tbody>
<c:forEach var="member" items="${members}">
<tr>
<td>${member.id}</td>
<td>${member.username}</td>
<td>${member.memberRole.role}</td>
</tr>
</c:forEach>
</tbody>
</table>
</div>
</div>
</div>
</div>
Here is part of the code in the MemberService.java
//for other member
#Override
public List<LoginMember> getOtherAllMember() {
List<LoginMember> memberAccountManagementList = new ArrayList<LoginMember>();
memberAccountManagementList = memberDao.list("from LoginMember u where u.memberRole.role='ROLE_Other' order by u.username");
return memberAccountManagementList;
}
//for regular member
#Override
public List<LoginMember> getRegularAllMember() {
List<LoginMember> memberAccountManagementList = new ArrayList<LoginMember>();
memberAccountManagementList = memberDao.list("from LoginMember u where u.memberRole.role='ROLE_Regular' order by u.username");
return memberAccountManagementList;
}
Here is part of the code in the MemberServiceImpl.java
public List<LoginMember> getOtherAllMember();
public List<LoginMember> getRegularAllMember();
I have the controller(MemberController.java) to call the function for display.
#RequestMapping(value = {"/memberAccountManagementList"}, method = RequestMethod.GET)
public String memberaccountmanagementlist(ModelMap model) {
model.addAttribute("members", memberService.getRegularAllMember());
return "memberAccountManagementList";
}
I run the application, When I click the Regular tab, it can retrieve memebers that belong to Regular. However when click the Other tab, it also shows the members belong to Regular.
I think maybe the controller needs to do something. So I duplicate the code in controller and rename it
#RequestMapping(value = {"/memberAccountManagementList"}, method = RequestMethod.GET)
public String memberaccountmanagementlist(#RequestParam("myTab") String myTab, ModelMap model) {
System.out.println(myTab); //try to show value
//model.addAttribute("members", memberService.getRegularAllUser());
model.addAttribute("members", memberService.getOtherAllMember());
return "memberAccountManagementList";
}
When I run the application, it occurs an error, it says
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping': Invocation of init method failed; nested exception is java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'memberController' method
I read this post Spring mvc Ambiguous mapping found. Cannot map controller bean method but I don't have the idea how to show relevant member by click the tab.
So my question is how to display relevant data by click the tab? I guess I should write code in the controller but
I should be grateful if someone can give advice on this issue please.
Update:
From this post #RequestParam vs #PathVariable , I tried to use #RequestParam to get the id myTab in the jsp file and pass to the controller.
#RequestMapping(value = {"/memberAccountManagementList"}, method = RequestMethod.GET)
public String memberaccountmanagementlist(#RequestParam("myTab") String myTab, ModelMap model) {
System.out.println(myTab); //try to show value
//model.addAttribute("members", memberService.getRegularAllMember());
model.addAttribute("members", memberService.getOtherAllMember());
return "memberAccountManagementList";
}
However when I run the code, I have this error message
Message: Required String parameter 'myTab' is not present
Description: The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
From the error message, it says myTab is not present, but the id myTab exists in the jsp file.
So from the error description, the id myTab is not pass to the controller, I don't know which part is not correct.
Would someone let me know my mistake. Grateful for your advice please. Thank you.
I updated the code in controller:
public String memberaccountmanagementlist(#RequestParam(value="#regular", required=false) String regular,ModelMap model) {
System.out.println("requestparam value is " + regular);
if (regular== "#regular")
{
model.addAttribute("members", memberService.getRegularAllUser());
}
else if(regular =="#other")
{
model.addAttribute("members", memberService.getOtherAllMember());
}
else
{
System.out.println("need to further investigate");
}
When I run the application, it still shows the similar error message and description.
Message: Required String parameter '#regular' is not present
Description: The server cannot or will not process the request due to something that is perceived to be a client error (e.g., malformed request syntax, invalid request message framing, or deceptive request routing).
In #RequestParam, I think maybe the # will effect the code but no matter I put #regular or regular or #other or other, the error message shows the similar thing. I don't understand why it does not work.
I have a table in my Thymeleaf template which shows database data. I have additional two columns which contain Like and Dislike buttons for each row. I want to enable liking/disliking "posts". My idea was to simply increment Like/Dislike count in a database by updating database entity. So, in my template I got this:
<table id="css-serial" class="table table-striped table-inverse">
<tbody>
<tr>
<th>#</th>
<th>Kategorija</th>
<th>Vic</th>
<th>Likes</th>
<th>Dislikes</th>
<th>Razlika</th>
<th>Like</th>
<th>Dislike</th>
</tr>
<tr th:each="joke : ${jokes}">
<form method="POST" th:object="${updateJoke}" th:action="#{/}">
<td></td>
<td> <pre th:text="${joke.getCatName()}"></pre> </td>
<td> <div class="preforma" th:text="${joke.getContent()}" th:field="*{content}"></div> </td>
<td th:text="${joke.getLikes()}" th:field="*{likes}"></td>
<td th:text="${joke.getDislikes()}" th:field="*{dislikes}"></td>
<td th:text="${joke.likesDislikesSubtraction()}"></td>
<td><input type="submit" value="Svidja mi se" name="like"/></td>
<td><input type="submit" value="Ne svidja mi se" name="dislike"/></td>
</form>
</tr>
</tbody>
</table>
Controller GET and POST methods:
#RequestMapping(value="", method=RequestMethod.GET)
public String showJokes(Model model){
model.addAttribute("jokes", jokeService.listAllJokes());
return "jokestable";
}
#RequestMapping(params="like", method=RequestMethod.POST)
public String likeJoke(Joke joke, Model model){
model.addAttribute("updateJoke", joke);
jokeService.likeJoke(joke);
return showJokes(model);
}
#RequestMapping(params="dislike", method=RequestMethod.POST)
public String dislikeJoke(Joke joke, Model model){
model.addAttribute("updateJoke", joke);
jokeService.dislikeJoke(joke);
return showJokes(model);
}
Relevant methods in service layer:
public void likeJoke(Joke joke){
jokeRepository.likeJoke(joke.getContent());
}
public void dislikeJoke(Joke joke){
jokeRepository.dislikeJoke(joke.getContent());
}
Repository:
public interface JokeRepository extends CrudRepository<Joke, Long> {
Joke findByContent(String content);
#Modifying
#Query("UPDATE Joke j SET j.likes = j.likes+1 WHERE j.content=:content")
void likeJoke(#Param("content") String content);
#Modifying
#Query("UPDATE Joke j SET j.dislikes=j.dislikes+1 WHERE j.content=:content")
void dislikeJoke(#Param("content") String content);
}
This obviously doesn't work, and every time I hit the Like button, nothing happens, but when I go to IntelliJ, I see "Request method 'GET' not supported" (first time after refresh it shows this two times for each button, then every next time, only once). Is this solution even possible, or I have to change my approach? Any ideas?
P.S. Service class is annotated with #Transactional
If anyone wonders why do I
return showJokes(model);
it's because if I return template name, it doesn't show database data anymore when I hit the buttons, there's just empty table then (only th elements).
I suppose that possible problem could be that every Like button for every row is named the same, as well as Dislike buttons. But I don't know how to solve this.
/welcome/employees
I have a input area where user enters `"empNo"` and clicks on "Find Employee", mapping to be done to /welcome/employees/find/{id}.
[`it should insted be move to url appended with empNo`][2]
Here I have everything fine while I directly enter the url, it gives up the result.
but the Find Employee button isn't mapped to entered /welcome/employees/find/{id} instead to /welcome/employees/find/.
and how to map the entered emp No to Find Employee button.Please shed some light.
Controller get employee method working fine only when I manually enter the URL.
list.jsp form page
<input type="text" path="empNo" value="${employee.empNo}" placeholder="Employee No."/>${employee.empNo}
<a href="/welcome/employees/find/${employee.empNo}" class="btn btn-primary">
<span class="glyphicon glyphicon-search"></span> Find Employee
</a>
find.jsp
<h2>Employee information</h2>
<form action="/welcome/employees/find/${employee.empNo}">
<table class="table table-bordered">
<tr>
<th>Employee No.</th>
<th>First Name</th>
<th>Last Name</th>
<th>Gender</th>
<th>Birth Date</th>
<th>Hire Date</th>
</tr>
<tr>
<td>"${employee.empNo}"</td>
<td>"${employee.firstName}"</td>
<td>"${employee.lastName}"</td>
<td>"${employee.gender}"</td>
<td>"${employee.birthDate}"</td>
<td>"${employee.hireDate}"</td>
</tr>
</table>
</form>
Back
#RequestMapping(value = "/employees/find/{empNo}", method = RequestMethod.GET)
public String getEmployee(#PathVariable("empNo") long empNo, Model model){
Employee employee1=this.employeeService.findEmployeeById(empNo);
model.addAttribute(employee1);
return "employees/find";
}
and when I click on find employee it directs to
http://localhost:8080/welcome/employees/find/
Here is the simple approach without any javascript.
listing.jsp
<form action="/welcome/employees/find" method="get">
<input name="empNo" placeholder="Employee No."/>
<button type="submit">Find Employee</button>
</form>
Explaination: when you do ${employee.empNo}, you're getting the value out of the empNo variable inside employee object. But you don't want to use this value to find. You want to use the value in the textbox to find.
controller
#RequestMapping(value = "/employees/find", method = RequestMethod.GET)
public String getEmployee(#RequestParam("empNo") long empNo, Model model){
Employee employee1=this.employeeService.findEmployeeById(empNo);
model.addAttribute(employee1);
return "employees/find";
}
You don't need <form> in find.jsp unless you want to do something.
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'm having a little fight here, this is my problem: I have a table whose content is taken from a bean that contains 2 string variables and a list of beans which in turn are formed by 2 string variables and a MultipartFile. So, in this scenario I present to the user a table with like this:
The problem comes when I try to implement the "Add image" functionality: a new row in the table is shown but after submit the values the bean that I receive in the controller put the new values into the last bean in the list, i.e. if in the new input for "Bean 2 - field 1" I put "new_value_5" in the controller I receive "value 5, new_value_5" into "field 1" of the last bean in the list, I don't receive a new instance of the bean, I just get all the values into the last bean of the list.
This is my code in the jsp page:
<form:form action="myaction.html" method="post" modelAttribute="main_bean" enctype="multipart/form-data">
<table>
<tr>
<td>Bean 1 - field 1: <form:input path="bean1_field1"/></td>
</tr>
<tr>
<td>Bean 1 - field 2: <form:input path="bean1_field2"/></td>
</tr>
<tr>
<td>Images:
<table id="imgTable">
<tbody>
<c:forEach var="bean_2" items="${main_bean.list_of_bean_2}" varStatus="imgStatus">
<tr>
<td>
<img src="image/${main_bean.id}/${bean_2.pathImage}" class="imagen" width="100px;" height="100px;" id="idImage">
<div id="divFile" style="display:none;"><!-- When the user wants to add a new image I enable this input-->
<form:input type="file" path="${bean_2.image}" name="imagen" id="imgFile"/>
</div>
</td>
<td>
Bean 2 - field 1: <form:input path="list_of_bean_2[${imgStatus.index}].field1" id="field1Id"/>
<form:input type="hidden" path="list_of_bean_2[${imgStatus.index}].id"/>
</td>
<td>
Bean 2 - field 2: <form:input path="list_of_bean_2[${imgStatus.index}].field2" id="field2Id"/>
</td>
</tr>
</c:forEach>
</tbody>
</table>
</td>
<td>
<input type="button" value="Add image" id="addImgBtn" class="addImgBtn">
</td>
</tr>
<tr>
<td><input type="submit" value="Confirm changes" > </td>
<td><input type="button" value="Cancel changes" > </td>
</tr>
</table>
</form:form>
And the structure of the beans:
public class Bean1 {
private List<Bean2> imagesCampaign;
private String field1;
private String field2;
//getters and setters
}
public class Bean2 {
private MultipartFile image;
private String field1;
private String field2;
//getters and setters
}
I didn't specified the code in the controller because I think is not necessary, but if you want it just let me know.
I will really appreciate if somebody could give me a clue about what's going on here. Thanks in advance.
EDIT: I tried access to the n+1 index (list_of_bean_2[${imgStatus.index + 1}]) but got a ConcurrentModificationException
Anybody please a little help here???
Finally I managed myself to solve this problem, if someone knows a better solution I would be grateful but in the meantime here is my solution. Instead of having a button to do all the work I created 2: the first one will just add a row for the new image, and the second one will submit the data added, so, this is my javascript:
$(".addImgBtn").click(function() {
$('#imgTable tbody>tr:last').clone(true).insertAfter('#imgTable tbody>tr:last');
$("#imgTable tbody>tr:last #field1").val('');
$("#imgTable tbody>tr:last #field2").val('');
$("#imgTable tbody>tr:last #divFile").show();
$(".addImgBtn").attr("disabled", "true");
return false;
});
The previous function will add a new row to the table, and it will disable the "Add image" button.
$(".confirmImgBtn").click(function(eve) {
var field1 = $("#imgTable tbody>tr:last #field1").val();
var field2 = $("#imgTable tbody>tr:last #field2").val();
var form = $('#confirmImgForm');
var newfile = $("#imgTable tbody>tr:last #imgFile")[0].files[0];
var formdata = false;
if (window.FormData) {
formdata = new FormData();
}
if ( window.FileReader ) {
reader = new FileReader();
reader.onloadend = function (e) {
//showUploadedItem(e.target.result, file.fileName);
};
reader.readAsDataURL(newfile);
}
if (formdata) {
formdata.append("image", newfile);
formdata.append("field1",field1);
formdata.append("field2",field2);
}
if (formdata) {
jQuery.ajax({
url: form.attr('action'),
type: "POST",
data: formdata,
processData: false,
contentType: false,
success: function (res) {
alert('succeed!!');
}
});
}
});
The last function will submit the values to the controller: I read the values for the field1, field2, the form (just to get the url) and finally the image, and when I submit everything to the controller I receive an instance of my bean, which contains 2 string variables (for filed1 and field2) and a org.springframework.web.multipart.MultipartFile for the image.