Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature: @on decorator #3

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
88 changes: 88 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<MyEvents>();

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)
130 changes: 122 additions & 8 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,25 @@ export interface EventHandlers {
[key: string]: Map<Function, boolean>
}

export default class EventManagment {
export interface IEventMananger<EventNames extends string = string> {
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<EventNames extends string = string> implements IEventMananger<EventNames> {
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();
}
Expand All @@ -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;
}
Expand All @@ -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);
Expand All @@ -59,3 +77,99 @@ export default class EventManagment {
public unsubscribe = this.off
///
}

type EventDecoratorOptions = {
placement: 'before' | 'after' | 'promise' | 'callback';
};

type FunctionalKeys<T extends object> = {
[key in keyof T]: T[key] extends Function ? key : never;
}[keyof T];

export function on<
T extends new (...args: any[]) => any,
Name extends FunctionalKeys<InstanceType<T>> = FunctionalKeys<InstanceType<T>>
>(target: T, name: Name, options?: EventDecoratorOptions): MethodDecorator;

export function on<
T extends object,
Name extends FunctionalKeys<T> = FunctionalKeys<T>
>(target: T, name: Name, options?: EventDecoratorOptions): MethodDecorator;

export function on<
T extends EventManager
>(target: T, name: T extends EventManager<infer U> ? U : FunctionalKeys<T>): MethodDecorator;

export function on<
T extends new (...args: any[]) => any,
Name extends FunctionalKeys<InstanceType<T>> = FunctionalKeys<InstanceType<T>>
>(
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<T>[Name] extends Function ? MethodDecorator : never;
}
}
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,6 @@
},
"homepage": "https://github.com/kaskar2008/js-simple-events#readme",
"devDependencies": {
"typescript": "^2.7.2"
"typescript": "^2.9.2"
}
}
13 changes: 1 addition & 12 deletions tsconfig.cjs.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
13 changes: 1 addition & 12 deletions tsconfig.es5.json
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
13 changes: 11 additions & 2 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -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": "./"
}
}
37 changes: 37 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
export interface EventHandlers {
[key: string]: Map<Function, boolean>;
}
export interface IEventMananger<EventNames extends string = string> {
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<EventNames extends string = string> implements IEventMananger<EventNames> {
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<string>;
subscribe: (eventName: EventNames, callback: Function) => EventManager<string>;
remove: (eventName: EventNames, callback: Function) => EventManager<string>;
unsubscribe: (eventName: EventNames, callback: Function) => EventManager<string>;
}
declare type EventDecoratorOptions = {
placement: 'before' | 'after' | 'promise' | 'callback';
};
declare type FunctionalKeys<T extends object> = {
[key in keyof T]: T[key] extends Function ? key : never;
}[keyof T];
export declare function on<T extends new (...args: any[]) => any, Name extends FunctionalKeys<InstanceType<T>> = FunctionalKeys<InstanceType<T>>>(target: T, name: Name, options?: EventDecoratorOptions): MethodDecorator;
export declare function on<T extends object, Name extends FunctionalKeys<T> = FunctionalKeys<T>>(target: T, name: Name, options?: EventDecoratorOptions): MethodDecorator;
export declare function on<T extends EventManager>(target: T, name: T extends EventManager<infer U> ? U : FunctionalKeys<T>): MethodDecorator;
export {};