Tag Helper Components in ASP.NET Core

By Scott Addie and Fiyaz Bin Hasan

A Tag Helper Component is a Tag Helper that allows you to conditionally modify or add HTML elements from server-side code. This feature is available in ASP.NET Core 2.0 or later.

ASP.NET Core includes two built-in Tag Helper Components: head and body. They're located in the Microsoft.AspNetCore.Mvc.Razor.TagHelpers namespace and can be used in both MVC and Razor Pages. Tag Helper Components don't require registration with the app in _ViewImports.cshtml.

View or download sample code (how to download)

Use cases

Two common use cases of Tag Helper Components include:

  1. Injecting a <link> into the <head>.
  2. Injecting a <script> into the <body>.

The following sections describe these use cases.

Inject into HTML head element

Inside the HTML <head> element, CSS files are commonly imported with the HTML <link> element. The following code injects a <link> element into the <head> element using the head Tag Helper Component:

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace RazorPagesSample.TagHelpers
{
    public class AddressStyleTagHelperComponent : TagHelperComponent
    {
        private readonly string _style = 
            @"<link rel=""stylesheet"" href=""/css/address.css"" />";

        public override int Order => 1;

        public override Task ProcessAsync(TagHelperContext context,
                                          TagHelperOutput output)
        {
            if (string.Equals(context.TagName, "head", 
                              StringComparison.OrdinalIgnoreCase))
            {
                output.PostContent.AppendHtml(_style);
            }

            return Task.CompletedTask;
        }
    }
}

In the preceding code:

  • AddressStyleTagHelperComponent implements TagHelperComponent. The abstraction:
    • Allows initialization of the class with a TagHelperContext.
    • Enables the use of Tag Helper Components to add or modify HTML elements.
  • The Order property defines the order in which the Components are rendered. Order is necessary when there are multiple usages of Tag Helper Components in an app.
  • ProcessAsync compares the execution context's TagName property value to head. If the comparison evaluates to true, the content of the _style field is injected into the HTML <head> element.

Inject into HTML body element

The body Tag Helper Component can inject a <script> element into the <body> element. The following code demonstrates this technique:

using System;
using System.IO;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace RazorPagesSample.TagHelpers
{
    public class AddressScriptTagHelperComponent : TagHelperComponent
    {
        public override int Order => 2;
        
        public override async Task ProcessAsync(TagHelperContext context,
                                                TagHelperOutput output)
        {
            if (string.Equals(context.TagName, "body",
                              StringComparison.OrdinalIgnoreCase))
            {
                var script = await File.ReadAllTextAsync(
                    "TagHelpers/Templates/AddressToolTipScript.html");
                output.PostContent.AppendHtml(script);
            }
        }
    }
}

A separate HTML file is used to store the <script> element. The HTML file makes the code cleaner and more maintainable. The preceding code reads the contents of TagHelpers/Templates/AddressToolTipScript.html and appends it with the Tag Helper output. The AddressToolTipScript.html file includes the following markup:

<script>
$("address[printable]").hover(function() {
    $(this).attr({
        "data-toggle": "tooltip",
        "data-placement": "right",
        "title": "Home of Microsoft!"
    });
});
</script>

The preceding code binds a Bootstrap tooltip widget to any <address> element that includes a printable attribute. The effect is visible when a mouse pointer hovers over the element.

Register a Component

A Tag Helper Component must be added to the app's Tag Helper Components collection. There are three ways to add to the collection:

Registration via services container

If the Tag Helper Component class isn't managed with ITagHelperComponentManager, it must be registered with the dependency injection (DI) system. The following Startup.ConfigureServices code registers the AddressStyleTagHelperComponent and AddressScriptTagHelperComponent classes with a transient lifetime:

public void ConfigureServices(IServiceCollection services)
{
    services.Configure<CookiePolicyOptions>(options =>
    {
        options.CheckConsentNeeded = context => true;
        options.MinimumSameSitePolicy = SameSiteMode.None;
    });

    services.AddMvc()
            .SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    services.AddTransient<ITagHelperComponent, 
        AddressScriptTagHelperComponent>();
    services.AddTransient<ITagHelperComponent, 
        AddressStyleTagHelperComponent>();
}

Registration via Razor file

If the Tag Helper Component isn't registered with DI, it can be registered from a Razor Pages page or an MVC view. This technique is used for controlling the injected markup and the component execution order from a Razor file.

ITagHelperComponentManager is used to add Tag Helper Components or remove them from the app. The following code demonstrates this technique with AddressTagHelperComponent:

@using RazorPagesSample.TagHelpers;
@using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
@inject ITagHelperComponentManager manager;

@{
    string markup;

    if (Model.IsWeekend)
    {
        markup = "<em class='text-warning'>Office closed today!</em>";
    }
    else
    {
        markup = "<em class='text-info'>Office open today!</em>";
    }

    manager.Components.Add(new AddressTagHelperComponent(markup, 1));
}

In the preceding code:

  • The @inject directive provides an instance of ITagHelperComponentManager. The instance is assigned to a variable named manager for access downstream in the Razor file.
  • An instance of AddressTagHelperComponent is added to the app's Tag Helper Components collection.

AddressTagHelperComponent is modified to accommodate a constructor that accepts the markup and order parameters:

private readonly string _markup;

public override int Order { get; }

public AddressTagHelperComponent(string markup = "", int order = 1)
{
    _markup = markup;
    Order = order;
}

The provided markup parameter is used in ProcessAsync as follows:

public override async Task ProcessAsync(TagHelperContext context,
                                        TagHelperOutput output)
{
    if (string.Equals(context.TagName, "address",
            StringComparison.OrdinalIgnoreCase) &&
        output.Attributes.ContainsName("printable"))
    {
        TagHelperContent childContent = await output.GetChildContentAsync();
        string content = childContent.GetContent();
        output.Content.SetHtmlContent(
            $"<div>{content}<br>{_markup}</div>{_printableButton}");
    }
}

Registration via Page Model or controller

If the Tag Helper Component isn't registered with DI, it can be registered from a Razor Pages page model or an MVC controller. This technique is useful for separating C# logic from Razor files.

Constructor injection is used to access an instance of ITagHelperComponentManager. The Tag Helper Component is added to the instance's Tag Helper Components collection. The following Razor Pages page model demonstrates this technique with AddressTagHelperComponent:

using System;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Mvc.RazorPages;
using RazorPagesSample.TagHelpers;

public class IndexModel : PageModel
{
    private readonly ITagHelperComponentManager _tagHelperComponentManager;

    public bool IsWeekend
    {
        get
        {
            var dayOfWeek = DateTime.Now.DayOfWeek;

            return dayOfWeek == DayOfWeek.Saturday ||
                   dayOfWeek == DayOfWeek.Sunday;
        }
    }

    public IndexModel(ITagHelperComponentManager tagHelperComponentManager)
    {
        _tagHelperComponentManager = tagHelperComponentManager;
    }

    public void OnGet()
    {
        string markup;

        if (IsWeekend)
        {
            markup = "<em class='text-warning'>Office closed today!</em>";
        }
        else
        {
            markup = "<em class='text-info'>Office open today!</em>";
        }

        _tagHelperComponentManager.Components.Add(
            new AddressTagHelperComponent(markup, 1));
    }
}

In the preceding code:

  • Constructor injection is used to access an instance of ITagHelperComponentManager.
  • An instance of AddressTagHelperComponent is added to the app's Tag Helper Components collection.

Create a Component

To create a custom Tag Helper Component:

The following code creates a custom Tag Helper Component that targets the <address> HTML element:

using System.ComponentModel;
using Microsoft.AspNetCore.Mvc.Razor.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;
using Microsoft.Extensions.Logging;

namespace RazorPagesSample.TagHelpers
{
    [HtmlTargetElement("address")]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public class AddressTagHelperComponentTagHelper : TagHelperComponentTagHelper
    {
        public AddressTagHelperComponentTagHelper(
            ITagHelperComponentManager componentManager, 
            ILoggerFactory loggerFactory) : base(componentManager, loggerFactory)
        {
        }
    }
}

Use the custom address Tag Helper Component to inject HTML markup as follows:

public class AddressTagHelperComponent : TagHelperComponent
{
    private readonly string _printableButton =
        "<button type='button' class='btn btn-info' onclick=\"window.open("
        "'https://binged.it/2AXRRYw')\">" +
        "<span class='glyphicon glyphicon-road' aria-hidden='true'></span>" +
        "</button>";

    public override int Order => 3;

    public override async Task ProcessAsync(TagHelperContext context,
                                            TagHelperOutput output)
    {
        if (string.Equals(context.TagName, "address",
                StringComparison.OrdinalIgnoreCase) &&
            output.Attributes.ContainsName("printable"))
        {
            var content = await output.GetChildContentAsync();
            output.Content.SetHtmlContent(
                $"<div>{content.GetContent()}</div>{_printableButton}");
        }
    }
}

The preceding ProcessAsync method injects the HTML provided to SetHtmlContent into the matching <address> element. The injection occurs when:

  • The execution context's TagName property value equals address.
  • The corresponding <address> element has a printable attribute.

For example, the if statement evaluates to true when processing the following <address> element:

<address printable>
    One Microsoft Way<br />
    Redmond, WA 98052-6399<br />
    <abbr title="Phone">P:</abbr>
    425.555.0100
</address>

Additional resources