Version: 1.0.0-rc.10 Type: Attribute Binding Plugin Bundle Size: ~2.4KB minified Dependencies: Eleva.js core
The Attr plugin provides intelligent attribute binding for Eleva components, automatically handling ARIA accessibility attributes, data attributes, boolean attributes, and dynamic property detection.
import Eleva from "eleva";
import { AttrPlugin } from "eleva/plugins";
const app = new Eleva("App", document.getElementById("app"));
app.use(AttrPlugin); // Enable attribute binding
| Feature | Syntax | Description |
|---|---|---|
| ARIA Attributes | aria-label="" |
Accessibility attributes |
| Data Attributes | data-id="" |
Custom data storage |
| Boolean Attributes | disabled="" |
Presence-based attributes |
| Dynamic Properties | value="" |
DOM property binding |
| Update Method | app.updateElementAttributes(old, new) |
Manual attribute sync |
| Option | Type | Default | Description |
|---|---|---|---|
enableAria |
boolean |
true |
Enable ARIA attribute handling |
enableData |
boolean |
true |
Enable data-* attribute handling |
enableBoolean |
boolean |
true |
Enable boolean attribute handling |
enableDynamic |
boolean |
true |
Enable dynamic property detection |
# npm
npm install eleva
# yarn
yarn add eleva
# pnpm
pnpm add eleva
# bun
bun add eleva
<!-- Core + All Plugins -->
<script src="https://unpkg.com/eleva/dist/eleva-plugins.umd.min.js"></script>
<!-- Attr Plugin Only -->
<script src="https://unpkg.com/eleva/dist/plugins/attr.umd.min.js"></script>
import Eleva from "eleva";
import { AttrPlugin } from "eleva/plugins";
// Create app instance
const app = new Eleva("MyApp", document.getElementById("app"));
// Install Attr plugin with default options
app.use(AttrPlugin);
// Or with custom configuration
app.use(AttrPlugin, {
enableAria: true, // Handle ARIA attributes
enableData: true, // Handle data-* attributes
enableBoolean: true, // Handle boolean attributes
enableDynamic: true // Handle dynamic properties
});
const AccessibleButton = {
setup({ signal }) {
const isLoading = signal(false);
const buttonLabel = signal("Submit Form");
const handleClick = () => {
isLoading.value = true;
// Simulate async operation
setTimeout(() => {
isLoading.value = false;
}, 2000);
};
return { isLoading, buttonLabel, handleClick };
},
template({ isLoading, buttonLabel }) {
return `
<button
aria-label=""
aria-busy=""
disabled=""
@click="handleClick"
>
</button>
`;
}
};
app.component("accessible-button", AccessibleButton).mount();
ARIA (Accessible Rich Internet Applications) attributes enhance web accessibility for users with disabilities. The Attr plugin automatically handles all aria-* attributes.
const AccessibleComponent = {
setup({ signal }) {
const isExpanded = signal(false);
const isSelected = signal(false);
const currentValue = signal(50);
const errorMessage = signal("");
return { isExpanded, isSelected, currentValue, errorMessage };
},
template({ isExpanded, isSelected, currentValue, errorMessage }) {
return `
<div>
<!-- Expandable Section -->
<button
aria-expanded=""
aria-controls="content-panel"
@click="isExpanded.value = !isExpanded.value"
>
Toggle Content
</button>
<div id="content-panel" aria-hidden="">
Panel content here...
</div>
<!-- Selectable Item -->
<div
role="option"
aria-selected=""
@click="isSelected.value = !isSelected.value"
>
Selectable Item
</div>
<!-- Slider/Progress -->
<div
role="slider"
aria-valuenow=""
aria-valuemin="0"
aria-valuemax="100"
aria-label="Volume"
>
Value: %
</div>
<!-- Form Field with Error -->
<input
type="text"
aria-invalid=""
aria-describedby="error-text"
/>
<span id="error-text" role="alert"></span>
</div>
`;
}
};
| Attribute | Purpose | Example Values |
|---|---|---|
aria-label |
Accessible name | "Submit form" |
aria-labelledby |
Reference to label element | "heading-id" |
aria-describedby |
Reference to description | "description-id" |
aria-expanded |
Expansion state | "true", "false" |
aria-hidden |
Hide from assistive tech | "true", "false" |
aria-selected |
Selection state | "true", "false" |
aria-checked |
Checkbox/radio state | "true", "false", "mixed" |
aria-disabled |
Disabled state | "true", "false" |
aria-busy |
Loading state | "true", "false" |
aria-live |
Live region updates | "polite", "assertive", "off" |
aria-valuenow |
Current value | "50" |
aria-valuemin |
Minimum value | "0" |
aria-valuemax |
Maximum value | "100" |
aria-invalid |
Validation state | "true", "false", "grammar", "spelling" |
Data attributes (data-*) provide a way to store custom data on HTML elements. The Attr plugin automatically synchronizes data attributes with your component state.
const ProductCard = {
setup({ signal }) {
const product = signal({
id: "prod-123",
name: "Premium Widget",
price: 29.99,
category: "electronics",
inStock: true
});
return { product };
},
template({ product }) {
return `
<article
class="product-card"
data-product-id=""
data-category=""
data-price=""
data-in-stock=""
>
<h3></h3>
<p class="price">\$</p>
<span class="stock-status">
</span>
</article>
`;
}
};
// Query by data attribute
const electronics = document.querySelectorAll('[data-category="electronics"]');
// Read data attribute value
const productCard = document.querySelector('.product-card');
const productId = productCard.dataset.productId; // "prod-123"
const price = parseFloat(productCard.dataset.price); // 29.99
// Use in event delegation
document.addEventListener('click', (e) => {
const card = e.target.closest('[data-product-id]');
if (card) {
console.log('Clicked product:', card.dataset.productId);
}
});
const DynamicList = {
setup({ signal }) {
const items = signal([
{ id: 1, status: "active", priority: "high" },
{ id: 2, status: "pending", priority: "medium" },
{ id: 3, status: "completed", priority: "low" }
]);
const updateStatus = (id, newStatus) => {
items.value = items.value.map(item =>
item.id === id ? { ...item, status: newStatus } : item
);
};
return { items, updateStatus };
},
template({ items }) {
return `
<ul class="task-list">
${items.value.map(item => `
<li
data-id="${item.id}"
data-status="${item.status}"
data-priority="${item.priority}"
class="task-item"
>
Task #${item.id} - ${item.status}
</li>
`).join('')}
</ul>
`;
}
};
Boolean attributes are special HTML attributes where the presence of the attribute (regardless of value) means true, and absence means false. The Attr plugin intelligently handles these attributes.
| Attribute | Element(s) | Description |
|---|---|---|
disabled |
button, input, select, textarea |
Disables the element |
checked |
input[type="checkbox"], input[type="radio"] |
Checked state |
selected |
option |
Selected option |
readonly |
input, textarea |
Read-only field |
required |
input, select, textarea |
Required field |
hidden |
Any element | Hides element |
open |
details, dialog |
Open state |
autofocus |
Form elements | Auto-focus on load |
multiple |
input, select |
Allow multiple values |
novalidate |
form |
Skip validation |
const FormControls = {
setup({ signal }) {
const isSubmitting = signal(false);
const agreeToTerms = signal(false);
const selectedPlan = signal("basic");
const showDetails = signal(false);
const emailRequired = signal(true);
return { isSubmitting, agreeToTerms, selectedPlan, showDetails, emailRequired };
},
template({ isSubmitting, agreeToTerms, selectedPlan, showDetails, emailRequired }) {
return `
<form>
<!-- Disabled button during submission -->
<button
type="submit"
disabled=""
>
</button>
<!-- Checkbox with checked binding -->
<label>
<input
type="checkbox"
checked=""
@change="agreeToTerms.value = $event.target.checked"
/>
I agree to the terms
</label>
<!-- Select with selected option -->
<select @change="selectedPlan.value = $event.target.value">
<option value="basic" selected="">
Basic Plan
</option>
<option value="pro" selected="">
Pro Plan
</option>
<option value="enterprise" selected="">
Enterprise Plan
</option>
</select>
<!-- Details element with open state -->
<details open="">
<summary @click="showDetails.value = !showDetails.value">
More Information
</summary>
<p>Additional details here...</p>
</details>
<!-- Required field -->
<input
type="email"
placeholder="Email"
required=""
/>
</form>
`;
}
};
// When signal value is truthy:
// disabled="true" → <button disabled>
// disabled="1" → <button disabled>
// disabled="yes" → <button disabled>
// When signal value is falsy:
// disabled="false" → <button>
// disabled="0" → <button>
// disabled="" → <button>
// disabled="" → <button>
// disabled="" → <button>
Some DOM elements have properties that don’t correspond directly to attributes. The Attr plugin detects these and handles them appropriately.
| Property | Element(s) | Description |
|---|---|---|
value |
input, textarea, select |
Current value |
checked |
input[type="checkbox/radio"] |
Checked state |
selected |
option |
Selection state |
indeterminate |
input[type="checkbox"] |
Indeterminate state |
innerHTML |
Any element | Inner HTML content |
textContent |
Any element | Text content |
className |
Any element | CSS classes |
src |
img, video, audio, iframe |
Source URL |
href |
a, link |
Link URL |
const DynamicInputs = {
setup({ signal }) {
const textValue = signal("Hello World");
const numberValue = signal(42);
const rangeValue = signal(50);
const imageUrl = signal("https://example.com/image.jpg");
return { textValue, numberValue, rangeValue, imageUrl };
},
template({ textValue, numberValue, rangeValue, imageUrl }) {
return `
<div class="form-controls">
<!-- Text input with value binding -->
<input
type="text"
value=""
@input="textValue.value = $event.target.value"
/>
<p>Text: </p>
<!-- Number input with value binding -->
<input
type="number"
value=""
@input="numberValue.value = parseInt($event.target.value)"
/>
<p>Number: </p>
<!-- Range slider with value binding -->
<input
type="range"
min="0"
max="100"
value=""
@input="rangeValue.value = parseInt($event.target.value)"
/>
<p>Range: %</p>
<!-- Dynamic image source -->
<img
src=""
alt="Dynamic image"
/>
</div>
`;
}
};
const SelectAllComponent = {
setup({ signal }) {
const items = signal([
{ id: 1, name: "Item 1", selected: false },
{ id: 2, name: "Item 2", selected: true },
{ id: 3, name: "Item 3", selected: false }
]);
const allSelected = () => items.value.every(i => i.selected);
const someSelected = () => items.value.some(i => i.selected) && !allSelected();
const toggleAll = () => {
const newState = !allSelected();
items.value = items.value.map(i => ({ ...i, selected: newState }));
};
const toggleItem = (id) => {
items.value = items.value.map(i =>
i.id === id ? { ...i, selected: !i.selected } : i
);
};
return { items, allSelected, someSelected, toggleAll, toggleItem };
},
template({ items, allSelected, someSelected }) {
return `
<div class="select-all-container">
<label>
<input
type="checkbox"
checked=""
indeterminate=""
@change="toggleAll"
/>
Select All
</label>
<ul>
${items.value.map(item => `
<li>
<label>
<input
type="checkbox"
checked="${item.selected}"
@change="toggleItem(${item.id})"
/>
${item.name}
</label>
</li>
`).join('')}
</ul>
</div>
`;
}
};
app.use(AttrPlugin, {
enableAria: true, // Enable ARIA attribute handling
enableData: true, // Enable data-* attribute handling
enableBoolean: true, // Enable boolean attribute handling
enableDynamic: true // Enable dynamic property detection
});
// Only ARIA attributes (accessibility-focused)
app.use(AttrPlugin, {
enableAria: true,
enableData: false,
enableBoolean: false,
enableDynamic: false
});
// Only data attributes (data storage)
app.use(AttrPlugin, {
enableAria: false,
enableData: true,
enableBoolean: false,
enableDynamic: false
});
// Form handling (boolean + dynamic)
app.use(AttrPlugin, {
enableAria: false,
enableData: false,
enableBoolean: true,
enableDynamic: true
});
The main plugin object to install on your Eleva application.
import { AttrPlugin } from "eleva/plugins";
app.use(AttrPlugin, 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 source element
* @param {HTMLElement} newElement - The target element to update
* @returns {void}
*/
app.updateElementAttributes(oldElement, newElement);
const MyComponent = {
setup({ signal }) {
const app = this; // Reference to app instance
const updateAttributes = () => {
const oldEl = document.getElementById('source');
const newEl = document.getElementById('target');
app.updateElementAttributes(oldEl, newEl);
};
return { updateAttributes };
},
template() {
return `
<div id="source" data-value="123" aria-label="Source">Source</div>
<div id="target">Target</div>
<button @click="updateAttributes">Sync Attributes</button>
`;
}
};
const AccessibleForm = {
setup({ signal }) {
const formData = signal({
name: "",
email: "",
message: ""
});
const errors = signal({});
const isSubmitting = signal(false);
const submitSuccess = signal(false);
const validate = () => {
const newErrors = {};
if (!formData.value.name.trim()) {
newErrors.name = "Name is required";
}
if (!formData.value.email.includes("@")) {
newErrors.email = "Valid email is required";
}
if (formData.value.message.length < 10) {
newErrors.message = "Message must be at least 10 characters";
}
errors.value = newErrors;
return Object.keys(newErrors).length === 0;
};
const handleSubmit = async (e) => {
e.preventDefault();
if (!validate()) return;
isSubmitting.value = true;
try {
// Simulate API call
await new Promise(resolve => setTimeout(resolve, 1500));
submitSuccess.value = true;
} catch (error) {
errors.value = { submit: "Failed to submit form" };
} finally {
isSubmitting.value = false;
}
};
const updateField = (field, value) => {
formData.value = { ...formData.value, [field]: value };
};
return { formData, errors, isSubmitting, submitSuccess, handleSubmit, updateField };
},
template({ formData, errors, isSubmitting, submitSuccess }) {
return `
<form @submit="handleSubmit" aria-label="Contact Form">
<!-- Success Message -->
<div
role="alert"
aria-live="polite"
hidden=""
>
Form submitted successfully!
</div>
<!-- Name Field -->
<div class="form-group">
<label id="name-label" for="name">Name</label>
<input
type="text"
id="name"
name="name"
value=""
@input="updateField('name', $event.target.value)"
aria-labelledby="name-label"
aria-describedby="name-error"
aria-invalid=""
aria-required="true"
required="true"
/>
<span
id="name-error"
role="alert"
class="error"
hidden=""
>
</span>
</div>
<!-- Email Field -->
<div class="form-group">
<label id="email-label" for="email">Email</label>
<input
type="email"
id="email"
name="email"
value=""
@input="updateField('email', $event.target.value)"
aria-labelledby="email-label"
aria-describedby="email-error"
aria-invalid=""
aria-required="true"
required="true"
/>
<span
id="email-error"
role="alert"
class="error"
hidden=""
>
</span>
</div>
<!-- Message Field -->
<div class="form-group">
<label id="message-label" for="message">Message</label>
<textarea
id="message"
name="message"
value=""
@input="updateField('message', $event.target.value)"
aria-labelledby="message-label"
aria-describedby="message-error message-hint"
aria-invalid=""
aria-required="true"
required="true"
></textarea>
<span id="message-hint" class="hint">
Minimum 10 characters
</span>
<span
id="message-error"
role="alert"
class="error"
hidden=""
>
</span>
</div>
<!-- Submit Button -->
<button
type="submit"
disabled=""
aria-busy=""
>
</button>
</form>
`;
}
};
const Accordion = {
setup({ signal }) {
const sections = signal([
{ id: "section1", title: "What is Eleva?", content: "Eleva is a minimalist JavaScript framework...", open: true },
{ id: "section2", title: "How do I get started?", content: "Install via npm and import...", open: false },
{ id: "section3", title: "Is it production ready?", content: "Yes, Eleva is stable and tested...", open: false }
]);
const toggleSection = (id) => {
sections.value = sections.value.map(section => ({
...section,
open: section.id === id ? !section.open : section.open
}));
};
return { sections, toggleSection };
},
template({ sections }) {
return `
<div class="accordion" role="tablist" aria-label="FAQ Accordion">
${sections.value.map((section) => `
<div class="accordion-item">
<h3>
<button
type="button"
class="accordion-trigger"
id="trigger-${section.id}"
aria-expanded="${section.open}"
aria-controls="panel-${section.id}"
@click="toggleSection('${section.id}')"
>
${section.title}
<span class="icon" aria-hidden="true">
${section.open ? '−' : '+'}
</span>
</button>
</h3>
<div
id="panel-${section.id}"
role="region"
aria-labelledby="trigger-${section.id}"
aria-hidden="${!section.open}"
hidden="${!section.open}"
class="accordion-panel"
>
<p>${section.content}</p>
</div>
</div>
`).join('')}
</div>
`;
}
};
const TabContainer = {
setup({ signal }) {
const activeTab = signal(0);
const tabs = signal([
{ id: "tab-1", label: "Overview", content: "Overview content goes here..." },
{ id: "tab-2", label: "Features", content: "Features content goes here..." },
{ id: "tab-3", label: "Pricing", content: "Pricing content goes here..." }
]);
const selectTab = (index) => {
activeTab.value = index;
};
const handleKeyDown = (e, index) => {
const tabCount = tabs.value.length;
switch (e.key) {
case 'ArrowLeft':
activeTab.value = (index - 1 + tabCount) % tabCount;
break;
case 'ArrowRight':
activeTab.value = (index + 1) % tabCount;
break;
case 'Home':
activeTab.value = 0;
break;
case 'End':
activeTab.value = tabCount - 1;
break;
}
};
return { activeTab, tabs, selectTab, handleKeyDown };
},
template({ activeTab, tabs }) {
return `
<div class="tab-container">
<!-- Tab List -->
<div role="tablist" aria-label="Content Tabs" class="tab-list">
${tabs.value.map((tab, index) => `
<button
role="tab"
id="${tab.id}"
aria-selected="${activeTab.value === index}"
aria-controls="panel-${tab.id}"
tabindex="${activeTab.value === index ? 0 : -1}"
class="tab-button ${activeTab.value === index ? 'active' : ''}"
@click="selectTab(${index})"
@keydown="handleKeyDown($event, ${index})"
>
${tab.label}
</button>
`).join('')}
</div>
<!-- Tab Panels -->
${tabs.value.map((tab, index) => `
<div
role="tabpanel"
id="panel-${tab.id}"
aria-labelledby="${tab.id}"
hidden="${activeTab.value !== index}"
tabindex="0"
class="tab-panel"
>
${tab.content}
</div>
`).join('')}
</div>
`;
}
};
const DataTable = {
setup({ signal }) {
const sortColumn = signal("name");
const sortDirection = signal("asc");
const selectedRows = signal(new Set());
const data = signal([
{ id: 1, name: "Alice", email: "alice@example.com", role: "Admin" },
{ id: 2, name: "Bob", email: "bob@example.com", role: "User" },
{ id: 3, name: "Charlie", email: "charlie@example.com", role: "User" },
{ id: 4, name: "Diana", email: "diana@example.com", role: "Editor" }
]);
const sortedData = () => {
const sorted = [...data.value].sort((a, b) => {
const aVal = a[sortColumn.value];
const bVal = b[sortColumn.value];
const direction = sortDirection.value === "asc" ? 1 : -1;
return aVal.localeCompare(bVal) * direction;
});
return sorted;
};
const toggleSort = (column) => {
if (sortColumn.value === column) {
sortDirection.value = sortDirection.value === "asc" ? "desc" : "asc";
} else {
sortColumn.value = column;
sortDirection.value = "asc";
}
};
const toggleRowSelection = (id) => {
const newSelection = new Set(selectedRows.value);
if (newSelection.has(id)) {
newSelection.delete(id);
} else {
newSelection.add(id);
}
selectedRows.value = newSelection;
};
const isSelected = (id) => selectedRows.value.has(id);
return { sortColumn, sortDirection, sortedData, toggleSort, toggleRowSelection, isSelected };
},
template({ sortColumn, sortDirection, sortedData, isSelected }) {
const columns = [
{ key: "name", label: "Name" },
{ key: "email", label: "Email" },
{ key: "role", label: "Role" }
];
return `
<table class="data-table" role="grid" aria-label="User Data">
<thead>
<tr>
<th scope="col">
<span class="visually-hidden">Select</span>
</th>
${columns.map(col => `
<th
scope="col"
aria-sort="${sortColumn.value === col.key
? (sortDirection.value === 'asc' ? 'ascending' : 'descending')
: 'none'}"
data-column="${col.key}"
>
<button @click="toggleSort('${col.key}')">
${col.label}
<span aria-hidden="true">
${sortColumn.value === col.key
? (sortDirection.value === 'asc' ? '▲' : '▼')
: '⇅'}
</span>
</button>
</th>
`).join('')}
</tr>
</thead>
<tbody>
${sortedData().map(row => `
<tr
data-row-id="${row.id}"
aria-selected="${isSelected(row.id)}"
class="${isSelected(row.id) ? 'selected' : ''}"
>
<td>
<input
type="checkbox"
checked="${isSelected(row.id)}"
@change="toggleRowSelection(${row.id})"
aria-label="Select ${row.name}"
/>
</td>
<td data-label="Name">${row.name}</td>
<td data-label="Email">${row.email}</td>
<td data-label="Role">${row.role}</td>
</tr>
`).join('')}
</tbody>
</table>
`;
}
};
const ModalDialog = {
setup({ signal }) {
const isOpen = signal(false);
const modalTitle = signal("Confirm Action");
const modalMessage = signal("Are you sure you want to proceed?");
const openModal = () => {
isOpen.value = true;
// Focus trap would be implemented here
};
const closeModal = () => {
isOpen.value = false;
};
const handleConfirm = () => {
console.log("Confirmed!");
closeModal();
};
const handleKeyDown = (e) => {
if (e.key === "Escape") {
closeModal();
}
};
return { isOpen, modalTitle, modalMessage, openModal, closeModal, handleConfirm, handleKeyDown };
},
template({ isOpen, modalTitle, modalMessage }) {
return `
<div>
<!-- Trigger Button -->
<button
@click="openModal"
aria-haspopup="dialog"
>
Open Modal
</button>
<!-- Modal Backdrop -->
<div
class="modal-backdrop"
hidden=""
aria-hidden=""
@click="closeModal"
></div>
<!-- Modal Dialog -->
<div
role="dialog"
aria-modal="true"
aria-labelledby="modal-title"
aria-describedby="modal-description"
aria-hidden=""
hidden=""
class="modal"
@keydown="handleKeyDown"
>
<header class="modal-header">
<h2 id="modal-title"></h2>
<button
@click="closeModal"
aria-label="Close modal"
class="modal-close"
>
×
</button>
</header>
<div id="modal-description" class="modal-body">
<p></p>
</div>
<footer class="modal-footer">
<button
@click="closeModal"
class="btn-secondary"
>
Cancel
</button>
<button
@click="handleConfirm"
class="btn-primary"
autofocus=""
>
Confirm
</button>
</footer>
</div>
</div>
`;
}
};
Always include appropriate ARIA attributes for interactive elements:
// Good - Accessible button
`<button
aria-label="Close navigation menu"
aria-expanded=""
@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=""
data-user-role=""
data-is-active=""
>`
// Bad - Inconsistent, unclear naming
`<div
data-id=""
data-r=""
data-a=""
>`
Be explicit about boolean attribute conditions:
// Good - Clear condition
`<button disabled="">
Submit
</button>`
// Good - Computed property
const canSubmit = () => !isLoading.value && isValid.value;
`<button disabled="">Submit</button>`
// Avoid - Complex inline logic
`<button disabled="">
Submit
</button>`
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 stays present regardless of value.
// Wrong - String "false" is truthy
`<button disabled="false">` // Still disabled!
// Correct - Use template binding
`<button disabled="">`
Solution: Always use template binding `` for dynamic boolean attributes.
Problem: ARIA attributes don’t reflect state changes.
// Check that you're using .value for signals
`aria-expanded=""` // Wrong - missing .value
`aria-expanded=""` // Correct
Solution: Ensure you’re accessing the .value property of signals.
Problem: Data attribute values contain quotes or special characters.
// Problem
`data-message=""` // message contains quotes
// Solution - Encode special characters
const safeMessage = () => encodeURIComponent(message.value);
`data-message=""`
Problem: Input value doesn’t update when signal changes.
// Ensure two-way binding
`<input
value=""
@input="inputValue.value = $event.target.value"
/>`
Solution: Implement both value binding and input event handler for two-way data flow.
const app = new Eleva("App", container);
app.use(AttrPlugin); // Must be before mount()
app.component("my-component", MyComponent).mount();
import { AttrPlugin } from "eleva/plugins";
// or
const { AttrPlugin } = window.ElevaPlugins;
// Log attribute updates
const DebugComponent = {
setup({ signal }) {
const value = signal("test");
// Watch for changes
value.watch((newVal, oldVal) => {
console.log(`Value changed: ${oldVal} → ${newVal}`);
});
return { value };
},
template({ value }) {
return `<div data-debug=""></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'));
┌─────────────────────────────────────────────────────────────────┐
│ Attr Plugin │
├─────────────────────────────────────────────────────────────────┤
│ │
│ Signal Change │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Attribute Detection │ │
│ └─────────────────────────────────────────────────────┘ │
│ │ │
│ ├─── aria-* ──► ARIA Handler ──► setAttribute() │
│ │ │
│ ├─── data-* ──► Data Handler ──► dataset[key] │
│ │ │
│ ├─── boolean ─► Boolean Handler ─► add/removeAttribute() │
│ │ (disabled, checked, etc.) │
│ │ │
│ └─── dynamic ─► Property Handler ──► element[property] │
│ (value, src, etc.) │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ DOM Updated │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
| 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 |