How to use a variable to set th:include value? - thymeleaf

To make this simple, let's assume we have a template html file(test.htm) like this:
<div th:fragment="test">
this is a test fragment
</div>
<div th:include=":: test"> <!-- we'll change this line later -->
this is a placeholder
</div>
And the following controller is used to return test.htm:
#Controller
public class HomeController {
#RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView get(ModelAndView mav) {
mav.setViewName("/test.htm");
mav.addObject("fragmentName", ":: test"); // we'll use this later
return mav;
}
}
In this case, we can get the following result if we access home index:
this is a test fragment
this is a test fragment
But if we use a variable fragmentName to set th:include value like this:
<div th:fragment="test">
this is a test fragment
</div>
<div th:include="${fragmentName}"> <!-- modified to use a variable value -->
this is a placeholder
</div>
Thymeleaf complains that this template ":: test" cannot be resolved:
There was an unexpected error (type=Internal Server Error, status=500).
Error resolving template [:: test], template might not exist or might not be accessible by any of the configured Template Resolvers (template: "/test.htm" - line 5, col 6)
Here comes the question: is there a way to set the th:include value using a variable?

You can use the Thymeleaf expression preprocessing:
<div th:include="__${fragmentName}__">
this is a placeholder
</div>
Basically you instructed thymeleaf to preprocess first __${fragmentName}__ and after resolving the value to use it in the normal processing phase when evaluates the th:include as if it was a static value th:include=":: test"

Related

Thymeleaf - getting locale and comparing

The below should set h1 to HOWDY as the locale is showing en_GB, but it doesn't fall into the case?
Am I doing something wrong here? Thanks
<h1 th:text="${#locale}"></h1>
<div th:switch="${#locale}">
<h1 th:case="'en_GB'">HOWDY</h1>
</div>
When you use Thymeleaf's #locale, you are using a reference to a Java Locale object.
What Works?
The following works the way you expect, because it has already evaluated the Java locale object to its string representation, before evaluating each case statement:
<div th:switch="${#locale.toString()}">
<h1 th:case="'en_GB'">HOWDY</h1>
</div>
The following also works:
<div th:switch="__${#locale}__">
<h1 th:case="'en_GB'">HOWDY</h1>
</div>
In this case, it works because you are using the Thymeleaf preprocessor syntax __${...}__ to force Thymeleaf to evaluate #locale (to its string representation) before it evaluates the remainder of the switch statement.
Additional Explanation
Because Thymeleaf's #locale represents a Java Locale object, you can use any of Locales suitable fields and methods, such as :
<div th:text="${#locale.country}"></div> <!-- a field -->
<div th:text="${#locale.toLanguageTag()}"></div> <!-- a method -->
This is why only using ${#locale} in the Thymeleaf switch statement does not match the 'en_GB' string literal that you are expecting it to match: Thymeleaf is comparing an object to a string.
When you use this...
<div th:text="${#locale}"></div>
...you are again accessing the object itself. But in this case the object will use its toString() method when being rendered by Thymeleaf, before it is displayed - giving you your en_GB displayed value.
However, when you use this...
<div th:switch="${#locale}">
<h1 th:case="'en_GB'">HOWDY</h1>
</div>
...Thymeleaf is using the locale object in the switch statement, not its string representation.

Cannot pass model attribute in thymeleaf th:each

I'm trying to iterate over a list and pass the current iteration and another model variable to a fragment, but the "other" model variable is always null.
<div th:each="place : ${results.placeResults}" class="col-sm-6 col-xl-4 mb-5">
<div th:replace="fragments/placecard :: placecard" th:with="place=${place},res=${results}"/>
</div> <!-- end for each-->
In the fragment ${res} is always blank.
I figured it out, the th:replace basically makes the th:with have no effect. I changed the code to use th:include and things look better.

How to pass action name from controller to view

I am new to razor and I need help to pass the action name from controller to view using ViewBag. Following is my code:
public ActionResult Index(int id = 0)
{
ViewBag.MasterId = id;
ViewBag.ActionName = "ApplicationProcess";
return View();
}
From the above action method I am passing action name of that is being used in view that is render in result of this function.
<body>
<h2>Index</h2>
<div style="width: 1100px;">
<div style="float: left; width: 400px;">
#{Html.RenderAction("MasterRecordProcess", "Process");}
</div>
<div style="float: right">
#{Html.RenderAction("MasterRecordBasicInfo", "Process", new { id = ViewBag.MasterId });}
</div>
<div style="float:right;margin-top:50px;" class="task-div">
#{Html.RenderAction(#ViewBag.ActionName, "Process", null);}
</div>
</div>
</body>
Now in above view I want to use the ViewBag.ActionName passed from controller in Html.RenderAction but this is giving me error message.
Compilation Error
Description: An error occurred during the compilation of a resource required to service this request. Please review the following specific error details and modify your source code appropriately.
Compiler Error Message: CS1973: 'System.Web.Mvc.HtmlHelper<OAC.Areas.Personal.Models.MasterModel>' has no applicable method named 'RenderAction' but appears to have an extension method by that name. Extension methods cannot be dynamically dispatched. Consider casting the dynamic arguments or calling the extension method without the extension method syntax.
Kindly help how can I render the action name passed from controller in viewBag object. Thank you.
As #Stephen mentioned you must first cast the view bag content:
#{
var actionName= (string)ViewBag.ActionName;
}
Then you can use it:
#{Html.RenderAction(actionName, "Process", null);}
The error itself says that "consider casting the dynamic arguments or calling the extension method without the extension method syntax."
C# does not support calling an extension method (Html.RenderAction()) when any of the arguments is of a dynamic type. You have to either call the extension method statically or cast the argument to a non-dynamic type.
In this case you can the ViewBag to a String and use

Grails nested taglibs that use templates?

I want to create taglibs to simplify a webpage that represents a user dashboard. There are 2 taglibs I want to create. The first is a generic panel that will be used elsewhere, the second is a more specific version of the panel.
The GSP is called from a controller with the following model:
def index() {
def timelineItems = [
[details: "Some text"],
[details: "Some other text"]
]
render(view: "index", model: [timelineItems: timelineItems])
}
My desired GSP looks like this:
<dash:panel panelClass="col-lg-8" panelTitle="Service History">
<p>Some text in the body here</p>
<dash:timelinePanel timelineItems="$timelineItems" />
</dash:panel>
I want to put my HTML code in templates to make editing it simpler, so I call render from inside the tag lib and pass a data model. The taglibs look like this (within a namespace called dash):
def panel = {attrs, body ->
out << render(template: "/dashboard/dashboardPanel", model:[panelClass: attrs.getAt("panelClass"), panelTitle: attrs.getAt("panelTitle"), body: body()])
}
def timelinePanel = { attrs ->
out << render(template: "/dashboard/dashboardTimelinePanel", model: [timelineItems: attrs.getAt("timelineItems")])
}
This is my generic panel template: it should render the data in the top divs and then simply render the body passed to it in the model:
<div class="${panelClass}">
<div class="panel panel-default">
<div class="panel-heading">
${panelTitle}
</div>
<div class="panel-body">
${body}
</div>
</div>
</div>
This is my timeline template: it should render the details of each of the timeline variables passed in the model by the controller.
<g:each in="${timelineItems}">
<p>${it.details}</p>
</g:each>
I'm expecting to see a div which includes something like:
Some text in the body here
<p>Some text</p>
<p>Some other text</p>
However, I get the following error:
Error evaluating expression [it.details] : No such property: details for class: java.lang.String
My map appears to be being converted to a string and I can't access the map. What am I doing wrong please? I want to define multiple tags which can be used inside one another. How do I do this?

Reusing Custom Validation Across Views

I have an ASP.NET MVC 5 project. I have some custom validation needs. I know that in the end, a possible result for my HTML looks like this:
<div class="form-group has-error">
<label for="field">Field Label</label>
<div id="field">
<input name="field" class="form-control" type="text" autocomplete="off">
</div>
<div>
Field label is invalid. You need to do something else.
</div>
</div>
The validation errors are stored in a Dictionary<string, string> in the ViewBag. I was thinking of doing something similar to the following:
#string fieldErrorMessage = getFieldError('field', ViewBag.ValidationErrors)
<div class="form-group #if(fieldErrorMessage.length >0) { 'has-error' } ">
<label for="field">Field Label</label>
<div id="field">
<input name="field" class="form-control" type="text" autocomplete="off">
</div>
#if (fieldErrorMessage.length > 0) {
<div>
#fieldErrorMessage
</div>
}
</div>
My problem is, I do not know where to define getFieldError. I would like to use this function in multiple views. So I'm not sure a. where to define it. or b. how to call it. I'm not even sure if my approach to applying 'has-error' is correct. I feel like I have pseudocode more than mvc code.
Embedded functions in the page
For including functions in the page, you have two options.
Extending HtmlHelper:
You can extend HtmlHelper so that you can call Html.getFieldError("field"). Because ViewBag is in HtmlHelper, you won't need to pass that into the function. The best way to demonstrate this is by showing an example.
public static class ErrorValidation
{
public static MvcHtmlString getFieldError(this HtmlHelper<TModel> h, string f)
{
if(h.ViewBag.ValidationErrors.ContainsKey(f))
{
return MvcHtmlString.Create(h.ViewBag.ValidationErrors[f]);
}
return MvcHtmlString.Empty;
}
}
Adding a namespace in your views:
You can include a namespace in your views by adding a line to the Views\Web.config file. Then, you could use static methods like you planned. This is done by adding a line of something like <add namespace="MyProj.Validation" /> inside of <configuration><system.web.webPages.razor><pages><namespaces>. In addition, you can leave this out by calling the full reference to your function each time with MyProj.Validation.getFieldError(...).
Relying on MVC error handling
You can also use API's already built into MVC, which do allow for customized validation.
Doing error checks through model attributes:
The most simple way to do validation is by adding attributes to your model. If your model had a required field, you can simply add [Required] above the field in the class that defines your model. A plethora of validation attributes are provided by System.ComponentModel.DataAnnotations.
If you wanted to do a custom check, you could create your own attribute by implementing abstract class ValidationAttribute. Simply define the IsValid function with your custom logic.
If you have validation checks that need to happen on multiple fields in your model at the same time, you can have your model implement IValidatableObject. And to make this simpler, in the Validate function you don't need to create a list and add all your errors to that; simply return each ValidationResult object by putting the keyword yield at the beginning of the line. This would look like...
public IEnumerable<ValidationResult> Validate(ValidationContext context)
{
// Duplicate checks
List<String> fields = new List<String>();
for (var i=0; i<PhoneNumbers.Count; i++)
{
var item = PhoneNumbers[i];
if (PhoneNumbers.IndexOf(item) != PhoneNumbers.LastIndexOf(item))
{
fields.Add("PhoneNumbers["+i+"]");
}
}
if(fields.Count > 0)
{
yield return new ValidationResult("You cannot include duplicate phone numbers.", fields.ToArray());
}
// More validation checks
}
Doing error checks in the controller:
You can also do error checks in the controller, which allows for validation to vary depending on the action. This also lets you use the validation that already happened in the model with the ModelState object. In order to add errors to ModelState, simply call ModelState.AddModelError(). In order to check if a model is valid after all checks are done, you can check ModelState.IsValid.
Displaying errors in the view:
With validation happening in the ModelState object, you can display errors in your view by using the Html object. This allows you to generate a list of errors by calling Html.ValidationSummary() or display errors for individual properties with Html.ValidationMessageFor(...). Here's an extensive example...
for (var x = 0; x < Model.PhoneNumbers.Count(); x++ )
{
<tr>
<td>#Html.EditorFor(m => m.PhoneNumbers.ElementAt(x))</td>
#if(ViewData.ModelState["PhoneNumbers[" + x + "]"].Errors.Any())
{
<td>#Html.ValidationMessageFor(m => m.PhoneNumbers.ElementAt(x))</td>
}
else
{
<td>Ok!</td>
}
</tr>
}

Resources