Lightweight event delegation library for modern web applications
YEH is fundamentally designed around event delegation - using a single listener to handle multiple elements, including dynamically added ones. This isnβt just an optimization; itβs the entire architectural foundation. Works on file:// with zero build tools.
Documentation Status: Early release - actively being refined
Built for developers who value speed and simplicity:
YEH is perfect for quick prototyping, experimental projects, and production apps alike. No build tools, no configuration files, no framework lock-in β just write code and see results instantly.
Technical Features:
Developer Experience:
file:// - No server needed, perfect for local experimentsNo setup, no build step, no server, just include YEH.
Get started in 30 seconds β or try it live on JSFiddle
Using the CDN Demo below obviously requires a internet connection, but the demo itself can be used via file:// protocol (tested in Opera, Firefox, Brave & Chrome; latest versions, DEC 2025). If installed via npm, it even works offline on file:// protocol.
<!DOCTYPE html>
<html>
<head><title>YEH Demo</title></head>
<body>
<div id="app">
<button data-click="save">Save</button>
<button data-click="delete">Delete</button>
</div>
<script type="module">
import { YEH } from 'https://cdn.jsdelivr.net/npm/@yaijs/core@1.1.2/yeh/yeh.min.js';
class MyHandler extends YEH {
constructor() {
super({ '#app': ['click'] }); // Falls back to handleClick()
}
handleClick(event, target, container) {
const action = target.dataset[event.type]; // require click data-attribute
if (action && this[action]) this[action](target, event, container);
}
save(target) { console.log('Saving...'); }
delete(target) { console.log('Deleting...'); }
}
new MyHandler(); // Adding click listeners done. Forever. In this session.
</script>
</body>
</html>
30-second setup: Create app.html, copy & paste the above code, then double-click to run.
π‘ Universal Delegation Pattern
One listener on parent +
custom-selector= handles unlimited elements within the parent
<script type="module"> // Standalone version
import { YEH } from 'https://cdn.jsdelivr.net/npm/@yaijs/core@1.1.2/yeh/yeh.min.js';
new YEH({ '#app': ['click'] });
</script>
npm install @yaijs/core
// Import from main bundle
import { YEH } from '@yaijs/core';
// Or import directly
import { YEH } from '@yaijs/core/yeh';
Note: Package is ESM-only. Use
import, notrequire().
Pass a third argument to the constructor to enable advanced features:
| Option | Type | Default | Description |
|---|---|---|---|
enableStats |
boolean |
false |
Track performance metrics like event count and distance cache hits. |
methods |
object |
null |
External method map for organizing handlers by event type. |
enableGlobalFallback |
boolean |
false |
Fallback to global window functions when no method is found. |
methodsFirst |
boolean |
false |
Check methods object before class methods during handler resolution. |
passiveEvents |
array |
auto | Override default passive events (scroll, touch, wheel, pointer). |
abortController |
boolean |
false |
Enable AbortController support for programmatic listener removal. |
enableDistanceCache |
boolean |
true |
Cache DOM distance calculations for performance (multi-handler scenarios). |
autoTargetResolution |
boolean |
false |
Automatically resolve event targets for nested elements (e.g., SVG icons). |
Example: new YEH(events, aliases, { enableStats: true });
new YEH(
{ '#app': ['click', { type: 'input', debounce: 300 }] }, // Event mapping
{ click: { save: 'handleSave' } }, // Aliases (optional, event type scoped)
{ enableStats: true } // Config (optional)
);
// Subscribe to custom events (adds a DOM listener per .on())
handler.on('data-ready', 'handleData');
handler.on('user-login', (event) => console.log(event.detail));
// Emit custom events (dispatches to document via default)
handler.emit('init-complete', { loaded: true }, document);
// Hook system (zero-cost, no DOM listener)
handler.hook('eventclick', (target) => console.log('clicked', target));
// Chain operations
handler.on('ready', 'init')
.emit('start', { time: Date.now() });
handler.destroy();
// Or with AbortController enabled
handler.abort();
With enableStats: true:
console.log(handler.getStats());
YEHβs event delegation enables powerful declarative patterns where HTML configuration drives behavior. This eliminates repetitive JavaScript handlers while maintaining full flexibility.
import { YEH } from 'https://cdn.jsdelivr.net/npm/@yaijs/core@1.1.2/yeh/yeh.min.js';
class App extends YEH {
constructor() {
super(
{ '#app': ['click'] },
{},
{ autoTargetResolution: true }
);
}
handleClick(event, target, container) {
const action = target.dataset.action;
if (action && this[action]) {
this[action](target, event, container);
}
}
toggleTarget(target, event, container) {
const targetSelector = target.dataset.target;
if (!targetSelector) return;
const config = {
targetAll: target.hasAttribute('data-target-all'), // document.querySelectorAll
toggleClass: target.dataset.toggleClass, // targetElement
toggleContent: target.dataset.toggleContent, // targetElement
toggleAttribute: target.dataset.toggleAttribute, // targetElement
toggleAttributeValue: target.dataset.toggleAttributeValue, // targetElement
toggleClassSelf: target.dataset.toggleClassSelf, // self (trigger)
toggleContentSelf: target.dataset.toggleContentSelf, // self
toggleOnce: target.hasAttribute('data-toggle-once'), // self
};
const targets = config.targetAll
? Array.from(document.querySelectorAll(targetSelector))
: [document.querySelector(targetSelector)].filter(Boolean);
if (!targets.length) return;
targets.forEach(targetEl => {
if (config.toggleClass) {
targetEl.classList.toggle(config.toggleClass);
}
if (config.toggleContent) {
if (!targetEl.dataset.yOriginalContent) {
targetEl.dataset.yOriginalContent = targetEl.textContent;
targetEl.textContent = config.toggleContent;
} else {
const current = targetEl.textContent;
targetEl.textContent = current === config.toggleContent
? targetEl.dataset.yOriginalContent
: config.toggleContent;
}
}
if (config.toggleAttribute) {
if (targetEl.hasAttribute(config.toggleAttribute)) {
targetEl.dataset.yOriginalAttr = targetEl.getAttribute(config.toggleAttribute);
targetEl.removeAttribute(config.toggleAttribute);
} else {
const value = config.toggleAttributeValue || targetEl.dataset.yOriginalAttr || '';
targetEl.setAttribute(config.toggleAttribute, value);
}
}
});
if (config.toggleClassSelf) {
target.classList.toggle(config.toggleClassSelf);
}
if (config.toggleContentSelf) {
if (!target.dataset.yOriginalContentSelf) {
target.dataset.yOriginalContentSelf = target.textContent;
target.textContent = config.toggleContentSelf;
} else {
const current = target.textContent;
target.textContent = current === config.toggleContentSelf
? target.dataset.yOriginalContentSelf
: config.toggleContentSelf;
}
}
if (config.toggleOnce) {
target.removeAttribute('data-action');
target.disabled = true;
}
}
}
// Initialize the app
new App();
Simple Show/Hide Toggle:
<button
data-action="toggleTarget"
data-target="#filters"
data-toggle-class="hidden">
Show Filters
</button>
<div id="filters" class="hidden">
<!-- Filter controls -->
</div>
Expand/Collapse All:
<button
data-action="toggleTarget"
data-target=".message-body, .message-subject"
data-target-all
data-toggle-class="collapsed"
data-toggle-content-self="Expand All">
Collapse All
</button>
Toggle with Self-Feedback:
<button
data-action="toggleTarget"
data-target="[data-y-id='attachment']"
data-target-all
data-toggle-class="visible"
data-toggle-class-self="btn-active"
data-toggle-content-self="Hide Attachments">
Show Attachments
</button>
One-Time Reveal:
<button
data-action="toggleTarget"
data-target="#secret-code"
data-toggle-class="blurred"
data-toggle-once>
Reveal Code (one-time only)
</button>
Attribute Toggle:
<button
data-action="toggleTarget"
data-target=".accordion"
data-target-all
data-toggle-class="expanded"
data-toggle-attribute="aria-expanded"
data-toggle-attribute-value="true">
Expand All Sections
</button>
Dark Mode Toggle:
<button
data-action="toggleTarget"
data-target="body"
data-toggle-class="dark-mode"
data-toggle-content-self="βοΈ Light Mode">
π Dark Mode
</button>
| Attribute | Description | Example |
|---|---|---|
data-target |
Required - CSS selector for target element(s) | "#id", ".class", "[data-y-id='x']" |
data-target-all |
Use querySelectorAll instead of querySelector |
boolean flag |
data-toggle-once |
Disable button after first click | boolean flag |
data-toggle-class |
Class to toggle on target | "visible", "active" |
data-toggle-content |
Content to swap on target | "New text" |
data-toggle-attribute |
Attribute to toggle on target | "disabled", "aria-hidden" |
data-toggle-attribute-value |
Value for toggled attribute | "true", "false" |
data-toggle-class-self |
Class to toggle on button itself | "btn-active" |
data-toggle-content-self |
Content to swap on button itself | "Hide", "Show" |
Before (Imperative):
expandAll() {
document.querySelectorAll('.item').forEach(el =>
el.classList.remove('collapsed'));
this.button.textContent = 'Collapse All';
}
collapseAll() {
document.querySelectorAll('.item').forEach(el =>
el.classList.add('collapsed'));
this.button.textContent = 'Expand All';
}
// Worst case: 2 controller for each togglable; exponentially worsening
After (Declarative):
<!-- One button to handle them all -->
<button data-click="toggleTarget"
data-target=".item"
data-target-all
data-toggle-class="collapsed"
data-toggle-content-self="Expand All">
Collapse All
</button>
First rule: Try Single-handlability, if it fails, alter your approach and try it again.
Benefits:
| Opera | Chrome | Firefox | Safari | Edge - Latest versions of DEC 2025 |
Works with legacy browsers via Webpack + Babel.
YaiTabs Live Demo Advanced tab system built on YEH with 50+ nested components
Performance Benchmark Stress test with X nesting levels demonstrating efficient event delegation
php-ymap Demo A interactive IMAP client demo build on YEH, using 4 event listener for the whole demo:
prev - nextLicense: MIT