Nesting TagHelpers in ASP.NET Core MVC

I did not find a good example of multiply nested tag helpers on the web; so, I created one at MultiplyNestedTagHelpers GitHub Repository.

When working with more than one level of nested tag helper, it is important to setup a "context" class for each tag that can contain child tags. These context classes are used by the child tags to write their output. The context class is a regular, POCO class that has a property for each child tag. The properties may be strings, but I use StringBuilder. For example,

public class MyTableContext{
    public StringBuilder TableHeaderBuilder { get; set; } = new StringBuilder();
    public StringBuilder TableBodyBuilder { get; set; } = new StringBuilder();
}
public class MyTableHeaderContext {
    public StringBuilder RowBuilder { get; set; } = new StringBuilder();
}
//...etc.

In each parent tag's Process method, you need to instantiate the parent's associated context class and add this new object to the TagHelperContext object's Items collection. For example:

    //create context for this tag helper
    var tableContext = new MyTableContext();
    context.Items.Add(typeof(MyTableContext), tableContext);

In the child tag's Process method, you write to the parent's registered context object like this:

    //get reference to parent context, and append content to the relevant builder
    var tableContext = context.Items[typeof(MyTableContext)] as MyTableContext;
    tableContext.TableHeaderBuilder.Append(sb.ToString());

    //suppress output (for any tag with a parent tag)
    output.SuppressOutput();

Back in the parent tag's Process method, you receive the child tag's output like this:

    //you can use a StringBuilder to build output 
    //    or just write to output.Content.AppendHtml() for top-level tags
    var sb = new StringBuilder();
    //...      

    //retrieve the child output and append it to sb
    await output.GetChildContentAsync();
    sb.Append(tableHeaderContext.RowBuilder.ToString());
    //...

    //write to TagHelperOutput for top-level tags      
    output.Content.AppendHtml(sb.ToString());

You can certainly nest tag helpers, although maybe other options like view components, partial views or display templates might be better suited for the scenario described by the OP.

This could be a very simple child tag helper:

[HtmlTargetElement("child-tag", ParentTag="parent-tag")]
public class ChildTagHelper : TagHelper
{
    public string Message { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        // Create parent div
        output.TagName = "span";
        output.Content.SetContent(Message);
        output.TagMode = TagMode.StartTagAndEndTag;     
    }
}

And this could also be another simple parent tag helper:

[HtmlTargetElement("parent-tag")]
[RestrictChildren("child-tag")]
public class ParentTagHelper: TagHelper
{
    public string Title { get; set; }

    public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
    {            
        output.TagName = "div";

        // Add some specific parent helper html
        var header = new TagBuilder("h1");
        header.Attributes.Add("class", "parent-title");
        header.InnerHtml.Append(this.Title);
        output.PreContent.SetContent(header);

        // Set the inner contents of this helper(Will process any nested tag helpers or any other piece of razor code)
        output.Content.SetContent(await output.GetChildContentAsync());            
    }
}

In a razor view you could then write the following:

<parent-tag title="My Title">
    <child-tag message="This is the nested tag helper" />
</parent-tag>

Which would be rendered as:

<div>
    <h1 class="parent-title">My Title</h1>
    <span>This is the nested tag helper</span>
</div>

You can optionally enforce tag helpers to be nested in a particular way:

  • Use [RestrictChildren("child-tag", "another-tag")] in the parent tag helper to limit the allowed nested tag helpers
  • Use the ParentTag parameter when declaring the child tag helper to enforce the tag being nested inside a particular parent tag [HtmlTargetElement("child-tag", ParentTag = "parent-tag")]