Nesting TagHelpers in ASP.NET Core MVC
Asked Answered
B

2

17

The ASP.NET Core TagHelper documentation gives the following example:

public class WebsiteContext
{
    public Version Version { get; set; }
    public int CopyrightYear { get; set; }
    public bool Approved { get; set; }
    public int TagsToShow { get; set; }
}

[TargetElement("website-information")]
public class WebsiteInformationTagHelper : TagHelper
{
    public WebsiteContext Info { get; set; }

    public override void Process(TagHelperContext context, TagHelperOutput output)
    {
        output.TagName = "section";
        output.Content.SetContent(
            $@"<ul><li><strong>Version:</strong> {Info.Version}</li>
            <li><strong>Copyright Year:</strong> {Info.CopyrightYear}</li>
            <li><strong>Approved:</strong> {Info.Approved}</li>
            <li><strong>Number of tags to show:</strong> {Info.TagsToShow}</li></ul>");
        output.TagMode = TagMode.StartTagAndEndTag;
    }
}

This can then be used in your Razor .cshtml as follows:

<website-information info="new WebsiteContext {
    Version = new Version(1, 3),
    CopyrightYear = 1790,
    Approved = true,
    TagsToShow = 131 }"/>

This will generate the following HTML:

<section>
    <ul>
        <li><strong>Version:</strong> 1.3</li>
        <li><strong>Copyright Year:</strong> 1790</li>
        <li><strong>Approved:</strong> true</li>
        <li><strong>Number of tags to show:</strong> 131 </li>
    </ul>
</section>

This is pretty ugly tag helper syntax. Is there some way to nest another tag helper and get full intelli-sense so that the only allowed child of website-information can be context? See example below:

<website-information>
    <context version="1.3" copyright="1790" approved tags-to-show="131"/>
</website-information>

In my use case, the website-information element already has many attributes and I want to add one or more separate nested elements.

UPDATE

I have raised this suggestion on the ASP.NET GitHub page to implement this feature for TagHelpers.

Buckler answered 21/9, 2015 at 10:25 Comment(2)
Why not just adding separated parameters to the website-information tag helper instead of a single info parameter? You could nest tag helpers, but you wont be able to force that only <context> helpers are nested inside the <website-information> helperKnockknee
@DanielJ.G.There are loads of reasons to do this. 1. You already had lots of attributes on website-information 2. If context made more logical sense being a child element 3. If the context properties are logically grouped together 4. You could have multiple context elements.Buckler
K
23

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")]
Knockknee answered 21/9, 2015 at 12:32 Comment(4)
ParentTag parameter reads as follows: "The required HTML element name of the direct parent. A null value indicates any HTML element name is allowed." Can I use CSS selector instead? The parent tag is just a <select>, but I don't want to affect all children of <select>, maybe just the childrens of a <select class="special"> -- should I make my own TagHelper for the parent, JUST so I can target specific elements? Or could I use CSS selectors like Parent="select.special"?Ceres
not talking about a direct implementation, but this would help in the implementation of foreach in a model right?Shirr
Note for NET 5 in ParentTagHelper replace SetContent with SetHtmlContentHarless
Is there any way to found out about child's index? let's say we have TabTagHelper as parent and TabHeaderTagHelper as child, and following code: <tab><tab-header title='Tab 1' /><tab-header title='Tab 2' /></tab>, I want to know Tab 2 is the second child, inside TabHeaderTagHelper class, is that possible?Explicative
S
9

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());
Sweater answered 10/1, 2019 at 18:12 Comment(3)
Yes, this seems like the only real solution right now. Have an upvote and welcome to StackOverflow!Buckler
Thanks, this is just what I have been searching for.Benzol
Very helpful example, the only one I've found that shows the child tag rendering itself completely (including the enclosing tag, attributes, pre/post content, etc.), rather than just adding the tag's content to the context, and depending on the parent to add the enclosing tag. I used WriteTo to render the tag to an HTML string, saved the string in a List<string> in the context, and the parent tag just appends each string to its content.Dickman

© 2022 - 2024 — McMap. All rights reserved.