-
Notifications
You must be signed in to change notification settings - Fork 1
/
Copy pathREADME.hbs
345 lines (257 loc) · 8.94 KB
/
README.hbs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
# captain-hook
![Test](https://github.com/michaelfranzl/captain-hook/workflows/Test/badge.svg)
## Configurable event emitter behavior for mixing into JavaScript objects/prototypes/classes
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"](https://en.wikipedia.org/wiki/Hooking).
# Why invent yet another event emitter?
* Attribute/method names are configurable
* Returns to the event emitter return values from event handlers as an array
* When adding event handlers, a supplied option object allows
* sorting the handler according to given priority,
* setting the `this` context of the handler,
* setting of a tag/label of the handler.
* Event handlers can only be removed when their tag is known. Prevents interaction between subscribers.
* The storage object for event handlers and their options can be privately scoped if needed. This is to ensure that external plugins cannot remove or inspect each other's event handlers (privacy).
* Flexible use: add the mix-in to prototypes, plain objects, classes or to instances thereof (see below).
* No dependencies.
* Only ~100 lines of code.
* Only ~2.4 kilobytes minified.
* Works in browsers and in Node.js.
* Extensive tests.
The [test file](tests/test.js) describes usage and features.
# Development
Run tests:
```sh
npm test
```
Generate `README.md` with API documentation parsed from `jsdoc` sources:
```sh
node scripts/make_readme.cjs
```
# How to use
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.
## 1\. Mix into prototypes
Methods will be shared across all instances.
If you prefer classes:
```javascript
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:
```javascript
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:
```javascript
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!
```
## 2\. Mix into instances
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.
```javascript
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:
```javascript
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.
```
# API Reference
{{>main}}
# Use cases
There are three distinct use cases for event handlers:
1. Simple callbacks (simple data type arguments, no return value)
2. Content filtering (arguments modified by reference, no return value)
3. Queries (with return value)
All three cases can be covered with the `on()` method.
To illustrate, we are going to implement a simple Cat:
```javascript
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:
```javascript
var felix = new Cat();
```
Generic behavior:
```javascript
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):
```javascript
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):
```javascript
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:
```javascript
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:
```javascript
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"]
```