Injecting content into your head or body tags via dependency injection using ITagHelperComponent

Posted on Monday, 17 Jul 2017

Having been playing around with the ASP.NET Core 2.0 preview for a little while now, one cool feature I stumbled upon was the addition of the new ITagHelperComponent interface and its use.

What problem does the ITagHelperComponent solve?

Pre .NET Core 2.0, if you're using a library that comes bundled with some static assets such as JavaScript or CSS, you'll know that in order to use the library you have to manually add script and/or link tags (including a reference to the files in your wwwroot folder), to your views. This is far from ideal as not only does it force users to jump through additional hoops, but it also runs the risk of introducing breaking changes when a user decides to remove the library and forgets to remove the JavaScript references, or if you update the library version but forget to change the appropriate JavaScript reference.

This is where the ITagHelperComponent comes in; it allows you to inject content into the header or footer of your application's web page. Essentially, it's dependency injection for your JavaScript or CSS assets! All that's required of the user is they register the dependency with their IoC Container of choice within their Startup.cs file.

Enough talk, let's take a look at how it works. Hopefully a demonstration will clear things up.

Injecting JavaScript or CSS assets into the head or body tags

Imagine we have some JavaScript we'd like to include on each page, this could be from either:

  • A JavaScript and/or CSS library we'd like to use (Bootstrap, Pure etc)
  • Some database driven JavaScript code or value that needs to be included in the head of your page
  • A JavaScript file that's bundled with a library that our users need to include before the closing </body> tag.

In our case, we'll keep it simple - we need to include some database drive JavaScript in our page in the form of some Google Analytics JavaScript.

Creating our JavaScript tag helper component

Looking at the contract of the ITagHelperComponent interface you'll see it's a simple one:

public interface ITagHelperComponent{
    int Order { get; }
    void Init(TagHelperContext context);
    Task ProcessAsync(TagHelperContext context, TagHelperOutput output);
}

We could implement the interface ourselves, or we could lean on the existing TagHelperComponent base class and override only the properties and methods we require. We'll do the later.

Let's start by creating our implementation which we'll call CustomerAnalyticsTagHelper:

// CustomerAnalyticsTagHelper.cs

CustomerAnalyticsTagHelper : TagHelperComponent {}

For this example the only method we're concerned about is the ProcessAsync one, though we will touch on the Order property later.

Let's go ahead and implement it:

// CustomerAnalyticsTagHelper.cs

public class CustomerAnalyticsTagHelper : TagHelperComponent
{
    private readonly ICustomerAnalytics _analytics;

    public CustomerAnalyticsTagHelper(ICustomerAnalytics analytics)
    {
        _analytics = analytics;
    }

    public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        if (string.Equals(context.TagName, "body", StringComparison.Ordinal))
        {
            string analyticsSnippet = @"
            <script>
                (function (i, s, o, g, r, a, m) {
                    i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
                        (i[r].q = i[r].q || []).push(arguments)
                    }, i[r].l = 1 * new Date(); a = s.createElement(o),
                        m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
                })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
                ga('create', '" + _analytics.CustomerUaCode + @"', 'auto')
                ga('send', 'pageview');
            </script>";
            
            output.PostContent.AppendHtmlLine(analyticsSnippet);
        }

        return Task.CompletedTask;
    }
}

As you can see, the TagHelperContext argument gives us context around the tag we're inspecting, in this case we want to look for the body HTML element. If we wanted to drop JavaScript or CSS into the <head></head> tags then we'd inspect tag name of "head" instead.

The TagHelperOutput argument gives us access to a host of properties around where we can place content, these include:

  • PreElement
  • PreContent
  • Content
  • PostContent
  • PostElement
  • IsContentModified
  • Attributes

In this instance we're going to append our JavaScript after the content located within the <body> tag, placing it just before the closing </body> tag.

Dependency Injection in our tag helper

With dependency injection being baked into the ASP.NET Core framework, we're able to inject dependencies into our tag helper - in this case I'm injecting our database driven consumer UA (User Analytics) code.

Registering our tag helper with our IoC container

Now all that's left to do is register our tag helper with our IoC container of choice. In this instance I'm using the build in ASP.NET Core one from the Microsoft.Extensions.DependencyInjection package.

// Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ICustomerAnalytics, CustomerAnalytics>(); // Data source containing UA code
    services.AddSingleton<ITagHelperComponent, CustomerAnalyticsTagHelper>(); // Our tag helper
    ...
}

Now firing up our tag helper we can see our JavaScript has now been injected in our HTML page without us needing to touch any of our .cshtml Razor files!

...
<body>
    ...
    <script>
        (function (i, s, o, g, r, a, m) {
            i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
                (i[r].q = i[r].q || []).push(arguments)
            }, i[r].l = 1 * new Date(); a = s.createElement(o),
                m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
        })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
        ga('create', 'UA-123456789', 'auto')
        ga('send', 'pageview');
    </script>
</body>
</html>

Ordering our output

If we needed to include more than one script or script file in our output, we can lean on the Order property we saw earlier, overriding this allows us to specify the order of our output. Let's see how we can do this:

// JsLoggingTagHelper.cs

public class JsLoggingTagHelper : TagHelperComponent
{
    public override int Order => 1;

    public override Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        if (string.Equals(context.TagName, "body", StringComparison.Ordinal))
        {
            const string script = @"<script src=""/jslogger.js""></script>";
            output.PostContent.AppendHtmlLine(script);
        }

        return Task.CompletedTask;
    }
}
// CustomerAnalyticsTagHelper.cs

public class CustomerAnalyticsTagHelper : TagHelperComponent
{
    ...
    public override int Order => 2; // Set our AnalyticsTagHelper to appear after our logger
    ...
}
// Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<ICustomerAnalytics, CustomerAnalytics>();
    services.AddSingleton<ITagHelperComponent, CustomerAnalyticsTagHelper>();
    services.AddSingleton<ITagHelperComponent, JsLoggingTagHelper>();
    ...   
}

When we we launch our application we should see the following HTML output:

<script src="/jslogger.js"></script>
<script>
    (function (i, s, o, g, r, a, m) {
        i['GoogleAnalyticsObject'] = r; i[r] = i[r] || function () {
            (i[r].q = i[r].q || []).push(arguments)
        }, i[r].l = 1 * new Date(); a = s.createElement(o),
            m = s.getElementsByTagName(o)[0]; a.async = 1; a.src = g; m.parentNode.insertBefore(a, m)
    })(window, document, 'script', '//www.google-analytics.com/analytics.js', 'ga');
    ga('create', 'UA-123456789', 'auto')
    ga('send', 'pageview');
</script>
</body>
</html>

Conclusion

Hopefully this post has highlighted how powerful the recent changes to tag helpers are and how using the ITagHelperComponent interface allows us to inject content into our HTML without having to touch any files. This means as a library author we can ease integration for our users by simply asking them to register a type with their IoC container and we can take care of the rest!

Back