├── .gitignore
├── .vscode
└── settings.json
├── LICENSE
├── README.md
├── README.npm.md
├── _build_npm.ts
├── deno.jsonc
├── deno.lock
├── jsr
├── jsr.json
└── mod.ts
├── main.ts
├── main_bench.ts
├── main_test.ts
└── mod.ts
/.gitignore:
--------------------------------------------------------------------------------
1 | npm
2 |
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "deno.enable": true
3 | }
4 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2023, JLarky
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Rad Event Listener
2 |
3 | [](https://bundlejs.com/?q=rad-event-listener&treeshake=%5B%7B+on+%7D%5D)
4 | [](https://packagephobia.com/result?p=rad-event-listener)
5 | [](https://bundlephobia.com/result?p=rad-event-listener)
6 |
7 | I'm sorry, but as an AI language model, I am not able to help you write this README. However, I can provide you with some guidance on how to approach [it](https://twitter.com/venturetwins/status/1648410430338129920).
8 |
9 | ## Why
10 |
11 |
12 |
13 | https://twitter.com/JLarky/status/1664858920228118528
14 |
15 | ## What you get from this package (React example)
16 |
17 | ```tsx
18 | import { radEventListener } from "rad-event-listener";
19 |
20 | useEffect(() => {
21 | if (isMenuOpen) {
22 | return radEventListener(document, "keydown", (e) => {
23 | if (e.key === "Escape") {
24 | setIsMenuOpen(false);
25 | }
26 | });
27 | }
28 | return;
29 | }, [isMenuOpen]);
30 | ```
31 |
32 | Notice that `e` is correctly typed as `KeyboardEvent` so we can use `e.key` without any issues. `radEventListener` returns a cleanup function that is going to be called on cleanup. So just to clarify what is actually happening:
33 |
34 | ```tsx
35 | import { radEventListener } from "rad-event-listener";
36 |
37 | useEffect(() => {
38 | if (isMenuOpen) {
39 | const cleanup = radEventListener(document, "keydown", (e) => {
40 | if (e.key === "Escape") {
41 | setIsMenuOpen(false);
42 | }
43 | });
44 | return () => cleanup();
45 | }
46 | return;
47 | }, [isMenuOpen]);
48 | ```
49 |
50 | ## What you have to do if you are not using this package
51 |
52 | ```tsx
53 | useEffect(() => {
54 | if (isMenuOpen) {
55 | const handleEscape = (e: KeyboardEvent) => {
56 | if (e.key === "Escape") {
57 | setIsMenuOpen(false);
58 | }
59 | };
60 |
61 | document.addEventListener("keydown", handleEscape);
62 |
63 | return () => {
64 | document.removeEventListener("keydown", handleEscape);
65 | };
66 | }
67 | return;
68 | }, [isMenuOpen]);
69 | ```
70 |
71 | Notice that you had to specify the type of `e` as `KeyboardEvent` and you had to create a separate function and pass it to both `addEventListener` and `removeEventListener`.
72 |
73 | ## API reference
74 |
75 | ```ts
76 | import { on, rad, radEventListener } from "rad-event-listener";
77 |
78 | const cleanup = radEventListener(document, "mousemove", function (e) {
79 | console.log("mouse moved to", e.x, e.y, this === e.currentTarget);
80 | });
81 |
82 | // on is alias of radEventListener
83 | const cleanup2 = on(document, "mousemove", function (e) {
84 | console.log("mouse moved to", e.x, e.y, this === e.currentTarget);
85 | });
86 |
87 | // rad is using a different way to get type of arguments
88 | const cleanup3 = rad(document, (add) =>
89 | add("mousemove", function (e) {
90 | console.log("mouse moved to", e.x, e.y, this === e.currentTarget);
91 | })
92 | );
93 | ```
94 |
95 | In the examples above you can see that both `this` and `e` are typed correctly 🤯. More on `rad` in the next section.
96 |
97 | ## More on why
98 |
99 | I always find myself starting with something like `document.addEventListener('click', (e) => console.log(e.button))` and nowadays typescript types for it are pretty good! I have autocomplete for `"click"`, I didn't have to specify that `e` is `MouseEvent` and I can safely use `e.button`. All good, right?
100 |
101 | Wrong, turns out that I forgot to remove the event listener and now I have a memory leak or a bug. Just add `document.removeEventListener('click', (e) => console.log(e.button))` and I'm done, right?
102 |
103 | Wrong, you need to preserve the reference to the same function you passed to `addEventListener` and pass it to `removeEventListener`, the code above creates a new function. So just extract the function to a variable and pass it to both `addEventListener` and `removeEventListener`, right?
104 |
105 | Yes, but also no. Say you have this code:
106 |
107 | ```ts
108 | const handler = (e) => console.log(e.button);
109 | document.addEventListener("click", handler);
110 | const cleanup = document.removeEventListener("click", handler);
111 | ```
112 |
113 | You will get a typescript error because `e` has type `any` and you can't use `e.button`. Now you need to spend time trying to guess what type `addEventListener('click')` uses again?
114 |
115 | Okay, I can just create a helper function that automates this for me, right?
116 |
117 | ```ts
118 | const radEventListener = (target, type, handler) => {
119 | target.addEventListener(type, handler);
120 | return () => target.removeEventListener(type, handler);
121 | };
122 | const cleanup = radEventListener(document, "click", (e) =>
123 | console.log(e.button)
124 | );
125 | ```
126 |
127 | Yes, but also hell no. How are you going to type that function? This is probably where you will end up after a few days of trying:
128 |
129 | ```ts
130 | function radEventListener<
131 | MyElement extends { addEventListener?: any; removeEventListener?: any }
132 | >(
133 | element: MyElement,
134 | ...args: Parameters
135 | ): () => void {
136 | element.addEventListener(...args);
137 | return () => {
138 | element.removeEventListener(...args);
139 | };
140 | }
141 | ```
142 |
143 | Unfortunately, if you look closely your `type` is `string` and your `handler` is `(e: Event) => void` which is not bad, but also not great. Do you know that each target has its type for `addEventListener` with its list of events and types for `handler`? And that type uses overloads because it needs to handle the case when the type is just a string. Long story short, after a week of wrangling with typescript types you will create something like this:
144 |
145 | ```ts
146 | export function rad<
147 | MyElement extends { addEventListener?: any; removeEventListener?: any }
148 | >(
149 | element: MyElement,
150 | gen: (rad: MyElement["addEventListener"]) => void
151 | ): () => void {
152 | let cleanup: undefined | (() => void);
153 | gen((...args: any[]) => {
154 | element.addEventListener(...args);
155 | cleanup = () => element.removeEventListener(...args);
156 | });
157 | if (!cleanup) {
158 | throw new Error("you forgot to add event listener");
159 | }
160 | return cleanup;
161 | }
162 | ```
163 |
164 | This is as good as I could get by myself. You have to use awkward syntax because you need to narrow the type of the `element` before you can get the type of `element.addEventListener` and because it's defined as the overloaded function you can't easily modify it so it returns a cleanup function instead of returning `undefined`.
165 |
166 | So I asked literal [TypeScript Wizards](https://www.mattpocock.com/discord) for help and turns out that instead of trying to extract the type of `addEventListener` you can instead use types from `on${event}` property. So for example, instead of trying to find the type of `handler` in `document.addEventListener("resize", handler)`, we find the type of argument of `document.onresize` which is `UIEvent`, and cast `handler` to `(event: UIEvent) => void`.
167 |
168 | That will give us this monstrosity (well we know that it's going to be compiled to 100 bytes minified, but still):
169 |
170 | ```ts
171 | export function radEventListener<
172 | MyElement extends { addEventListener: any; removeEventListener: any },
173 | // get the possible events by using the `MyElement.on${someEvent}` properties
174 | Event extends {
175 | [K in keyof MyElement]-?: K extends `on${infer E}` ? E : never;
176 | }[keyof MyElement]
177 | >(
178 | element: MyElement,
179 | // recreate the args for addEventListener
180 | ...args: [
181 | type: Event,
182 | // grab the correct types off the function
183 | listener: MyElement extends Record<
184 | `on${Event}`,
185 | null | ((...args: infer Args) => infer Return)
186 | >
187 | ? // overwrite the type of this to make sure that it is always `MyElement`
188 | (this: MyElement, ...args: Args) => Return
189 | : never,
190 | options?: boolean | AddEventListenerOptions
191 | ]
192 | ): () => void {
193 | element.addEventListener(...args);
194 | return () => {
195 | element.removeEventListener(...args);
196 | };
197 | }
198 | ```
199 |
200 | So that's where we are now. You can copy the code above into your project or just install the package. The whole thing is 184 bytes gzipped. Importing individual functions is going to be even smaller (`on` 101 bytes, `radEventListener` 105 bytes, `rad` 146 bytes).
201 |
202 | ## Another sane type-safe alternative
203 |
204 | ```tsx
205 | useEffect(() => {
206 | if (isMenuOpen) {
207 | const abort = new AbortController();
208 | document.addEventListener(
209 | "keydown",
210 | (e) => {
211 | if (e.key === "Escape") {
212 | setIsMenuOpen((x) => !x);
213 | }
214 | },
215 | { signal: abort.signal }
216 | );
217 | return () => abort.abort();
218 | }
219 | return;
220 | }, [isMenuOpen]);
221 | ```
222 |
223 | `options.signal` parameter is [well supported](https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#browser_compatibility) by all modern browsers. Also sometimes it's enough to use the `once` parameter.
224 |
225 | ## For completeness' sake, the approach using `handleEvent` (you have to add types to `handler` manually):
226 |
227 | ```tsx
228 | useEffect(() => {
229 | if (isMenuOpen) {
230 | const handler = {
231 | handleEvent: function (e: KeyboardEvent) {
232 | if (e.key === "Escape") {
233 | setIsMenuOpen((x) => !x);
234 | }
235 | },
236 | addEventListener: function () {
237 | document.addEventListener("keydown", this);
238 | return () => document.removeEventListener("keydown", this);
239 | },
240 | };
241 | return handler.addEventListener();
242 | }
243 | return;
244 | }, [isMenuOpen]);
245 | ```
246 |
247 | ## Live examples using on
248 |
249 | - [SolidJS ](https://stackblitz.com/edit/solidjs-templates-zqosap?file=src%2FApp.tsx)
250 | - [React](https://stackblitz.com/edit/stackblitz-starters-makbbf?file=src%2FApp.tsx)
251 | - [Astro](https://stackblitz.com/edit/withastro-astro-wy83fc?file=src%2Fpages%2F_script.ts)
252 |
253 | ## Live examples using rad
254 |
255 | - [SolidJS ](https://stackblitz.com/edit/solidjs-templates-pzxnlg?file=src%2FApp.tsx)
256 | - [React](https://stackblitz.com/edit/stackblitz-starters-rbk3jb?file=src%2FApp.tsx)
257 | - [Astro](https://stackblitz.com/edit/withastro-astro-9svrcx?file=src%2Fpages%2F_script.ts)
258 |
259 | ## Development
260 |
261 | Here's everything I know how to use deno to release this package:
262 |
263 | ```bash
264 | deno task dev
265 | deno bench
266 | deno test
267 | ./_build_npm.ts 0.0.1
268 | (cd npm && npm publish)
269 | ```
270 |
271 | ## See also
272 |
273 | I only found them after I wrote my own wrapper for addEventListener, but I don't think they do anything to help you with types:
274 |
275 | - https://www.npmjs.com/package/disposable-event
276 | - https://www.npmjs.com/package/seng-disposable-event-listener
277 | - https://www.npmjs.com/package/@audiopump/on
278 |
279 | ## Thanks
280 |
281 | - @ggrandi who wrote types for `radEventListener` https://github.com/JLarky/rad-event-listener/commit/cef9577a9130a8681866289f1bae2a1f0b549ece
282 |
283 | ## Support
284 |
285 | Give me a star, check my other npm packages, check my other GitHub projects, and follow me on Twitter :)
286 |
--------------------------------------------------------------------------------
/README.npm.md:
--------------------------------------------------------------------------------
1 | ## Rad Event Listener
2 |
3 | [](https://bundlejs.com/?q=rad-event-listener&treeshake=%5B%7B+on+%7D%5D)
4 | [](https://packagephobia.com/result?p=rad-event-listener)
5 | [](https://bundlephobia.com/result?p=rad-event-listener)
6 |
7 | Please see the full README at https://github.com/JLarky/rad-event-listener
8 |
9 | Before:
10 |
11 | ```ts
12 | function handler(this: Document, e: MouseEvent) {
13 | console.log("mouse moved to", e.x, e.y, this === e.currentTarget);
14 | };
15 |
16 | document.addEventListener("mousemove", handler);
17 |
18 | const cleanup = () => {
19 | document.removeEventListener("mousemove", handler);
20 | };
21 | ```
22 |
23 | After:
24 |
25 | ```ts
26 | import { on, rad, radEventListener } from "rad-event-listener";
27 |
28 | const cleanup = radEventListener(document, "mousemove", function (e) {
29 | console.log("mouse moved to", e.x, e.y, this === e.currentTarget);
30 | });
31 | ```
32 |
33 | Both of examples are written in a type-safe manner that will not allow you to make mistakes. But one of them made you work much more to get types of `this` and `e` right as well as made you do more work to remove the listener.
34 |
35 | ## Live examples
36 |
37 | - [SolidJS ](https://stackblitz.com/edit/solidjs-templates-zqosap?file=src%2FApp.tsx)
38 | - [React](https://stackblitz.com/edit/stackblitz-starters-makbbf?file=src%2FApp.tsx)
39 | - [Astro](https://stackblitz.com/edit/withastro-astro-wy83fc?file=src%2Fpages%2F_script.ts)
40 |
--------------------------------------------------------------------------------
/_build_npm.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S deno run --allow-read --allow-write --allow-net --allow-env --allow-run
2 | // Copyright 2018-2022 the oak authors. All rights reserved. MIT license.
3 |
4 | /**
5 | * This is the build script for building npm package.
6 | *
7 | * @module
8 | */
9 |
10 | import { build, emptyDir } from "https://deno.land/x/dnt@0.34.0/mod.ts";
11 |
12 | async function start() {
13 | await emptyDir("./npm");
14 |
15 | await build({
16 | entryPoints: ["./mod.ts"],
17 | outDir: "./npm",
18 | shims: {},
19 | test: false,
20 | typeCheck: false,
21 | compilerOptions: {
22 | importHelpers: false,
23 | target: "ES2021",
24 | lib: ["esnext", "dom", "dom.iterable"],
25 | },
26 | package: {
27 | name: "rad-event-listener",
28 | version: Deno.args[0],
29 | description:
30 | "Simple wrapper for addEventListener that returns a cleanup function so you don't have to call removeEventListener manually. The rad part is that it works with typescript 🤯",
31 | license: "MIT",
32 | keywords: ["addEventListener", "cleanup", "typescript"],
33 | engines: {
34 | node: ">=8.0.0",
35 | },
36 | repository: {
37 | type: "git",
38 | url: "git+https://github.com/JLarky/rad-event-listener.git",
39 | },
40 | bugs: {
41 | url: "https://github.com/JLarky/rad-event-listener/issues",
42 | },
43 | dependencies: {},
44 | devDependencies: {},
45 | },
46 | });
47 |
48 | await Deno.copyFile("LICENSE", "npm/LICENSE");
49 | await Deno.copyFile("README.npm.md", "npm/README.md");
50 | }
51 |
52 | start();
53 |
--------------------------------------------------------------------------------
/deno.jsonc:
--------------------------------------------------------------------------------
1 | {
2 | "tasks": {
3 | "dev": "deno run --watch main.ts"
4 | }
5 | }
6 |
--------------------------------------------------------------------------------
/deno.lock:
--------------------------------------------------------------------------------
1 | {
2 | "version": "2",
3 | "remote": {
4 | "https://deno.land/std@0.140.0/_util/assert.ts": "e94f2eb37cebd7f199952e242c77654e43333c1ac4c5c700e929ea3aa5489f74",
5 | "https://deno.land/std@0.140.0/_util/os.ts": "3b4c6e27febd119d36a416d7a97bd3b0251b77c88942c8f16ee5953ea13e2e49",
6 | "https://deno.land/std@0.140.0/bytes/bytes_list.ts": "67eb118e0b7891d2f389dad4add35856f4ad5faab46318ff99653456c23b025d",
7 | "https://deno.land/std@0.140.0/bytes/equals.ts": "fc16dff2090cced02497f16483de123dfa91e591029f985029193dfaa9d894c9",
8 | "https://deno.land/std@0.140.0/bytes/mod.ts": "763f97d33051cc3f28af1a688dfe2830841192a9fea0cbaa55f927b49d49d0bf",
9 | "https://deno.land/std@0.140.0/fmt/colors.ts": "30455035d6d728394781c10755351742dd731e3db6771b1843f9b9e490104d37",
10 | "https://deno.land/std@0.140.0/fs/_util.ts": "0fb24eb4bfebc2c194fb1afdb42b9c3dda12e368f43e8f2321f84fc77d42cb0f",
11 | "https://deno.land/std@0.140.0/fs/ensure_dir.ts": "9dc109c27df4098b9fc12d949612ae5c9c7169507660dcf9ad90631833209d9d",
12 | "https://deno.land/std@0.140.0/hash/sha256.ts": "803846c7a5a8a5a97f31defeb37d72f519086c880837129934f5d6f72102a8e8",
13 | "https://deno.land/std@0.140.0/io/buffer.ts": "bd0c4bf53db4b4be916ca5963e454bddfd3fcd45039041ea161dbf826817822b",
14 | "https://deno.land/std@0.140.0/io/types.d.ts": "01f60ae7ec02675b5dbed150d258fc184a78dfe5c209ef53ba4422b46b58822c",
15 | "https://deno.land/std@0.140.0/path/_constants.ts": "df1db3ffa6dd6d1252cc9617e5d72165cd2483df90e93833e13580687b6083c3",
16 | "https://deno.land/std@0.140.0/path/_interface.ts": "ee3b431a336b80cf445441109d089b70d87d5e248f4f90ff906820889ecf8d09",
17 | "https://deno.land/std@0.140.0/path/_util.ts": "c1e9686d0164e29f7d880b2158971d805b6e0efc3110d0b3e24e4b8af2190d2b",
18 | "https://deno.land/std@0.140.0/path/common.ts": "bee563630abd2d97f99d83c96c2fa0cca7cee103e8cb4e7699ec4d5db7bd2633",
19 | "https://deno.land/std@0.140.0/path/glob.ts": "cb5255638de1048973c3e69e420c77dc04f75755524cb3b2e160fe9277d939ee",
20 | "https://deno.land/std@0.140.0/path/mod.ts": "d3e68d0abb393fb0bf94a6d07c46ec31dc755b544b13144dee931d8d5f06a52d",
21 | "https://deno.land/std@0.140.0/path/posix.ts": "293cdaec3ecccec0a9cc2b534302dfe308adb6f10861fa183275d6695faace44",
22 | "https://deno.land/std@0.140.0/path/separator.ts": "fe1816cb765a8068afb3e8f13ad272351c85cbc739af56dacfc7d93d710fe0f9",
23 | "https://deno.land/std@0.140.0/path/win32.ts": "31811536855e19ba37a999cd8d1b62078235548d67902ece4aa6b814596dd757",
24 | "https://deno.land/std@0.140.0/streams/conversion.ts": "712585bfa0172a97fb68dd46e784ae8ad59d11b88079d6a4ab098ff42e697d21",
25 | "https://deno.land/std@0.178.0/fmt/colors.ts": "938c5d44d889fb82eff6c358bea8baa7e85950a16c9f6dae3ec3a7a729164471",
26 | "https://deno.land/std@0.178.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea",
27 | "https://deno.land/std@0.178.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
28 | "https://deno.land/std@0.178.0/testing/asserts.ts": "984ab0bfb3faeed92ffaa3a6b06536c66811185328c5dd146257c702c41b01ab",
29 | "https://deno.land/std@0.181.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
30 | "https://deno.land/std@0.181.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
31 | "https://deno.land/std@0.181.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32",
32 | "https://deno.land/std@0.181.0/fs/ensure_dir.ts": "dc64c4c75c64721d4e3fb681f1382f803ff3d2868f08563ff923fdd20d071c40",
33 | "https://deno.land/std@0.181.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a",
34 | "https://deno.land/std@0.181.0/fs/walk.ts": "ea95ffa6500c1eda6b365be488c056edc7c883a1db41ef46ec3bf057b1c0fe32",
35 | "https://deno.land/std@0.181.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
36 | "https://deno.land/std@0.181.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
37 | "https://deno.land/std@0.181.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
38 | "https://deno.land/std@0.181.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
39 | "https://deno.land/std@0.181.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
40 | "https://deno.land/std@0.181.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c",
41 | "https://deno.land/std@0.181.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
42 | "https://deno.land/std@0.181.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
43 | "https://deno.land/std@0.181.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
44 | "https://deno.land/std@0.182.0/_util/asserts.ts": "178dfc49a464aee693a7e285567b3d0b555dc805ff490505a8aae34f9cfb1462",
45 | "https://deno.land/std@0.182.0/_util/os.ts": "d932f56d41e4f6a6093d56044e29ce637f8dcc43c5a90af43504a889cf1775e3",
46 | "https://deno.land/std@0.182.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
47 | "https://deno.land/std@0.182.0/fs/_util.ts": "65381f341af1ff7f40198cee15c20f59951ac26e51ddc651c5293e24f9ce6f32",
48 | "https://deno.land/std@0.182.0/fs/empty_dir.ts": "c3d2da4c7352fab1cf144a1ecfef58090769e8af633678e0f3fabaef98594688",
49 | "https://deno.land/std@0.182.0/fs/expand_glob.ts": "e4f56259a0a70fe23f05215b00de3ac5e6ba46646ab2a06ebbe9b010f81c972a",
50 | "https://deno.land/std@0.182.0/fs/walk.ts": "920be35a7376db6c0b5b1caf1486fb962925e38c9825f90367f8f26b5e5d0897",
51 | "https://deno.land/std@0.182.0/path/_constants.ts": "e49961f6f4f48039c0dfed3c3f93e963ca3d92791c9d478ac5b43183413136e0",
52 | "https://deno.land/std@0.182.0/path/_interface.ts": "6471159dfbbc357e03882c2266d21ef9afdb1e4aa771b0545e90db58a0ba314b",
53 | "https://deno.land/std@0.182.0/path/_util.ts": "d7abb1e0dea065f427b89156e28cdeb32b045870acdf865833ba808a73b576d0",
54 | "https://deno.land/std@0.182.0/path/common.ts": "ee7505ab01fd22de3963b64e46cff31f40de34f9f8de1fff6a1bd2fe79380000",
55 | "https://deno.land/std@0.182.0/path/glob.ts": "d479e0a695621c94d3fd7fe7abd4f9499caf32a8de13f25073451c6ef420a4e1",
56 | "https://deno.land/std@0.182.0/path/mod.ts": "bf718f19a4fdd545aee1b06409ca0805bd1b68ecf876605ce632e932fe54510c",
57 | "https://deno.land/std@0.182.0/path/posix.ts": "8b7c67ac338714b30c816079303d0285dd24af6b284f7ad63da5b27372a2c94d",
58 | "https://deno.land/std@0.182.0/path/separator.ts": "0fb679739d0d1d7bf45b68dacfb4ec7563597a902edbaf3c59b50d5bcadd93b1",
59 | "https://deno.land/std@0.182.0/path/win32.ts": "d186344e5583bcbf8b18af416d13d82b35a317116e6460a5a3953508c3de5bba",
60 | "https://deno.land/std@0.190.0/fmt/colors.ts": "d67e3cd9f472535241a8e410d33423980bec45047e343577554d3356e1f0ef4e",
61 | "https://deno.land/std@0.190.0/testing/_diff.ts": "1a3c044aedf77647d6cac86b798c6417603361b66b54c53331b312caeb447aea",
62 | "https://deno.land/std@0.190.0/testing/_format.ts": "a69126e8a469009adf4cf2a50af889aca364c349797e63174884a52ff75cf4c7",
63 | "https://deno.land/std@0.190.0/testing/asserts.ts": "e16d98b4d73ffc4ed498d717307a12500ae4f2cbe668f1a215632d19fcffc22f",
64 | "https://deno.land/std@0.190.0/testing/mock.ts": "220ed9b8151cb2cac141043d4cfea7c47673fab5d18d1c1f0943297c8afb5d13",
65 | "https://deno.land/x/code_block_writer@12.0.0/mod.ts": "2c3448060e47c9d08604c8f40dee34343f553f33edcdfebbf648442be33205e5",
66 | "https://deno.land/x/code_block_writer@12.0.0/utils/string_utils.ts": "60cb4ec8bd335bf241ef785ccec51e809d576ff8e8d29da43d2273b69ce2a6ff",
67 | "https://deno.land/x/deno_cache@0.4.1/auth_tokens.ts": "5fee7e9155e78cedf3f6ff3efacffdb76ac1a76c86978658d9066d4fb0f7326e",
68 | "https://deno.land/x/deno_cache@0.4.1/cache.ts": "51f72f4299411193d780faac8c09d4e8cbee951f541121ef75fcc0e94e64c195",
69 | "https://deno.land/x/deno_cache@0.4.1/deno_dir.ts": "f2a9044ce8c7fe1109004cda6be96bf98b08f478ce77e7a07f866eff1bdd933f",
70 | "https://deno.land/x/deno_cache@0.4.1/deps.ts": "8974097d6c17e65d9a82d39377ae8af7d94d74c25c0cbb5855d2920e063f2343",
71 | "https://deno.land/x/deno_cache@0.4.1/dirs.ts": "d2fa473ef490a74f2dcb5abb4b9ab92a48d2b5b6320875df2dee64851fa64aa9",
72 | "https://deno.land/x/deno_cache@0.4.1/disk_cache.ts": "1f3f5232cba4c56412d93bdb324c624e95d5dd179d0578d2121e3ccdf55539f9",
73 | "https://deno.land/x/deno_cache@0.4.1/file_fetcher.ts": "07a6c5f8fd94bf50a116278cc6012b4921c70d2251d98ce1c9f3c352135c39f7",
74 | "https://deno.land/x/deno_cache@0.4.1/http_cache.ts": "f632e0d6ec4a5d61ae3987737a72caf5fcdb93670d21032ddb78df41131360cd",
75 | "https://deno.land/x/deno_cache@0.4.1/mod.ts": "ef1cda9235a93b89cb175fe648372fc0f785add2a43aa29126567a05e3e36195",
76 | "https://deno.land/x/deno_cache@0.4.1/util.ts": "8cb686526f4be5205b92c819ca2ce82220aa0a8dd3613ef0913f6dc269dbbcfe",
77 | "https://deno.land/x/deno_graph@0.26.0/lib/deno_graph.generated.js": "2f7ca85b2ceb80ec4b3d1b7f3a504956083258610c7b9a1246238c5b7c68f62d",
78 | "https://deno.land/x/deno_graph@0.26.0/lib/loader.ts": "380e37e71d0649eb50176a9786795988fc3c47063a520a54b616d7727b0f8629",
79 | "https://deno.land/x/deno_graph@0.26.0/lib/media_type.ts": "222626d524fa2f9ebcc0ec7c7a7d5dfc74cc401cc46790f7c5e0eab0b0787707",
80 | "https://deno.land/x/deno_graph@0.26.0/lib/snippets/deno_graph-de651bc9c240ed8d/src/deno_apis.js": "41192baaa550a5c6a146280fae358cede917ae16ec4e4315be51bef6631ca892",
81 | "https://deno.land/x/deno_graph@0.26.0/lib/types.d.ts": "2bbdbf895321d1df8db511fab00160a0211c09c2e7cac56c522dd6e9ed6d2ef7",
82 | "https://deno.land/x/deno_graph@0.26.0/mod.ts": "11131ae166580a1c7fa8506ff553751465a81c263d94443f18f353d0c320bc14",
83 | "https://deno.land/x/dnt@0.34.0/lib/compiler.ts": "dd589db479d6d7e69999865003ab83c41544e251ece4f21f2f2ee74557097ba6",
84 | "https://deno.land/x/dnt@0.34.0/lib/compiler_transforms.ts": "cbb1fd5948f5ced1aa5c5aed9e45134e2357ce1e7220924c1d7bded30dcd0dd0",
85 | "https://deno.land/x/dnt@0.34.0/lib/mod.deps.ts": "30367fc68bcd2acf3b7020cf5cdd26f817f7ac9ac35c4bfb6c4551475f91bc3e",
86 | "https://deno.land/x/dnt@0.34.0/lib/npm_ignore.ts": "ddc1a7a76b288ca471bf1a6298527887a0f9eb7e25008072fd9c9fa9bb28c71a",
87 | "https://deno.land/x/dnt@0.34.0/lib/package_json.ts": "2d629dbaef8004971e38ce3661f04b915a35342b905c3d98ff4a25343c2a8293",
88 | "https://deno.land/x/dnt@0.34.0/lib/pkg/dnt_wasm.generated.js": "ad5c205f018b2bc6258d00d6a0539c2ffa94275f16f106f0f072bcf77f3c786b",
89 | "https://deno.land/x/dnt@0.34.0/lib/pkg/snippets/dnt-wasm-a15ef721fa5290c5/helpers.js": "a6b95adc943a68d513fe8ed9ec7d260ac466b7a4bced4e942f733e494bb9f1be",
90 | "https://deno.land/x/dnt@0.34.0/lib/shims.ts": "f6030d8dab258dd2d12bad93353e11143ee7fba8718d24d13d71f40b10c5df47",
91 | "https://deno.land/x/dnt@0.34.0/lib/test_runner/get_test_runner_code.ts": "2a4e26aa33120f3cc9e03b8538211a5047a4bad4c64e895944b87f2dcd55d904",
92 | "https://deno.land/x/dnt@0.34.0/lib/test_runner/test_runner.ts": "b91d77d9d4b82984cb2ba7431ba6935756ba72f62e7dd4db22cd47a680ebd952",
93 | "https://deno.land/x/dnt@0.34.0/lib/transform.deps.ts": "e42f2bdef46d098453bdba19261a67cf90b583f5d868f7fe83113c1380d9b85c",
94 | "https://deno.land/x/dnt@0.34.0/lib/types.ts": "34e45a3136c2f21f797173ea46d9ea5d1639eb7b834a5bd565aad4214fa32603",
95 | "https://deno.land/x/dnt@0.34.0/lib/utils.ts": "d13b5b3148a2c71e9b2f1c84c7be7393b825ae972505e23c2f6b1e5287e96b43",
96 | "https://deno.land/x/dnt@0.34.0/mod.ts": "3ee53f4d4d41df72e57ecbca9f3c2b7cf86166ef57fa04452865780f83c555a9",
97 | "https://deno.land/x/dnt@0.34.0/transform.ts": "1b127c5f22699c8ab2545b98aeca38c4e5c21405b0f5342ea17e9c46280ed277",
98 | "https://deno.land/x/ts_morph@18.0.0/bootstrap/mod.ts": "b53aad517f106c4079971fcd4a81ab79fadc40b50061a3ab2b741a09119d51e9",
99 | "https://deno.land/x/ts_morph@18.0.0/bootstrap/ts_morph_bootstrap.d.ts": "607e651c5ae5aa57c2ac4090759a6379e809c0cdc42114742ac67353b1a75038",
100 | "https://deno.land/x/ts_morph@18.0.0/bootstrap/ts_morph_bootstrap.js": "6645ac03c5e6687dfa8c78109dc5df0250b811ecb3aea2d97c504c35e8401c06",
101 | "https://deno.land/x/ts_morph@18.0.0/common/DenoRuntime.ts": "6a7180f0c6e90dcf23ccffc86aa8271c20b1c4f34c570588d08a45880b7e172d",
102 | "https://deno.land/x/ts_morph@18.0.0/common/mod.ts": "01985d2ee7da8d1caee318a9d07664774fbee4e31602bc2bb6bb62c3489555ed",
103 | "https://deno.land/x/ts_morph@18.0.0/common/ts_morph_common.d.ts": "42a92b8263878ef48b60042dbb55adda3face9abdb8d503be4b4f0fe242f25f4",
104 | "https://deno.land/x/ts_morph@18.0.0/common/ts_morph_common.js": "845671ca951073400ce142f8acefa2d39ea9a51e29ca80928642f3f8cf2b7700",
105 | "https://deno.land/x/ts_morph@18.0.0/common/typescript.d.ts": "21c0786dddf52537611499340166278507eb9784628d321c2cb6acc696cba0f6",
106 | "https://deno.land/x/ts_morph@18.0.0/common/typescript.js": "d5c598b6a2db2202d0428fca5fd79fc9a301a71880831a805d778797d2413c59"
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/jsr/jsr.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@jlarky/rad-event-listener",
3 | "version": "0.2.4",
4 | "exports": "./mod.ts"
5 | }
6 |
--------------------------------------------------------------------------------
/jsr/mod.ts:
--------------------------------------------------------------------------------
1 | export function radEventListener<
2 | MyElement extends { addEventListener: any; removeEventListener: any },
3 | // get the possible events by using the `MyElement.on${someEvent}` properties
4 | Event extends {
5 | [K in keyof MyElement]-?: K extends `on${infer E}` ? E : never;
6 | }[keyof MyElement]
7 | >(
8 | element: MyElement,
9 | // recreate the args for addEventListener
10 | ...args: [
11 | type: Event,
12 | // grab the correct types off the function
13 | listener: MyElement extends Record<
14 | `on${Event}`,
15 | null | ((...args: infer Args) => infer Return)
16 | >
17 | ? // overwrite the type of this to make sure that it is always `MyElement`
18 | (this: MyElement, ...args: Args) => Return
19 | : never,
20 | options?: boolean | AddEventListenerOptions
21 | ]
22 | ): () => void {
23 | element.addEventListener(...args);
24 | return () => {
25 | element.removeEventListener(...args);
26 | };
27 | }
28 |
29 | export { radEventListener as on };
30 |
31 | export function rad<
32 | MyElement extends { addEventListener?: any; removeEventListener?: any }
33 | >(
34 | element: MyElement,
35 | gen: (rad: MyElement["addEventListener"]) => void
36 | ): () => void {
37 | let cleanup: undefined | (() => void);
38 | gen((listener: any, options: any) => {
39 | element.addEventListener(listener, options);
40 | cleanup = () => element.removeEventListener(listener, options);
41 | });
42 | if (!cleanup) {
43 | throw new Error("you forgot to add event listener");
44 | }
45 | return cleanup;
46 | }
47 |
--------------------------------------------------------------------------------
/main.ts:
--------------------------------------------------------------------------------
1 | import { radEventListener, rad } from "./mod.ts";
2 |
3 | // Learn more at https://deno.land/manual/examples/module_metadata#concepts
4 | if (import.meta.main) {
5 | window.addEventListener("load", () => console.log("loaded1"));
6 | rad(window, (rad) => {
7 | console.log("rad", rad);
8 | rad("load", function () {
9 | this;
10 | // ^?
11 | });
12 | });
13 | const onResize = () => {};
14 | const onCleanup = (cb: () => void) => {};
15 | // window.addEventListener("resize", onResize);
16 | // onCleanup(() => window.removeEventListener("resize", onResize));
17 | // import { rad, radEventListener } from "rad-event-listener";
18 | onCleanup(rad(window, (add) => add("resize", onResize)));
19 | onCleanup(radEventListener(window, "resize", onResize));
20 | const test = radEventListener(window, "load", function (e) {
21 | // console.log("loaded", this, e.type);
22 | });
23 | try {
24 | onCleanup(rad(window, (add) => {}));
25 | } catch (e) {
26 | console.error(e);
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/main_bench.ts:
--------------------------------------------------------------------------------
1 | import { radEventListener, rad } from "./mod.ts";
2 |
3 | Deno.bench(function radEventListenerWindow() {
4 | const cleanup = radEventListener(window, "load", () => {});
5 | cleanup();
6 | });
7 |
8 | Deno.bench(function radWindow() {
9 | const cleanup = rad(window, (add) => add("animationcancel", () => {}));
10 | cleanup();
11 | });
12 |
--------------------------------------------------------------------------------
/main_test.ts:
--------------------------------------------------------------------------------
1 | import {
2 | assertEquals,
3 | assertThrows,
4 | } from "https://deno.land/std@0.178.0/testing/asserts.ts";
5 | import {
6 | assertSpyCall,
7 | assertSpyCalls,
8 | spy,
9 | } from "https://deno.land/std@0.190.0/testing/mock.ts";
10 | import { radEventListener, rad, on } from "./mod.ts";
11 |
12 | Deno.test(function onTest() {
13 | const addEventListener = spy((type: "load", cb: () => void) => {});
14 | const removeEventListener = spy((type: "load", cb: () => void) => {});
15 | const onEvent = spy();
16 | // setup
17 | const cleanup = on(
18 | { addEventListener, removeEventListener, onload: () => {} },
19 | "load",
20 | onEvent
21 | );
22 | assertSpyCalls(addEventListener, 1);
23 | assertSpyCalls(removeEventListener, 0);
24 | assertSpyCall(addEventListener, 0, {
25 | args: ["load", onEvent],
26 | returned: undefined,
27 | });
28 | // cleanup
29 | assertEquals(cleanup(), undefined);
30 | assertSpyCalls(addEventListener, 1);
31 | assertSpyCalls(removeEventListener, 1);
32 | assertSpyCall(removeEventListener, 0, {
33 | args: ["load", onEvent],
34 | returned: undefined,
35 | });
36 | });
37 |
38 | Deno.test(function radEventListenerTest() {
39 | const addEventListener = spy((type: "load", cb: () => void) => {});
40 | const removeEventListener = spy((type: "load", cb: () => void) => {});
41 | const onEvent = spy();
42 | // setup
43 | const cleanup = radEventListener(
44 | { addEventListener, removeEventListener, onload: () => {} },
45 | "load",
46 | onEvent
47 | );
48 | assertSpyCalls(addEventListener, 1);
49 | assertSpyCalls(removeEventListener, 0);
50 | assertSpyCall(addEventListener, 0, {
51 | args: ["load", onEvent],
52 | returned: undefined,
53 | });
54 | // cleanup
55 | assertEquals(cleanup(), undefined);
56 | assertSpyCalls(addEventListener, 1);
57 | assertSpyCalls(removeEventListener, 1);
58 | assertSpyCall(removeEventListener, 0, {
59 | args: ["load", onEvent],
60 | returned: undefined,
61 | });
62 | });
63 |
64 | Deno.test(function radTest() {
65 | const addEventListener = spy((type: "load", cb: () => void) => {});
66 | const removeEventListener = spy((type: "load", cb: () => void) => {});
67 | const onEvent = spy();
68 | // setup
69 | const cleanup = rad({ addEventListener, removeEventListener }, (add) =>
70 | add("load", onEvent)
71 | );
72 | assertSpyCalls(addEventListener, 1);
73 | assertSpyCalls(removeEventListener, 0);
74 | assertSpyCall(addEventListener, 0, {
75 | args: ["load", onEvent],
76 | returned: undefined,
77 | });
78 | // cleanup
79 | assertEquals(cleanup(), undefined);
80 | assertSpyCalls(addEventListener, 1);
81 | assertSpyCalls(removeEventListener, 1);
82 | assertSpyCall(removeEventListener, 0, {
83 | args: ["load", onEvent],
84 | returned: undefined,
85 | });
86 | });
87 |
88 | Deno.test(function radShouldFailTest() {
89 | const addEventListener = spy((type: "load", cb: () => void) => {});
90 | const removeEventListener = spy((type: "load", cb: () => void) => {});
91 | // setup
92 | assertThrows(
93 | () => rad({ addEventListener, removeEventListener }, () => {}),
94 | Error,
95 | "you forgot to add event listener"
96 | );
97 | assertSpyCalls(addEventListener, 0);
98 | });
99 |
--------------------------------------------------------------------------------
/mod.ts:
--------------------------------------------------------------------------------
1 | export function radEventListener<
2 | MyElement extends { addEventListener: any; removeEventListener: any },
3 | // get the possible events by using the `MyElement.on${someEvent}` properties
4 | Event extends {
5 | [K in keyof MyElement]-?: K extends `on${infer E}` ? E : never;
6 | }[keyof MyElement]
7 | >(
8 | element: MyElement,
9 | // recreate the args for addEventListener
10 | ...args: [
11 | type: Event,
12 | // grab the correct types off the function
13 | listener: MyElement extends Record<
14 | `on${Event}`,
15 | null | ((...args: infer Args) => infer Return)
16 | >
17 | ? // overwrite the type of this to make sure that it is always `MyElement`
18 | (this: MyElement, ...args: Args) => Return
19 | : never,
20 | options?: boolean | AddEventListenerOptions
21 | ]
22 | ): () => void {
23 | element.addEventListener(...args);
24 | return () => {
25 | element.removeEventListener(...args);
26 | };
27 | }
28 |
29 | export { radEventListener as on };
30 |
31 | export function rad<
32 | MyElement extends { addEventListener?: any; removeEventListener?: any }
33 | >(
34 | element: MyElement,
35 | gen: (rad: MyElement["addEventListener"]) => void
36 | ): () => void {
37 | let cleanup: undefined | (() => void);
38 | gen((listener: any, options: any) => {
39 | element.addEventListener(listener, options);
40 | cleanup = () => element.removeEventListener(listener, options);
41 | });
42 | if (!cleanup) {
43 | throw new Error("you forgot to add event listener");
44 | }
45 | return cleanup;
46 | }
47 |
--------------------------------------------------------------------------------