loading...

Tag Helpers 101

turnerj profile image James Turner ・6 min read

For a big web project of mine, I recently changed from the full .NET Framework to .NET Core. One of the many new tools in your arsenal when doing this is Tag Helpers.

This post will cover the basics of using a Tag Helper and making your own. It does expect you to have had a basic look over ASP.NET, particularly with forms.

The examples below will be based on the following model:

class PersonViewModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Background

Prior to Tag Helpers, something like a form would more usually look like this in a Razor template:

@using (Html.BeginForm("Edit"))
{
    @Html.AntiForgeryToken()

    <div class="field">
        @Html.LabelFor(m => m.FirstName)
        <div class="input-container">
            @Html.TextBoxFor(m => m.FirstName)
        </div>
        @Html.ValidationMessageFor(m => m.FirstName)
    </div>
    <div class="field">
        @Html.LabelFor(m => m.LastName)
        <div class="input-container">
            @Html.TextBoxFor(m => m.LastName)
        </div>
        @Html.ValidationMessageFor(m => m.LastName)
    </div>
    <div class="field">
        <button>Continue</button>
    </div>
}

Things that would make it better like @helper aren't (currently) in .NET Core. You could use partial views but here that isn't really a great option. You could look at View Components but it really isn't the right tool here.

Let's look at how Tag Helpers might make this a bit easier to read and avoid some of the weird quirks with syntax highlighting and bracing that normally plague Razor templates.

Basic Tag Helper

Let's look at an input tag helper for our FirstName property.

<input asp-for="FirstName" />

Yep, that's it - we are done. We have a Tag Helper binding the input tag to our FirstName property.

Thanks for reading! Like, subscribe and comment!

No seriously, that is all the InputTagHelper actually is set to require. By specifying asp-for, the normal HTML element actually becomes a processed tag on the server by the InputTagHelper.

You will know it is applied as the tag colours in Visual Studio will change:

Input Tag, with Tag Helper styling

You might be asking why is this better than @Html.TextBoxFor(m => m.FirstName)? It really comes down to flexibility, simplicity and the power you can perform with Tag Helpers.

Let's look at another example:

<form asp-controller="Person" asp-action="Edit" method="post">
    <!-- Input and Submit elements -->
</form>

The Tag Helper for the form tag is actually enabled without needing to specify any attributes - another cool feature of Tag Helpers. In the example above, I am using asp-controller and asp-action to set where I want the form to submit to.

Because it is still just HTML, nothing stops you adding a class attribute and specify anything you need like you would to any other HTML element.

<form asp-controller="Person" asp-action="Edit" method="post" class="my-super-special-form-class">
    <!-- Input and Submit elements -->
</form>

If you wanted to set a class when using the old method, it gets a lot messier:

using (Html.BeginForm("Edit", "Person", FormMethod.Post, new { @class = "my-super-special-form-class" }))

So Tag Helpers can help you keep using HTML mostly the same way you have been already without needing to work with the Razor syntax. Tag Helpers also have better IntelliSense support - what look like strings in asp-for are actually model expressions. Same for other custom attributes you might make, everything is type-safe.

Full example with Tag Helpers

Now you have seen a little about what Tag Helpers are and how they can work, let's see what that first example would look like if everything was tag helpers:

<form asp-action="Edit" method="post">
    <div class="field">
        <label asp-for="FirstName"></label>
        <div class="input-container">
            <input asp-for="FirstName" />
        </div>
        <span asp-validation-for="FirstName"></span>
    </div>
    <div class="field">
        <label asp-for="LastName"></label>
        <div class="input-container">
            <input asp-for="LastName" />
        </div>
        <span asp-validation-for="LastName"></span>
    </div>
    <div class="field">
        <button>Continue</button>
    </div>
</form>

Make your own Tag Helper

First we should probably answer: Why would I want to make my own Tag Helper?

Partly the flexibility it offers, partly making the HTML markup easier to work with and partly because you can do some really cool stuff.

We are going to make our "Continue" button look nicer with adding our own Tag Helper to define some classes for us based on an attribute we set.

To make a Tag Helper, you will need to make a new class that extends TagHelper - I am going to call mine ButtonStyleTagHelper. It will use the attribute button-style to apply itself to the tag.

This use case for a Tag Helper can work great if you are writing utility-first CSS.

We need to specify a few attributes on the class to define what tags we want our Tag Helper to apply for and what attributes we need define.

[HtmlTargetElement("button", Attributes = "button-style", TagStructure = TagStructure.NormalOrSelfClosing)]
[HtmlTargetElement("a", Attributes = "button-style", TagStructure = TagStructure.NormalOrSelfClosing)]
public class ButtonStyleTagHelper : TagHelper
{

}

The example above says we are allowing our Tag Helper to work for <button> and <a> as long as button-style is set as an attribute.

Now let's define the property our attribute will go on and its type:

[HtmlTargetElement("button", Attributes = "button-style", TagStructure = TagStructure.NormalOrSelfClosing)]
[HtmlTargetElement("a", Attributes = "button-style", TagStructure = TagStructure.NormalOrSelfClosing)]
public class ButtonStyleTagHelper : TagHelper
{
    [HtmlAttributeName("button-style")]
    public Style ButtonStyle { get; set; }

    public enum Style
    {
        Primary,
        Secondary
    }
}

We have our property ButtonStyle explicitly bound to the attribute button-style and using our custom Style enum, allowing us to pick either Primary or Secondary.

We need to define our binding for the Style enum to the classes we want:

private static readonly Dictionary<Style, string> ButtonStyles = new Dictionary<Style, string>
{
    { Style.Primary, "whatever-classes i-might-specifically want-for-all primary-button-styles" },
    { Style.Secondary, "the-classes i-might-specifically want-for-secondary button-styles" }
};

To actually do work with a Tag Helper, we need to override the Process method and give it something to do.

public override void Process(TagHelperContext context, TagHelperOutput output)
{
    var classes = ButtonStyles[ButtonStyle];
    var classAttr = output.Attributes.Where(
        a => a.Name.Equals("class", StringComparison.InvariantCultureIgnoreCase)
    ).FirstOrDefault();

    if (classAttr != null)
    {
        output.Attributes.Remove(classAttr);
        classes += " " + classAttr.Value.ToString();
    }

    classAttr = new TagHelperAttribute("class", classes);
    output.Attributes.Add(classAttr);
}

That might look a little daunting but let's break it down:

var classes = ButtonStyles[ButtonStyle];
var classAttr = output.Attributes.Where(
    a => a.Name.Equals("class", StringComparison.InvariantCultureIgnoreCase)
).FirstOrDefault();

First we get the classes we defined based on button style that we set. The property ButtonStyle has the enum value we define in HTML so we just pass that to the dictionary.

Because you may have defined a class attribute already on the element, we want to make sure we just prepend our new classes.

if (classAttr != null)
{
    output.Attributes.Remove(classAttr);
    classes += " " + classAttr.Value.ToString();
}

classAttr = new TagHelperAttribute("class", classes);
output.Attributes.Add(classAttr);

We now check whether there was a class attribute, remove it from the tag and apply those classes to our ones defined for our ButtonStyle.

Finally, we recreate a new attribute object and apply that to our tag.

Before we can use our tag, we need to add it to the _ViewImports.cshtml file, something like @addTagHelper *, <YourAssemblyName> will add all Tag Helpers in that assembly to all Razor templates.

Your final class should look something similar to this:

[HtmlTargetElement("button", Attributes = "button-style", TagStructure = TagStructure.NormalOrSelfClosing)]
[HtmlTargetElement("a", Attributes = "button-style", TagStructure = TagStructure.NormalOrSelfClosing)]
public class ButtonStyleTagHelper : TagHelper
{
    private static readonly Dictionary<Style, string> ButtonStyles = new Dictionary<Style, string>
    {
        { Style.Primary, "whatever-classes i-might-specifically want-for-all primary-button-styles" },
        { Style.Secondary, "the-classes i-might-specifically want-for-secondary button-styles" }
    };

    [HtmlAttributeName("button-style")]
    public Style ButtonStyle { get; set; }

    public enum Style
    {
        Primary,
        Secondary
    }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        var classes = ButtonStyles[ButtonStyle];
        var classAttr = output.Attributes.Where(
            a => a.Name.Equals("class", StringComparison.InvariantCultureIgnoreCase)
        ).FirstOrDefault();

        if (classAttr != null)
        {
            output.Attributes.Remove(classAttr);
            classes += " " + classAttr.Value.ToString();
        }

        classAttr = new TagHelperAttribute("class", classes);
        output.Attributes.Add(classAttr);
    }
}

Our end result would turn something like this:

<button button-style="Primary">Continue</button>

Into this:

<button class="whatever-classes i-might-specifically want-for-all primary-button-styles">Continue</button>

This is just one example of Tag Helpers, there are many other things you could do with Tag Helpers. I may touch on a few of these in a later article.

Conclusion

Tag Helpers are not just a cleaner way of doing what we could before but a powerful tool to add more functionality to our Razor templates.

Further Reading

Discussion

pic
Editor guide