Have you ever wanted to enrich HTML server-side before reaching the client? Of course, you have! The idea of declaring behavior and then having the tedious parts handled by a framework is what most developers want.

Luckily for us, the advent of tag helpers is here to make our wildest HTML dreams a reality. In this post, we’ll see how we can scan our pages for an HTML element with a particular attribute. From there, we’ll alter the rendered HTML to include the existing content of our tag and add additional inputs.

The Goal

Given we have a Razor view with many HTML elements on the page, we want to find a form element with an attribute of formal. From here, we’ll copy the existing content, add two additional inputs to the form, and alter the form’s attributes.

Let’s take a look at the HTML Form.

@page
@model IndexModel
@{
    ViewData["Title"] = "Home page";
}
<form formal return-url="@Url.Page("Index")">
    <div class="form-group">
        <label for="First Name">First Name</label>
        <input class="form-control" type="text" name="First Name" id="firstName" required/>
    </div>
    <div class="form-group">
        <label for="Last Name">Last Name</label>
        <input class="form-control" type="text" name="Last Name" id="lastName" required/>
    </div>
    <div class="form-group">
        <label for="exampleFormControlSelect2">Favorite Numbers</label>
        <select name="Favorite Numbers" multiple class="form-control" id="exampleFormControlSelect2">
            <option>1</option>
            <option>2</option>
            <option>3</option>
            <option>4</option>
            <option>5</option>
        </select>
    </div>
    <button type="submit">Submit</button>
</form>

Note that we have a form with the attribute of formal and an additional attribute of return-url. Inputs comprise the rest of the form, regular text fields, and multiple select inputs.

The Wire-up

The first step is to create a class that inherits from TagHelper. The TagHelper is a class used by Razor to process the C# and HTML of our page into its final output. Let’s look at the ultimate form of our tag helper and then break it down.

[HtmlTargetElement("form", Attributes = "formal")]
public class FormalTagHelper : TagHelper
{
    private readonly FormalOptions _formalOptions;

    public string ReturnUrl { get; set; }
    public string Name { get; set; }

    public FormalTagHelper(FormalOptions formalOptions)
    {
        _formalOptions = formalOptions;
    }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {
        output.Attributes.SetAttribute("action", _formalOptions.SubmitUrl);
        output.Attributes.SetAttribute("method", HttpMethod.Post);
        output.Attributes.Remove(output.Attributes["formal"]);
        
        var childContent = await output.GetChildContentAsync();
        string content = childContent.GetContent();
        output.Content.SetHtmlContent(content);

        if (ReturnUrl is not null)
        {
            output.Content.AppendHtml(
                new HtmlString(
                    $"<input type=\"hidden\" name=\"{_formalOptions.ReturnUrlName}\" value=\"{ReturnUrl}\" />")
            );
        }

        output.Content.AppendHtml(
            new HtmlString(
                $"<input type=\"hidden\" name=\"{_formalOptions.KindFieldName}\" value=\"{Name}\" />")
        );
    }
}

Starting at the top, we see the HtmlTargetElement. The attribute declares what HTML element we want to scan for and the attributes the item should have.

[HtmlTargetElement("form", Attributes = "formal")]

In this particular case, we want to find all HTML elements that are of form and have an attribute of formal.

Moving down the class, we see the properties of ReturnUrl and Name. By convention, tag helpers will set the property to any attribute that matches. In the case of ReturnUrl it can be set by using an attribute of return-url on the HTML form.

<form formal return-url="@Url.Page("Index")">

Past the properties, we come to the constructor of our tag helper. Like many things in ASP.NET, tag helpers also go through the inversion of control (IoC) resolution process. That means we can have our dependencies injected, as long as we register them with our services locator.

Now we’ve come to the heart of every tag helper, the ProcessAsync method. Here we can see the current HTML in the Razor page and alter the resulting output.

In this example, we add the two attributes of action and method. We also clean up our formal attribute, since its not a recognized HTML attribute.

output.Attributes.SetAttribute("action", _formalOptions.SubmitUrl);
output.Attributes.SetAttribute("method", HttpMethod.Post);
output.Attributes.Remove(output.Attributes["formal"]);

The next bit of code is significant for folks who want to retain the existing form’s internal content. We need to read the content from the current output parameter.

var childContent = await output.GetChildContentAsync();
string content = childContent.GetContent();
output.Content.SetHtmlContent(content);

Excluding this code will leave us with an empty HTML element on the page. Through testing, I’ve also found that we get the results from the previous tag helpers. Ordering the tag helpers in our _ViewImports.cshtml seems to influence the order that our Razor pages are processed.

@using DotNetFive
@namespace DotNetFive.Pages
// asp.net core tag helpers
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers
// our tag helpers
@addTagHelper *, DotNetFive

Our final code inside of ProcessAsync checks our set properties and adds content when necessary.

if (ReturnUrl is not null)
{
    output.Content.AppendHtml(
        new HtmlString(
            $"<input type=\"hidden\" name=\"{_formalOptions.ReturnUrlName}\" value=\"{ReturnUrl}\" />")
    );
}

output.Content.AppendHtml(
    new HtmlString(
        $"<input type=\"hidden\" name=\"{_formalOptions.KindFieldName}\" value=\"{Name}\" />")
);

Running Our Tag Helper

Combined with the Razor view and tag helper, we get a processed output that is what we expected.

<form action="/_formal/submit" method="POST">
    <div class="form-group">
        <label for="First Name">First Name</label>
        <input class="form-control" type="text" name="First Name" id="firstName" required="">
    </div>
    <div class="form-group">
        <label for="Last Name">Last Name</label>
        <input class="form-control" type="text" name="Last Name" id="lastName" required="">
    </div>
    <div class="form-group">
        <label for="exampleFormControlSelect2">Favorite Numbers</label>
        <select name="Favorite Numbers" multiple="" class="form-control" id="exampleFormControlSelect2">
            <option>1</option>
            <option>2</option>
            <option>3</option>
            <option>4</option>
            <option>5</option>
        </select>
    </div>
    <button type="submit">Submit</button>
<input type="hidden" name="__formal_redirect" value="/"><input type="hidden" name="__formal_kind" value=""></form>

We know it worked because we have the following characteristics:

  • An action and method attribute.
  • No formal attribute is visible.
  • All of our content is still intact.
  • A hidden input for our redirect URL.
  • A hidden input for our name value.

Conclusion

There’s a lot more tag helpers can do, and this example is an extreme case where we are enriching an existing HTML element with values. From a developer’s perspective, we can get large returns for little effort. It’s critical when working with TagHelper that we don’t forget the HtmlTargetElement attribute, the conventional property setting from HTML to C#, the dependency injection supported creation of our tag helper, and the necessary steps to maintain and alter the final HTML output.

I hope you found this post helpful, and if you enjoyed it, please consider sharing it with your friends and team members. Also, don’t forget to follow me on Twitter at @buhakmeh.