Author : HASSAN MD TAREQ

What is a Tag Helper

  • Tag helpers are a new feature similar to HTML helpers which help to render HTML (available in ASP.NET Core 2.0 or later)
  • Tag Helpers enable server-side code to participate in creating and rendering HTML elements in Razor files (Razor views and Razor pages)
  • Tag Helpers are classes written in C# but are attached to HTML elements in order to run server-side code from Razor files
  • Tag Helpers provide
    • an HTML-friendly development experience
    • a rich IntelliSense environment for creating HTML and Razor markup
    • a way to make you more productive and able to produce more robust, reliable, and maintainable code using information only available on the server
  • Tag Helpers reduce the explicit transitions between HTML and C# in Razor views
  • Tag Helpers don’t replace HTML Helpers and there’s not a Tag Helper for each HTML Helper
  • In order to use Tag Helpers, we need to install a NuGet library Microsoft.AspNet.Mvc.TagHelpers and also add an addTagHelper directive to the view or views that use these tag helpers
  • Links:

Ways of using TagHelpers

  • HTML attribute
  • HTML tag/element
  • as a child element inside HTML element

Attribute example

<label asp-for="Email" class="control-label"></label>
<input asp-for="Email" class="form-control" />

Tag/element example

<email>...</email>

Child element example

<tab active-page="Sessions">
    <tab-item title="Home"></tab-item>
    <tab-item title="Speakers"></tab-item>
    <tab-item title="Sessions"></tab-item>
    <tab-item title="Registration"></tab-item>
</tab>

Making Tag Helpers available

  • @addTagHelper directive makes Tag Helpers available to the Razor files
  • @addTagHelper FQN, Namespace (FQN = full qualified name i.e. FooNamespace.TagHelpers.BarTagHelper)
  • use * for all tag helpers

Views/_ViewImports.cshtml or Pages/_ViewImports.cshtml

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper *, FooNamespace

See: Managing Tag Helper scope

Creating a custom Tag Helper

  • tag helper naming convention
    • tag name: kebab-case (to explicitly mention tag name, use [HtmlTargetElement("tag-name")])
    • tag helper class name: CamelCase (TagName)
    • tag helper class name suffix: ‘TagHelper’ i.e. FooBarTagHelper (suffix is not required, but it’s considered a best practice convention)
  • tag helper attribute naming convention
    • tag attribute: kebab-case (baz-bax)
    • property inside tag helper calss: CamelCase (BazBax)
    • all properties in tag helper class are mapped to attribute of tag helper (to exclude, use: [HtmlAttributeNotBound])
  • A tag helper is any class that implements the ITagHelper interface
  • Create a folder to hold the Tag Helpers called TagHelpers. The TagHelpers folder is not required, but it’s a reasonable convention
  • Links:

By convension

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Runtime.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace WebAppTagHelper.TagHelpers
{

    public class AutoPriceTagHelper : TagHelper
    {

        public string Make { get; set; }


        public string Model { get; set;  }

        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
			// await here
        }
    }
}

Usage

<auto-price make="chevy" model-name="camero">

</auto-price>

Using attribute

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.Runtime.TagHelpers;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace WebAppTagHelper.TagHelpers
{
    [HtmlTargetElement("auto-price")]
    public class AutoPriceTagHelper : TagHelper
    {
        [HtmlAttributeName("maker")]
        public string Make { get; set; } // to prevent mapping to attribute => [HtmlAttributeNotBound]

        [HtmlAttributeName("model-name")]
        public string Model { get; set;  }

        public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
			// await here
        }
    }
}

Usage

<auto-price maker="chevy" model-name="camero">

</auto-price>

EmailTagHelper example

Using Process instead of ProcessAsync for simplicity

TagHelpers/EmailTagHelper.cs

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

namespace AuthoringTagHelpers.TagHelpers
{

    [HtmlTargetElement("email")] 
    public class EmailTagHelper : TagHelper
    {
	
		private const string EmailDomain = "hovermind.com";
		
		public string MailTo { get; set; }
		
		public override void Process(TagHelperContext context, TagHelperOutput output)
		{
			output.TagName = "a";    // Replaces <email> with <a> tag

			var address = MailTo + "@" + EmailDomain;
			output.Attributes.SetAttribute("href", "mailto:" + address);
			output.Content.SetContent(address);
		}
    }
}

Usage

<email mail-to="Support">Support</email>

Output

<a href="mailto:Support@hovermind.com">Support@hovermind.com</a>

To use without end tag: [HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)]

[HtmlTargetElement("email", TagStructure = TagStructure.WithoutEndTag)] 
public class EmailVoidTagHelper : TagHelper
{
    // ... ... ...
}

TagHelperContext

Process method parameter - TagHelperContext

See: TagHelperContext Class

TagHelperOutput

Process method parameter - TagHelperOutput

Content

public override void Process(TagHelperContext context, TagHelperOutput output)
{
	output.PreElement.SetHtmlContent("<div>PreElement</div>");
	output.PreContent.SetHtmlContent("<div>PreContent</div>");
	output.Content.SetHtmlContent("<div>Content</div>");
	output.PostContent.SetHtmlContent("<div>PostContent</div>");
	output.PostElement.SetHtmlContent("<div>PostElement</div>");
}

<div>PreElement</div>
	<my-test>
		<div>PreContent</div>
		<div>Content</div>
		<div>PostContent</div>
	</my-test>
<div>PostElement</div>

Attribute

public override void Process(TagHelperContext context, TagHelperOutput output)
{
	output.Attributes.Add("class","highlight");
}

<my-test class="highlight">
	My Test Tag Helper
</my-test>

TagName

public override void Process(TagHelperContext context, TagHelperOutput output)
{
	output.TagName = div";
	output.Attributes.Add("class","highlight");
}

<div class="highlight">
	My Test Tag Helper
</div>

See: TagHelperOutput Class

ModelExpression

  • Model is exposed to view by @model directive
  • tag helper can use Model.Prop uisng razor syntax @Model.Prop i.e. <foo modle-prop="@Model.Prop"> </foo>
  • ModelExpression => use just Prop inside tag helper i.e. <foo modle-prop="Prop"> </foo>
  • ModelExpression would be type of property in tag helpers (ModelExpression type property will be mapped to attribute and that attribute can use model property name only)
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace WebAppTagHelper.TagHelpers
{
    [HtmlTargetElement("mod-exp")]
    public class ModExpTagHelper : TagHelper
    {
        public ModelExpression HelperFor { get; set; }

        public override void Process(TagHelperContext context, 
            TagHelperOutput output)
        {
            var str = HelperFor == null ? "" :" Name: " + HelperFor.Name + " Model: " + HelperFor.Model;
            output.Content.SetContent(str);
        }
    }
}

Usage

<mod-exp helper-for="FirstName"></mod-exp>

See: ModelExpression Class

ViewContext

  • ViewContext => access more info about the request
  • Get additional info from ViewContext if needed
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.AspNetCore.Mvc.ViewFeatures;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace WebAppTagHelper.TagHelpers
{
    [HtmlTargetElement("view-ctx")]
    public class ViewCtxTagHelper : TagHelper
    {
        [HtmlAttributeNotBound]
        [ViewContext]
        public ViewContext ViewContext { get; set; }

        public string MiscString { get; set; }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            var isHttps = ViewContext.HttpContext.Request.IsHttps;
            var isFormValid = ViewContext.ModelState.IsValid;
            MiscString = ViewContext.ExecutingFilePath;
			
            output.Content.SetContent("use above info here if needed");
        }
    }
}

Usage

<view-ctx misc-string="@ViewContext.ExecutingFilePath"></view-ctx>

See:

Passing data berween tag helpers

  • parent: <tab> </tab>
  • child: <tab-item> </tab-item>
  • data will be passed from tab to tab-item

TagHelpers/TabTagHelper.cs

using Microsoft.AspNetCore.Razor.TagHelpers;

namespace WebAppTagHelper.TagHelpers
{  
    [RestrictChildren("tab-item")]
    [HtmlTargetElement("tab")]
    public class TabTagHelper : TagHelper
    {
        [HtmlAttributeName("active-page")]
        public string ActivePage { get; set; }

        public override void Process(TagHelperContext context, 
            TagHelperOutput output)
        {
            context.Items["ActivePage"] = ActivePage;

            output.TagName = "div";
            output.PreContent.SetHtmlContent
                (@"<h3>Silicon Valley Code Camp</h3>
                   <nav class='navbar navbar-expand-sm bg-light navbar-light'>
                     <ul class='nav nav-pills'>");
            output.PostContent.SetHtmlContent("</ul></nav>");
            output.Attributes.Add("class", "container");
        }
    }
}

TagHelpers/TabItemTagHelper.cs


using Microsoft.AspNetCore.Razor.TagHelpers;

namespace WebAppTagHelper.TagHelpers
{
    [HtmlTargetElement("tab-item")]
    public class TabItemTagHelper : TagHelper
    {
        public string Title { get; set; }

        public override void Process(TagHelperContext context, 
            TagHelperOutput output)
        {
            string activePage = context.Items["ActivePage"] as string;
              
            output.TagName = "li";

            var activeLabel = activePage == Title ? "active" : "";
            var str = string.Format(@"
                  <a class='nav-link {0}' data-toggle='pill' href='#'>{1}</a>
            ", activeLabel, Title);
            output.Content.SetHtmlContent(str);
        }
    }
}

Usage

<tab active-page="Sessions">
    <tab-item title="Home"></tab-item>
    <tab-item title="Speakers"></tab-item>
    <tab-item title="Sessions"></tab-item>
    <tab-item title="Registration"></tab-item>
</tab>

Dependency injection

  • Constructor injection
  • To register to DI container, use: services.AddTransient<IFoo, Foo>(); (in Startup.cs)
using System;
using Microsoft.AspNetCore.Razor.TagHelpers;

namespace WebAppTagHelper.TagHelpers
{
    [HtmlTargetElement("foo-bar")]
    public class FooBarTagHelper : TagHelper
    {
        public string Baz { get; set; }

        private IFoo _foo;

        public TimeSinceTagHelper(IFoo foo)
        {
            _foo = foo;
        }

        public override void Process(TagHelperContext context, TagHelperOutput output)
        {
            output.Content.SetContent(_foo.GetBax());
        }
    }
}

HtmlTargetElement

  • HtmlTargetElementAttribute Class
  • Multiple [HtmlTargetElement] attribute can be applied to same tag helper class
  • [HtmlTargetElement(Attributes = "")]
    • Attributes = comma separetd list of attributes i.e. [HtmlTargetElement(Attributes = "foo, bar, baz")]
    • bot “…” and “[…]” are valid i.e [HtmlTargetElement(Attributes = "foo")] & [HtmlTargetElement(Attributes = "[foo]")]
    • pattern can be used for Attributes and only =, ^(start) & $(end) are valid in pattern
    • pattern in Attributes example: [HtmlTargetElement(Attributes = "[foo ^= 'a'], [bar $= 'z']")]

Restricting tag

[RestrictChildren("tab-item")]
[HtmlTargetElement("tab")]
public class TabTagHelper : TagHelper
{
	// ... ... ...
}

[HtmlTargetElement("tab-item", ParentTag = "tab")]
public class TabItemTagHelper : TagHelper
{
	// ... ... ...
}

TagStructure Enum

[HtmlTargetElement("tab", TagStructure = TagStructure.NormalOrSelfClosing)]
public class TabTagHelper : TagHelper
{
	// ... ... ...
}

TagMode Enum

[HtmlTargetElement("tab", TagMode = TagMode.SelfClosing)]
public class TabTagHelper : TagHelper
{
	// ... ... ...
}

Tag Helper Components

  • A Tag Helper Component is a Tag Helper that allows you to conditionally modify or add HTML elements from server-side code (aspnet 2.0+)
  • ASP.NET Core includes two built-in Tag Helper Components: head and body
  • Two common use cases of Tag Helper Components include:
    • Injecting a <link> into the <head>
    • Injecting a <script> into the <body>
  • Links:

Register TagHelperComponent

  • A Tag Helper Component must be added to the app’s Tag Helper Components collection
  • Unless registered, <link> or <script> can not be injected

Dependency injection in ConfigureServices(...)

public void ConfigureServices(IServiceCollection services)
{
    // ... ... ...
	
    services.AddTransient<ITagHelperComponent, AddressStyleTagHelperComponent>();
    services.AddTransient<ITagHelperComponent, AddressScriptTagHelperComponent>();
	
    // ... ... ...
}

Registration via Razor file

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

@{
	// ... ... ...
	
    manager.Components.Add(new AddressTagHelperComponent(...));
}

Registration via controller or Page Model (constructor injection)

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 IndexModel(ITagHelperComponentManager tagHelperComponentManager)
    {
        _tagHelperComponentManager = tagHelperComponentManager;
    }

    public void OnGet()
    {
        // ... ... ...

        _tagHelperComponentManager.Components.Add(new AddressTagHelperComponent(...));
    }
}

See:

Create a custom TagHelper Component

  • All TagHelperComponent you create will be executed for TagHelper that derives from TagHelperComponentTagHelper and imported by _ViewImports.cshtml
  • There are two built-in TagHelpers (HeadTagHelper & BodyTagHelper) that derives from TagHelperComponentTagHelper in ...Mvc.Razor.TagHelpers namespace
    • Any TagHelperComponent you create will be executed for head and body since ...Mvc.Razor.TagHelpers is imported by _ViewImports.cshtml and built-in HeadTagHelper & BodyTagHelper are being available
    • Use if to filter for a specific tag

To create a custom TagHelperComponent

  • Create a public class deriving from TagHelperComponentTagHelper.
  • Apply an [HtmlTargetElement] attribute to the class. Specify the name of the target HTML element.
  • Optional: Apply an [EditorBrowsable(EditorBrowsableState.Never)] attribute to the class to suppress the type’s display in IntelliSense.

See: Create a custom Tag Helper Component

Injecting Google analytics in the body using custom TagHelperComponent

TagHelperComponents/GoogleAnalyticsTagHelperComponent.cs

using System;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Razor.TagHelpers;
namespace WebAppTHC.TagHelperComponents
{
    public class GoogleAnalyticsTagHelperComponent : TagHelperComponent
    {
        public GoogleAnalyticsTagHelperComponent(string trackingCode)
        {
            TrackingCode = trackingCode;
        }
		
        [HtmlAttributeName("tracking-code")]
        public string TrackingCode { get; set; } = "UA-XXXXXX-XX";

        public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (string.Equals(context.TagName, "body", StringComparison.OrdinalIgnoreCase))
            {
                var gaString = "<script>window.ga=window.ga||function(){(ga.q=ga.q||[]).push(arguments)};ga.l=+new Date;ga('create', '" 
				+ TrackingCode 
				+ "', 'auto');ga('send', 'pageview');</script><script async src='https://www.google-analytics.com/analytics.js'></script>"; output.PostContent.AppendHtml(gaString);
            }
            

            return Task.CompletedTask;
        }
    }
}

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
	// ... ... ...

	services.AddSingleton<ITagHelperComponent>(new GoogleAnalyticsTagHelperComponent("UA-654321-99"));
}

Now Google analytics will be injected to every page body by GoogleAnalyticsTagHelperComponent

TagHelper component for a tag other than head and body

To create TagHelperComponent for a tag other than head and body, create a TagHelper that derives from TagHelperComponentTagHelper (just like aspnet core team did for (HeadTagHelper & BodyTagHelper))

Example: targeting footer
TagHelperComponentTagHelpers/FooterTagHelper.cs

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

namespace WebAppTHC.TagHelperComponentTagHelpers
{
    [HtmlTargetElement("footer")]
    [EditorBrowsable(EditorBrowsableState.Never)]
    public class FooterTagHelper : TagHelperComponentTagHelper
    {
        public FooterTagHelper(ITagHelperComponentManager manager, ILoggerFactory loggerFactory): base(manager, loggerFactory)
        {
        }
    }
}

TagHelperComponents/FooterTagHelperComponent.cs

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

namespace WebAppTHC.TagHelperComponents
{
    public class FooterTagHelperComponent : TagHelperComponent
    {
        public string CopyrightNotice { get; set; }

        public FooterTagHelperComponent(string copyrightNotice)
        {
            CopyrightNotice = copyrightNotice;
        }

        public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
        {
            if (string.Equals(context.TagName, "footer", StringComparison.OrdinalIgnoreCase))
            {
                output.PostContent.AppendHtml("<br/>" + CopyrightNotice);
            }
			
            return Task.CompletedTask;
        }
    }
}

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
	// ... ... ...

	services.AddSingleton<ITagHelperComponent>(new FooterTagHelperComponent("<i>Copyright Foo 2019</i>"));

	// ... ... ...
}

_ViewImports.cshtml

@using WebAppTHC
@using WebAppTHC.Models
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
@addTagHelper WebAppTHC.TagHelperComponentTagHelpers.*, WebAppTHC  

Built-in Tag Helpers

https://docs.microsoft.com/en-us/aspnet/core/mvc/views/working-with-forms

Community developed Tag Helpers