I'm using Thymeleaf 2.1.2.RELEASE with Spring MVC 4.0.4.RELEASE
I have a dynamic form that I use to add new order lines to an order.
The problem I am facing is that each time I add a line and the content is re-rendered then an extra symbol is added before the currency symbol on the price column of each of the previous lines.
So if I add three rows i get
£22.00
£22.00
£22.00
The price field is BigDecimal with #NumberFormat(style = NumberFormat.Style.CURRENCY) so Spring should handle the conversion.
<div>
<label th:text="#{order.lines}">Order Lines</label>
<table id="addTable" dt:table="true" dt:sort="false" dt:paginate="false" dt:info="false" dt:lengthchange="false">
<thead>
<tr>
<th th:text="#{order.lines.head.linenum}">line</th>
<th th:text="#{order.lines.head.product}">Product</th>
<th th:text="#{order.lines.head.description}">Description</th>
<th th:text="#{order.lines.head.quantity}">Quantity</th>
<th th:text="#{order.lines.head.price}">Price</th>
<th>
<button type="submit" name="addLine" th:text="#{order.line.add}">Add line</button>
</th>
</tr>
</thead>
<tbody>
<tr th:each="line,lineStat : *{lines}">
<td th:text="${lineStat.count}">1</td>
<td>
<input type="text" th:field="*{lines[__${lineStat.index}__].productIdentifier}"
th:errorclass="fieldError"/>
</td>
<td>
<input type="text" th:field="*{lines[__${lineStat.index}__].description}"
th:errorclass="fieldError"/>
</td>
<td>
<input type="text" th:field="*{lines[__${lineStat.index}__].quantity}"
th:errorclass="fieldError"/>
</td>
<td>
<input type="text" th:field="*{{lines[__${lineStat.index}__].price}}"
th:errorclass="fieldError"/>
</td>
<td>
<button type="submit" name="removeLine" th:value="${lineStat.index}"
th:text="#{order.line.remove}">Remove line
</button>
</td>
</tr>
</tbody>
</table>
</div>
This is then backed by class with
public class OrderLine implements Serializable {
#NotEmpty
private String description;
#NotNull
#NumberFormat(style = NumberFormat.Style.CURRENCY)
private BigDecimal price;
#NotEmpty
private String productIdentifier;
#NotNull
#Min(value = 1)
private Integer quantity;
and then in my controller
#RequestMapping(value="/customer/orders", params={"addLine"})
public String addLine(final Order order, final BindingResult bindingResult) {
order.getLines().add(new OrderLine());
return "customer/orders";
}
The html page aleady includes
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
and the character encoding servlet filter is set up as below
#Override
protected Filter[] getServletFilters() {
CharacterEncodingFilter characterEncodingFilter = new CharacterEncodingFilter();
characterEncodingFilter.setEncoding("UTF-8");
characterEncodingFilter.setForceEncoding(true);
return new Filter[] {characterEncodingFilter};
}
Further to this from using Fiddler I can see that the response header to the dandelion datatables ajax request is incorrectly encoded as ISO-88591. I'm using datatables-thymeleaf 0.93 with datatables 1.9.4
From experimenting if I set thymeleaf encoding, the spring servlet filter and the html meta tag to ISO-88591 then the currency symbol appears correctly rendered although I would like this to work with UTF-8
Eventually I found an answer in this post CharacterEncodingFilter don't work together with Spring Security 3.2.0 provided by #Christian Nilsson. Basically I needed to force the character encoding filter to be registered using the onStartup method rather than the usual getServletFilters.
Related
The application is web-application using spring boot and thymeleaf. The application has a questionnaire along with some fields that needs to filled to submit the form. The model object (here answeredQuestionnaire) in template th:object="${answeredQuestionnaire}" has nested objects with as shown below.
When I submit the form, I need to assign the questionId and possibleAnswerId to answers (Set) in answeredQuestionnaire. How can I assign this in thymeleaf template. Please provide some suggestions.
AnsweredQuestionnaire .java
public class AnsweredQuestionnaire {
private UUID id;
private Boolean isRirelevant;
private Boolean isITrelevant;
private String descriptionProcess;
private int classCalculated;
private LocalDateTime createdDateAndTime;
private Questionnaire questionnaire;//separate model class
private Process process;//separate model class
private Set<Answer> answers = new HashSet<>();// Set of separate model class
}
Answer.java
public class Answer {
private UUID id;
private Question question;// separate class
private PossibleAnswer selectedPossibleAnswer;// separate class
private String answerComment;
private AnsweredQuestionnaire answeredQuestionnaire;
}
The controller handler to display the questionnaire page is as shown below.
#GetMapping("/review/process/{id}")
public String reviewProcess(#PathVariable UUID id, Model model){
Process byId = processService.findById(id);
if(byId != null){
if(byId.getAnsweredQuestionnaires().size() > 0){
Set<AnsweredQuestionnaire> answeredQuestionnaires = byId.getAnsweredQuestionnaires();
AnsweredQuestionnaire lastUpdatedAnsweredQuestionnaire = answeredQuestionnaires.stream().sorted(Comparator.comparing(AnsweredQuestionnaire::getCreatedDateAndTime).reversed())
.collect(Collectors.toList()).get(0);
System.out.println("AnsweredQuestionnaire for a given process :"+lastUpdatedAnsweredQuestionnaire);
model.addAttribute("answeredQuestionnaire", lastUpdatedAnsweredQuestionnaire);
model.addAttribute("isIT_SiG_relevant", lastUpdatedAnsweredQuestionnaire.getIsIT_SiG_relevant());
model.addAttribute("isRiDARrelevant", lastUpdatedAnsweredQuestionnaire.getIsRiDARrelevant());
model.addAttribute("possibleAnswersIds", getAllSelectedPossibleAnswersWithId(lastUpdatedAnsweredQuestionnaire.getAnswers()) );
}else{
System.out.println("Process without answeredQuestionnaire");
Questionnaire byIsActive = questionnaireService.findByIsActive(true);
AnsweredQuestionnaire emptyAnsweredQuestionnaire = new AnsweredQuestionnaire();
emptyAnsweredQuestionnaire.addQuestionnaireForAnsweredQuestionnaire(byIsActive);
System.out.println("Questionnaire found"+byIsActive);
model.addAttribute("answeredQuestionnaire", emptyAnsweredQuestionnaire);
model.addAttribute("isIT_relevant", false);
model.addAttribute("isRi_relevant", false);
// model.addAttribute("questionnaire", byIsActive);
model.addAttribute("possibleAnswersIds", getAllSelectedPossibleAnswersWithId(emptyAnsweredQuestionnaire.getAnswers()) );
}
}
review.html
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Review page</title>
</head>
<body>
<h1>This is the review page</h1>
<form action="#" th:action="#{/review_questionnaire}" th:object="${answeredQuestionnaire}" method="post">
<label>
isRirelevant :
</label>
<select th:field="*{isRirelevant}">
<option th:value="false" th:text="no" th:selected="${false==isRirelevant}"></option>
<option th:value="true" th:text="yes" th:selected="${true==isRirelevant}"></option>
</select>
<br>
<select th:field="*{isITrelevant}">
<option th:value="false" th:text="no" th:selected="${false==isITrelevant}"></option>
<option th:value="true" th:text="yes" th:selected="${true==isITrelevant}"></option>
</select>
<table>
<thead>
<tr>
<th>Question</th>
<th>Answer</th>
<th>Comment</th>
</tr>
</thead>
<tbody>
<tr th:each="question : ${answeredQuestionnaire.questionnaire.getQuestions()}">
<td th:text="${question.text}" th:field="*{#lists.toList(answers)}">Question</td>
<td>
<select>
<option value="0" >Select Answer</option>
<option th:each="possibleAnswer : ${question.getPossibleAnswers()}" th:value="${possibleAnswer.id}" th:text="${possibleAnswer.text}" ></option>
</select>
</td>
</tr>
</tbody>
</table>
<br>
<input type="submit" />
<br>
<label >Class calculated : </label>
<input type="number" th:field="*{classCalculated}" th:value="${answeredQuestionnaire.classCalculated}" readonly >
</form>
</body>
</html>
Update
I separated the answers (Set) with th:if conditional, but still I cannot submit the form.
<div th:if="${answeredQuestionnaire.answers.size()>0}">
<table >
<thead>
<tr>
<th>Question</th>
<th>Answer</th>
<th>Comment</th>
</tr>
</thead>
<tbody th:field="*{answers}" >
<tr th:each="answer,iStat : ${answeredQuestionnaire.answers}" >
<td>
<input th:text="${answer.question.text}" th:value="${answer.question.id}" th:field="*{answers[__${iStat.index}__].question.id}"/>
</td>
<td>
<select th:field="*{answers[__${iStat.index}__].selectedPossibleAnswer.id}">
<option th:value="0" >Select Answer</option>
<option th:each="possibleAnswer : ${answer.question.getPossibleAnswers()}" th:value="${possibleAnswer.id}" th:text="${possibleAnswer.text}" th:selected="${possibleAnswer.id==answer.getSelectedPossibleAnswer().id}" ></option>
</select>
</td>
<td>
<input th:text="${answer.getAnswerComment()}" th:field="*{answers[__${iStat.index}__].answerComment}" >
</td>
</tr>
</tbody>
</table>
</div>
Some suggestions :
1/ You should use Command objects or DTO and not Entity from your model directly to bind datas from your web forms.
2/ You should use a List to bind datamodel relations, it works with Thymeleaf but Set will not work.
3/ You can manage relations with DTO which are containing subsets of your model objects, and simplify the business process a lot and the wireload to your services and database(s). It depends how your model are related too btw.
4/ Disable OpenSessionInView and make a service layer with Transactional annotated methods
5/ Never use an Entity inside a Controller. It's an architectural conception's nonsense.
I have searched multiple questions similar to my question but it still doesn't solve it.
Here is my web.php (route)
Route::get('/page/dpage1','ApplyProgramController#index')->name('dpage1');
Route::get('/page/dpage1', 'ApplyProgramController#create');
Route::post('/page/dpage1','ApplyProgramController#store');
My blade.php
<tr>
<strong>
<th>Kod Program</th>
<th>Nama Program</th>
<th>Jawatan</th>
<th>Mohon</th>
</strong>
</tr>
#foreach($programs as $program)
<tr>
<td><strong> Kod Program : {{ $program->kod_program}}</strong></td>
<td>
<input type="text" name="kod_program" readonly value="{{ $program->kod_program}}" class="form-control"></td>
<td><strong> Nama Program :</strong></td>
<td>
<input type="text" name="nama_program" readonly value="{{ $program->nama_program}}" class="form-control"></td>
</tr>
<td>
The error is at the row #foreach($programs as $program).
Here is my controller php
class ApplyProgramController extends Controller
{
public function index()
{
$programs = Program::all();
dd($programs);
return view('page.dropdown1',compact('programs');
I tried several methods such as changing compact to ['programs'=>$program]. When I tried using ifisset($programs), I am able to view the blade page but there is no data shown on the page from my database. The code earlier can be used from the other pages.
What is want to achieve is I have a form to adds rows with data to a html table, it's like a temporary table and all the data from it will be added in just one submit button. How can I possibly do this?
This is my sample table structure, data from it must be added to db
<form asp-action="Create">
<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="form-group">
<table class="table">
<thead>
<tr>
<!--some other fields-->
<th>Amount</th>
<th>Check #</th>
<th>Check Date</th>
</tr>
</thead>
<tbody>
<tr>
<!--some other fields-->
<td>
<input asp-for="Amount" class="form-control" />
<span asp-validation-for="Amount" class="text-danger"></span>
</td>
<td>
<input asp-for="Check_No" class="form-control" />
<span asp-validation-for="Check_No" class="text-danger"></span>
</td>
<td>
<input asp-for="Check_Date" class="form-control" />
<span asp-validation-for="Check_Date" class="text-danger"></span>
</td>
</tr>
<!--row 2-->
<!--row 3-->
<!--row 4-->
<!--etc..-->
</tbody>
</table>
</div>
<div class="form-group">
<input type="submit" value="Create" class="btn btn-default" />
</div>
And here is my controller code so far, i don't know what do I need to change
// POST: Books/Create
// To protect from overposting attacks, please enable the specific properties you want to bind to, for
// more details see http://go.microsoft.com/fwlink/?LinkId=317598.
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Id,Year,Month,Document_Code,GLA_Code,Expense_Code,Function_Code,Document_Reference_No,Document_Date,Quantity,Amount,Check_No,Check_Date,Remarks,Encoder")] Book book)
{
if (ModelState.IsValid)
{
_context.Add(book);
await _context.SaveChangesAsync();
return RedirectToAction(nameof(Index));
}
return View(book);
}
What must needed to be change in my view and controller, and help will is appreciated.
Thank you.
There's a couple of issues here.
First, if you've got a table of "books" (plural), the input names need to be indexed. You then also need to accept something like List<Book> instead of Book as the param to your action method. It's a little hard to tell with just the code provided, but I'd imagine you're repeating these inputs, all with the same names for each row. If that's the case, only the values for the last item will be posted. Making it a list of items enables you to post them all.
Simplistically, that means your inputs need to have names like [0].Amount, which Razor will generate for you if you use a for loop and render the inputs like:
<input asp-for="#Model[i].Amount" class="form-control" />
If you're adding the additional rows (and contained inputs) via JavaScript, you'll need to ensure that you're generating these indexed names properly yourself. A JS templating library may help in this regard.
Second, do not use Bind. Just don't. It's awful, horrible, and kills both puppies and kittens. For more explanation see my post, Bind is Evil. Use a view model instead. As a general rule you should never post an entity class. Your entity classes serve the database and its concerns, which are almost always different from the concerns of the view. Additionally, you should never just blindly save something posted by a user. Even if you insist on using your entity class to bind to, you can improve the security and safety of your code exponentially by literally mapping the values from the posted version of the class over to a new instance you create. Then, you know exactly what is being persisted to the database (without the godforsaken Bind) and you also have the opportunity to sanitize input as necessary.
I was facing a similar problem, but using ASP.NET Core 3.1 and Razor Pages. I was looking for a way to add and remove rows from a table with JavaScript, and then post it. My problem was to post the table without Ajax. Based in the question and in the accepted answer, I could do that.
Here is Index.cshtml.cs:
public class IndexViewModel {
public string Name { get; set; }
public IList<ResourceViewModel> Resources { get; set; }
}
public class ResourceViewModel {
public string Key { get; set; }
public string Value { get; set; }
}
public class IndexModel: PageModel {
[BindProperty]
public IndexViewModel ViewModel {get; set; }
public void OnGet() {
// You can fill your ViewModel property here.
}
public void OnPost() {
// You can read your posted ViewModel property here.
}
}
Here is Index.cshtml:
#page
#model IndexModel
#{
ViewData["Title"] = "Index";
}
<form method="post">
<div class="form-group">
<label asp-for="ViewModel.Name"></label>
<input asp-for="ViewModel.Name" class="form-control" />
</div>
<div class="form-group">
<table class="table">
<thead>
<th>Key</th>
<th>Value</th>
</thead>
<tbody>
#for(int i = 0; i < Model.ViewModel.Resources.Count; i++) {
<tr>
<td>
<input asp-for="ViewModel.Resources[i].Key" type="hidden" />
#Model.ViewModel.Resources[i].Key
</td>
<td>
<input asp-for="ViewModel.Resources[i].Value" type="hidden" />
#Model.ViewModel.Resources[i].Value
</td>
</tr>
}
</tbody>
</table>
</div>
<button type="submit" class="btn btn-primary">Send</button>
</form>
Notice I've used type="hidden" because I didn't want the user to edit the table directly.
I hope you find this useful!
I'd like to use HashMap as List has been used in following example: http://www.thymeleaf.org/doc/thymeleafspring.html#dynamic-fields
I've tried however without effect.
Edit: I'm providing some code. So the problem is that I'd like to use hashmap products in the form, the example I've provided above works great with a list, however I'd like to use it with hashmap.
Entity:
#Entity
#Table(name = "meals")
public class Meal extends BaseEntity{
#NotEmpty
private String name;
#NotEmpty
private String recipe;
private String image;
private double cost;
#NotNull
private int kcal;
#NotNull
private int proteins;
#NotNull
private int fats;
#NotNull
private int carbs;
#NotNull
private int portions;
#ElementCollection
#Column(name = "quantity")
private Map<Product, Integer> products = new HashMap<>();
//getters & setters
Form:
<!DOCTYPE html>
<html lang="en">
<head th:replace="fragments/headTag :: headTag"/>
<body>
<div class="container">
<div th:replace="fragments/bodyHeader :: bodyHeader"></div>
<h2 th:text="#{meals.new.title}"/>
<form th:object="${meal}" th:method="post" th:action="#{${#httpServletRequest.servletPath}}"
enctype="multipart/form-data"
class="form-horizontal" id="newMealForm">
//other fields
<div class="control-group" th:classappend="${#fields.hasErrors('products')} ? error">
<label class="control-label" th:text="#{meals.products}"/>
<div class="controls"><span class="help-inline"
th:errors="*{products}">[error]</span>
<table class="table table-striped">
<thead>
<tr>
<th th:text="#{product.name}"/>
<th th:text="#{product.quantity}"/>
<th><button type="submit" name="addProduct" th:text="#{meals.addProduct}"/></th>
</tr>
</thead>
<tbody>
<tr th:each="products,rowStat : *{products}">
<td><input type="text" th:field="*{products[__${rowStat.index}__].value}" /></td>
<td>
<input type="number" th:field="*{products}" />
</td>
<td>
<button type="submit" name="removeProduct"
th:value="${rowStat.index}" th:text="#{meals.removeProduct}"/>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<div class="form-actions">
<button type="submit" class="btn btn-primary" th:text="#{submit}"/>
</div>
</form>
</div>
<div th:replace="fragments/footer :: footer"></div>
</body>
</html>
Controller methods to add/remove row:
#RequestMapping(value="/meals/new", params={"addProduct"})
public String addProduct(final Meal meal, final BindingResult bindingResult) {
meal.getProducts().put(new Product(), 1);
return "/meals/new";
}
#RequestMapping(value="/meals/new", params={"removeProduct"})
public String removeRow(
final Meal meal, final BindingResult bindingResult,
final HttpServletRequest req) {
final Integer rowId = Integer.valueOf(req.getParameter("removeProduct"));
meal.getProducts().remove(rowId.intValue());
return "/meals/new";
}
Error I've got is:
org.springframework.beans.InvalidPropertyException: Invalid property 'products[0]' of bean class [org.cybuch.incessantfeasting.model.Meal]: Invalid index in property path 'products[0]'; nested exception is org.springframework.beans.TypeMismatchException: Failed to convert property value of type 'java.lang.String' to required type 'org.cybuch.incessantfeasting.model.Product' for property 'null'; nested exception is org.springframework.core.convert.ConversionFailedException: Failed to convert from type java.lang.String to type org.cybuch.incessantfeasting.model.Product for value '0'; nested exception is java.lang.IllegalArgumentException: Unable to parse '0'
I've solved this problem some time ago, so I guess it would be nice to share with the solution. I've changed map to list in controller with a wrapper, so it was List where ProductQuantity class contains 2 fields - Product product and String quantity. After that I'm converting that list to map and save to the database.
I'm newbie in Thymeleaf. I'm trying to create simple crud application. I'm trying to delete object of Customer class on delete button. How can I set parameter(for example - id) to the method which called deleteUser using Thymeleaf. Here's my controller.
package controllers;
//imports
#Controller
public class WebController extends WebMvcConfigurerAdapter {
#Autowired
private CustomerDAO customerDAO;
#Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/results").setViewName("results");
}
//show all users
#RequestMapping(value="/users", method=RequestMethod.GET)
public String contacts(Model model) {
model.addAttribute("users",customerDAO.findAll());
return "list";
}
//show form
#RequestMapping(value="/users/add", method=RequestMethod.GET)
public String showForm(Customer customer) {
return "form";
}
//add user
#RequestMapping(value="/users/doAdd", method=RequestMethod.POST)
public String addUser(#RequestParam("firstName") String firstName,
#RequestParam("lastName") String lastName,
#RequestParam("lastName") String email) {
customerDAO.save(new Customer(firstName, lastName, email));
return "redirect:/users";
}
//delete user
#RequestMapping(value="users/doDelete/{id}", method = RequestMethod.POST)
public String deleteUser (#PathVariable Long id) {
customerDAO.delete(id);
return "redirect:/users";
}
}
Here's my view.
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Getting Started: Serving Web Content</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
</head>
<body>
List of users
Add new user
<table>
<tr>
<th>Id</th>
<th>First Name</th>
<th>Last Name</th>
<th>Email</th>
<th>Action</th>
</tr>
<tr th:each="user : ${users}">
<td th:text="${user.id}">Id</td>
<td th:text="${user.firstName}">First name</td>
<td th:text="${user.lastName}">Last Name</td>
<td th:text="${user.email}">Email</td>
<td>
<form th:action="#{/users/doDelete/}" th:object="${customer}" method="post">
<button type="submit">Delete</button>
</form>
</td>
</tr>
</table>
</body>
</html>
You do not need form to do this:
<td>
<a th:href="#{'/users/doDelete/' + ${user.id}}">
<span>Delete</span>
</a>
</td>
Blejzer answer is an easy and straight forward solution unless you are also working with spring security (always recommended) in which case you should prefer POST instead of GET for all modification operations such as delete to protect against CSRF attack. This is exactly why spring recommends logout be done like this only. In order for you to adapt to POST, change your controller to read this parameter from request parameters instead of path variable
//delete user
#RequestMapping(value="users/doDelete", method = RequestMethod.POST)
public String deleteUser (#RequestParam Long id) {
customerDAO.delete(id);
return "redirect:/users";
}
Add a hidden field in the form which posts with the id as name and it's value in hidden parameter
<form th:action="#{/users/doDelete}" th:object="${customer}" method="post">
<input type="hidden" th:field="${id}" />
<button type="submit">Delete</button>
</form>
On a side note, even if you are not using spring security, it is always recommended to use post for any entity modification operations (like delete or update). Saves you from lots of trouble on web in long run. Take a look at Quick Checklist for Choosing HTTP GET or POST for detailed information.
path variables can be set as:
<a th:href="#{/users/doDelete/__${user.id}__}"><span>Delete</span></a>