{"id":15062501,"url":"https://github.com/michaelfranzl/captain-hook","last_synced_at":"2025-04-05T06:41:22.319Z","repository":{"id":26065270,"uuid":"108274341","full_name":"michaelfranzl/captain-hook","owner":"michaelfranzl","description":"Tiny configurable and isomorphic event emitter library for mixing into JavaScript objects","archived":false,"fork":false,"pushed_at":"2023-03-04T05:52:45.000Z","size":490,"stargazers_count":1,"open_issues_count":3,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-23T09:16:29.713Z","etag":null,"topics":["actions","eventemitter","filters","hooks","javascript","javascript-library","mixin","plugin-api"],"latest_commit_sha":null,"homepage":"","language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":"Unmaintained","scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/michaelfranzl.png","metadata":{"files":{"readme":"README.hbs","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2017-10-25T13:29:51.000Z","updated_at":"2021-08-13T17:24:01.000Z","dependencies_parsed_at":"2024-06-21T20:20:43.137Z","dependency_job_id":"2cac27ec-49c6-49fa-a1bc-fa925f546ea4","html_url":"https://github.com/michaelfranzl/captain-hook","commit_stats":null,"previous_names":[],"tags_count":5,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelfranzl%2Fcaptain-hook","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelfranzl%2Fcaptain-hook/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelfranzl%2Fcaptain-hook/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/michaelfranzl%2Fcaptain-hook/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/michaelfranzl","download_url":"https://codeload.github.com/michaelfranzl/captain-hook/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247299790,"owners_count":20916185,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["actions","eventemitter","filters","hooks","javascript","javascript-library","mixin","plugin-api"],"created_at":"2024-09-24T23:41:40.965Z","updated_at":"2025-04-05T06:41:22.286Z","avatar_url":"https://github.com/michaelfranzl.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# captain-hook\n\n![Test](https://github.com/michaelfranzl/captain-hook/workflows/Test/badge.svg)\n\n## Configurable event emitter behavior for mixing into JavaScript objects/prototypes/classes\n\nAn 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.\n\nMethods 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.\n\nThe name \"Captain Hook\" is a play on the term [\"Software Hook\"](https://en.wikipedia.org/wiki/Hooking).\n\n\n# Why invent yet another event emitter?\n\n* Attribute/method names are configurable\n* Returns to the event emitter return values from event handlers as an array\n* When adding event handlers, a supplied option object allows\n  * sorting the handler according to given priority,\n  * setting the `this` context of the handler,\n  * setting of a tag/label of the handler.\n* Event handlers can only be removed when their tag is known. Prevents interaction between subscribers.\n* 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).\n* Flexible use: add the mix-in to prototypes, plain objects, classes or to instances thereof (see below).\n* No dependencies.\n* Only ~100 lines of code.\n* Only ~2.4 kilobytes minified.\n* Works in browsers and in Node.js.\n* Extensive tests.\n\nThe [test file](tests/test.js) describes usage and features.\n\n\n# Development\n\nRun tests:\n\n```sh\nnpm test\n```\n\nGenerate `README.md` with API documentation parsed from `jsdoc` sources:\n\n```sh\nnode scripts/make_readme.cjs\n```\n\n# How to use\n\nThe default export of the module is a factory function (see `CaptainHook()` in the API section).\n\nThe following 5 methods are equivalent in their effects.\n\n## 1\\. Mix into prototypes\n\nMethods will be shared across all instances.\n\nIf you prefer classes:\n\n```javascript\ncaptain_hook = CaptainHook(); // use defaults\n\nclass Dog {\n  constructor(name) {\n    this.name = name;\n  }\n  poop() {\n    console.log(`I am pooping.`)\n    this._emit('poop');\n  }\n}\n\nObject.assign(Dog.prototype, captain_hook);\n\nluna = new Dog('Luna');\nluna.on('poop', function() { console.log(`Cleaning up poop of ${this.name}`); } )\nluna.poop();\n// -\u003e I am pooping\n// -\u003e Cleaning up poop of Luna\n\nelvis = new Dog('Elvis');\nelvis.on('poop', function() { console.log(\"Oh no, another dog pooped!\"); })\nelvis.poop();\n// -\u003e I am pooping\n// -\u003e Oh no, another dog pooped!\n```\n\nIf you prefer prototype functions:\n\n```javascript\ncaptain_hook = CaptainHook();\n\nfunction Dog(name) {\n  this.name = name;\n}\n\nObject.assign(Dog.prototype, captain_hook);\n\nDog.prototype.poop = function() {\n  console.log(`I am pooping.`)\n  this._emit('poop');\n}\n\nluna = new Dog('Luna');\nluna.on('poop', function() { console.log(`Cleaning up poop of ${this.name}`); } )\nluna.poop();\n// -\u003e I am pooping\n// -\u003e Cleaning up poop of Luna\n\nelvis = new Dog('Elvis');\nelvis.on('poop', function() { console.log(\"Oh no, another dog pooped!\"); })\nelvis.poop();\n// -\u003e I am pooping\n// -\u003e Oh no, another dog pooped!\n```\n\nIf you prefer to work with plain objects:\n\n```javascript\ncaptain_hook = CaptainHook();\n\nproto_dog = {};\nproto_dog.poop = function() {\n  console.log(`I am pooping.`);\n  this._emit('poop', this.name);\n}\n\nproto_eventful_dog = Object.assign(proto_dog, captain_hook);\n\n// create a new object from a prototype\nluna = Object.create(proto_eventful_dog);\nluna.name = 'Luna';\nluna.on('poop', function() { console.log(`Cleaning up poop of ${this.name}`); });\nluna.poop();\n// -\u003e I am pooping\n// -\u003e Cleaning up poop of Luna\n\n// create a new object from a prototype\nelvis = Object.create(proto_eventful_dog);\nelvis.name = 'Elvis';\nelvis.on('poop', function() { console.log(\"Oh no, another dog pooped!\"); });\nelvis.poop();\n// -\u003e I am pooping\n// -\u003e Oh no, another dog pooped!\n```\n\n## 2\\. Mix into instances\n\nEach instance will have a full copy of the attributes and methods.\n\nIn 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.\n\n```javascript\nclass Dog {\n  constructor(name) {\n    var captain_hook = CaptainHook({handlers_prop: null});\n    Object.assign(this, captain_hook);\n    this.name = name;\n  }\n  poop() {\n    console.log(`I am pooping.`)\n    this._emit('poop');\n  }\n}\n\nluna = new Dog('Luna');\nluna.on('poop', function() { console.log(`Cleaning up poop of ${this.name}`); })\nluna.poop();\n// -\u003e I am pooping\n// -\u003e Cleaning up poop of Luna\n\nelvis = new Dog('Elvis');\nelvis.on('poop', function() { console.log(\"Oh no, another dog pooped!\"); })\nelvis.poop();\n// -\u003e I am pooping\n// -\u003e Oh no, another dog pooped!\n\n// Note that there is no way to read or modify the added event handlers via the `luna` or `elvis` instances.\n```\n\nIf you prefer to work with plain objects:\n\n```javascript\ndog = {};\ndog.poop = function() {\n  console.log(`I am pooping.`);\n  this._emit('poop', this.name);\n}\n\nluna = Object.assign({}, CaptainHook({handlers_prop: null}), dog);\nluna.name = 'Luna';\nluna.on('poop', function() { console.log(`Cleaning up poop of ${this.name}`); });\nluna.poop();\n// -\u003e I am pooping\n// -\u003e Cleaning up poop of Luna\n\nelvis = Object.assign({}, CaptainHook({handlers_prop: null}), dog);\nelvis.on('poop', function() { console.log(\"Oh no, another dog pooped!\"); })\nelvis.poop();\n// -\u003e I am pooping\n// -\u003e Oh no, another dog pooped!\n\n// Note that there is no way to read or modify the added event handlers via the `luna` or `elvis` instances.\n```\n\n\n# API Reference\n\n{{\u003emain}}\n\n\n# Use cases\n\nThere are three distinct use cases for event handlers:\n\n1. Simple callbacks (simple data type arguments, no return value)\n2. Content filtering (arguments modified by reference, no return value)\n3. Queries (with return value)\n\nAll three cases can be covered with the `on()` method.\n\nTo illustrate, we are going to implement a simple Cat:\n\n```javascript\nvar Cat = function() {\n  var self = this; // be explicit\n\n  // Generate the mix-in object with default property names\n  var hook_mixin = CaptainHook();\n\n  // Mix in the generated hook functionality.\n  // This makes available to us self.on(), self.off(), self._emit()\n  Object.assign(self, hook_mixin);\n\n  self.makeSound = function() {\n    var obj = {sound: 'meow'};\n    self._emit('makeSound', obj);\n    console.log(`I make sound: \"${obj.sound}\"`);\n  };\n\n  self.scratch = function() {\n    var allowed = self._emit('scratch').reduce(function(acc, val) {\n      return acc \u0026\u0026 val\n    }, true);\n\n    // All event handlers need to return true if this action is to be allowed.\n    if (allowed) {\n      console.log(\"Scratch!\");\n    } else {\n      console.log(\"I am not allowed to scratch, so I won't do it!\");\n    }\n  };\n\n  self.beHungry = function() {\n    Promise.all(self._emit('askForFood'))\n    .then(function(given_foods) {\n      console.log(\"I am eating\", given_foods);\n    })\n  }\n};\n```\n\nInstantiate the application:\n\n```javascript\nvar felix = new Cat();\n```\n\nGeneric behavior:\n\n```javascript\nfelix.makeSound();\n// -\u003e I make sound: \"meow\"\n\nfelix.scratch();\n// -\u003e Scratch!\n```\n\nUse event handlers in three possible ways:\n\n**1\\. Simple observer** (no return value, no content filtering):\n\n```javascript\nfelix.on('makeSound', function() {\n  console.log(\"Felix is about to make a sound.\")\n});\n\nfelix.makeSound();\n\n// -\u003e Felix is about to make a sound.\n// -\u003e I make sound: \"meow\"\n```\n\n**2\\. Filter content passed by reference** (no return value):\n\n```javascript\nfelix.on('makeSound', function(opts) {\n  opts.sound += ' hiss';\n});\n\nfelix.makeSound();\n// -\u003e I make sound: \"meow hiss\"\n```\n\n**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:\n\n```javascript\nfelix.on('scratch', function() {\n  return false; // I do not allow scratching.\n});\n\nfelix.on('scratch', function() {\n  return true; // I allow scratching.\n});\n\nfelix.scratch();\n// -\u003e I am not allowed to scratch, so I won't do it!\n```\n\nThis is also useful for Promises:\n\n```javascript\nfelix.on('askForFood', function() {\n  console.log('Felix is asking for food');\n  return new Promise(function(resolve, reject) {\n    setTimeout(function() {\n      console.log('I am giving felix food');\n      resolve('dryfood');\n    }, 1000);\n  })\n});\n\nfelix.on('askForFood', function() {\n  console.log('Felix is asking for food');\n  return new Promise(function(resolve, reject) {\n    setTimeout(function() {\n      console.log('I am giving felix food');\n      resolve('sardines');\n    }, 2000);\n  })\n});\n\nfelix.beHungry()\n// after 2 seconds -\u003e I am eating [\"dryfood\", \"sardines\"]\n```\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichaelfranzl%2Fcaptain-hook","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmichaelfranzl%2Fcaptain-hook","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmichaelfranzl%2Fcaptain-hook/lists"}