ASP.NET Core Blazor attribute splatting and arbitrary parameters

Note

This isn't the latest version of this article. For the current release, see the .NET 8 version of this article.

Important

This information relates to a pre-release product that may be substantially modified before it's commercially released. Microsoft makes no warranties, express or implied, with respect to the information provided here.

For the current release, see the .NET 8 version of this article.

Components can capture and render additional attributes in addition to the component's declared parameters. Additional attributes can be captured in a dictionary and then splatted onto an element when the component is rendered using the @attributes Razor directive attribute. This scenario is useful for defining a component that produces a markup element that supports a variety of customizations. For example, it can be tedious to define attributes separately for an <input> that supports many parameters.

Attribute splatting

In the following Splat component:

  • The first <input> element (id="useIndividualParams") uses individual component parameters.
  • The second <input> element (id="useAttributesDict") uses attribute splatting.

Splat.razor:

@page "/splat"

<PageTitle>SPLAT!</PageTitle>

<h1>Splat Parameters Example</h1>

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}
@page "/splat"

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}
@page "/splat"

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}
@page "/splat"

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}
@page "/splat"

<input id="useIndividualParams"
       maxlength="@maxlength"
       placeholder="@placeholder"
       required="@required"
       size="@size" />

<input id="useAttributesDict"
       @attributes="InputAttributes" />

@code {
    private string maxlength = "10";
    private string placeholder = "Input placeholder text";
    private string required = "required";
    private string size = "50";

    private Dictionary<string, object> InputAttributes { get; set; } =
        new Dictionary<string, object>()
        {
            { "maxlength", "10" },
            { "placeholder", "Input placeholder text" },
            { "required", "required" },
            { "size", "50" }
        };
}

The rendered <input> elements in the webpage are identical:

<input id="useIndividualParams"
       maxlength="10"
       placeholder="Input placeholder text"
       required="required"
       size="50">

<input id="useAttributesDict"
       maxlength="10"
       placeholder="Input placeholder text"
       required="required"
       size="50">

Arbitrary attributes

To accept arbitrary attributes, define a component parameter with the CaptureUnmatchedValues property set to true:

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public Dictionary<string, object>? InputAttributes { get; set; }
}

The CaptureUnmatchedValues property on [Parameter] allows the parameter to match all attributes that don't match any other parameter. A component can only define a single parameter with CaptureUnmatchedValues. The property type used with CaptureUnmatchedValues must be assignable from Dictionary<string, object> with string keys. Use of IEnumerable<KeyValuePair<string, object>> or IReadOnlyDictionary<string, object> are also options in this scenario.

The position of @attributes relative to the position of element attributes is important. When @attributes are splatted on the element, the attributes are processed from right to left (last to first). Consider the following example of a parent component that consumes a child component:

AttributeOrderChild1.razor:

<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}
<div @attributes="AdditionalAttributes" extra="5" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}

AttributeOrder1.razor:

@page "/attribute-order-1"

<PageTitle>Attribute Order 1</PageTitle>

<h1>Attribute Order Example 1</h1>

<AttributeOrderChild1 extra="10" />

<p>
    View the HTML markup in your browser to inspect the attributes on
    the AttributeOrderChild1 component.
</p>

AttributeOrderParent1.razor:

@page "/attribute-order-parent-1"

<AttributeOrderChild1 extra="10" />

AttributeOrderParent1.razor:

@page "/attribute-order-parent-1"

<AttributeOrderChild1 extra="10" />

AttributeOrderParent1.razor:

@page "/attribute-order-parent-1"

<AttributeOrderChild1 extra="10" />

AttributeOrderParent1.razor:

@page "/attribute-order-parent-1"

<AttributeOrderChild1 extra="10" />

The AttributeOrderChild1 component's extra attribute is set to the right of @attributes. The AttributeOrderParent1 component's rendered <div> contains extra="5" when passed through the additional attribute because the attributes are processed right to left (last to first):

<div extra="5" />

In the following example, the order of extra and @attributes is reversed in the child component's <div>:

AttributeOrderChild2.razor:

<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object>? AdditionalAttributes { get; set; }
}
<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}
<div extra="5" @attributes="AdditionalAttributes" />

@code {
    [Parameter(CaptureUnmatchedValues = true)]
    public IDictionary<string, object> AdditionalAttributes { get; set; }
}

AttributeOrder2.razor:

@page "/attribute-order-2"

<PageTitle>Attribute Order 2</PageTitle>

<h1>Attribute Order Example 2</h1>

<AttributeOrderChild2 extra="10" />

<p>
    View the HTML markup in your browser to inspect the attributes on
    the AttributeOrderChild2 component.
</p>

AttributeOrderParent2.razor:

@page "/attribute-order-parent-2"

<AttributeOrderChild2 extra="10" />

AttributeOrderParent2.razor:

@page "/attribute-order-parent-2"

<AttributeOrderChild2 extra="10" />

AttributeOrderParent2.razor:

@page "/attribute-order-parent-2"

<AttributeOrderChild2 extra="10" />

AttributeOrderParent2.razor:

@page "/attribute-order-parent-2"

<AttributeOrderChild2 extra="10" />

The <div> in the parent component's rendered webpage contains extra="10" when passed through the additional attribute:

<div extra="10" />