Skip to content

StaticAttributeCollection

Sealed Implements: IReadOnlyList<TagHelperAttribute>

StaticAttributeCollection holds the HTML attributes a consumer placed on a headless input component that were not bound to one of its C# properties. Every component inheriting StaticInputBase exposes one as AdditionalAttributes.

You don’t construct it yourself — it’s populated for you during rendering. In the component’s Razor template you pass it to an element via the static-attributes tag helper.

TechGems.StaticComponents.Headless
@model PinesInput
<input static-for="@Model.InputExpression"
static-attributes="@Model.AdditionalAttributes"
class="block w-full rounded-md border px-3 py-2" />
public int Count { get; }

The number of attributes in the collection.

public TagHelperAttribute this[int index] { get; }

The attribute at the given position. Attributes keep the order the consumer wrote them in.

public TagHelperAttribute? this[string name] { get; }

The attribute with the given name, or null when it isn’t present. Name comparison is case-insensitive.

public static StaticAttributeCollection Empty { get; }

A shared empty collection. Applying it via static-attributes is a no-op.

public bool Contains(string name)

Returns true when an attribute with the given name is present. Case-insensitive.

public StaticAttributeCollection Only(params string[] names)

Returns a new collection containing only the named attributes.

public StaticAttributeCollection Except(params string[] names)

Returns a new collection with the named attributes removed.

public StaticAttributeCollection WithPrefix(params string[] prefixes)

Returns a new collection containing only the attributes whose names start with one of the given prefixes.

public StaticAttributeCollection WithoutPrefix(params string[] prefixes)

Returns a new collection with the prefixed attributes removed.

The collection is immutable and every filter returns a new instance, so filters chain. This is the main reason the filters exist — a component that renders a wrapper around its input often needs some attributes on the wrapper and the rest on the input, because AlpineJS and HTMX attributes act on the element they’re written on.

WithPrefix and WithoutPrefix cover the common case. Alpine and HTMX attributes are identified by their prefix, so sending everything prefixed x- or hx- to the wrapper and everything else to the input is a one-line split:

@model PinesInput
<div class="relative" static-attributes="@Model.AdditionalAttributes.WithPrefix("x-", "hx-")">
<label static-for="@Model.InputExpression" class="block text-sm font-medium"></label>
<input static-for="@Model.InputExpression"
static-attributes="@Model.AdditionalAttributes.WithoutPrefix("x-", "hx-")"
class="block w-full rounded-md border px-3 py-2" />
</div>
@* consumer view *@
<pines-input asp-for="Form.Email" x-show="wantsUpdates" hx-target="#feedback" placeholder="you@example.com" />

x-show and hx-target land on the <div>, placeholder on the <input>.

Only and Except filter by exact name, for when an attribute needs different placement than its prefix suggests. x-model is the usual one — it binds an input’s value, so it has to stay on the <input> even though the other x- attributes belong on the wrapper. Naming the wrapper’s attributes rather than matching a prefix leaves everything else on the input:

<div class="relative" static-attributes="@Model.AdditionalAttributes.Only("x-show", "x-transition")">
<input static-for="@Model.InputExpression"
static-attributes="@Model.AdditionalAttributes.Except("x-show", "x-transition")"
class="block w-full rounded-md border px-3 py-2" />
</div>

type is never captured, because the static-* tag helpers own it — see StaticInputTagHelper. Anything bound to a C# property on the component (asp-for, show-label, disabled, and any property you add yourself) never reaches the collection either, since it was consumed during model binding.