Aspnetcore: Hierarchical Tag Helper binding

Created on 17 Jun 2016  路  2Comments  路  Source: dotnet/aspnetcore

Summary

This feature would add support for directly binding Tag Helper properties to child Tag Helper instances from the source document, thus making it much simpler to author related Tag Helpers that are designed to work together in a hierarchy.

Background

Today, we provide a number of features to assist with authoring sets of Tag Helpers that are intended to be used together in a logical hierarchy, including:

  • RestrictChildrenAttribute to restrict which tags are valid as children of the element a Tag Helper instance is attached to
  • HtmlTargetElementAttribute.ParentTag to restrict which tag is valid as a parent of an element a Tag Helper instance is attached to
  • TagHelperContext.Items to allow passing of data between different Tag Helpers attached to the same element, and between Tag Helpers attached to an element and Tag Helpers attached to that element's children

The use of TagHelperContext.Items to pass data between related Tag Helpers is somewhat cumbersome, e.g.:

``` c#
public class ParentChildContext
{
public string SomeData { get; set; }
}

[RestrictChildren("child")]
public class ParentTagHelper : TagHelper
{
private ParentChildContext _context;

public override void Init(TagHelperContext context)
{
    _context = new ParentChildContext();
    context.Items.Add(typeof(ParentChildContext), _context);
}

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
    // Force children to process
    var childContent = await output.GetChildContentAsync();

    // Read context data set by child here
    // _context.SomeData
}

}

[HtmlTargetElement("child", ParentTag = "parent", TagStructure = TagStructure.WithoutEndTag)]
public class ChildTagHelper : TagHelper
{
public string SomeData { get; set; }
public override void Process(TagHelperContext context, TagHelperOutput output)
{
var parentContext = context.Items[typeof(ParentChildContext)];
parentContext.SomeData = SomeData;
}
}

``` html
<parent>
    <child some-data="This will be accessible to the parent" />
</parent>

Proposal

Tag Helper property binding would be expanded to support directly binding Tag Helper instances to properties on a Tag Helper using Tag Helpers attached to the current element's children as the source.

Binding will:

  • Support the binding of any type that implements ITagHelper

    • In the case where multiple children of the matching type are present, the first instance will be bound

  • Support the binding of IEnumerable<T> where T : ITagHelper to enable simple access to multiple child Tag Helper instances of the same type

    • When the binding applies, it will always create and set an IEnumerable<T> instance to the property, irrespective of the property's initial value (i.e. it will overwrite any current value of the property)

  • Bind only the first level of children (i.e. it would not perform a "deep" or "recursive" bind)
  • Only support binding to public settable properties (like other bindings)
  • Be implied (i.e. automatic) based on the property being public and the type being derived from ITagHelper

    • _Review: Should this be opt-in via a new attribute rather than implied?_

  • Support being disabled via HtmlAttributeNotBoundAttribute on the property that would otherwise be bound

Example

Here is the example from above reimplemented with the proposed behavior implied:

``` c#
[RestrictChildren("child")]
public class ParentTagHelper : TagHelper
{
public ChildTagHelper Child { get; set; }

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
    // Force children to process
    var childContent = await output.GetChildContentAsync();

    // Read data directly from child here
    // Child.SomeData
}

}

[HtmlTargetElement("child", ParentTag = "parent", TagStructure = TagStructure.WithoutEndTag)]
public class ChildTagHelper : TagHelper
{
public string SomeData { get; set; }
}

``` html
<parent>
    <child some-data="This will be accessible to the parent" />
</parent>

More Examples

``` c#
///

This Tag Helper applies to all elements and effectively creates a server-side DOM, albeit with a single, forward-only pass rather than being stateful.
[HtmlTargetElement("*")]
public class DomTagHelper : TagHelper
{
public DomTagHelper Parent { get; set; }

public IEnumerable<DomTagHelper> Children { get; set; }

public override void Init(TagHelperContext context)
{
    // Set parent on all children
    foreach (var child in children)
    {
        child.Parent = this;
    }
}

public override async Task ProcessAsync(TagHelperContext context, TagHelperOutput output)
{
    // Run children
    var childContent = await output.GetChildContentAsync();

    // 
}

}
```

area-mvc feature feature-razor-pages

Most helpful comment

This could be extended into a more general TagHelperAwareness concept with common constructs. Awareness determines helper participation scope and drives things like automatic binding. I've been experimenting in this area and use four basic types of awareness.

Self Awareness -> Tag. This is the current case. A TagHelper is aware only of itself.

Positional Awareness -> Parent/Parents, Child/Children. These make the TH aware of its parent and/or children tag or tags depending on settings. This is much like the examples.

Scope Awareness -> Page/Type. Normally I use this to make the TH aware of the Page. It automatically binds/sets things like HtmlHelper and Contextualize. Helps when TH shares info through TempData. But the category is Type-based, and it can reach out farther. For example, a routing tag inspects a NamedRoute, finds out things like the operation parameters on the controller, and then builds an appropriate request.

Referential Awareness -> Group/Master. This extends awareness into the layout level using attributes. This could go several ways, but example use would be "cloning" for more complex helpers. You can set values on one instance that has an id and point others to that one. They don't need a positional relationship.

<Complex id="C1" para-set-one="(3, 6, 12, 8)" injected-para="@theme.Complex1" data="@somedata"/>
<other stuff>...</other stuff>
<div>
    <Complex clone="C1" data="@otherdata" />
</div>

This would set properties on the clone to those from the master. The properties could require [Cloneable] attribute.

Displaying all items in a logical group, regardless of tag type or location. These helpers implement ITagGroup.Visible features. If a tag doesn't contain data it becomes invisible. The other tags inspect the group, and check the members for visibility. If any one is not visible each turns itself off.

<tagone group="g1" para="sdfsd/>
...   
<tagtwo group="g1" p1="werwe" p2="ityut"/>
...
   <tagthree group="g1" p1="werwe" p2="ityut"/>

Group collections are built at runtime at Page level. Rules can be run at the page level with the page calling into the helper, or the helper can access a group and other tags and then manipulate itself and/or them. A simple approach is having something on the interface that leads to a rule in the helper code to do

  Groups("g1").Any(!Visible))  Visible = false; 

All 2 comments

This could be extended into a more general TagHelperAwareness concept with common constructs. Awareness determines helper participation scope and drives things like automatic binding. I've been experimenting in this area and use four basic types of awareness.

Self Awareness -> Tag. This is the current case. A TagHelper is aware only of itself.

Positional Awareness -> Parent/Parents, Child/Children. These make the TH aware of its parent and/or children tag or tags depending on settings. This is much like the examples.

Scope Awareness -> Page/Type. Normally I use this to make the TH aware of the Page. It automatically binds/sets things like HtmlHelper and Contextualize. Helps when TH shares info through TempData. But the category is Type-based, and it can reach out farther. For example, a routing tag inspects a NamedRoute, finds out things like the operation parameters on the controller, and then builds an appropriate request.

Referential Awareness -> Group/Master. This extends awareness into the layout level using attributes. This could go several ways, but example use would be "cloning" for more complex helpers. You can set values on one instance that has an id and point others to that one. They don't need a positional relationship.

<Complex id="C1" para-set-one="(3, 6, 12, 8)" injected-para="@theme.Complex1" data="@somedata"/>
<other stuff>...</other stuff>
<div>
    <Complex clone="C1" data="@otherdata" />
</div>

This would set properties on the clone to those from the master. The properties could require [Cloneable] attribute.

Displaying all items in a logical group, regardless of tag type or location. These helpers implement ITagGroup.Visible features. If a tag doesn't contain data it becomes invisible. The other tags inspect the group, and check the members for visibility. If any one is not visible each turns itself off.

<tagone group="g1" para="sdfsd/>
...   
<tagtwo group="g1" p1="werwe" p2="ityut"/>
...
   <tagthree group="g1" p1="werwe" p2="ityut"/>

Group collections are built at runtime at Page level. Rules can be run at the page level with the page calling into the helper, or the helper can access a group and other tags and then manipulate itself and/or them. A simple approach is having something on the interface that leads to a rule in the helper code to do

  Groups("g1").Any(!Visible))  Visible = false; 

Thanks for contacting us. We're closing this issue as there was no community involvement here for quite a while now.

Was this page helpful?
0 / 5 - 0 ratings