Important: This documentation covers Yarn 1 (Classic).
For Yarn 2+ docs and migration guide, see yarnpkg.com.

Package detail

@thenja/event-manager

nathan-andosen77MIT2.0.1TypeScript support: included

Easily manager events in web applications, works great with Angular.

event, events, event emitter, publish, subscribe, event manager

readme

Test Coverage-shield-badge-1

Event Manager

Event manager is an easy way to manage events in a web applications. Its a basic class that you can use to add extra event functionality using methods like: emit(), on(), off() and so on.

Key features

  • Bind and unbind to events using a decorator
    • Reduce code by automatically binding and unbinding to events with a method decorator
    • Works great with Angular components, binds on the ngOnInit method and unbinds on the ngOnDestroy method
  • Run code after all event listeners have executed
    • Easily run code after all event listeners have completed. Works with async event listeners as well
  • Small & light weight
  • Inheritance or Composition
    • Use the EventManager class via inheritance or composition

How to use

  1. Install the module:

npm install @thenja/event-manager --save

  1. You have two ways to use the EventManager. Inheritance via extends or composition via property composition.

Inheritance:

import { EventManager, EventListener, INextFn } from '@thenja/event-manager';

// Important: event names have to be unique for the whole application
const USER_EVENTS = {
  SIGN_IN: 'user-sign-in'
};

export class UserService extends EventManager {
  constructor() {
    super(USER_EVENTS);
  }

  userSignIn() {
    this.emit(USER_EVENTS.SIGN_IN, {});
  }
}

// Now you could listen to events
userService.on(USER_EVENTS.SIGN_IN, () => {});
import { EventManager, EventListener, INextFn } from '@thenja/event-manager';

// Important: event names have to be unique for the whole application
const USER_EVENTS = {
  SIGN_IN: 'user-sign-in'
};

export class UserService {
  events = new EventManager(USER_EVENTS);

  userSignIn() {
    this.events.emit(USER_EVENTS.SIGN_IN, {});
  }
}

// Now you could listen to events
userService.events.on(USER_EVENTS.SIGN_IN, () => {});

Methods / API

constructor (emittedEvents: { [key: string]: string })

The possible emitted events that the manager can emit.

emit (eventName: string, data?: any)

Emit an event. The second parameter is the data that is passed to the listener function.

.on (eventName: string, fn: (data?: any, next?: INextFn) => void, scope?: any)

Bind to an event. If the emitted event sends data, it will be the first parameter.

.once (eventName: string, fn: (data?: any, next?: INextFn) => void, scope?: any)

Bind to an event once, the listener will only fire once.

.off (eventName: string, fn: (data?: any, next?: INextFn) => void)

Unbind from an event.

.offAll (eventName?: string)

Unbind from all events. If you pass in an eventName, it will only unbind all listeners for that event.

@EventListener decorator

The @EventListener decorator is a method decorator that will automatically subscribe and unsubscribe to events, its very useful in Angular components, but can be used anywhere.

IMPORTANT: If using the EventListener decorator, make event names unique throughout your app. If not, the EventListener decorator will throw an error.

How it works:

The decorator simple binds the event on an initialisation method, in the case of Angular, its the ngOnInit method. It unbinds the event on a destroy method, in the case of Angular, its the ngOnDestroy method.

The decorator can be used in two ways:

Two individual arguments:

@EventListener(eventName: string, classObject?: Object)
  • eventName : The name of the emitted event.
  • classObject: (optional) The class that is emitting the event. (view example 1 below in the Use cases / Examples section). If the listener method is listening to internal events that are emitted from within the same class, this can be left blank (view example 2 below in the Use cases / Examples section).

One object argument:

@EventListener(args: IEventListenerArgs)

// Example
@EventListener({
  eventName: 'user-sign-in',
  eventClass: UserService.name,
  initFn: 'ngOnInit',
  destroyFn: 'ngOnDestroy'
})
  • eventName : Same as above
  • eventClass : The constructor name of the class that is emitting the event.
  • initFn : [Default = ngOnInit] The function that is fired when the component / class is initialised. This is where binding to events will occur. If you want to bind to events when the constructor is fired, view example 3 below in the Use cases / Examples section.
  • destroyFn : [Default = ngOnDestroy] The function that is fired when the component is destroyed. This is where unbinding from events will occur.

Use cases / Examples

Example 1 - Listen to an event thats emitted from a service inside an Angular component:

In this example, we have a service that is injected into an Angular component, the service emits events, the component can bind to these events.

import { EventManager, EventListener, INextFn } from '@thenja/event-manager';

const USER_EVENTS = {
  SIGN_IN: 'user-sign-in'
};

// our service which extends EventManager
export class UserService extends EventManager {
  constructor() {
    super(USER_EVENTS);
  }

  userSignIn() {
    const userData = {};
    this.emit(USER_EVENTS.SIGN_IN, userData);
  }
}

// our angular component
@Component(...)
export class HomePageComponent {
  constructor(private userSrv: UserService) {}

  @EventListener(USER_EVENTS.SIGN_IN, UserService)
  userSignInListener(userData: any) {
    // This method will be fired when the user-sign-in event is emitted
  }
}

OR you could use composition like:

import { EventManager, EventListener, INextFn } from '@thenja/event-manager';

const USER_EVENTS = {
  SIGN_IN: 'user-sign-in'
};

// our service which extends EventManager
export class UserService {
  events: EventManager;

  constructor() {
    this.events = new EventManager(USER_EVENTS);
  }

  userSignIn() {
    const userData = {};
    this.events.emit(USER_EVENTS.SIGN_IN, userData);
  }
}

// our angular component
@Component(...)
export class HomePageComponent {
  constructor(private userSrv: UserService) {}

  @EventListener(USER_EVENTS.SIGN_IN, UserService)
  userSignInListener(userData: any) {
    // This method will be fired when the user-sign-in event is emitted
  }
}

Example 2 - Listen to internal events:

In this example, we will listen to internal events that are emitted within the same class.

import { EventManager, EventListener, INextFn } from '@thenja/event-manager';

const USER_EVENTS = {
  SIGN_IN: 'user-sign-in'
};

export class UserService extends EventManager {
  constructor() {
    super(USER_EVENTS);
  }

  userSignIn() {
    const userData = {};
    this.emit(USER_EVENTS.SIGN_IN, userData);
  }

  @EventListener(USER_EVENTS.SIGN_IN)
  userSignInListener(userData: any) {
    // This method will be fired when the user-sign-in event is emitted
  }
}

Example 3 - Bind to events inside constructor:

In this example, we set different init and destroy functions. In this case, we bind to events when the constructor is fired.

import { EventManager, EventListener, INextFn } from '@thenja/event-manager';

const USER_EVENTS = {
  SIGN_IN: 'user-sign-in'
};

export class UserService extends EventManager {
  constructor() {
    super(USER_EVENTS);
    this.init();
  }

  protected init();
  destroy();

  @EventListener({
    eventName: USER_EVENTS.SIGN_IN,
    initFn: 'init',
    destroyFn: 'destroy'
  })
  userSignInListener(userData: any) {
    // This method will now be bound to the event when the constructor fires
  }
}

Example 4 - Run code after all event listeners have completed execution:

In this example, we will run code after all event listeners have finished executing. In this example, we are not using the EventListener decorator, but you could still do the same with it.

import { EventManager, EventListener, INextFn } from '@thenja/event-manager';

const USER_EVENTS = {
  SIGN_OUT: 'user-sign-out'
};

// our user settings service
class UserSettingsService {
  constructor(private appEventsHub: AppEventsHub) {
    this.appEventsHub.on(USER_EVENTS.SIGN_OUT, this.userSignedOutListener, this);
  }

  private userSignedOutListener(data, next: INextFn) {
    setTimeout(() => {
      // fire the next function to indicate we are done
      next();
    }, 10);
  }
}

// our user service
class UserService {
  constructor(private appEventsHub: AppEventsHub) {
    this.appEventsHub.on(USER_EVENTS.SIGN_OUT, this.userSignedOutListener, this);
  }

  private userSignedOutListener(data, next: INextFn) {
    setTimeout(() => {
      // the next function returns a completed function, you can set a callback
      // function that gets fired when all listeners have completed and fired
      // their next() functions.
      next().completed(() => {
        // all listeners are done...
      });
    }, 5);
  }
}

// our app events hub service
class AppEventsHub extends EventManager {
  constructor() {
    super(USER_EVENTS);
  }

  userSignedOut() {
    this.emit(USER_EVENTS.SIGN_OUT);
  }
}

Example 5 - Setting the scope

Most of the time you will want to set the scope to this so that the keyword this inside your listener function points to your class instance.

const USER_EVENTS = {
  SIGN_OUT: 'user-sign-out'
};

class UserService {
  private userIsSignedIn = true;
  constructor(private appEventsHub: AppEventsHub) {
    // set this as the scope
    this.appEventsHub.on(USER_EVENTS.SIGN_OUT, this.userSignedOutListener, this);
  }

  private userSignedOutListener(data, next: INextFn) {
    this.userIsSignedIn = false;
  }
}

Development

npm run init - Setup the app for development (run once after cloning)

npm run dev - Run this command when you want to work on this app. It will compile typescript, run tests and watch for file changes.

Distribution

npm run build -- -v <version> - Create a distribution build of the app.

-v (version) - [Optional] Either "patch", "minor" or "major". Increase the version number in the package.json file.

The build command creates a /compiled directory which has all the javascript compiled code and typescript definitions. As well, a /dist directory is created that contains a minified javascript file.

Testing

Tests are automatically ran when you do a build.

npm run test - Run the tests. The tests will be ran in a nodejs environment. You can run the tests in a browser environment by opening the file /spec/in-browser/SpecRunner.html.

License

MIT © Nathan Anderson

changelog

[2.0.1] - 2020-01-29

Removed:

  • Removed the mixin decorator and helpers as they can no longer be used in version 2.x.x

Fixed:

  • Fixed error being thrown when properties of a class are set to undefined

2.0.0

Breaking changes

  • When initialising the EventManager class, you must supply an object of the possible events that can be emitted from the manager.
import { EventManager, EventListener, INextFn } from '@thenja/event-manager';

const USER_EVENTS = {
  SIGN_IN: 'user-sign-in'
};

export class UserService {
  events = new EventManager(USER_EVENTS);

  userSignIn() {
    this.events.emit(USER_EVENTS.SIGN_IN, {});
  }
}
  • The EventListener decorator's second parameter is now just the class object itself, not the name.
// Old, version: 1.x.x
@EventListener(USER_EVENTS.SIGN_IN, UserService.name)

// New, version: 2.x.x
@EventListener(USER_EVENTS.SIGN_IN, UserService)

Fixed:

  • Fix issue with having multiple properties of the same type on a class. The EventListener decorator could not find the correct one to subscribe too.

1.3.1

Important: When using with Angular, your components must implement ngOnInit and ngOnDestroy functions. See fixed issue below.

Fixed:

  • Fixed issue with Angular life cycle hooks not working with Decorators 16023, 31495.

1.3.0

Added:

  • Added the ability to set an events property which is set to the EventManager class
import { EventManager, EventListener, INextFn } from '@thenja/event-manager';

export class UserService {
  events = new EventManager();

  userSignIn() {
    this.events.emit('user-sign-in', {});
  }
}

1.2.0

Added:

  • Added mixin decorator for better composition, updated docs

1.1.0

Added:

  • Added ability to use composition via Typescript mixins

Fixed:

  • Fixed issue with Typescript mixins
  • Refactor and improve unit tests.

1.0.0

First release.