Web Components with ASP.NET Core 1.0

What are HtmlHelpers and TagHelpers?

With ASP.NET Core 1.0 comes MVC 6, and with the latest MVC framework there is a transition from HtmlHelpers to TagHelpers.

For those who aren’t familiar with HtmlHelpers, have a look in any MVC5 implementation and you’ll likely see:

@using(Html.BeginForm())
{
    ...
}

Anything beginning with @Html. is invoking a static extension method on a C# HtmlHelper class that resides within the System.Web.MVC namespace. HtmlHelpers are a really useful way to abstract logic away and avoid unnecessary duplication of mark-up within your views.

TagBuilders provide a similar abstraction but rather than rely upon the @Html. invocation they are implemented as tags – either by extending existing mark-up elements (such as <form>) or creating new ones (e.g. <custom-map>).

For an overview of TagHelpers see: https://docs.asp.net/en/latest/mvc/views/tag-helpers/intro.html

What are Web Components

Web components are custom DOM elements that encapsulate mark-up, styling, and JavaScript to be reused across multiple web sites. There are a number of different web component frameworks built against a common set of standards. One such example is Google Polymer.

Google Polymer also provide a number of prebuilt components in their Element Catalog.

screen2bshot2b2015-05-292bat2b11-02-192bpm

You can use existing elements as is, combine elements to create new ones by composition, or create custom elements from scratch.

How can a TagHelper be used with Polymer?

Let’s take an existing Polymer web component as an example. Google provide the google-map to add a map to your web page(s):

<google-map latitude="37.77493" longitude="-122.41942" fit-to-markers>

This is great if you have the latitude and longitude for a map available in your view model but what if you only want to expose the unique identifier of an address in your view and use a service to provide the latitude and longitude values?

One of the limitations of HtmlHelpers was the fact that they were static extension methods and as such didn’t compliment dependency injection. This often resulted in abusing the ViewData or TempData dictionaries that MVC provides to pass services into a view and subsequently into HtmlHelper(s) as a parameter.

TagHelpers are NOT static and are ideally suited to dependency injection meaning that you can combine the readability benefits of a HtmlHelper with the enforcement of single responsibility and dry principles (and testability) that dependency injection provide.

Creating a TagHelper

To create a new TagHelper you need to create a class decorated with a [HtmlTargetElement] attribute which is used to set the element/tag name and any required attributes. You can decorate properties with a [HtmlAttributeName] attribute to have them auto populated.

Dave Paquette has provided an excellent blog post on creating a custom TagHelper.

Note: Optional element/tag attributes can be omitted from the [HtmlTargetElement] attribute.

[HtmlTargetElement("custom-map", Attributes = AddressIdAttributeName)]
public class CustomMapTagHelper : TagHelper
{
    private const string AddressIdAttributeName = "address-id";

    private IAddressService AddressService { get; set; }
    [HtmlAttributeName(AddressIdAttributeName)]
    public string AddressId { get; set; }
 
    public CustomMapTagHelper(IAddressService addressService)
    {
        AddressService = addressService;
    }
 
    ...
}

Here, I have used constructor injection to wire up a dependency on IAddressService. This is a service that has a method called ResolveLocation that takes a string addressId and returns a LatLong:

public interface IAddressService
{
    LatLong ResolveLocation(string addressId)
}

public struct LatLong
{
    public double Latitude { get; set; }
    public double Longitude { get; set; }
}

Finally, you need to override the Process or ProcessAsync method of TagHelper and provide your implementation:

public override void Process(TagHelperContext context, TagHelperOutput output)
{
    var latlong = AddressService.ResolveLocation(AddressId);
 
    string content = $"<google-map latitude=\"{latlong.Latitude}\" longitude=\"{latlong.Longitude}\" fit-to-markers>";

    output.Content.AppendHtml(content);
}

We can reference our custom tag helper using the @addTagHelper Razor command. We can do this within the .cshtml templates we want to use it or make it available to all templates by adding it to our _GlobalImports.cshtml.

We can use the tag with the following mark-up:

<custom-map address-id="@Model.AddressId"></custom-map>

The above tag will render the following mark-up (with the latitude and longitude attributes dynamically populated):

<custom-map address-id="@Model.AddressId">
    <google-map latitude="37.77493" longitude="-122.41942" fit-to-markers>
</custom-map>

Benefits Recap

By using a TagHelper to generate the mark-up you have a single place to maintain it. Any future requirements that involve changing the way maps can be dealt with once rather than crawling through all the views and changing it manually.

Additionally we have reduced the effort required to re-use the custom-map element across our web application and reduced the potential for typographical mistakes.

Next Steps

This is a particularly simple (and contrived) example but you can use the same mechanism for lots of scenarios.

You should consider the following…

  1. How to replace the <custom-map> element with a <google-map>?
  2. How to toggle the fit-to-markers attribute?
  3. What if the AddressService throws an Exception? How to deal with failures.