A springboot-thymeleaf newbie, I have been reading many similar questions on this topic but I'm still missing something in the syntax and overall Springboot Thymeleaf paradigm.
The web application pulls project data from the backend database which is rendered by Thymeleaf templates. The user also can generate a pdf report of the data which is also rendered in the browser.
The entity:
#Entity
#Table(name = "REPORT_CS")
public class ReportItemCs {
public ReportItemCs () {}
#Id
#Column(name = "ITEM_ID")
private Long itemId;
#Column(name = "PROJ_NUM")
private String projectNumber;
#Column(name = "REGION")
private String region;
// additional fields, getters, setters
First, something that works:
A simple text field in thymeleaf is used to pass a string in post request. The controller picks it up where it's passed to the repo as a query parameter on projectNumber. The query returns a list of objects which thymeleaf renders in a table. Note there's no binding to any object - it's just posting the string from the UI then passing it as a query param.
Html:
//Works without binding to backend
<section layout:fragment="content">
<p>Quick Project Search:</p>
<form method="post">
<input type="text" name="keyword"
placeholder="Project number keywords" /> <input type="submit"
value="Search" />
</form>
<br /> <span th:text=" ${noProjectsMessage}"></span>
</section>
Controller:
#RequestMapping(value = "/", method = RequestMethod.POST)
public String showProject(String keyword, ModelMap model) {
List<ProjectView> p = repository.findByProjectsContaining(keyword);
if (p.size() == 0) {
model.put("noProjectsMessage",
String.format("Project with id containing \"%s\" not found...", keyword));
return "home";
} else {
model.put("projectViews", p);
}
return "show-projects";
}
Repository:
#Query("SELECT p FROM ProjectView p WHERE p.projectNumber like %?1%")
List<ProjectView> findByProjectsContaining(#Param("keyword") String keyword);
So, now, I need to add some checkboxes to provide additional filtering by region, project category, etc. I plan to use the checkboxes in two ways: 1) to dynamically filter the project list in the UI using jQuery and, also, to pass the checkbox values back to the controller so they can be used to populate a pdf template header. I'd then either do another database query or use Stream() to filter the list that was generated by the original query and send the filtered list to the pdf service. When the user clicks the "PDF" button, the checkbox values are forwarded to the pdf service where the report header and report are generated and returned as a byte stream in a separate tab.
Html
<div class="form-check">
<form th:action="#{/cs-report}" method="post">
<label for="form-check">Region</label>
<input class="form-check-input" type="checkbox" value="all" name="regions" id="allOffice" />
<label class="form-check-label" for="allOffice">Select All</label>
<input class="form-check-input" type="checkbox" value="region1" name="regions" id="region1"/>
<label class="form-check-label" for="region1">Region 1</label>
<input class="form-check-input" type="checkbox" value="region2" name="regions" id="region2"/>
<label class="form-check-label" for="region2">Region 2</label>
<input class="form-check-input" type="checkbox" value="region3" name= "regions" id="region3"/>
<label class="form-check-label" for="region1">Region 3</label>
<button type="submit" class="btn btn-primary">Test Checkboxes</button>
</form>
</div>
Controller
//Test the post method
#RequestMapping(value = "/cs-report", method = RequestMethod.POST)
public void printCheckboxValues(List<String> regions)
{
regions.foreach(s -> System.out.println(s));
}
Where, if this approach worked, the repo would look something like:
#Query("SELECT p FROM ProjectView p WHERE p.region IN 1")
List<ProjectView> findByRegion(#Param("regions") List<String> regions);
I think the controller uses the name attribute to reference the list of checkbox values, but I'm unclear on how to set up the controller to do this. Most of the examples I've seen have had the checkboxes bound to their parent object, and maybe that's what needs to be done. I have the checkboxes hard coded as there aren't that many and I don't expect the values to change in the database. But if I do need to bind the checkbox "region" values to the reportCs entity, an example of syntax would be greatly appreciated.
Any other suggestions on approach are greatly appreciated, and big bonus if the code can be generalized to take multiple params from multiple checkbox groups. Thank you.
OK, the checkboxes need to bind to a form-backing bean. It took some tinkering with the Thymeleaf syntax, but doing it this way is actually is quite convenient for binding multiple checkbox groups to multiple query parameters. Also, I've realized hard coding the checkbox values in the templates is a bad idea (Not loosely-coupled code and will create problems down the line) so my next step is to get the checkbox values dynamically from the database. Thank you for reading.
html:
<div class="form-check">
<form action="#" th:action="#{/cs-report}" th:object="${queryDto}" method="post">
<button type="submit" class="btn btn-primary">Get Report</button>
<input class="form-check-input" type="checkbox" value="all" name="all" id="all" />
<label class="form-check-label" for="all">Select All</label>
<input class="form-check-input" type="checkbox" value="region1" name="regions" th:field="*{regions}" id="region1" />
label class="form-check-label" for="region1">Region 1</label>
<input class="form-check-input" type="checkbox" value="region2" name="regions" th:field="*{regions}" id="region2" />
label class="form-check-label" for="region2">Region 2</label>
<input class="form-check-input" type="checkbox" value="region3" name="regions" th:field="*{regions}" id="region3" />
label class="form-check-label" for="region3">Region 3</label>
</form>
</div>
The DTO object:
// Form-backing bean to hold checkbox values on post submission
public class QueryDto {
private List<String> regions;
// Getter, setters
Controller
#PostMapping(value = "/cs-report")
public String testCheckboxes(#ModelAttribute QueryDto queryDto) throws IOException {
List<ReportDto> dtos = repository.findByRegion(queryDto.getRegions());
dtos.foreach(s -> System.out.println(s.getProjectRegion()));
}
Repository
#Query("SELECT p FROM ProjectView p WHERE p.region IN :regions")
List<ProjectView> findByRegion(#Param("regions") List<String> regions);
Related
I'm asking for help because thymeleaf does somthing weird:
Here is my form:
<form action="#" th:action="#{/add-new-board}" method="post">
<p>Board name: <input type="text" th:name="board" th:field="${board.name}" /></p>
<p th:if="${#fields.hasErrors('board.name')}" th:errors="${board.name}">Name Error</p>
<p>Section #1 name: <input type="text" th:name="section" th:field="${section.name}" /></p>
<p th:if="${#fields.hasErrors('section.name')}" th:errors="${section.name}">Name Error</p>
<p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
</form>
Here is my controller:
#GetMapping(path = "/add-new-board")
public String addNewBoardForm(Model model) {
model.addAttribute("board", new Board());
model.addAttribute("section", new Section());
return "fragments/forms/add-new-board";
}
#PostMapping(path = "/add-new-board")
public String addNewBoardSubmit(#Valid #ModelAttribute Board board,
#Valid #ModelAttribute Membership membership,
#Valid #ModelAttribute Section section,
#AuthenticationPrincipal UserDetailsImpl principal,
BindingResult result,
RedirectAttributes attributes) {
if (result.hasErrors()) {
attributes.addFlashAttribute("create_board_fail", "Check if you have all fields");
return "fragments/forms/add-new-board";
} else {
board.setCreated_at(LocalDateTime.now());
Slugify slug = new Slugify();
board.setSlug(slug.parse(board.getName()));
boardRepository.save(board);
User user = userRepository.findByEmail(principal.getEmail()).get();
membership.setMember_type(MemberType.MANAGER);
membership.setBoardId(board);
membership.setUserId(user);
membershipRepository.save(membership);
section.setBoard(board);
section.setColor(ColorType.BLUE_BASIC);
section.setOrdering(1);
sectionRepository.save(section);
attributes.addFlashAttribute("create_board_success", "You successfully added a new board!");
return "redirect:/";
}
So, my goal is to insert text from 1st input to "board" table to a column "name", and insert text from 2nd input to "section" table to a column "name". So this column titles are similar. Now when I run code, fill inputs and submit it, I'm getting to my database:
database tables img
Where "AAA" is what I wrote in 1st input, and "BBB" in 2nd
This is happening because you are using th:field incorrectly. th:field is designed to be used with a single th:object but right now you are using 2 different objects board and section. When the HTML is rendered, both inputs probably have the same name="name" and when that is submitted, the values are concatenated together and you get the behavior you are seeing.
You should instead add Board and Section to a single object, and use that as your form. For example, if you created a BoardForm object:
public class BoardForm {
private Board board = new Board();
private Section section = new Section();
// Getters and setters...
}
added that to your model instead
model.addAttribute("form", new BoardForm());
then your html would look like this
<form action="#" th:action="#{/add-new-board}" th:object="${form} method="post">
<p>Board name: <input type="text" th:name="board" th:field="*{board.name}" /></p>
<p>Section #1 name: <input type="text" th:name="section" th:field="*{section.name}" /></p>
<p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
</form>
I'm asking for help because thymeleaf does somthing weird:
Here is my form:
<form action="#" th:action="#{/add-new-board}" method="post">
<p>Board name: <input type="text" th:name="board" th:field="${board.name}" /></p>
<p th:if="${#fields.hasErrors('board.name')}" th:errors="${board.name}">Name Error</p>
<p>Section #1 name: <input type="text" th:name="section" th:field="${section.name}" /></p>
<p th:if="${#fields.hasErrors('section.name')}" th:errors="${section.name}">Name Error</p>
<p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
</form>
Here is my controller:
#GetMapping(path = "/add-new-board")
public String addNewBoardForm(Model model) {
model.addAttribute("board", new Board());
model.addAttribute("section", new Section());
return "fragments/forms/add-new-board";
}
#PostMapping(path = "/add-new-board")
public String addNewBoardSubmit(#Valid #ModelAttribute Board board,
#Valid #ModelAttribute Membership membership,
#Valid #ModelAttribute Section section,
#AuthenticationPrincipal UserDetailsImpl principal,
BindingResult result,
RedirectAttributes attributes) {
if (result.hasErrors()) {
attributes.addFlashAttribute("create_board_fail", "Check if you have all fields");
return "fragments/forms/add-new-board";
} else {
board.setCreated_at(LocalDateTime.now());
Slugify slug = new Slugify();
board.setSlug(slug.parse(board.getName()));
boardRepository.save(board);
User user = userRepository.findByEmail(principal.getEmail()).get();
membership.setMember_type(MemberType.MANAGER);
membership.setBoardId(board);
membership.setUserId(user);
membershipRepository.save(membership);
section.setBoard(board);
section.setColor(ColorType.BLUE_BASIC);
section.setOrdering(1);
sectionRepository.save(section);
attributes.addFlashAttribute("create_board_success", "You successfully added a new board!");
return "redirect:/";
}
So, my goal is to insert text from 1st input to "board" table to a column "name", and insert text from 2nd input to "section" table to a column "name". So this column titles are similar. Now when I run code, fill inputs and submit it, I'm getting to my database:
database tables img
Where "AAA" is what I wrote in 1st input, and "BBB" in 2nd
This is happening because you are using th:field incorrectly. th:field is designed to be used with a single th:object but right now you are using 2 different objects board and section. When the HTML is rendered, both inputs probably have the same name="name" and when that is submitted, the values are concatenated together and you get the behavior you are seeing.
You should instead add Board and Section to a single object, and use that as your form. For example, if you created a BoardForm object:
public class BoardForm {
private Board board = new Board();
private Section section = new Section();
// Getters and setters...
}
added that to your model instead
model.addAttribute("form", new BoardForm());
then your html would look like this
<form action="#" th:action="#{/add-new-board}" th:object="${form} method="post">
<p>Board name: <input type="text" th:name="board" th:field="*{board.name}" /></p>
<p>Section #1 name: <input type="text" th:name="section" th:field="*{section.name}" /></p>
<p><input type="submit" value="Submit" /> <input type="reset" value="Reset" /></p>
</form>
I have a page where I get a list of entries. Now, I want to be able to search from those list.
my current url for retrieving list is this /show/products. I want to add a search form in this page so that I can search with request parameter.
Yes, I can use ajax but I have to do it with request parameters.
So if I search for a product name, then - /show/products?name=someName
<form ui-jp="parsley" th:action="#{/show/products(name=${pName})}" th:object="${pName}" method="get">
<div class="row m-b">
<div class="col-sm-6">
Search by Name:
<input id="filter" type="text" th:field="*{pName}" class="form-control input-sm w-auto inline m-r"/>
<button class="md-btn md-fab m-b-sm indigo">
<i class="material-icons md-24"></i>
</button>
</div>
</div>
</form>
And this is what I tried in controller:
#GetMapping("/show/products")
public String getProduct(Model model,
#RequestParam(required = false) String name,
#ModelAttribute String pName) {
List<Product> products = this.productService.getAllProducts(name)
model.addAttribute("products", products);
return "show_product";
}
I am getting this error:
Neither BindingResult nor plain target object for bean name 'pName' available as request attribute
at org.springframework.web.servlet.support.BindStatus.<init>(BindStatus.java:153)
at org.springframework.web.servlet.support.RequestContext.getBindStatus(RequestContext.java:897)
You are trying to use variable pName (Model attribute) as a form object.
In your view you are passing a model attribute to form like this th:object="${pName}" but instead you need to pass a form object.
A form object is not a class but rather a simple java object (POJO). You can think of form object as your form but on server side.
Before you can use form object in your view, you need to create it and add it to the Model.
you will define it like this
public class MyFormObject{
private String pName;
public String getPName(){
return pName;
}
public void setPName(String pName){
this.pName = pName;
}
}
now your controller method will become
#GetMapping("/show/products")
public String getProduct(Model model,
#ModelAttribute("myFormObject") MyFormObject myFormObject,
BindingResult result) {
List<Product> products = this.productService.getAllProducts(myFormObject.getPName());
model.addAttribute("products", products);
return "show_product";
}
Then you can pass the form object to your form like this
<form ui-jp="parsley" th:action="#{/show/products}" th:object="${myFormObject}" method="get">
<div class="row m-b">
<div class="col-sm-6">
Search by Name: <input id="filter" type="text" th:field="*{pName}" class="form-control input-sm w-auto inline m-r"/>
<button class="md-btn md-fab m-b-sm indigo"><i class="material-icons md-24"></i></button>
</div>
</div>
</form>
You need to read the documentation, all these are explained there in detail.
I am building a MVC application using thymeleaf and Spring and Hibernate. My question here is more about hibernate than spring.
This is what i have so far.
A UI
<form role="form" th:action="#{/user/{userId}/official(userId=${userId})}" th:object="${user}" method="post">
<!-- first form group -->
<div class="form-group">
<label class="control-label col-xs-2">First Name</label>
<div class="col-xs-2">
<input type="text" class="form-control" th:field="*{firstName}" placeholder="First Name" />
</div>
<label class="control-label col-xs-2">Last Name</label>
<div class="col-xs-3">
<input type="text" class="form-control" th:field="*{lastName}" placeholder="Last Name" />
<!-- first form group end -->
</div>
<br/><br/>
<!-- third form group -->
<div class="form-group">
<label class="control-label col-xs-2">Email Address</label>
<div class="col-xs-2">
<input type="text" class="form-control" th:field="*{emailAddress}" placeholder="Email Address" />
</div>
</div>
<div class="form-group">
<div class="col-xs-2">
<input type="submit" value="Update" class="btn btn-primary" />
</form>
Controller :
#Controller
public class UserController {
#Autowired
private IUserService userServiceImpl;
#RequestMapping(value = "/user/{userId}/official", method = RequestMethod.GET)
public String getUserOfficialInfo(#PathVariable("userId") Integer userId, Model model) throws ServiceBusinessException {
UserBO userBO = userServiceImpl.findUserByUserId(userId);
model.addAttribute("user", userBO);
model.addAttribute("userId", userId);
model.addAttribute("genders", EnumSet.allOf(Gender.class));
return "official";
}
#RequestMapping(value = "/user/{userId}/official", method = RequestMethod.POST)
public String updateUserOfficialInfo(#PathVariable("userId") String userId, #ModelAttribute("user") UserBO user,BindingResult result, Model model) throws ServiceBusinessException {
userServiceImpl.updateUser(user);
UserBO userBO = userServiceImpl.findUserByUserId(Integer.parseInt(userId));
model.addAttribute("user", userBO);
model.addAttribute("userId", userId);
model.addAttribute("genders", EnumSet.allOf(Gender.class));
return "official";
}
}
DAO :
#Override
public void updateUser(UserEntity user) throws DaoException {
entityManager.merge(user);
}
The GET method in the controller, gets the user object to the view. But on the VIew i am just displaying few of those attributes of a user object in the form.
On the form Submit, the POST method in the controller gets called, which calls the service layer and then the merge method in the DAO gets executed.
Now what I have observed is that this merge method on the entity manager is updating the attributes which are not there in the form to null.
I think this is the expected behaviour since the object is detached when its called from the POST method. So the right thing to do here is to first fetch the entity object from the database and then to that object set the fields which are changed in the form and then call the merge method.
Can some one let me know if the above what I said is correct ?
If yes, then my next question would be that isnt this quite tedious and kind of bit more effort. I mean there are going to be cases where in I would not want to display the entire object in the form. Also not in hidden fields. I am quite surprise that there is no way to handle this and I have to follow the approach I just described above each time.
Is there a better way to do this ? Wouldn't i just use JDBC template instead ? I know I would be writing boiler plate code there but I am kind of writing getters and setters here as well for each round trip to the UI.
Consider annotating your entity with org.hibernate.annotations.Entity.
#Entity
#Table(name = "user")
#org.hibernate.annotations.Entity(
dynamicInsert = true, dynamicUpdate=true
)
public class User implements java.io.Serializable {
// your properties
}
If you are using a 4.x version of Hibernate, you may want to use #DynamicUpdate instead since the usage of #org.hibernate.annotations.Entity has been deprecated recently.
References:
https://www.mkyong.com/hibernate/hibernate-dynamic-update-attribute-example/
https://docs.jboss.org/hibernate/orm/4.2/javadocs/org/hibernate/annotations/Entity.html
https://docs.jboss.org/hibernate/orm/4.2/javadocs/org/hibernate/annotations/DynamicInsert.html
https://docs.jboss.org/hibernate/orm/4.2/javadocs/org/hibernate/annotations/DynamicUpdate.html
you can put the following code in a util class and invoke it when you want to fill your object based on another reference object:
public static <T> void fillNotNullFields(T reference, T source) {
try {
Arrays.asList(Introspector.getBeanInfo(reference.getClass(), Object.class)
.getPropertyDescriptors())
.stream()
.filter(pd -> Objects.nonNull(pd.getReadMethod()))
.forEach(pd -> {
try {
Object value = pd.getReadMethod().invoke(source);
if (value != null)
pd.getWriteMethod().invoke(reference, value);
} catch (Exception e) {
e.printStackTrace();
}
});
} catch (IntrospectionException e) {
e.printStackTrace();
}
}
So, you can do this on the service:
public UserBO updateUser(String userId, UserBO user ) {
UserBO reference = findOne(Integer.parseInt(userId));
fillNotNullFields(reference, user);
return updateUser(reference);
}
I found the suport for this answer here. Hope it helps.
You will need to store the unnecessary bean properties in hidden fields so that they get remapped when the form is posted to the controller. For example, if you do not want to show the date of birth on the page (assuming that date of birth is already an attribute of the user), simply add the following tag inside the form.
<input type='hidden' th:field="*{dateOfBirth}" />
i am working with Spring where my form fields are same with attribute fields so when i submit form it directly maps to database fields and save the data it works perfectly, but what if i want to save multiple objects with one form,
HTML:
<form>
Payment:<br>
<input type="text" name="payment"><br>
Date:<br>
<input type="date" name="paymentDate">
</form>
POJO:
public class ProjectPayment
{
private Double payment;
private Date paymentDate;
// setters and getters
}
Controller:
#RequestMapping(value = "/addnewproject", method = RequestMethod.POST)
public #ResponseBody String SaveProject(ProjectPayment projectPayment) {
projectPaymentService.saveProjectPayment( projectPayment);
}
this works perfectly,
but now in my some scenario i need multiple objects dynamically then how to save in database, how controller should look like
for example:
Now my Form is
<form>
Payment:<br>
<input type="text" name="payment"><br>
Date:<br>
<input type="date" name="paymentDate">
Payment:<br>
<input type="text" name="payment"><br>
Date:<br>
<input type="date" name="paymentDate">
Payment:<br>
<input type="text" name="payment"><br>
Date:<br>
<input type="date" name="paymentDate">
Payment:<br>
<input type="text" name="payment"><br>
Date:<br>
<input type="date" name="paymentDate">
</form>
Now this form have multiple objects of ProjectPayment class but it saves only one object
please tell me how my controller should like, i have done like this but it occurs exception
Controller:
#RequestMapping(value = "/addnewproject", method = RequestMethod.POST)
public #ResponseBody String SaveProject(ProjectPayment[] projectPayment) {
for(ProjectPayment propay : projectPayment)
{
projectPaymentService.saveProjectPayment( propay );
}
}
I can understand that you want to post data from a grid/table, however it's too ambiguous to determine which field map to which object.
Example:
field1
field2
field3 ==>Map to object at index 1 or 2?
filed1
So you think field3 should map to array index=1 or index=2?
So I suggest you should submit one by one to solve this issue.
Simple way to solve this problem is create a ViewModel.
e.g.
public class ProjectPaymentViewModel
{
private List<ProjectPayment> listProjectPayment;
// setters and getters
}
Use this view model on web page and controller
<form>
Payment:<br>
<input type="text" name="listProjectPayment[0].payment"><br>
Date:<br>
<input type="date" name="listProjectPayment[0].paymentDate">
Payment:<br>
<input type="text" name="listProjectPayment[1].payment"><br>
Date:<br>
<input type="date" name="listProjectPayment[1].paymentDate">
Payment:<br>
</form>
On controller
#RequestMapping(value = "/addnewproject", method = RequestMethod.POST)
public #ResponseBody String SaveProject(ProjectPaymentViewModel projectPaymentViewModel) {
for(ProjectPayment propay : projectPaymentViewModel.getListProjectPayment())
{
projectPaymentService.saveProjectPayment( propay );
}
}