Aurelia Custom Attributes — Enhancing HTML Elements
In this tutorial, you will learn about Aurelia Custom Attributes. We cover key concepts, practical examples, and best practices to help you master this topic.
Aurelia custom attributes add behavior to existing HTML elements without creating new tags. They enhance elements with tooltips, validation, animations, and Accessibility features. Custom attributes use the same lifecycle and Dependency Injection as components.
What You'll Learn
You will learn how to create custom attributes, bind values, handle attribute lifecycle, and add reusable behaviors to elements.
Why It Matters
Custom attributes keep HTML semantic while adding rich behavior. A <button tooltip="Save"> is cleaner than wrapping every button in a custom element. Attributes compose naturally with existing HTML.
Real-World Use
A form library uses custom attributes for validation, tooltips, auto-focus, click-outside handling, and keyboard shortcuts. Each attribute adds a specific behavior without changing the element structure.
flowchart LR
A[Custom Attribute] --> B[Bind to element]
B --> C[Lifecycle hooks]
B --> D[DOM manipulation]
B --> E[Event handling]
D --> F[CSS classes]
D --> G[DOM properties]
E --> H[Keyboard, mouse, scroll]
Basic Custom Attribute
// src/resources/attributes/tooltip.ts
import { inject, DOM } from 'aurelia-framework';
@inject(DOM.Element)
export class TooltipCustomAttribute {
constructor(element) {
this.element = element;
this.tooltipElement = null;
}
// Called when value changes
valueChanged(newValue, oldValue) {
if (this.tooltipElement) {
this.tooltipElement.textContent = newValue;
}
}
// Attached to DOM
attached() {
this.element.classList.add('has-tooltip');
this.tooltipElement = document.createElement('span');
this.tooltipElement.className = 'tooltip-text';
this.tooltipElement.textContent = this.value;
this.element.appendChild(this.tooltipElement);
}
// Removed from DOM
detached() {
this.element.classList.remove('has-tooltip');
if (this.tooltipElement) {
this.element.removeChild(this.tooltipElement);
}
}
}
<!-- Usage -->
<button tooltip="Save your changes">Save</button>
<span tooltip.bind="helpText">?</span>
Click Outside Attribute
// src/resources/attributes/click-outside.ts
import { inject, DOM } from 'aurelia-framework';
@inject(DOM.Element)
export class ClickOutsideCustomAttribute {
constructor(element) {
this.element = element;
this.handler = this.onClick.bind(this);
}
// The callback function passed to the attribute
value;
attached() {
setTimeout(() => {
document.addEventListener('click', this.handler);
}, 0);
}
detached() {
document.removeEventListener('click', this.handler);
}
onClick(event) {
if (!this.element.contains(event.target) && this.value) {
this.value();
}
}
}
<div click-outside.call="closeDropdown()" class="dropdown">
Dropdown content
</div>
Auto-Focus Attribute
// src/resources/attributes/auto-focus.ts
import { inject, DOM } from 'aurelia-framework';
@inject(DOM.Element)
export class AutoFocusCustomAttribute {
constructor(element) {
this.element = element;
}
attached() {
setTimeout(() => {
this.element.focus();
this.element.select?.();
}, 100);
}
}
<input type="text" auto-focus placeholder="Search..." />
Debounce Attribute
// src/resources/attributes/debounce.ts
import { inject, DOM } from 'aurelia-framework';
@inject(DOM.Element)
export class DebounceCustomAttribute {
constructor(element) {
this.element = element;
this.timer = null;
this.originalHandler = null;
}
value = 300; // Default debounce time
attached() {
let eventName = 'input';
this.originalHandler = (event) => {
if (this.timer) clearTimeout(this.timer);
this.timer = setTimeout(() => {
this.element.dispatchEvent(
new CustomEvent('debounced-input', { detail: event.target.value })
);
}, this.value);
};
this.element.addEventListener(eventName, this.originalHandler);
}
detached() {
if (this.originalHandler) {
this.element.removeEventListener('input', this.originalHandler);
}
}
}
<input type="text" debounce="500" debounced-input.delegate="search($event.detail)" />
Intersection Observer Attribute
// src/resources/attributes/intersection.ts
import { inject, DOM } from 'aurelia-framework';
@inject(DOM.Element)
export class IntersectionCustomAttribute {
constructor(element) {
this.element = element;
this.observer = null;
}
value; // Callback when element becomes visible
attached() {
this.observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && this.value) {
this.value(entry);
// Optionally unobserve after first visibility
// this.observer.unobserve(this.element);
}
});
});
this.observer.observe(this.element);
}
detached() {
if (this.observer) {
this.observer.disconnect();
this.observer = null;
}
}
}
<div intersection.call="onVisible($event)">
<img data-src="large.jpg" />
</div>
Dynamic CSS Class Attribute
// src/resources/attributes/css-class.ts
import { inject, DOM } from 'aurelia-framework';
@inject(DOM.Element)
export class CssClassCustomAttribute {
constructor(element) {
this.element = element;
}
valueChanged(newValue) {
if (this.previousValue) {
this.element.classList.remove(...this.previousValue.split(' '));
}
if (newValue) {
this.element.classList.add(...newValue.split(' '));
}
this.previousValue = newValue;
}
detached() {
if (this.previousValue) {
this.element.classList.remove(...this.previousValue.split(' '));
}
}
}
<div css-class.bind="isActive ? 'active highlighted' : 'inactive'">
Conditional classes
</div>
Validation Attribute
// src/resources/attributes/validate.ts
import { inject, DOM } from 'aurelia-framework';
@inject(DOM.Element)
export class ValidateCustomAttribute {
constructor(element) {
this.element = element;
}
value; // Validation rules object
valueChanged() {
this.validate();
}
attached() {
this.element.addEventListener('blur', () => this.validate());
this.element.addEventListener('input', () => this.validate());
}
validate() {
let rules = this.value || {};
let value = this.element.value;
if (rules.required && !value) {
this.setError(`${rules.label || 'This field'} is required`);
return false;
}
if (rules.minLength && value.length < rules.minLength) {
this.setError(`Minimum ${rules.minLength} characters`);
return false;
}
if (rules.pattern && !rules.pattern.test(value)) {
this.setError(rules.message || 'Invalid format');
return false;
}
this.clearError();
return true;
}
setError(message) {
this.element.classList.add('invalid');
let errorEl = this.element.nextElementSibling;
if (errorEl?.classList.contains('validation-error')) {
errorEl.textContent = message;
}
}
clearError() {
this.element.classList.remove('invalid');
let errorEl = this.element.nextElementSibling;
if (errorEl?.classList.contains('validation-error')) {
errorEl.textContent = '';
}
}
detached() {
this.element.removeEventListener('blur', () => this.validate());
this.element.removeEventListener('input', () => this.validate());
}
}
Attribute Options Pattern
// src/resources/attributes/toggle-class.ts
import { inject, DOM } from 'aurelia-framework';
@inject(DOM.Element)
export class ToggleClassCustomAttribute {
constructor(element) {
this.element = element;
}
// Primary value: class name to toggle
value;
// Options
target; // Selector for target element
event = 'click'; // Trigger event
attached() {
let target = this.target
? this.element.querySelector(this.target)
: this.element;
this.listener = () => {
target.classList.toggle(this.value);
};
this.element.addEventListener(this.event, this.listener);
}
detached() {
if (this.listener) {
this.element.removeEventListener(this.event, this.listener);
}
}
}
<button toggle-class="active" target=".menu">
Toggle Menu
</button>
<button toggle-class="dark-mode" event="dblclick">
Double-click for dark mode
</button>
Common Mistakes
- Not injecting DOM.Element. The element the attribute is placed on must be injected via
@inject(DOM.Element). - Forgetting
detached()cleanup. Event listeners added inattached()must be removed indetached(). - Using
.callinstead of.bindfor function attributes. Usecallwhen passing a function reference.bindis for property binding. - Mutating the element's innerHTML carelessly. Attribute values often come from user input. Sanitize before setting innerHTML.
- Not handling
valueChangedfor dynamic updates. If the attribute value changes, the attribute must respond. ImplementvalueChangedor usebind.
Practice Questions
- What is the difference between a custom attribute and a custom element?
- How do you access the host element inside a custom attribute?
- What lifecycle hooks are available for custom attributes?
- How do you pass options to a custom attribute?
- Challenge: Create a custom attribute
infinite-scrollthat detects when the user scrolls near the bottom of an element and calls a callback. Support options for threshold distance and debounce time.
FAQ
{{< faq "How do I pass a callback to a custom attribute?" "Use `.call` binding: `click-outside.call=\"close()\"`" >}}Mini Project
Create a library of 6 custom attributes: (1) tooltip — shows a tooltip on hover, (2) click-outside — detects clicks outside the element, (3) long-press — detects press-and-hold, (4) debounce — debounces input events, (5) copy-to-clipboard — copies value on click, (6) lazy-img — loads images when visible. Demonstrate each in a demo page.
What's Next
Now that you understand custom attributes, learn Aurelia Templating for advanced template features. Then explore Aurelia Value Converters for data transformation.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro