Note: phiên bản Tiếng Việt của bài này ở link dưới.

https://duongnt.com/tag-helpers-vie

Tag Helpers and custom tags in ASP.NET Core

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.

HelloWorldTagHelper demo

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>

include-time-stamp="true"

And below is the output when we set include-time-stamp to false.

include-time-stamp="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.

AllExchangeRateRowsTagHelper demo

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.

ExchangeRateRowTagHelper demo

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.

HttpContextTagHelper demo

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.

A software developer from Vietnam and is currently living in Japan.

One Thought on “Tag Helpers and custom tags in ASP.NET Core”

Leave a Reply