πΊπΈ English | π·πΊ Π ΡΡΡΠΊΠΈΠΉ
A lightweight library for simplifying event handling in JavaScript and TypeScript.
- Compact and intuitive event handler management
- Handle multiple events simultaneously with a single call
- Full TypeScript support with automatic event type inference
- Works with
window, DOM elements, and custom event buses - Zero runtime dependencies
Working with native browser events has a few rough edges that add up quickly.
Removing a listener requires keeping an exact reference to the function. An anonymous function passed to addEventListener can never be removed β you have to store every handler in a variable beforehand:
// β This does nothing β a new function object is created each time
element.addEventListener('click', () => doSomething());
element.removeEventListener('click', () => doSomething());
// β
Only this works
function handleClick() {
doSomething();
}
element.addEventListener('click', handleClick);
element.removeEventListener('click', handleClick);With multiple events, it turns into a lot of boilerplate. You have to store every reference separately, repeat the target on every line, and then carefully mirror the same calls to remove them:
const handleClick = () => {
/* ... */
};
const handleFocus = () => {
/* ... */
};
const handleKeydown = () => {
/* ... */
};
button.addEventListener('click', handleClick);
button.addEventListener('focus', handleFocus);
button.addEventListener('keydown', handleKeydown);
// Later, to clean up...
button.removeEventListener('click', handleClick);
button.removeEventListener('focus', handleFocus);
button.removeEventListener('keydown', handleKeydown);Options must match exactly. If you registered a listener with { capture: true }, you must pass the same option to removeEventListener β forget it, and the handler stays attached silently.
event-manager solves this. It tracks all registered handlers internally, so you never have to hold references yourself. Clean up one event, several, or all of them in a single call:
const listener = listen(button, {
click: () => {
/* ... */
},
focus: () => {
/* ... */
},
keydown: () => {
/* ... */
},
});
// Remove everything β no references, no repetition
listener.remove();npm install @webeach/event-managerpnpm add @webeach/event-manageryarn add @webeach/event-managerNo build step needed β load directly in the browser via unpkg or jsDelivr:
<script type="module">
import { listen } from 'https://unpkg.com/@webeach/event-manager';
listen(document.getElementById('my-button')).add('click', () =>
console.log('clicked!'),
);
</script>import { listen } from '@webeach/event-manager';
listen(window)
.add('resize', () => console.log('Window resized!'))
.add('scroll', () => console.log('Window scrolled!'));import { listen } from '@webeach/event-manager';
const myButton = document.getElementById('my-button');
listen(myButton)
.add('click', () => console.log('Button clicked!'))
.add('focus', () => console.log('Button focused!'));import { listen } from '@webeach/event-manager';
const myButton = document.getElementById('my-button');
const myButtonListener = listen(myButton).add('click', () =>
console.log('Button clicked!'),
);
// Unsubscribe from the event after 10 seconds
window.setTimeout(() => {
myButtonListener.remove('click');
}, 10000);import { listen } from '@webeach/event-manager';
const myListener = listen();
myListener
.add('test', () => console.log('Event "test" triggered'))
.add('hello', (event) => console.log(`Hello, ${event.detail.name}!`));
window.setTimeout(() => {
myListener.trigger('test');
myListener.trigger('hello', { name: 'Alex' });
}, 3000);import { listen } from '@webeach/event-manager';
const windowListener = listen(window);
windowListener
.add('focus', () => console.log('Focus event triggered'))
.add('resize', () => console.log('Resize event triggered'))
.add('scroll', () => console.log('Scroll event triggered'));
window.setTimeout(() => {
windowListener.remove();
// or: windowListener.remove(['focus', 'resize', 'scroll']);
}, 3000);import { listen } from '@webeach/event-manager';
const myButton = document.getElementById('my-button');
const myButtonListener = listen(myButton);
// Both handlers will be executed
myButtonListener.add('click', () => console.log('Handler A'));
myButtonListener.add('click', () => console.log('Handler B'));
// Pass an array of handlers at once
myButtonListener.add('focus', [
() => console.log('Focus handler A'),
() => console.log('Focus handler B'),
]);Creates an EventManager for the given target. Pass null or no argument to create a standalone custom event bus.
listen(window);
listen(element, { click: handler });
listen(); // custom event busAdds one or more event handlers.
| Parameter | Type | Description |
|---|---|---|
type |
string |
Event type name, e.g. "click" |
handler |
function | function[] |
Handler function or array of handlers |
options |
object |
Optional: { capture?: boolean, once?: boolean } |
Shorthand for add(type, handler, { capture: true }).
Shorthand for add(type, handler, { once: true }).
Removes handlers for the given event type(s). Only affects handlers registered through this instance.
| Parameter | Type | Description |
|---|---|---|
type |
string | string[] |
Event type or array of types to remove |
Removes all handlers registered through this instance.
Dispatches a CustomEvent with the given type and optional detail payload.
| Parameter | Type | Description |
|---|---|---|
type |
string |
Event type to dispatch |
detail |
any |
Optional event.detail payload |
Dispatches an existing Event object directly.
| Parameter | Type | Description |
|---|---|---|
event |
Event |
An Event instance to dispatch |
The library is fully typed and automatically infers event types based on the observed target.
import { listen } from '@webeach/event-manager';
// Window events β fully typed
const windowListener = listen(window);
windowListener.add('resize', (event) => {
// event is UIEvent
});
// Define custom event types
interface MyEvents {
hello: CustomEvent<{ name: string }>;
ping: CustomEvent;
}
const bus = listen<EventTarget, MyEvents>();
bus.add('hello', (event) => {
console.log(`Hello, ${event.detail.name}!`);
});
bus.trigger('hello', { name: 'Alex' });import { listen } from '@webeach/event-manager';
const basketButton = document.querySelector(
'.basket-button',
) as HTMLButtonElement;
listen(basketButton, {
mouseenter: () => {
basketButton.textContent = 'Go to basket';
},
mouseleave: () => {
basketButton.textContent = 'Shopping basket';
},
});import { listen } from '@webeach/event-manager';
listen(document).add('click', ({ target }) => {
const anchor = (target as Element).closest('a') as HTMLAnchorElement | null;
if (
anchor !== null &&
anchor.href !== '' &&
(anchor.hostname !== window.location.hostname ||
anchor.pathname !== window.location.pathname ||
anchor.search !== window.location.search)
) {
navigator.sendBeacon('/track', JSON.stringify({ link: anchor.href }));
}
});import { listen } from '@webeach/event-manager';
const banner = document.getElementById('banner') as HTMLIFrameElement;
listen(window).add('message', (event) => {
const { type, height } = event.data || {};
if (type === 'setHeight' && typeof height === 'number') {
banner.style.height = `${height}px`;
}
});import { FC, PropsWithChildren, useEffect, useState } from 'react';
import { listen } from '@webeach/event-manager';
const SHOW_TOP_BUTTON_SCROLL_OFFSET = 120;
export const PageLayout: FC<PropsWithChildren> = ({ children }) => {
const [topButtonShown, setTopButtonShown] = useState(false);
useEffect(() => {
const { remove } = listen(window, {
scroll: () => {
setTopButtonShown(window.scrollY >= SHOW_TOP_BUTTON_SCROLL_OFFSET);
},
});
return () => {
remove();
};
}, []);
return (
<div className="page-layout">
<main className="page-layout__content">{children}</main>
{topButtonShown && (
<button
aria-label="Scroll to top"
className="page-layout__top-button"
onClick={() => window.scrollTo(0, 0)}
/>
)}
</div>
);
};Development and support: Ruslan Martynov
If you have suggestions or found a bug, feel free to open an issue or submit a pull request.
This package is distributed under the MIT License.