Injecting content into your head or body tags via dependency injection using ITagHelperComponent
Posted on Monday, 17th July 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!
Enjoy this post? Don't be a stranger!
Follow me on Twitter at @_josephwoodward and say Hi! I love to learn in the open, meet others in the community and talk Go, software engineering and distributed systems related topics.