Attr Plugin Complete API reference.
The main plugin object to install on your Eleva application.
import { Attr } from "eleva/plugins";
app.use(Attr, options);
| Property | Type | Default | Description |
|---|---|---|---|
enableAria |
boolean |
true |
When enabled, automatically handles ARIA attributes (aria-*) |
enableData |
boolean |
true |
When enabled, automatically handles data attributes (data-*) |
enableBoolean |
boolean |
true |
When enabled, intelligently handles boolean attributes based on truthy/falsy values |
enableDynamic |
boolean |
true |
When enabled, detects and binds dynamic DOM properties |
Manually synchronize attributes from one element to another. This method is exposed on the app instance when the Attr plugin is installed.
/**
* Update element attributes
* @param {HTMLElement} oldElement - The element to update (modified in-place)
* @param {HTMLElement} newElement - The reference element with desired attributes
* @returns {void}
*/
app.updateElementAttributes(oldElement, newElement);
// Store reference to app for use in components
const app = new Eleva("MyApp");
app.use(Attr);
// Option 1: Use outside of component setup
const syncElements = () => {
const oldEl = document.getElementById('source');
const newEl = document.getElementById('target');
app.updateElementAttributes(oldEl, newEl);
};
// Option 2: Pass app reference via closure
app.component("MyComponent", {
setup({ signal }) {
const updateAttributes = () => {
const oldEl = document.getElementById('source');
const newEl = document.getElementById('target');
// app is available via closure (defined in outer scope)
app.updateElementAttributes(oldEl, newEl);
};
return { updateAttributes };
},
template: () => `
<div id="source" data-value="123" aria-label="Source">Source</div>
<div id="target">Target</div>
<button @click="updateAttributes">Sync Attributes</button>
`
});
Note: The
setupfunction receivesctx(context) as its argument, notthis. To access the app instance inside setup, use a closure reference toappdefined in the outer scope.
The Attr plugin provides an uninstall() method to completely remove it from an Eleva instance.
Removes the Attr plugin and restores the original renderer behavior.
import Eleva from "eleva";
import { Attr } from "eleva/plugins";
const app = new Eleva("MyApp");
app.use(Attr, {
enableAria: true,
enableData: true,
enableBoolean: true,
enableDynamic: true
});
// Use the plugin...
app.mount(document.getElementById("app"), "MyComponent");
// Later, to completely remove the Attr plugin:
Attr.uninstall(app);
// After uninstall:
// - app.updateElementAttributes (undefined)
// - Original renderer._patchNode() is restored
// - Plugin removed from registry
Attr.uninstall() Doesapp.renderer._patchNode → restored to originalapp.updateElementAttributesapp.plugins.delete("attr")When using multiple plugins, uninstall in reverse order of installation:
// Installation order
app.use(Attr);
app.use(Store, { state: {} });
app.use(Router, { routes: [] });
// Uninstall in reverse order (LIFO)
await Router.uninstall(app); // Last installed, first uninstalled
Store.uninstall(app);
Attr.uninstall(app);
Note: Attr’s
uninstall()is synchronous (not async), unlike Router’s which is async.
Always include appropriate ARIA attributes for interactive elements:
// Good - Accessible button
`<button
aria-label="Close navigation menu"
aria-expanded="${ctx.isMenuOpen.value}"
@click="toggleMenu"
>
<span aria-hidden="true">×</span>
</button>`
// Bad - No accessibility information
`<button @click="toggleMenu">×</button>`
Let HTML do the work before reaching for ARIA:
// Good - Semantic HTML
`<nav aria-label="Main navigation">
<ul>
<li><a href="/home">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>`
// Avoid - ARIA overuse
`<div role="navigation" aria-label="Main navigation">
<div role="list">
<div role="listitem"><span role="link">Home</span></div>
</div>
</div>`
Use consistent, descriptive data attribute names:
// Good - Clear, consistent naming
`<div
data-user-id="${ctx.user.id}"
data-user-role="${ctx.user.role}"
data-is-active="${ctx.user.isActive}"
>`
// Bad - Inconsistent, unclear naming
`<div
data-id="${ctx.user.id}"
data-r="${ctx.user.role}"
data-a="${ctx.user.isActive}"
>`
Be explicit about boolean attribute conditions and use actual boolean values:
// Good - Boolean expression produces "true" or "false" string
`<button disabled="${ctx.isLoading.value || !ctx.isValid.value}">
Submit
</button>`
// Good - Computed property returning boolean
const canSubmit = () => !isLoading.value && isValid.value;
`<button disabled="${!ctx.canSubmit()}">Submit</button>`
// Avoid - Complex inline logic
`<button disabled="${!(ctx.data.value && ctx.data.value.name && !ctx.errors.value.name)}">
Submit
</button>`
// Important: Only these string values are recognized as truthy:
// - "true"
// - "" (empty string)
// - attribute name (e.g., disabled="disabled")
Minimize attribute updates for better performance:
// Good - Batch related state
const formState = signal({
isSubmitting: false,
isValid: true,
errorMessage: ""
});
// Avoid - Many separate signals for related state
const isSubmitting = signal(false);
const isValid = signal(true);
const errorMessage = signal("");
Problem: Boolean attribute doesn’t behave as expected.
// These work correctly with Attr plugin:
`<button disabled="${ctx.isDisabled.value}">` // true/false signals work
`<button disabled="true">` // attribute present
`<button disabled="false">` // attribute removed
`<button disabled="">` // attribute present (empty = true)
// These do NOT work as you might expect:
`<button disabled="1">` // attribute REMOVED (not recognized as truthy)
`<button disabled="yes">` // attribute REMOVED (not recognized as truthy)
Solution: The Attr plugin only recognizes "true", "" (empty), or matching attribute name (e.g., disabled="disabled") as truthy values. Use boolean signals that produce true/false strings.
Problem: ARIA attributes don’t reflect state changes.
// Check that you're using .value for signals
`aria-expanded="${ctx.isOpen}"` // Wrong - missing .value
`aria-expanded="${ctx.isOpen.value}"` // Correct
Solution: Ensure you’re accessing the .value property of signals.
Problem: Data attribute values contain quotes or special characters.
// Problem
`data-message="${ctx.message.value}"` // message contains quotes
// Solution - Encode special characters
const safeMessage = () => encodeURIComponent(message.value);
`data-message="${ctx.safeMessage()}"`
Problem: Input value doesn’t update when signal changes.
// Ensure two-way binding
`<input
value="${ctx.inputValue.value}"
@input="(e) => inputValue.value = e.target.value"
/>`
Solution: Implement both value binding and input event handler for two-way data flow.
const app = new Eleva("App");
app.use(Attr); // Must be before mount()
app.component("MyComponent", { /* ... */ });
app.mount(document.getElementById("app"), "MyComponent");
import { Attr } from "eleva/plugins";
// or
const { Attr } = window.ElevaPlugins;
// Log attribute updates
const DebugComponent = {
setup({ signal }) {
const value = signal("test");
// Watch for changes (receives new value only)
value.watch((newVal) => {
console.log(`Value changed to: ${newVal}`);
});
return { value };
},
template({ value }) {
return `<div data-debug="${value.value}">${value.value}</div>`;
}
};
// Inspect element attributes
const el = document.querySelector('[data-debug]');
console.log('Attributes:', el.attributes);
console.log('Dataset:', el.dataset);
console.log('ARIA:', el.getAttribute('aria-label'));
Eleva uses render batching via queueMicrotask to optimize performance. This means attribute updates happen asynchronously after signal changes.
When signals change, attribute updates are batched with template re-renders:
// Both changes result in ONE DOM update
isDisabled.value = true;
ariaLabel.value = "Loading...";
// Attributes update together in next microtask
After changing a signal, attributes won’t reflect the change immediately:
isExpanded.value = true;
console.log(element.getAttribute('aria-expanded')); // Still "false"!
// Wait for batched update
isExpanded.value = true;
queueMicrotask(() => {
console.log(element.getAttribute('aria-expanded')); // Now "true"
});
When testing attribute bindings, allow time for batched updates:
test("button becomes disabled", async () => {
isLoading.value = true;
// Wait for batched update
await new Promise(resolve => queueMicrotask(resolve));
expect(button.disabled).toBe(true);
expect(button.getAttribute('aria-busy')).toBe('true');
});
Boolean attribute toggling follows the same batching rules:
// These are batched together
disabled.value = true;
hidden.value = false;
checked.value = true;
// All three attributes update in one DOM operation
| Feature | Description |
|---|---|
| ARIA Handling | Automatic accessibility attribute management |
| Data Attributes | Custom data storage on elements |
| Boolean Attributes | Intelligent truthy/falsy handling |
| Dynamic Properties | DOM property synchronization |
| Zero Config | Works out of the box with sensible defaults |
| Selective Enable | Configure which features to enable |
| Manual API | updateElementAttributes() for advanced use cases |
For questions or issues, visit the GitHub repository.
| ← Back to Usage Patterns | Back to Attr Overview | Router Plugin → |