@attr
The @attr directive binds one or more HTML attributes to reactive template expressions.
Use it when an attribute value depends on component state. For fixed values, use ordinary HTML attributes.
@attr works at the attribute level: values are written with setAttribute() and removed with removeAttribute(). It does not assign DOM properties such as element.disabled or element.value.
Basic Usage
<a @attr="href=url title=tooltip">
Documentation
</a>Each binding has the form:
attribute=expressionMultiple bindings are separated by whitespace:
<a @attr="href=url title=tooltip target=target rel=rel">
Open
</a>The expression is evaluated against the component context.
state() {
return {
url: "/docs",
tooltip: "Documentation",
target: "_blank",
rel: "noopener",
};
}Each binding is evaluated as part of the same reactive effect.
Example
import { createComponent, html, render } from "udodi";
const DocLink = createComponent({
name: "DocLink",
state() {
return {
href: "https://example.com",
label: "Open docs",
};
},
template: () => html`
<a
@attr="href=href title=label"
@text="label"
></a>
`,
});
render(DocLink(), "#app");When href or label changes, the corresponding HTML attribute is updated automatically.
Binding Values
Each binding produces a value for its target attribute.
| Result | Behavior |
|---|---|
null | Contributes no attribute |
undefined | Contributes no attribute |
"" | Contributes no attribute |
| Other values | Converted with String(value) and applied |
For example:
<img @attr="src=imageUrl alt=imageAlt" />
<button @attr="aria-label=label">
Save
</button>state() {
return {
imageUrl: "/hero.png",
imageAlt: "Hero",
label: "Save",
};
}A numeric value is stringified:
state() {
return {
tabIndex: 2,
};
}<div @attr="tabindex=tabIndex"></div>The resulting attribute is:
<div tabindex="2"></div>Boolean Attributes
@attr uses setAttribute() and therefore does not interpret JavaScript booleans as HTML boolean-attribute presence.
For example:
<button @attr="disabled=isDisabled">
Save
</button>If isDisabled is false, the value is stringified to "false":
<button disabled="false">
Save
</button>The disabled attribute is therefore still present and the button remains disabled.
For boolean attributes that should only be present when a condition is truthy, use the Template DSL conditional form:
<<button @attr="disabled=isDisabled=>'disabled'">
Save
</button>When isDisabled is truthy:
<button disabled="disabled">
Save
</button>When it is falsy, the dynamic disabled attribute is removed.
See Template DSL for supported conditional expressions.
Static Attribute Values
A standalone quoted string is not a valid @attr directive:
<!-- Invalid -->
<div @attr="'title=Hello'"></div>@attr expects one or more attribute=expression bindings.
A literal attribute value can be quoted on the right-hand side:
<div @attr="title='Hello'"></div>Or use component state:
<div @attr="title=title"></div>state() {
return {
title: "Hello",
};
}Base Attributes
Attributes already present on the element before @attr is processed are treated as base attributes.
The directive ignores other @... attributes when capturing this base set.
<a
href="/fallback"
title="Default"
@attr="href=url title=tooltip"
>
Link
</a>If url or tooltip produces a value, @attr temporarily owns that attribute.
If a binding later produces null, undefined, or "":
- the original base value is restored when one exists;
- otherwise the attribute is removed.
For example, if url becomes null, the element returns to:
<a href="/fallback" title="Default">
Link
</a>This allows reactive attributes to temporarily override normal HTML attributes without permanently destroying their original values.
Dynamic Ownership
@attr tracks the attributes produced by the directive separately from the element's original attributes.
Conceptually:
HTML attributes
│
├── base attributes
│
└── @attr managed attributes
│
▼
reactive evaluation
│
▼
attribute diffOnly attributes previously produced by @attr are considered for removal or restoration during subsequent evaluations.
This prevents unrelated attributes from being modified by the directive.
Diffing
On every reactive evaluation, Udodi builds the next set of dynamic attributes and compares it with the previous set.
The process is:
- Evaluate all
attribute=expressionbindings. - Ignore
null,undefined, and empty-string results. - Convert remaining values to strings.
- Merge bindings into the next attribute set.
- Restore or remove attributes that are no longer produced.
- Set attributes whose values changed.
- Leave unchanged attributes untouched.
Conceptually:
previous attributes
│
▼
evaluate bindings
│
▼
string values
│
▼
compare with previous
┌──────────┴──────────┐
▼ ▼
restore / remove set changedThis avoids unnecessary setAttribute() and removeAttribute() calls.
Multiple Bindings
Multiple attributes can be declared in one @attr directive:
<a
@attr="
href=url
title=tooltip
target=target
rel=rel
"
>
Open
</a>All bindings are compiled and evaluated together.
If the same attribute name appears more than once, the later binding wins:
<div @attr="title=firstTitle title=secondTitle"></div>The resulting title comes from secondTitle.
The merged result contains each target attribute only once.
Reactive Attributes
Because dynamic @attr bindings run inside a reactive effect, dependencies are tracked automatically.
<a @attr="href=url title=label">
Link
</a>If url changes, only href needs to be updated. If label changes, only title needs to be updated.
Unchanged attributes are left untouched.
This makes @attr suitable for state-dependent values such as:
hrefsrcalttitletargetrelaria-*data-*- custom attributes
@attr vs Other Directives
| Need | Prefer |
|---|---|
| Arbitrary HTML attributes | @attr |
| CSS class membership | @class |
| Inline CSS properties | @style |
| Form control values | @bind |
| Element references | @ref |
For example:
<input
@bind="email"
@attr="aria-label=label"
@class="invalid=>'is-invalid'"
@style="inputStyle"
/>Each directive owns a different part of the element's behavior.
Attributes vs DOM Properties
@attr always manipulates HTML attributes:
element.setAttribute(name, String(value));and:
element.removeAttribute(name);It does not perform property assignment such as:
element[name] = value;This distinction matters for attributes that have reflected DOM properties or boolean semantics.
For example:
<input @attr="value=value">sets the HTML value attribute. It is not equivalent to:
input.value = value;For two-way form state, use @bind.
Example
import { createComponent, html, render } from "udodi";
const Avatar = createComponent({
name: "Avatar",
state() {
return {
src: "/avatar.png",
alt: "User avatar",
profileUrl: "/profile",
};
},
template: html`
<a @attr="href=profileUrl">
<img @attr="src=src alt=alt" />
</a>
`,
});
render(Avatar(), "#app");The attributes remain synchronized with the component state:
profileUrl ──► href
src ──► src
alt ──► altChanging any of these state values updates only its corresponding attribute.
Behavior
@attr:
- Accepts one or more
attribute=expressionbindings. - Evaluates all bindings reactively.
- Accepts strings, numbers, booleans, and other values that can be stringified.
- Ignores
null,undefined, and empty-string results. - Converts other results with
String(value). - Merges duplicate attribute targets with the later binding taking precedence.
- Records existing non-directive attributes as base attributes.
- Restores base attributes when dynamic ownership is removed.
- Removes dynamic attributes that have no base value.
- Diffs against the previous dynamic attribute set.
- Avoids rewriting unchanged attributes.
- Uses
setAttribute()andremoveAttribute()rather than DOM property assignment. - Removes the
@attrattribute after setup. - Disposes its reactive effect with the component scope.
Syntax Summary
| Form | Behavior |
|---|---|
@attr="href=url" | Bind href to url |
@attr="href=url title=label" | Bind multiple attributes |
@attr="title='Hello'" | Bind a literal string |
null / undefined / "" | Drop the dynamic attribute or restore its base value |
| Other values | Convert with String(value) |
| Duplicate target | Later binding wins |
Constraints
| Rule | Detail |
|---|---|
| Binding form | attribute=expression |
| Standalone literal | Not valid as the entire directive value |
| Stringification | Non-empty results are converted with String(value) |
| Boolean values | false becomes "false"; it does not remove the attribute |
| Base attributes | Restored when dynamic ownership ends |
| DOM properties | Not assigned; use the appropriate property-oriented directive |
| Diffing | Only changed dynamic attributes are written |
| Runtime attribute | @attr is removed after binding |
Minimal Example
import { createComponent, html, render } from "udodi";
const Avatar = createComponent({
name: "Avatar",
state() {
return {
src: "/avatar.png",
alt: "User",
};
},
template: html`
<img @attr="src=src alt=alt" />
`,
});
render(Avatar(), "#app");The src and alt attributes are kept synchronized with component state.