Tiny configurable and isomorphic event emitter library for mixing into JavaScript objects
MIT License
An event emitter API clearly defines interaction between separate pieces of code (e.g. main application vs. plugins). Event emitting allows you to keep the functionality of your application general (make it more suitable to be published Open Source), while external (perhaps even proprietary) code makes the application's behavior more specific.
Methods of your objects will be able to emit "events" to external "event handlers". External code can add event handlers via .on()
and remove them via .off()
, while your own object can call them via ._emit()
. The names of these three methods can be explicitly configured via the factory function.
The name "Captain Hook" is a play on the term "Software Hook".
this
context of the handler,The test file describes usage and features.
Run tests:
npm test
Generate README.md
with API documentation parsed from jsdoc
sources:
node scripts/make_readme.cjs
The default export of the module is a factory function (see CaptainHook()
in the API section).
The following 5 methods are equivalent in their effects.
Methods will be shared across all instances.
If you prefer classes:
captain_hook = CaptainHook(); // use defaults
class Dog {
constructor(name) {
this.name = name;
}
poop() {
console.log(`I am pooping.`)
this._emit('poop');
}
}
Object.assign(Dog.prototype, captain_hook);
luna = new Dog('Luna');
luna.on('poop', function() { console.log(`Cleaning up poop of ${this.name}`); } )
luna.poop();
// -> I am pooping
// -> Cleaning up poop of Luna
elvis = new Dog('Elvis');
elvis.on('poop', function() { console.log("Oh no, another dog pooped!"); })
elvis.poop();
// -> I am pooping
// -> Oh no, another dog pooped!
If you prefer prototype functions:
captain_hook = CaptainHook();
function Dog(name) {
this.name = name;
}
Object.assign(Dog.prototype, captain_hook);
Dog.prototype.poop = function() {
console.log(`I am pooping.`)
this._emit('poop');
}
luna = new Dog('Luna');
luna.on('poop', function() { console.log(`Cleaning up poop of ${this.name}`); } )
luna.poop();
// -> I am pooping
// -> Cleaning up poop of Luna
elvis = new Dog('Elvis');
elvis.on('poop', function() { console.log("Oh no, another dog pooped!"); })
elvis.poop();
// -> I am pooping
// -> Oh no, another dog pooped!
If you prefer to work with plain objects:
captain_hook = CaptainHook();
proto_dog = {};
proto_dog.poop = function() {
console.log(`I am pooping.`);
this._emit('poop', this.name);
}
proto_eventful_dog = Object.assign(proto_dog, captain_hook);
// create a new object from a prototype
luna = Object.create(proto_eventful_dog);
luna.name = 'Luna';
luna.on('poop', function() { console.log(`Cleaning up poop of ${this.name}`); });
luna.poop();
// -> I am pooping
// -> Cleaning up poop of Luna
// create a new object from a prototype
elvis = Object.create(proto_eventful_dog);
elvis.name = 'Elvis';
elvis.on('poop', function() { console.log("Oh no, another dog pooped!"); });
elvis.poop();
// -> I am pooping
// -> Oh no, another dog pooped!
Each instance will have a full copy of the attributes and methods.
In the example below, note that we pass the configuration handlers_prop: null
. This makes the storage of the event handler functions truly private, preventing information leaks to external code.
class Dog {
constructor(name) {
var captain_hook = CaptainHook({handlers_prop: null});
Object.assign(this, captain_hook);
this.name = name;
}
poop() {
console.log(`I am pooping.`)
this._emit('poop');
}
}
luna = new Dog('Luna');
luna.on('poop', function() { console.log(`Cleaning up poop of ${this.name}`); })
luna.poop();
// -> I am pooping
// -> Cleaning up poop of Luna
elvis = new Dog('Elvis');
elvis.on('poop', function() { console.log("Oh no, another dog pooped!"); })
elvis.poop();
// -> I am pooping
// -> Oh no, another dog pooped!
// Note that there is no way to read or modify the added event handlers via the `luna` or `elvis` instances.
If you prefer to work with plain objects:
dog = {};
dog.poop = function() {
console.log(`I am pooping.`);
this._emit('poop', this.name);
}
luna = Object.assign({}, CaptainHook({handlers_prop: null}), dog);
luna.name = 'Luna';
luna.on('poop', function() { console.log(`Cleaning up poop of ${this.name}`); });
luna.poop();
// -> I am pooping
// -> Cleaning up poop of Luna
elvis = Object.assign({}, CaptainHook({handlers_prop: null}), dog);
elvis.on('poop', function() { console.log("Oh no, another dog pooped!"); })
elvis.poop();
// -> I am pooping
// -> Oh no, another dog pooped!
// Note that there is no way to read or modify the added event handlers via the `luna` or `elvis` instances.
{{>main}}
There are three distinct use cases for event handlers:
All three cases can be covered with the on()
method.
To illustrate, we are going to implement a simple Cat:
var Cat = function() {
var self = this; // be explicit
// Generate the mix-in object with default property names
var hook_mixin = CaptainHook();
// Mix in the generated hook functionality.
// This makes available to us self.on(), self.off(), self._emit()
Object.assign(self, hook_mixin);
self.makeSound = function() {
var obj = {sound: 'meow'};
self._emit('makeSound', obj);
console.log(`I make sound: "${obj.sound}"`);
};
self.scratch = function() {
var allowed = self._emit('scratch').reduce(function(acc, val) {
return acc && val
}, true);
// All event handlers need to return true if this action is to be allowed.
if (allowed) {
console.log("Scratch!");
} else {
console.log("I am not allowed to scratch, so I won't do it!");
}
};
self.beHungry = function() {
Promise.all(self._emit('askForFood'))
.then(function(given_foods) {
console.log("I am eating", given_foods);
})
}
};
Instantiate the application:
var felix = new Cat();
Generic behavior:
felix.makeSound();
// -> I make sound: "meow"
felix.scratch();
// -> Scratch!
Use event handlers in three possible ways:
1. Simple observer (no return value, no content filtering):
felix.on('makeSound', function() {
console.log("Felix is about to make a sound.")
});
felix.makeSound();
// -> Felix is about to make a sound.
// -> I make sound: "meow"
2. Filter content passed by reference (no return value):
felix.on('makeSound', function(opts) {
opts.sound += ' hiss';
});
felix.makeSound();
// -> I make sound: "meow hiss"
3. Query responses. Note that event handlers do not have access to the return values of any other event handler. Here, we define two event handlers who vote for different outcomes:
felix.on('scratch', function() {
return false; // I do not allow scratching.
});
felix.on('scratch', function() {
return true; // I allow scratching.
});
felix.scratch();
// -> I am not allowed to scratch, so I won't do it!
This is also useful for Promises:
felix.on('askForFood', function() {
console.log('Felix is asking for food');
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('I am giving felix food');
resolve('dryfood');
}, 1000);
})
});
felix.on('askForFood', function() {
console.log('Felix is asking for food');
return new Promise(function(resolve, reject) {
setTimeout(function() {
console.log('I am giving felix food');
resolve('sardines');
}, 2000);
})
});
felix.beHungry()
// after 2 seconds -> I am eating ["dryfood", "sardines"]