diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..dc4f29f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "deno.enable": true, + "deno.lint": true, + "deno.unstable": true, + "deno.config": "../deno.jsonc" +} diff --git a/EventEmitter.test.ts b/EventEmitter.test.ts new file mode 100644 index 0000000..926554e --- /dev/null +++ b/EventEmitter.test.ts @@ -0,0 +1,122 @@ +import type { CustomEventMap, TypedCustomEvent } from "./EventEmitter.ts"; +import { + assertEquals, + assertThrows, + fail, +} from "https://deno.land/std@0.92.0/testing/asserts.ts"; + +import { EventEmitter } from "./EventEmitter.ts"; + +interface Events extends CustomEventMap { + ping: TypedCustomEvent<"ping", undefined>; + pong: TypedCustomEvent<"pong", string>; + peng: TypedCustomEvent<"peng", { data: string }>; +} + +Deno.test("types of detail", () => { + const emitter = new EventEmitter(); + + emitter.on("ping", (event) => { + assertEquals(event.detail, undefined); + }); + + emitter.emit("ping"); + + emitter.on("pong", (event) => { + assertEquals(event.detail, "hello"); + }); + + emitter.emit("pong", "hello"); + + emitter.on("peng", (event) => { + assertEquals(event.detail.data, "peng emitted!") + }); + + emitter.emit("peng", { + data: "peng emitted!", + }); +}); + +/** + * @see https://deno.land/x/event@2.0.0/test.ts + */ + +Deno.test("on", () => { + const ee = new EventEmitter(); + + ee.on("foo", (event) => { + assertEquals(event.detail, "bar"); + }); + + ee.emit("foo", "bar"); +}); + +Deno.test("once", () => { + const ee = new EventEmitter(); + + ee.once("foo", (event) => { + assertEquals(event.detail, "bar"); + }); + + ee.emit("foo", "bar"); +}); + +Deno.test("off", () => { + const ee = new EventEmitter(); + + function foo() { + fail(); + } + + ee.on("foo", foo); + + ee.off("foo", foo); + + ee.emit("foo", "bar"); +}); + +Deno.test("offEvent", () => { + const ee = new EventEmitter(); + + let i = 0; + + ee.on("foo", () => i++); + + ee.on("foo", () => i++); + + ee.off(); + + ee.emit("foo", "bar"); + + assertEquals(i, 0); +}); + +Deno.test("offAll", () => { + const ee = new EventEmitter(); + + let i = 0; + + ee.on("foo", () => i++); + + ee.on("bar", () => i++); + + ee.off(); + + ee.emit("foo", "bar"); + + assertEquals(i, 0); +}); + +Deno.test("chainable", () => { + const ee = new EventEmitter(); + + function foo() { + fail(); + } + + ee + .on("foo", foo) + .off("foo", foo); + + ee.emit("foo", "bar"); +}); diff --git a/EventEmitter2.ts b/EventEmitter.ts similarity index 60% rename from EventEmitter2.ts rename to EventEmitter.ts index 1cb539c..e0585c7 100644 --- a/EventEmitter2.ts +++ b/EventEmitter.ts @@ -2,82 +2,74 @@ export type Values = T[keyof T]; -export type Fn = - (...params: Params) => Result; +export type Fn< + Params extends readonly unknown[] = readonly unknown[], + Result = unknown, +> = (...params: Params) => Result; export type TypedCustomEvent = - CustomEvent & { type: Type }; + & CustomEvent + & { type: Type }; -export type CustomEventCallback = - Fn<[event: TypedCustomEvent], void>; +export type CustomEventCallback< + Type extends string = string, + Detail = unknown, +> = Fn<[event: TypedCustomEvent], void>; -// deno-lint-ignore no-explicit-any -export type EventCallbackFromCustomEvent> = - Fn<[event: T], void>; +export type EventCallbackFromCustomEvent< + T extends TypedCustomEvent, +> = Fn<[event: T], void>; export type CustomEventMap = Record; -export type EventTargetCompatible = Extract[1], Fn>; +export type EventTargetCompatible = Extract< + Parameters[1], + Fn +>; -export class EventEmitter> extends EventTarget { +export class EventEmitter> + extends EventTarget { /** * @var __listeners__ A Map with all listeners, sorted by event */ - protected __listeners__: Map> = new Map; + protected __listeners__: Map> = new Map(); static createEvent( type: Type, detail?: Detail, - init?: Omit, + init?: Omit, ): TypedCustomEvent { const evInit = { ...init, detail }; return new CustomEvent(type, evInit) as TypedCustomEvent; } - protected getOrCreateListeners(type: K): Set> { - if (!this.__listeners__.has(type)) + protected getOrCreateListeners( + type: K, + ): Set> { + if (!this.__listeners__.has(type)) { this.__listeners__.set(type, new Set()); + } return this.__listeners__.get(type)!; } - /** - * add a callback to an event - * @param type the event name the callback should listen to - * @param callback the callback to execute when the event is dispatched - * @param options event options {@link EventTarget["addEventListener"]} - */ - // @ts-expect-error - addEventListener( - type: K, - callback: EventCallbackFromCustomEvent, - options?: Parameters[2] - ): this; /** - * add a callback to multiple events - * @param types an array of the event names the callback should be listen to + * add a callback to an event or multiple events + * @param types the event name(s) the callback should listen to * @param callback the callback to execute when the event is dispatched * @param options event options {@link EventTarget["addEventListener"]} */ - // @ts-expect-error - addEventListener( - types: K[], - callback: EventCallbackFromCustomEvent, - options?: Parameters[2] - ): this; - // @ts-expect-error addEventListener( types: K | K[], callback: EventCallbackFromCustomEvent, - options?: Parameters[2], + options?: Parameters[2], ): this { - const doAdd = ( type: K, callback: EventCallbackFromCustomEvent, - options?: Parameters[2] + options?: Parameters[2], ) => { this .getOrCreateListeners(type) @@ -88,115 +80,108 @@ export class EventEmitter> exten callback as EventTargetCompatible, options, ); - } + }; if (typeof types === "string") { doAdd(types, callback, options); } else { types.forEach((type) => { doAdd(type, callback, options); - }) + }); } - return this; } on = this.addEventListener; /** - * remove a callback for a given event - * @param type the typed event name - * @param callback the typed callback function to remove - * @param options the options + * add a callback to an event only once. After that, the listener is removed. + * @param type the event name the callback should listen to + * @param callback the callback to execute when the event is dispatched + * @param options event options {@link EventTarget["addEventListener"]} */ - removeEventListener( + once( type: K, callback: EventCallbackFromCustomEvent, - options?: Parameters[2], + options?: Parameters[2], ): this; /** - * remove a callback for a multiple given events - * @param type the array with the typed event names - * @param callback the typed callback function to remove - * @param options the options + * add a callback to multiple events only once. After that, the listener is removed. + * @param types an array of the event names the callback should be listen to + * @param callback the callback to execute when the event is dispatched + * @param options event options {@link EventTarget["addEventListener"]} */ - removeEventListener( + once( types: K[], callback: EventCallbackFromCustomEvent, - options?: Parameters[2], + options?: Parameters[2], ): this; - /** - * remove all callbacks for a given event - * @param type the array with the typed event names - */ - removeEventListener( - type: K - ): this; + once( + types: K | K[], + callback: EventCallbackFromCustomEvent, + options?: Parameters[2], + ): this { + const once = ( + event: Parameters>[0], + ) => { + this.removeEventListener(types, once, options); - /** - * remove all callback for multiple given events - */ - removeEventListener(): this; + callback(event); + }; - /** - * remove all callbacks for all events - * @param types the array with the event name - * @param callback the typed callback function to remove - * @param options the options - */ - removeEventListener( - type: K[] - ): this; + this.addEventListener(types, callback); + + return this; + } /** - * remove a callback for an or multiple events + * remove a callback for an or multiple events or remove all callbacks for an or multiple events or even reomve all callbacks * @param type the optional typed event name(s) * @param callback the optional typed callback function to remove * @param options the optional options * @returns this */ + // @ts-expect-error removeEventListener( types?: K | K[], callback?: EventCallbackFromCustomEvent, - options?: Parameters[2], + options?: Parameters[2], ): this { const doRemove = ( type: K, - optionalCallback?: EventCallbackFromCustomEvent + optionalCallback?: EventCallbackFromCustomEvent, ) => { super.removeEventListener( type, (optionalCallback ?? callback) as EventTargetCompatible, - options + options, ); - } + }; // remove all EventListeners if (!types && !callback) { this.__listeners__.forEach((set, type) => { - set.forEach(callback => { + set.forEach((callback) => { doRemove(type as K, callback); }); }); this.__listeners__ = new Map(); - } - // remove all EventListeners for specific event(s) + } // remove all EventListeners for specific event(s) else if (types && !callback) { if (typeof types === "string") { doRemove( - types + types, ); } else { types.forEach((type) => { doRemove(type); }); } - } - // remove specific EventListener for specific event(s) + } // remove specific EventListener for specific event(s) else if (types && callback) { if (typeof types === "string") { doRemove(types); @@ -205,10 +190,9 @@ export class EventEmitter> exten doRemove(type); }); } - } - // unknown case + } // unknown case else { - throw new Error("Unknown case for removing event!") + throw new Error("Unknown case for removing event!"); } return this; @@ -216,12 +200,10 @@ export class EventEmitter> exten off = this.removeEventListener; - /** - * * @param type the name of the event - * @returns Dispatches a synthetic event event to target and returns true - * if either event's cancelable attribute value is false or its preventDefault() method was not invoked, + * @returns Dispatches a synthetic event event to target and returns true + * if either event's cancelable attribute value is false or its preventDefault() method was not invoked, * and false otherwise. */ dispatchEvent>(type: E): boolean { @@ -238,13 +220,18 @@ export class EventEmitter> exten emit( type: K, ...[detail]: ( - unknown extends T[K]['detail'] ? [detail?: unknown] - : T[K]['detail'] extends undefined ? [detail?: undefined] - : T[K]['detail'] extends never ? [] - : [detail: T[K]['detail']] + unknown extends T[K]["detail"] ? [detail?: unknown] + : T[K]["detail"] extends undefined ? [detail?: undefined] + : T[K]["detail"] extends never ? [] + : [detail: T[K]["detail"]] ) ): this { - const event = EventEmitter.createEvent(type, detail) as unknown as Values; + const event = EventEmitter.createEvent( + type, + detail, + ) as unknown as Values< + T + >; this.dispatchEvent(event); @@ -266,7 +253,7 @@ export class EventEmitter> exten resolve(event); }, { - once: true + once: true, }); if (timeout) { @@ -280,4 +267,4 @@ export class EventEmitter> exten } } -export default EventEmitter; \ No newline at end of file +export default EventEmitter; diff --git a/README.md b/README.md index dbdbdbd..e5d40f7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,29 @@ # evtemitter - Eventemitter for deno. + +A typed Eventemitter for deno. + +This EventEmitter is based on +[CustomEvents](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/CustomEvent). + +Using EventEmitter: + +```typescript +import { EventEmitter } from "https://deno.land/x/evtemitter@1.0.0/mod.ts"; +import type { TypedCustomEvent } from "https://deno.land/x/evtemitter@1.0.0/mod.ts"; + +interface Events { + ping: TypedCustomEvent<"ping">; + pong: TypedCustomEvent< + "pong", + "pong" + >; + peng: TypedCustomEvent< + "peng", + { data: "peng" } + >; +} + +const emitter = new EventEmitter(); + +emitter.on(""); +``` diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..541a5c4 --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,7 @@ +{ + "fmt": { + "options": { + "indentWidth": 4 + } + } +} diff --git a/mod.ts b/mod.ts new file mode 100644 index 0000000..40593f0 --- /dev/null +++ b/mod.ts @@ -0,0 +1 @@ +export * from "./EventEmitter.ts";