Attribute Binding
Dynamically set HTML attributes based on component state using data-bind-attr. This is particularly useful for accessibility attributes, data attributes for third-party libraries, and any attribute that needs to change reactively.
- Dynamic HTML attributes via
data-bind-attr - Object expression syntax for multiple attributes
- Computed property support
- Automatic attribute removal for null/undefined/false values
- Security: Event handlers and framework attributes are blocked
Basic Usage
Single and Multiple Attributes
Use data-bind-attr with an object expression to bind one or more attributes:
<div data-component="attr-demo">
<!-- Single attribute -->
<div data-bind-attr="{ 'data-status': status }">
Status: <span data-bind="status"></span>
</div>
<!-- Multiple attributes -->
<button data-bind-attr="{
'aria-pressed': isActive,
'aria-label': buttonLabel,
'data-action-type': actionType
}">
Toggle (<span data-bind="isActive ? 'Active' : 'Inactive'"></span>)
</button>
<!-- Controls -->
<div class="mt-3">
<button class="btn btn-primary btn-sm me-2" data-action="toggleActive">
Toggle Active
</button>
<button class="btn btn-secondary btn-sm" data-action="cycleStatus">
Cycle Status
</button>
</div>
<!-- Show current attribute values -->
<div class="mt-3 p-2" style="background: var(--bs-tertiary-bg); color: var(--bs-body-color); border-radius: 4px; font-family: monospace; font-size: 0.85em;">
<div>data-status: "<span data-bind="status"></span>"</div>
<div>aria-pressed: "<span data-bind="isActive"></span>"</div>
<div>aria-label: "<span data-bind="buttonLabel"></span>"</div>
</div>
</div>
wildflower.component('attr-demo', {
state: {
status: 'pending',
isActive: false,
actionType: 'toggle'
},
computed: {
buttonLabel() {
return this.isActive
? 'Click to deactivate'
: 'Click to activate';
}
},
toggleActive() {
this.isActive = !this.isActive;
},
cycleStatus() {
const statuses = ['pending', 'in-progress', 'completed'];
const currentIndex = statuses.indexOf(this.status);
this.status = statuses[(currentIndex + 1) % statuses.length];
}
});
Attribute Removal
When a bound value becomes null, undefined, or false, the attribute is automatically removed from the element:
<div data-component="removal-demo">
<!-- Attribute added/removed based on state -->
<input type="text" class="form-control"
placeholder="Type here..."
data-bind-attr="{
'disabled': isDisabled,
'readonly': isReadonly
}"
data-bind-class="validationClass">
<div class="mt-3">
<label class="form-check form-check-inline">
<input type="checkbox" class="form-check-input" data-model="isDisabled">
Disabled
</label>
<label class="form-check form-check-inline">
<input type="checkbox" class="form-check-input" data-model="isReadonly">
Readonly
</label>
<button class="btn btn-sm btn-primary" data-action="cycleValidation">
Cycle Validation
</button>
</div>
<div class="mt-2 small text-muted">
disabled: <code data-bind="isDisabled"></code> ·
readonly: <code data-bind="isReadonly"></code> ·
validation: <code data-bind="validationState === null ? 'null (attr removed)' : validationState"></code>
</div>
</div>
wildflower.component('removal-demo', {
state: {
isDisabled: false,
isReadonly: false,
validationState: null // null = no validation styling
},
computed: {
validationClass() {
// Bootstrap validation classes on the input
if (this.validationState === 'valid') return 'form-control is-valid';
if (this.validationState === 'invalid') return 'form-control is-invalid';
return 'form-control';
}
},
cycleValidation() {
// null -> valid -> invalid -> null (removed)
const states = [null, 'valid', 'invalid'];
const i = states.indexOf(this.validationState);
this.validationState = states[(i + 1) % states.length];
}
});
Using in Lists
Data Attributes for Item Identification
A common use case is setting data attributes on list items for integration with JavaScript libraries or for DOM queries:
<div data-component="list-attr-demo">
<ul class="list-group" data-list="items">
<template>
<li class="list-group-item d-flex justify-content-between"
data-bind-attr="{
'data-item-id': id,
'data-category': category,
'data-priority': priority
}">
<span data-bind="name"></span>
<span class="badge" data-bind-class="priority === 'high' ? 'bg-danger' : priority === 'medium' ? 'bg-warning text-dark' : 'bg-secondary'">
<span data-bind="priority"></span>
</span>
</li>
</template>
</ul>
<button class="btn btn-sm btn-primary mt-3" data-action="findHighPriority">
Query High Priority Items
</button>
<div class="mt-2 small" data-bind="queryResult"></div>
</div>
wildflower.component('list-attr-demo', {
state: {
items: [
{ id: 1, name: 'Task A', category: 'work', priority: 'high' },
{ id: 2, name: 'Task B', category: 'personal', priority: 'low' },
{ id: 3, name: 'Task C', category: 'work', priority: 'medium' },
{ id: 4, name: 'Task D', category: 'urgent', priority: 'high' }
],
queryResult: ''
},
findHighPriority() {
// Query DOM using the data attributes
const highPriority = this.element.querySelectorAll('[data-priority="high"]');
const ids = Array.from(highPriority).map(el => el.dataset.itemId);
this.queryResult = `Found ${ids.length} high priority items: IDs ${ids.join(', ')}`;
}
});
List Context Variables
Inside lists, you have access to special context variables: _index, _first, and _last:
<div data-component="context-vars-demo">
<ul class="list-group" data-list="steps">
<template>
<li class="list-group-item"
data-bind-attr="{
'data-step-index': _index,
'data-is-first': _first,
'data-is-last': _last
}"
data-bind-class="_first ? 'list-group-item list-group-item-success' : _last ? 'list-group-item list-group-item-info' : 'list-group-item'">
Step <span data-bind="_index + 1"></span>: <span data-bind="label"></span>
<span class="badge bg-secondary ms-2" data-show="_first">First</span>
<span class="badge bg-secondary ms-2" data-show="_last">Last</span>
</li>
</template>
</ul>
</div>
wildflower.component('context-vars-demo', {
state: {
steps: [
{ label: 'Initialize' },
{ label: 'Process' },
{ label: 'Validate' },
{ label: 'Complete' }
]
}
});
Third-Party Library Integration
A primary use case for data-bind-attr is integrating with third-party libraries that rely on data attributes to track elements:
SortableJS Integration
Libraries like SortableJS need to identify elements after drag-and-drop operations. Use data-bind-attr to set the identifying attributes:
<!-- Task list with SortableJS integration -->
<ul id="task-list" data-list="tasks">
<template>
<li class="task-item"
data-bind-attr="{ 'data-task-id': id }"
data-bind-class="done ? 'task-item done' : 'task-item'">
<span class="drag-handle">⋮⋮</span>
<span data-bind="text"></span>
</li>
</template>
</ul>
// SortableJS reads the data-task-id after drag operations
new Sortable(document.getElementById('task-list'), {
animation: 150,
handle: '.drag-handle',
onEnd(evt) {
const items = evt.to.querySelectorAll('[data-task-id]');
const newOrder = Array.from(items).map(el => el.dataset.taskId);
// Update your state with the new order
store.reorderTasks(newOrder);
}
});
data-bind-attr sets the data-task-id that SortableJS reads to report the new order.
Using Computed Properties
For complex attribute logic, use a computed property that returns an attributes object:
<div data-component="computed-attr-demo">
<!-- Using computed property for complex attribute logic -->
<div data-bind-attr="cardAttributes"
style="padding: 1rem; border: 2px solid; border-radius: 8px;">
<h5 data-bind="title"></h5>
<p data-bind="description"></p>
</div>
<div class="mt-3">
<select class="form-select form-select-sm d-inline-block w-auto me-2" data-model="status">
<option value="draft">Draft</option>
<option value="published">Published</option>
<option value="archived">Archived</option>
</select>
<label class="form-check form-check-inline">
<input type="checkbox" class="form-check-input" data-model="featured">
Featured
</label>
</div>
<div class="mt-2 small" style="font-family: monospace;">
Computed attributes: <span data-bind="attributeDisplay"></span>
</div>
</div>
wildflower.component('computed-attr-demo', {
state: {
title: 'Article Title',
description: 'This card uses computed attributes.',
status: 'draft',
featured: false
},
computed: {
cardAttributes() {
const attrs = {
'data-status': this.status,
'aria-label': `${this.title} - ${this.status}`
};
// Set featured attribute - use null to remove when unchecked
attrs['data-featured'] = this.featured ? 'true' : null;
// Add role for accessibility
attrs['role'] = 'article';
return attrs;
},
attributeDisplay() {
const attrs = this.cardAttributes;
return Object.entries(attrs)
.map(([k, v]) => `${k}="${v}"`)
.join(', ');
}
}
});
Accessibility (ARIA) Attributes
data-bind-attr fully supports ARIA attributes, making it straightforward to build accessible components with dynamic state. Hyphenated names like aria-expanded are automatically handled. Just quote them in the object expression.
Common ARIA Patterns
<div data-component="accessible-panel">
<!-- Toggle button with aria-expanded -->
<button data-action="toggle"
data-bind-attr="{
'aria-expanded': isOpen,
'aria-controls': 'panel-content'
}">
<span data-bind="isOpen ? 'Collapse' : 'Expand'"></span> Details
</button>
<!-- Collapsible panel -->
<div id="panel-content" role="region"
data-show="isOpen"
data-bind-attr="{ 'aria-hidden': !isOpen }">
Panel content here
</div>
<!-- Status with aria-live for screen readers -->
<div aria-live="polite" data-bind-attr="{
'aria-busy': isLoading
}">
<span data-bind="statusMessage"></span>
</div>
</div>
wildflower.component('accessible-panel', {
state: {
isOpen: false,
isLoading: false,
statusMessage: 'Ready'
},
toggle() {
this.isOpen = !this.isOpen;
}
});
ARIA in Lists
ARIA attributes work inside data-list templates just like any other attribute binding:
<ul role="listbox" data-list="options">
<template>
<li role="option"
data-bind-attr="{
'aria-selected': selected,
'aria-disabled': disabled
}"
data-action="selectOption">
<span data-bind="label"></span>
</li>
</template>
</ul>
data-bind-attr for dynamic ARIA attributes that change with state (like aria-expanded or aria-selected). For static ARIA attributes that never change (like role="button"), just write them directly in your HTML. No binding needed.
Security
data-bind-attr includes security measures to prevent common attack vectors:
Blocked Attributes
| Category | Blocked Attributes | Reason |
|---|---|---|
| Event Handlers | onclick, onload, onerror, etc. |
Prevents XSS via inline event handlers |
| Framework Directives | data-bind, data-action, data-list, etc. |
Prevents framework state corruption |
| Style/Class | class, style |
Use dedicated bindings instead |
URL Sanitization
For URL-type attributes (href, src, formaction), dangerous protocols are blocked:
javascript:URLs are blockeddata:text/htmlinsrcattributes is blocked (iframe XSS vector)
Syntax Reference
| Syntax | Example | Description |
|---|---|---|
| Object expression | data-bind-attr="{ 'data-id': id }" |
Bind attributes from an inline object |
| Multiple attributes | data-bind-attr="{ 'aria-label': label, 'data-status': status }" |
Bind multiple attributes at once |
| Computed property | data-bind-attr="myAttrs" |
Use a computed property that returns an object (computed: prefix optional) |
| Expressions in values | data-bind-attr="{ 'aria-expanded': isOpen ? 'true' : 'false' }" |
Use ternary or other expressions for values |
data-bind-attr also supports the data-wf- prefix (e.g., data-wf-bind-attr) for compatibility with third-party libraries that might conflict with the standard prefix.
Best Practices
- Use computed properties for complex attribute logic - keeps templates clean
- Quote hyphenated attribute names -
{ 'data-item-id': id }not{ data-item-id: id } - Use semantic attributes - prefer
aria-*for accessibility over customdata-* - Don't overuse - for
classusedata-bind-class, forstyleusedata-bind-style - Return null/undefined/false to remove an attribute rather than setting it to empty string