Skip to content

Commit 035288e

Browse files
authored
feat(events): add onEvent utility (#658)
* refactor(on-event): streamline function signature and improve type definitions * feat(on-event): update options include manualCleanup and improve destroyRef handling * feat(on-event): rename removeListener to destroy; onEvent -> on-event
1 parent 648f7c6 commit 035288e

File tree

8 files changed

+613
-0
lines changed

8 files changed

+613
-0
lines changed
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
title: onEvent
3+
description: A small utility to add DOM/EventTarget listeners with automatic cleanup via DestroyRef.
4+
entryPoint: ngxtension/on-event
5+
badge: stable
6+
contributors: ['endlacer']
7+
---
8+
9+
A small utility for listening to events on any `EventTarget` with automatic cleanup when the current Angular lifecycle
10+
scope is destroyed (via `DestroyRef.onDestroy`).
11+
It uses an `AbortController` and passes its `signal` to `addEventListener`, so aborting removes the listener without
12+
needing `removeEventListener`.
13+
14+
## Usage
15+
16+
### Basic Usage with Auto-Cleanup
17+
18+
Listen to an event and auto-cleanup on destroy (when called inside an injection context):
19+
20+
```ts
21+
import { onEvent } from 'ngxtension/on-event';
22+
23+
onEvent(window, 'resize', (event) => {
24+
console.log('Window resized', event);
25+
});
26+
```
27+
28+
### Manual Control (Abort inside callback)
29+
30+
Stop listening manually (e.g., after the first "meaningful" event):
31+
32+
```ts
33+
onEvent(document, 'scroll', (event, abort) => {
34+
if (window.scrollY > 500) {
35+
console.log('User scrolled past 500px');
36+
abort(); // removes the listener immediately
37+
}
38+
});
39+
```
40+
41+
### Control via Return Value
42+
43+
You can control the listener from outside the callback using the returned object. This is useful for UI feedback based
44+
on the listener's state.
45+
46+
```ts
47+
const { destroy, active } = onEvent(window, 'mousemove', (e) => {
48+
// heavy logic
49+
});
50+
51+
// Check if we are currently listening (returns Signal<boolean>)
52+
console.log(active()); // true
53+
54+
// Stop listening from outside
55+
destroy();
56+
57+
console.log(active()); // false
58+
```
59+
60+
## Options
61+
62+
```ts
63+
type OnEventOptions = {
64+
once?: boolean;
65+
capture?: boolean;
66+
passive?: boolean;
67+
};
68+
```
69+
70+
- `once`: Uses the native `addEventListener({ once: true })` behavior (listener runs once and is then removed).
71+
- `capture`: A boolean indicating that events of this type will be dispatched to the registered listener before being
72+
dispatched to any EventTarget beneath it in the DOM tree.
73+
- `passive`: A boolean which, if true, indicates that the function specified by listener will never call
74+
`preventDefault()`.
75+
- `destroyRef`: Provide a `DestroyRef` explicitly (useful outside injection context, or when you want a specific
76+
lifecycle scope).
77+
- `injector`: Provide an `Injector` explicitly so the utility can resolve `DestroyRef` from it when you're not in a
78+
direct injection context.
79+
80+
## Injection notes
81+
82+
If you call `onEvent()` somewhere without an active injection context (for example, inside a plain function that isn't
83+
run via Angular DI), pass either:
84+
85+
- `options.destroyRef`, or
86+
- `options.injector`.
87+
88+
If no `DestroyRef` can be determined, a warning will be logged in DevMode, and the listener will **not** be
89+
automatically cleaned up (you must call `destroy` manually).
90+
91+
## API
92+
93+
```ts
94+
export type OnEventResult = {
95+
destroy: () => void;
96+
active: Signal<boolean>;
97+
}
98+
99+
onEvent(target: EventTarget, eventKey: string, listener: (event: Event, abort: () => void) => void, options?: OnEventOptions): OnEventResult;
100+
```
101+
102+
### Arguments
103+
104+
- `target`: The DOM element or EventTarget (e.g., `window`, `document`, `ElementRef.nativeElement`).
105+
- `eventKey`: The name of the event (e.g., `'click'`, `'scroll'`).
106+
- `listener`: The callback function. Receives:
107+
- `event`: The dispatched event (typed via `GlobalEventHandlersEventMap` when possible).
108+
- `abort()`: Call to stop listening immediately.
109+
- `options`: Configuration object for event behavior and dependency injection.
110+
111+
### Return Value
112+
113+
Returns an `OnEventResult` object:
114+
115+
- `destroy`: A function to remove the event listener manually.
116+
- `active`: A generic Angular `Signal<boolean>` indicating whether the listener is currently attached.

libs/ngxtension/on-event/README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# ngxtension/on-event
2+
3+
Secondary entry point of `ngxtension`. It can be used by importing from `ngxtension/on-event`.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"lib": {
3+
"entryFile": "src/index.ts"
4+
}
5+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
{
2+
"name": "ngxtension/on-event",
3+
"$schema": "../../../node_modules/nx/schemas/project-schema.json",
4+
"projectType": "library",
5+
"sourceRoot": "libs/ngxtension/on-event/src",
6+
"targets": {
7+
"test": {
8+
"executor": "@nx/jest:jest",
9+
"outputs": ["{workspaceRoot}/coverage/{projectRoot}"],
10+
"options": {
11+
"jestConfig": "libs/ngxtension/jest.config.ts",
12+
"testPathPattern": ["on-event"],
13+
"passWithNoTests": true
14+
},
15+
"configurations": {
16+
"ci": {
17+
"ci": true,
18+
"codeCoverage": true
19+
}
20+
}
21+
},
22+
"lint": {
23+
"executor": "@nx/eslint:lint",
24+
"outputs": ["{options.outputFile}"]
25+
}
26+
}
27+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './on-event';

0 commit comments

Comments
 (0)