Capturing unmatched attributes and attribute splatting in Blazor

 
 
  • Gérald Barré

In the previous posts on Blazor, we have seen how to create a component and define parameters. For instance, you can check the posts about the Modal component, the Repeater component, or the Enum Select component.

Sometimes, you want to allow the user to add any arbitrary attributes to the component. For instance, you may want the user to add validation attributes (required, maxlength, etc.) to an input element. Or maybe you want to define the id attribute. Blazor allows to capture additional attributes in a dictionary and then splatted onto an element when the component is rendered using the @attributes Razor directive.

You can add one parameter with the attribute [Parameter(CaptureUnmatchedValues = true)] with a type assignable from Dictionary<string, object>. This includes Dictionary<string, object>, IDictionary<string, object>, IReadOnlyDictionary<string, object>.

Then, you can apply all attributes to an element by using @attributes="Parameter". This will create one attribute per entry of the dictionary (KeyValuePair<string, object>). It uses the Key property to get the attribute name and the Value property to create the attribute value.

Let's create the component:

Razor
@* 👇 @attributes will be expanded with all unmatched parameters *@
<input @attributes="AdditionalAttributes" list="@listId" />
<datalist id="@listId">
    @foreach (var option in Options)
    {
        <option>@option</option>
    }
</datalist>

@code {
    private string listId = Guid.NewGuid().ToString();

    [Parameter]
    public IEnumerable<string> Options { get; set; }

    // 👇 Capture all attributes that doesn't match a parameter
    // Key (string): attribute name
    // Value (object): value of the attribute
    [Parameter(CaptureUnmatchedValues = true)]
    public IReadOnlyDictionary<string, object> AdditionalAttributes { get; set; }
}

You can use this component as follows:

Razor
@* 👇 required and minLength don't match a parameter, so they'll be added to AdditionalAttributes *@
<SelectComponent required minLength="2" Options='new[] { "aa", "bb", "cc" }' />

Here's the generated DOM:

Here's the visual result:

#The position of @attributes is important

The position of @attributes may impact the generated DOM. Attributes are evaluated from left to right. If 2 attributes have the same name, the last one wins and overwrite the previous one. Let's consider these 2 examples:

Razor
@* 👇 "list" is before @attributes *@
<input list="@listId" @attributes="AdditionalAttributes" />

@* 👇 @attributes is before "list" *@
<input @attributes="AdditionalAttributes" list="@listId" />

Let's use it with the following code:

Razor
@* Set the "list" attribute *@
<SelectComponent list="abc" Options='new[] { "aa", "bb", "cc" }' />

Here's the generated code for both cases:

HTML
<!-- 👉 <input list="@listId" @attributes="AdditionalAttributes" /> -->
<!-- The Guid is overrided by the value provided by the parent -->
<input list="abc">

<!-- 👉 <input @attributes="AdditionalAttributes" list="@listId" /> -->
<!-- The value provided by the parent is not used -->
<input list="52880b4c-02c9-4ddd-8752-34dd35b16ba4">

So, if you want the parent to be able to overwrite an attribute defined by the component, place @attributes on the right.

Razor
@* The "list" attribute **can be replaced** if AdditionalAttributes contains an attribute named "list" *@
<input list="@listId" @attributes="AdditionalAttributes" />

@* The "list" attribute **cannot be replaced** if AdditionalAttributes contains an attribute named "list" *@
<input @attributes="AdditionalAttributes" list="@listId" />

Do you have a question or a suggestion about this post? Contact me!

Follow me:
Enjoy this blog?Buy Me A Coffee💖 Sponsor on GitHub