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.

Attribute Binding Features:
  • 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];
    }
});
Live Preview

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> &middot;
        readonly: <code data-bind="isReadonly"></code> &middot;
        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];
    }
});
Live Preview

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(', ')}`;
    }
});
Live Preview

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' }
        ]
    }
});
Live Preview

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);
    }
});
Why this works: WildflowerJS has no virtual DOM, so SortableJS can directly manipulate the real DOM elements. The 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(', ');
        }
    }
});
Live Preview

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>
Tip: Use 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 blocked
  • data:text/html in src attributes 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
Note: 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

Recommendations:
  • 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 custom data-*
  • Don't overuse - for class use data-bind-class, for style use data-bind-style
  • Return null/undefined/false to remove an attribute rather than setting it to empty string