├── .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 | [![minzip size](https://deno.bundlejs.com/?q=rad-event-listener&treeshake=[{on}]&badge=)](https://bundlejs.com/?q=rad-event-listener&treeshake=%5B%7B+on+%7D%5D) 4 | [![install size](https://badgen.deno.dev/packagephobia/install/rad-event-listener)](https://packagephobia.com/result?p=rad-event-listener) 5 | [![dependency count](https://badgen.deno.dev/bundlephobia/dependency-count/rad-event-listener)](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 | image 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 | [![minzip size](https://deno.bundlejs.com/?q=rad-event-listener&treeshake=[{on}]&badge=)](https://bundlejs.com/?q=rad-event-listener&treeshake=%5B%7B+on+%7D%5D) 4 | [![install size](https://badgen.deno.dev/packagephobia/install/rad-event-listener)](https://packagephobia.com/result?p=rad-event-listener) 5 | [![dependency count](https://badgen.deno.dev/bundlephobia/dependency-count/rad-event-listener)](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 | --------------------------------------------------------------------------------