diff --git a/README.md b/README.md index 2e449e0..3ffe285 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,94 @@ eventManager.emit('test', 'Hello!'); // (The 'once' handler isn't fired) ``` +## Typed events + +Additionally, it's possible to define types for event names using TypeScript and generics: + +```ts +import EventManager from 'js-simple-events' + +type MyEvents = 'click' | 'hover'; + +const eventManager = new EventManager(); + +eventManager.emit('click') // event name now autocompletes as either 'click' or 'hover' +``` + +## `@on` decorator + +This library also includes a handy `@on` decorator, that allows to bind methods of your class as listeners to `EventManager` events or method calls of any object or class! + +Examples: +```ts +import EventManager, { on } from 'js-simple-events' + +class Other extends EventManager { + test(callback) { + console.log('test'); + + setTimeout(callback, 1000); + } + + testPromiseResolve() { + return Promise.resolve('test'); + } + + testPromiseReject() { + return Promise.reject('test'); + } +} + +const other = new Other(); + +class Test { + @on(Other, 'test') + onBeforeTest() { + console.log('Other.test is called', Array.from(arguments)) + } + + @on(Other, 'test', { placement: 'after' }) + onAfterTest() { + console.log('Other.test was called', Array.from(arguments)) + } + + @on(Other, 'test', { placement: 'callback' }) + cbTest() { + console.log('Other.test callback is called', Array.from(arguments)) + } + + @on(Other, 'testPromiseResolve', { placement: 'promise' }) + onTestResolve() { + console.log('Other.testPromiseResolve is settled', Array.from(arguments)) + } + + @on(Other, 'testPromiseReject', { placement: 'promise' }) + onTestReject() { + console.log('Other.testPromiseReject is settled', Array.from(arguments)) + } + + @on(other, 'test') + onTestEvent() { + console.log('"test" event handler called') + } +} + +const test = new Test(); + +other.test(() => console.log('after you')); +// => Other.test is called >[ƒ] +// => test +// => Other.test was called >[ƒ] +// *1 second pause* +// => Other.test callback is called >[] +// => after you + +other.emit('test'); +// => "test" event handler called +``` + +--- + ## Plugins ### [For Vue.js](https://github.com/kaskar2008/vue-simple-events) diff --git a/index.ts b/index.ts index 15d5b41..b779f6f 100644 --- a/index.ts +++ b/index.ts @@ -2,10 +2,25 @@ export interface EventHandlers { [key: string]: Map } -export default class EventManagment { +export interface IEventMananger { + on(eventName: EventNames, callback: Function): EventManager; + listen(eventName: EventNames, callback: Function): EventManager; + subscribe(eventName: EventNames, callback: Function): EventManager; + + once(eventName: EventNames, callback: Function): boolean; + + off(eventName: EventNames, callback: Function): EventManager; + remove(eventName: EventNames, callback: Function): EventManager; + unsubscribe(eventName: EventNames, callback: Function): EventManager; + + emit(eventName: EventNames, ...args: any[]): void; + fire(eventName: EventNames, ...args: any[]): void; +} + +export default class EventManager implements IEventMananger { private eventHandlersMap: EventHandlers = {} - private addEventHandler(eventName: string, callback: Function, isOnce: boolean = false) { + private addEventHandler(eventName: EventNames, callback: Function, isOnce: boolean = false) { if (!this.eventHandlersMap[eventName]) { this.eventHandlersMap[eventName] = new Map(); } @@ -15,17 +30,20 @@ export default class EventManagment { } } - public on(eventName: string, callback: Function): EventManagment { + public on(eventName: EventNames, callback: Function): EventManager { this.addEventHandler(eventName, callback) return this; } - public once(eventName: string, callback: Function): EventManagment { - this.addEventHandler(eventName, callback, true) - return this; + public once(eventName: EventNames, callback: Function): boolean { + this.addEventHandler(eventName, (...args: any[]) => { + callback(...args); + this.off(eventName, callback); + }) + return true; } - public off(eventName: string, callback: Function): EventManagment { + public off(eventName: EventNames, callback: Function): EventManager { if (!this.eventHandlersMap[eventName]) { return this; } @@ -38,7 +56,7 @@ export default class EventManagment { return this; } - public emit(eventName: string, ...args): void { + public emit(eventName: EventNames, ...args: any[]): void { if (this.eventHandlersMap[eventName]) { this.eventHandlersMap[eventName].forEach((value: boolean, handler: Function) => { handler && handler(...args); @@ -59,3 +77,99 @@ export default class EventManagment { public unsubscribe = this.off /// } + +type EventDecoratorOptions = { + placement: 'before' | 'after' | 'promise' | 'callback'; +}; + +type FunctionalKeys = { + [key in keyof T]: T[key] extends Function ? key : never; +}[keyof T]; + +export function on< + T extends new (...args: any[]) => any, + Name extends FunctionalKeys> = FunctionalKeys> +>(target: T, name: Name, options?: EventDecoratorOptions): MethodDecorator; + +export function on< + T extends object, + Name extends FunctionalKeys = FunctionalKeys +>(target: T, name: Name, options?: EventDecoratorOptions): MethodDecorator; + +export function on< + T extends EventManager +>(target: T, name: T extends EventManager ? U : FunctionalKeys): MethodDecorator; + +export function on< + T extends new (...args: any[]) => any, + Name extends FunctionalKeys> = FunctionalKeys> +>( + decoTarget: T, name: Name, options?: EventDecoratorOptions +): MethodDecorator { + if (decoTarget instanceof EventManager) { + return function (target, key, _) { + decoTarget.on(name, target[key]); + }; + } + + let obj; + + if (typeof decoTarget === 'function') { + obj = decoTarget.prototype; + } else { + obj = decoTarget; + } + + if (typeof obj !== 'undefined' && typeof obj[name] === 'function') { + const temp = obj[name]; + + return function (target, key, _) { + if (!options || options.placement === 'before') { + obj[name] = function () { + target[key].apply(target, arguments); + return temp.apply(this, arguments); + }; + } + + else if (options.placement === 'after') { + obj[name] = function () { + const res = temp.apply(this, arguments); + target[key].apply(target, arguments); + + return res; + }; + } + + else if (options.placement === 'callback') { + obj[name] = function (...args: any[]) { + const cb = args.pop(); + + return temp.apply(this, args.concat([function () { + target[key].apply(target, arguments); + return cb(arguments); + }])) + } + } + + else if (options.placement === 'promise' && typeof Promise !== 'undefined') { + obj[name] = function () { + const result = temp.apply(this, arguments); + + if (result instanceof Promise) { + const settled = function (res) { + target[key].apply(target, [res]); + + return res; + } + + return result + .then(settled) + .catch(settled); + } + + return result; + }; + } + } as InstanceType[Name] extends Function ? MethodDecorator : never; + } +} diff --git a/package-lock.json b/package-lock.json index dbc1652..06e70f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,9 +5,9 @@ "requires": true, "dependencies": { "typescript": { - "version": "2.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.7.2.tgz", - "integrity": "sha512-p5TCYZDAO0m4G344hD+wx/LATebLWZNkkh2asWUFqSsD2OrDNhbAHuSjobrmsUmdzjJjEeZVU9g1h3O6vpstnw==", + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-2.9.2.tgz", + "integrity": "sha512-Gr4p6nFNaoufRIY4NMdpQRNmgxVIGMs4Fcu/ujdYk3nAZqk7supzBE9idmvfZIlH/Cuj//dvi+019qEue9lV0w==", "dev": true } } diff --git a/package.json b/package.json index a44ec86..ff8305d 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,6 @@ }, "homepage": "https://github.com/kaskar2008/js-simple-events#readme", "devDependencies": { - "typescript": "^2.7.2" + "typescript": "^2.9.2" } } diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json index 9981f70..9b5f719 100644 --- a/tsconfig.cjs.json +++ b/tsconfig.cjs.json @@ -1,19 +1,8 @@ { + "extends": "./tsconfig.json", "compilerOptions": { - "target": "es5", - "lib": [ - "es5", - "es2015" - ], "outDir": "./cjs", - "moduleResolution": "node", "module": "commonjs", - "alwaysStrict": true, - "allowSyntheticDefaultImports": true, - "experimentalDecorators": true, - "noUnusedParameters": true, - "emitDecoratorMetadata": true, - "rootDir": "./" }, "exclude": [ "./node_modules" diff --git a/tsconfig.es5.json b/tsconfig.es5.json index 7f886aa..71c90f4 100644 --- a/tsconfig.es5.json +++ b/tsconfig.es5.json @@ -1,19 +1,8 @@ { + "extends": "./tsconfig.json", "compilerOptions": { - "target": "es5", - "lib": [ - "es5", - "es2015" - ], "outDir": "./es5", - "moduleResolution": "node", "module": "es2015", - "alwaysStrict": true, - "allowSyntheticDefaultImports": true, - "experimentalDecorators": true, - "noUnusedParameters": true, - "emitDecoratorMetadata": true, - "rootDir": "./" }, "exclude": [ "./node_modules" diff --git a/tsconfig.json b/tsconfig.json index 07e2268..899298c 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,10 +1,19 @@ { "compilerOptions": { - "noEmit": true, + "target": "es5", "lib": [ "es5", "es2015" ], - "types": ["./types"] + "moduleResolution": "node", + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "emitDeclarationOnly": true, + "declaration": true, + "declarationDir": "./types", + "alwaysStrict": true, + "allowSyntheticDefaultImports": true, + "noUnusedParameters": true, + "rootDir": "./" } } diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..0a44ba8 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,37 @@ +export interface EventHandlers { + [key: string]: Map; +} +export interface IEventMananger { + on(eventName: EventNames, callback: Function): EventManager; + listen(eventName: EventNames, callback: Function): EventManager; + subscribe(eventName: EventNames, callback: Function): EventManager; + once(eventName: EventNames, callback: Function): boolean; + off(eventName: EventNames, callback: Function): EventManager; + remove(eventName: EventNames, callback: Function): EventManager; + unsubscribe(eventName: EventNames, callback: Function): EventManager; + emit(eventName: EventNames, ...args: any[]): void; + fire(eventName: EventNames, ...args: any[]): void; +} +export default class EventManager implements IEventMananger { + private eventHandlersMap; + private addEventHandler; + on(eventName: EventNames, callback: Function): EventManager; + once(eventName: EventNames, callback: Function): boolean; + off(eventName: EventNames, callback: Function): EventManager; + emit(eventName: EventNames, ...args: any[]): void; + fire: (eventName: EventNames, ...args: any[]) => void; + listen: (eventName: EventNames, callback: Function) => EventManager; + subscribe: (eventName: EventNames, callback: Function) => EventManager; + remove: (eventName: EventNames, callback: Function) => EventManager; + unsubscribe: (eventName: EventNames, callback: Function) => EventManager; +} +declare type EventDecoratorOptions = { + placement: 'before' | 'after' | 'promise' | 'callback'; +}; +declare type FunctionalKeys = { + [key in keyof T]: T[key] extends Function ? key : never; +}[keyof T]; +export declare function on any, Name extends FunctionalKeys> = FunctionalKeys>>(target: T, name: Name, options?: EventDecoratorOptions): MethodDecorator; +export declare function on = FunctionalKeys>(target: T, name: Name, options?: EventDecoratorOptions): MethodDecorator; +export declare function on(target: T, name: T extends EventManager ? U : FunctionalKeys): MethodDecorator; +export {};