Note: phiên bản Tiếng Việt của bài này ở link dưới.
https://duongnt.com/tag-helpers-vie
When doing front-end tasks, we may sometimes encounter a group of HTML elements that are repeated on different pages. To reduce code repetition, we should encapsulate those elements into one object for simpler reuse. In such cases, the Tag Helpers feature in ASP.NET Core is very helpful.
You can download the sample code in this article from the link below.
https://github.com/duongntbk/TagHelpersDemo
Overview of Tag Helpers
Generally speaking, Tag Helpers are used to create custom HTML tags, or to create custom attributes for a default HTML tag. When encountering such custom tags, the compiler will automatically convert them into a group of HTML tags according to our logic.
To use Tag Helpers, we need to inherit from the TagHelper
class and override the Process
(synchronous) or ProcessAsync
(asynchronous) method.
The Hello, World! of Tag Helpers
Please refer to this link for a very simple Tag Helper. Let’s see what each line does.
[HtmlTargetElement("hello-world")]
We use the HtmlTargetElement
attribute to name our custom tag, so that we can add it to a web page as <hello-world></hello-world>
.
output.TagName = "div";
output.TagMode = TagMode.StartTagAndEndTag;
Here, output
is the HTML element created by our tag. In this case, we use a div
, but we can use almost any other tag here. Also, we close this div
by adding TagMode.StartTagAndEndTag
.
var hello = new TagBuilder("span");
hello.InnerHtml.AppendHtml("Hello,");
output.Content.AppendHtml(hello);
First, we add a span
to hold the text Hello,
. Then we append that span
to output
.
var world = new TagBuilder("a");
world.InnerHtml.AppendHtml("World!");
world.Attributes.Add("href", "https://example.com");
output.Content.AppendHtml(world);
Then we insert an a
tag to hold the text World!
. This link points to https://example.com
.
Add HelloWorldTagHelper to a page
To enable Tag Helpers, we need to add the following code to Pages/_ViewImports.cshtml
.
@addTagHelper *, <project name>
// For our sample project, we add the code below.
// @addTagHelper *, TagHelpersDemo
After that, every time we add <hello-world></hello-world>
to a page, the compiler will automatically turn it into this.
Not very impressive yet, but not bad for a first try.
Add properties to a custom tag
We will use this custom tag in the rest of this article. Although more complicated, it’s not too different from the HelloWorldTagHelper
tag above.
We defined a new tag called exchange-rate-table
to display a table of exchange rates between currencies. In that table, we allow users to choose whether to include the timestamp in the caption
section. They can display that timestamp by setting the include-time-stamp
flag to true
. Then we can check that flag inside ExchangeRateTableTagHelper
and add the caption
section to output
if necessary.
One way to check the value of include-time-stamp
is by using the TagHelperContext context
argument.
var flag = (bool)context.AllAttributes["include-time-stamp"].Value;
I don’t recommend this approach though. Because in that case, we need to check for null, verify the attribute name, etc. by ourselves. Instead, it’s much better to define a public property inside ExchangeRateTableTagHelper
and link it to include-time-stamp
by the HtmlAttributeName
attribute.
[HtmlAttributeName("include-time-stamp")]
public bool IncludeTimeStamp { get; set; }
Then we can use IncludeTimeStamp
whenever we want to access the value of include-time-stamp
.
Below is the output when we set include-time-stamp
to true
.
<exchange-rate-table include-time-stamp="true"></exchange-rate-table>
And below is the output when we set include-time-stamp
to false
.
Nested Tag Helpers
Clearly, the table in the previous section is useless on its own, because its body is empty. But attentive readers might notice this method.
var bodySection = new TagBuilder("tbody");
var childContent = await output.GetChildContentAsync();
bodySection.InnerHtml.AppendHtml(childContent);
output.Content.AppendHtml(bodySection);
Especially noticeable is the line var childContent = await output.GetChildContentAsync();
. We use it to render the content of ExchangeRateTableTagHelper
‘s child tags into its body section. We will define another tag called all-exchange-rates
. That tag can display the exchange rates from all currencies in the database to a currency of our choice. That currency is given by the target
attribute. For example, we display all exchange rate to Japanese yen like this.
<exchange-rate-table include-time-stamp="true">
<all-exchange-rates target="JPY"></all-exchange-rates>
</exchange-rate-table>
The AllExchangeRateRowsTagHelper class
You can find the complete code of AllExchangeRateRowsTagHelper
here. The important bits are below.
[HtmlTargetElement("all-exchange-rates", ParentTag = "exchange-rate-table")]
We limit all-exchange-rates
to be used inside exchange-rate-table
only by using the ParentTag
property.
[HtmlAttributeName("target")]
public string TargetText { get; set; }
We provide the code of the target currency via the target
attribute. That attribute is linked to the TargetText
public property.
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
Because we use an asynchronous method to retrieve the exchange rates, we need to override ProcessAsync
instead of Process
.
var rateDtos = await _exchangeRateRepository.RetrieveAllAsync(target);
foreach (var rateDto in rateDtos)
{
var row = new TagBuilder("tr");
AddCellToRow(row, rateDto.SourceName);
AddCellToRow(row, rateDto.TargetName);
AddCellToRow(row, $"{rateDto.Rate:0.0000}");
output.Content.AppendHtml(row);
}
We retrieve the exchange rate from other currencies to the target. Then we use a loop to create a row for each currency pair.
The final result is below.
Use Tag Helpers to create attributes for a default tag
Tag Helpers can also be used to create custom attributes for default HTML tags. In this section, we will define two new attributes for the tr
tag called source
and target
. By providing the values for those attributes, we can create a row to display the exchange rate between two currencies. For example, below is the code to display the rate between Japanese yen and Vietnamese dong.
<exchange-rate-table include-time-stamp="true">
<tr source="JPY" target="VND"></tr>
</exchange-rate-table>
You can find the complete code here. Below are the important bits.
[HtmlTargetElement("tr", ParentTag = "exchange-rate-table")]
The custom attributes only take effect when the parent tr
tag is placed inside an exchange-rate-table
tag.
[HtmlAttributeName("source")]
public string SourceText { get; set; }
[HtmlAttributeName("target")]
public string TargetText { get; set; }
Again, we use the HtmlAttributeName
to link HTML attributes to properties in the Tag Helpers class.
public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
And because we use an asynchronous method to retrieve the exchange rates, we need to override ProcessAsync
.
The result is exactly as we expected.
Of course we can add more than one tr
tag inside the same exchange-rate-table
tag. Our table will then have multiple rows.
Access the HTTP context with Tag Helpers
Sometimes, when rendering a tag, we might want to access the HTTP context of the current request. This can be done by passing an IHttpContextAccessor
to our custom tag. This is an example.
private IHttpContextAccessor _httpContextAccessor;
public HttpContextTagHelper(IHttpContextAccessor httpContextAccessor) =>
_httpContextAccessor = httpContextAccessor;
Keep in mind that we also need to add the HTTP context service to the Startup.cs
file.
services.AddHttpContextAccessor()
Through the IHttpContextAccessor
, we can access information about the request and the response. Such data includes, but not limited to, request headers, queries, hostnames, ports, and so on. Below is how we display all request queries and their values.
foreach (var kvp in _httpContextAccessor.HttpContext.Request.Query)
{
var pair = new TagBuilder("span");
pair.InnerHtml.AppendHtml($"{kvp.Key}: {kvp.Value}");
output.Content.AppendHtml(pair);
output.Content.AppendHtml("<br/>");
}
When accessing /?foo=bar&bar=foo
you should see the following result.
Conclusion
With clever applications of Tag Helpers, we can reduce code repetition and encourage code reuse. Personally, I’m not really a front-end guy. But when necessary, Tag Helpers are valuable tools in my toolbox.
One Thought on “Tag Helpers and custom tags in ASP.NET Core”