Thymeleaf form submit with model having associations - spring-mvc

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.

Related

ErrorException (E_ERROR) Undefined variable: programs

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.

How to post table rows data to a controller w/o using ajax in asp.net core

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!

spring Form binding using path not attribute not working

I have a form ,when I am using spring form tags with the path attribute,I get the following error
java.lang.IllegalStateException: Neither BindingResult nor plain target object for bean name 'selectedPerson' available as request attribute
However when I use the regular HTML input with the name attribute I don't see any issues.
What am I doing wrong.
Here is the controller code
#RequestMapping(value="/savePersonChanges.htm",method=RequestMethod.POST)
public ModelAndView savePersonChanges(HttpSession session,#Valid #ModelAttribute(value="selectedPerson")PersonBean editedPersonBean,BindingResult bindingResult){
ModelAndView mav=new ModelAndView();
mav.setViewName(LANDING_PAGE;
mav.addObject(TAB_SELECTOR,"EditPerson");
if(session!=null){
Rpt_date=(java.util.Date)session.getAttribute("editDate");
}
if(bindingResult.hasErrors()){
mav.addObject("errorDescription",errorDescription );
}
else{
/* Call service that updates database with table changes */
try{
PersonService.updatePerson(Rpt_date, editedPersonBean
}
catch(Exception e){
log.logError("Exception while updating Person", e.toString());
}
}
return mav;
}
The form is as follows:
<form:form modelAttribute="selectedPerson" id="editPersonForm">
<table id="selectPerson">
<tbody>
<tr>
<td>
<select id="personId" name="personId" onchange="getPersonDetails(this)">
<c:choose>
<c:when test="${empty selectedPerson}">
<option value="0" selected ="selected">Select A Person</option>
<c:forEach var="personIdItem" items="${editPersonProperties.personproperties}">
<option value="${personIdItem.personId}"><spring:escapeBody>${personIdItem.personName}</spring:escapeBody></option>
</c:forEach>
</c:when>
<c:otherwise>
<c:forEach var="personIdItem" items="${editPersonProperties.personproperties}">
<c:if test="${personIdItem.personId eq selectedPerson.personId}">
<option value="${personIdItem.personId}" selected ="selected">${personIdItem.personName}</option>
</c:if>
<c:if test="${personIdItem.personId ne selectedPerson}">
<option value="${personIdItem.personId}"><spring:escapeBody>${personIdItem.personName}</spring:escapeBody></option>
</c:if>
</c:forEach>
</c:otherwise>
</c:choose>
</select>
</td>
</tr>
</tbody>
</table>
<!-- Person Details -->
<table id="editPersonTable">
<tr>
<td>First Name</td>
<td> <form:input type ="text" path="fname" value="${selectedPerson.fname}"></form:input></td>
<td>Last Name</td>
<td> <form:input type ="text" path="lname" value="${selectedPerson.lname}"/></td>
</tr>
</table>
<div class="editcancelstyle">
<input id="savebtn" type="button" onclick="savePerson()" value="Save" />
<input id="cancelbtn" type="button" onclick="cancelPersonEdits ()"value="Cancel" />
</div>
</form:form>
I understand that the path attribute will bind the field to the form. However, I keep getting the bind error. If I replace using plain HTML, the controller sees the edited values for the fname and lname for the SelectedPerson bean.
Here is your problem, when you do :
<form:... modelAttribute="selectedPerson" ...>, the selectedPerson is the key of model object mapped from the holder of both Model & View class ( ex : ModelAndView), suppose you bind a model in your controller with new ModelAndView ("yourForm", "selectedPerson", new Person()),
#RequestMapping(value = "/form")
public ModelAndView userInput() {
ModelAndView mv = new ModelAndView("personForm", "selectedPerson", new Person());
return mv;
}
now the Person is mapped with selectedPerson so when this form returned as the response from controller, your form has already been bound to the Person model, so that you use path to refer to this Person's attributes (ex. path="name" , means it refers to Person's name attribute)
In your form, on modelAttribute="selectedPerson", the selectedPerson is not binded to any models, since "selectedPerson" is never assigned to any object because you didn't do any binding first before processing ( submitting ) the form.
this is why you got
> java.lang.IllegalStateException: Neither BindingResult nor plain
> target object for bean name 'selectedPerson' available as request
> attribute.
Note that to get this binding works, add also the following on the top of your form
<%# taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%# taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
you can then populate the fields of the form (assuming you put action="result.html"):
#RequestMapping(value = "/result")
public ModelAndView processUser( #ModelAttribute("selectedPerson") Person person, BindingResult result) {
if (result.hasErrors()) {
return new ModelAndView("errorPage","message","Something goes wrong");
}
/*System.out.println(person.getName());*/
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("personResult");
modelAndView.addObject("person", person);
return modelAndView;
}

Thymeleaf-Spring BigDecimal formatting with #NumberFormat adds extra symbols

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.

Laravel 4 - ReflectionException Class / does not exist

When attempting to load the page, I'm getting the error that the ReflectionException Class / does not exist (open: /var/www/laravel_guestbook/vendor/laravel/framework/src/Illuminate/Routing/ControllerInspector.php), could use some insight on what is causing this error.
Furthermore, I've also run 'composer dump-autoload' at the root of my project folder to no avail.
routes.php
Route::controller('EntriesController', '/');
Entry.php
<?php
class Entry extends Eloquent {
/**
* The database table used by the model.
*
* #var string
*/
protected $table = 'entries';
}
?>
home.blade.php
<html>
<head>
<title>Laravel 4 Guestbook</title>
</head>
<body>
#foreach ($entries as $entry)
<p>{{ $entry->comment }}</p>
<p>Posted on {{ $entry->created_at->format('M jS, Y') }} by
{{ $entry->username}}
</p><hr>
#endforeach
<form action="/" method="post">
<table border="0">
<tr>
<td>Name</td>
<td><input type="text" name="frmName" value="" size="30" maxlength="50"></td>
</tr>
<tr>
<td>Email</td>
<td><input type="text" name="frmEmail" value="" size="30" maxlength="100"></td>
</tr>
<tr>
<td>Comment</td>
<td><input textarea name="frmComment" row="5" cols="30"></textarea></td>
</tr>
<tr>
<td></td>
<td>
<input type="submit" name="submit" value="submit">
<input type="reset" name="reset" value="reset">
</td>
</tr>
</table>
</form>
</body>
EntriesController.php
<?php
class EntriesController extends BaseController {
# Handles "GET /" request
public function getIndex()
{
return View::make('home')
->with('entries', Entry::all());
}
# Handles "POST /" request
public function postIndex()
{
// get form input data
$entry = array(
'username' => Input::get('frmName'),
'email' => Input::get('frmEmail'),
'comment' => Input::get('frmComment'),
);
// save the guestbook entry to the database
Entry::create($entry);
return Redirect::to('/');
}
}
?>
It's suppose to be:
Route::controller('/', 'EntriesController');
If your naming is correct but you still get this type of error do a
composer update
This command will refresh your composer autoload files (among others).
In my case the name of the file was PostController.php, but inside I had
class Post extends \BaseController {
Instead of
class PostController extends \BaseController {
I had to rename the file as "php artisan generate:controller " command requires the word controller to be specified.

Resources