├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .travis.yml ├── EventClass.js ├── README.md ├── config.js ├── package.json └── test ├── es6-test-setup.js └── test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "rules": { 3 | "indent": [ 4 | 2, 5 | 4 6 | ], 7 | "quotes": [ 8 | 2, 9 | "single" 10 | ], 11 | "linebreak-style": [ 12 | 2, 13 | "unix" 14 | ], 15 | "semi": [ 16 | 2, 17 | "always" 18 | ] 19 | }, 20 | "env": { 21 | "es6": true, 22 | "node": true, 23 | "browser": true 24 | }, 25 | "extends": "eslint:recommended", 26 | "ecmaFeatures": { 27 | "modules": true 28 | } 29 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | 7 | # Standard to msysgit 8 | *.doc diff=astextplain 9 | *.DOC diff=astextplain 10 | *.docx diff=astextplain 11 | *.DOCX diff=astextplain 12 | *.dot diff=astextplain 13 | *.DOT diff=astextplain 14 | *.pdf diff=astextplain 15 | *.PDF diff=astextplain 16 | *.rtf diff=astextplain 17 | *.RTF diff=astextplain 18 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Windows image file caches 2 | Thumbs.db 3 | ehthumbs.db 4 | 5 | # Folder config file 6 | Desktop.ini 7 | 8 | # Recycle Bin used on file shares 9 | $RECYCLE.BIN/ 10 | 11 | # Windows Installer files 12 | *.cab 13 | *.msi 14 | *.msm 15 | *.msp 16 | 17 | # Windows shortcuts 18 | *.lnk 19 | 20 | # ========================= 21 | # Operating System Files 22 | # ========================= 23 | 24 | # OSX 25 | # ========================= 26 | 27 | .DS_Store 28 | .AppleDouble 29 | .LSOverride 30 | 31 | # Thumbnails 32 | ._* 33 | 34 | # Files that might appear on external disk 35 | .Spotlight-V100 36 | .Trashes 37 | 38 | # Directories potentially created on remote AFP share 39 | .AppleDB 40 | .AppleDesktop 41 | Network Trash Folder 42 | Temporary Items 43 | .apdisk 44 | 45 | # Package files 46 | jspm_packages/* 47 | node_modules/* 48 | npm-debug.log 49 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | before_script: 5 | - "jspm config registries.github.auth $GH_TOKEN" 6 | - "jspm install" 7 | -------------------------------------------------------------------------------- /EventClass.js: -------------------------------------------------------------------------------- 1 | const multiChannelSep = /(?:,|\s)+/g; 2 | const channelSep = /:+/g; 3 | const channelsSymbol = Symbol('channels'); 4 | 5 | class EventClass { 6 | constructor(){ 7 | this[channelsSymbol] = {}; 8 | } 9 | 10 | _getChannels(channelString){ 11 | return channelString.trim().split(multiChannelSep); 12 | } 13 | 14 | _getNameSpaces(channel){ 15 | let namespaces = []; 16 | let splittedChannels = channel.trim().split(channelSep); 17 | 18 | for (let i = splittedChannels.length; i >= 1; i--) { 19 | namespaces.push(splittedChannels.slice(0, i).join(':')); 20 | } 21 | 22 | return namespaces; 23 | } 24 | 25 | trigger(event, data){ 26 | let channels = this._getChannels(event); 27 | 28 | for (let channel of channels){ 29 | let namespaces = this._getNameSpaces(channel); 30 | for (let namespace of namespaces){ 31 | if(!this[channelsSymbol][namespace]){ 32 | continue; 33 | } 34 | 35 | for(let callback of this[channelsSymbol][namespace]){ 36 | callback.call(this, data); 37 | } 38 | } 39 | } 40 | } 41 | 42 | on(event, callback){ 43 | let channels = this._getChannels(event); 44 | 45 | for (let channel of channels){ 46 | if(!this[channelsSymbol][channel]){ 47 | this[channelsSymbol][channel] = []; 48 | } 49 | 50 | this[channelsSymbol][channel].push(callback); 51 | } 52 | } 53 | 54 | off(event, callback){ 55 | let channels = this._getChannels(event); 56 | 57 | for (let channel of channels){ 58 | if(!this[channelsSymbol][channel]){ 59 | return; 60 | } 61 | 62 | let index = this[channelsSymbol][channel].indexOf(callback); 63 | 64 | if(index > -1){ 65 | this[channelsSymbol][channel].splice(index, 1); 66 | } 67 | } 68 | } 69 | 70 | once(event, callback){ 71 | function offCallback(){ 72 | this.off(event, callback); 73 | this.off(event, offCallback); 74 | } 75 | 76 | this.on(event, callback); 77 | this.on(event, offCallback); 78 | } 79 | } 80 | 81 | export default EventClass; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/sroucheray/event-class.svg?branch=master)](https://travis-ci.org/sroucheray/event-class) 2 | # Easy JavaScript/ES6 Events 3 | 4 | Trigger and listen to events the ES6 way. 5 | 6 | This script is an ES6 `module`. It exports a simple ES6 `class`. 7 | 8 | ## API 9 | 10 | The `class` provided in this `module` can be directly instantiated or can extend your own class. 11 | 12 | ```javascript 13 | import EventClass from "event-class"; 14 | 15 | class AnyClass extends EventClass{} 16 | 17 | let anyObject = new AnyClass(); 18 | let otherObject = new EventClass(); 19 | ``` 20 | 21 | The `EventClass` provides only four methods to its instances `on` to register handlers and its counterpart `trigger` to emit events, `once` similar to `on` but for one time only and `off` to stop listening to a specific event. 22 | 23 | ### .on(`event`, `callback`) 24 | --- 25 | Attaches the `callback` to the `event` triggering. 26 | 27 | `event` is a string representing one or several events separated by space or coma. 28 | Examples of valid events are : 29 | * `"init"` 30 | * `"change"` 31 | * `"init change"` 32 | 33 | Each event can be more specific using colons. In this case you create event channels. 34 | Other valid events are : 35 | * `"change:name"` 36 | * `"change:attribute:gender"` 37 | 38 | When listening to an event you listen also to all the channels of this event. By listening to `"change"`, you'll be notified when `"change:name"` and `"change:attribute:gender"` are triggered. By listening to `"change:attribute"` you won't be notified when `"change:name"` is triggered. 39 | 40 | You can mix channels and multiple events. 41 | Other valid events are : 42 | * `"init change:name"` 43 | * `"change:name change:attribute:gender"` 44 | 45 |
46 | `callback` is a function called when the listened event is triggered. 47 | If multiple callbacks listen to the same event they are called in order. `callback` as a single arguments, the data passed to the `trigger` method. 48 | 49 | 50 | ### .trigger(`event`, `data`) 51 | --- 52 | `event` is a string representing one or several events separated by space or coma. 53 | The `event` string has the same caracteristics as for the `on` method. 54 |

55 | `data` can be anything and will be passed to the callback handlers. 56 | 57 | ### .once(`event`, `callback`) 58 | --- 59 | Idem as `on` but is `off`ed after the first trigger. 60 | 61 | ### .off(`event`, `callback`) 62 | --- 63 | Detaches the `callback` from the event triggering. 64 | 65 | `event` is a string representing one or several events separated by space or coma. 66 | The `event` string has the same caracteristics as for the `on` method. 67 |

68 | `callback` is the function used by `on` or `one`. 69 | 70 | ## Example 71 | 72 | ```javascript 73 | import EventClass from "event-class"; 74 | 75 | // Extends 76 | class AnyClass extends EventClass{ 77 | } 78 | 79 | let anyObject = new AnyClass(); 80 | 81 | function namedFunction(data){ 82 | console.log("change event :", data); 83 | } 84 | 85 | // Listen to the 'change' event 86 | anyObject.on("change", namedFunction); 87 | 88 | // Listen once to the 'change:attribute' event 89 | anyObject.once("change:attribute", function(data){ 90 | console.log("change:attribute event :", data); 91 | }); 92 | 93 | anyObject.trigger("change:attribute", "Hello 1 !"); 94 | anyObject.trigger("change:attribute", "Hello 2 !"); 95 | anyObject.off("change", namedFunction); 96 | anyObject.trigger("change:attribute", "Hello 3 !"); 97 | 98 | 99 | 100 | /* console output 101 | > change:attribute event : Hello 1 ! 102 | > change event : Hello 1 ! 103 | > change event : Hello 2 ! 104 | 105 | No output with "Hello 3 !" because there is no listener anymore 106 | */ 107 | 108 | /* How to listen to or to trigger several events at the same time */ 109 | // Space separated events style 110 | anyObject.on("change:attribute change:value ping"); 111 | anyObject.trigger("change:attribute change:value ping"); 112 | 113 | // Coma separated events style 114 | anyObject.on("change:attribute, change:value, ping"); 115 | anyObject.trigger("change:attribute, change:value, ping"); 116 | ``` 117 | 118 | ## Installation 119 | 120 | Use [jspm](http://jspm.io/) to eases the use of ES6 features, the package is installed from the npm registry 121 | 122 | ```bash 123 | jspm install npm:event-class 124 | ``` 125 | or simply use npm 126 | 127 | ```bash 128 | npm install event-class --save 129 | ``` -------------------------------------------------------------------------------- /config.js: -------------------------------------------------------------------------------- 1 | System.config({ 2 | "baseURL": "/", 3 | "transpiler": "traceur", 4 | "paths": { 5 | "*": "*.js", 6 | "github:*": "jspm_packages/github/*.js", 7 | "npm:*": "jspm_packages/npm/*.js" 8 | } 9 | }); 10 | 11 | System.config({ 12 | "map": { 13 | "jspm": "jspm_packages/system", 14 | "traceur": "github:jmcriffey/bower-traceur@0.0.88", 15 | "traceur-runtime": "github:jmcriffey/bower-traceur-runtime@0.0.88" 16 | } 17 | }); 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "event-class", 3 | "version": "0.1.3", 4 | "description": "A lightweight Event class defined in small module (JavaScript/ES6)", 5 | "main": "EventClass.js", 6 | "directories": { 7 | "test": "test" 8 | }, 9 | "scripts": { 10 | "test": "mocha test/test" 11 | }, 12 | "files": [ 13 | "EventClass.js" 14 | ], 15 | "format": "es6", 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/sroucheray/event-class.git" 19 | }, 20 | "keywords": [ 21 | "es6", 22 | "class", 23 | "event", 24 | "channel", 25 | "pubsub", 26 | "javascript" 27 | ], 28 | "author": "@sroucheray", 29 | "license": "ISC", 30 | "bugs": { 31 | "url": "https://github.com/sroucheray/event-class/issues" 32 | }, 33 | "homepage": "https://github.com/sroucheray/event-class#readme", 34 | "jspm": { 35 | "directories": { 36 | "test": "test" 37 | }, 38 | "devDependencies": { 39 | "traceur": "github:jmcriffey/bower-traceur@0.0.88", 40 | "traceur-runtime": "github:jmcriffey/bower-traceur-runtime@0.0.88" 41 | } 42 | }, 43 | "devDependencies": { 44 | "jspm": "^0.15.7", 45 | "mocha": "^2.2.5" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /test/es6-test-setup.js: -------------------------------------------------------------------------------- 1 | import EventClass from "EventClass"; 2 | 3 | class DummyClass extends EventClass{ 4 | } 5 | 6 | export default DummyClass; -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | /*eslint-env mocha*/ 2 | var System = require('jspm'); 3 | var assert = require('assert'); 4 | 5 | var promise = System.import('test/es6-test-setup').catch(function(e) { 6 | describe('JSPM', function() { 7 | it('ES6 module not loaded properly', function() { 8 | assert.fail(null, '', e); 9 | }); 10 | }); 11 | }); 12 | 13 | describe('Private methods', function() { 14 | describe('#_getChannels', function() { 15 | it('string events should splitted by spaces and comas in channels', function(done) { 16 | promise.then(function(value) { 17 | var DummyClass = value.default; 18 | var dummyObject = new DummyClass(); 19 | 20 | var result = dummyObject._getChannels('change'); 21 | 22 | assert.equal(result.length, 1); 23 | assert.equal(result[0], 'change'); 24 | 25 | result = dummyObject._getChannels('test, change'); 26 | 27 | assert.equal(result.length, 2); 28 | assert.equal(result[0], 'test'); 29 | assert.equal(result[1], 'change'); 30 | 31 | result = dummyObject._getChannels('test change'); 32 | 33 | assert.equal(result.length, 2); 34 | assert.equal(result[0], 'test'); 35 | assert.equal(result[1], 'change'); 36 | 37 | result = dummyObject._getChannels(' test2 change2, change3'); 38 | 39 | assert.equal(result.length, 3); 40 | assert.equal(result[0], 'test2'); 41 | assert.equal(result[1], 'change2'); 42 | assert.equal(result[2], 'change3'); 43 | done(); 44 | }); 45 | 46 | }); 47 | 48 | it('namespaces should be extracted from channels', function(done) { 49 | promise.then(function(value) { 50 | var DummyClass = value.default; 51 | var dummyObject = new DummyClass(); 52 | 53 | var result = dummyObject._getNameSpaces('change'); 54 | 55 | assert.equal(result.length, 1); 56 | assert.equal(result[0], 'change'); 57 | 58 | result = dummyObject._getNameSpaces('change:test'); 59 | 60 | assert.equal(result.length, 2); 61 | assert.equal(result[0], 'change:test'); 62 | assert.equal(result[1], 'change'); 63 | 64 | result = dummyObject._getNameSpaces(' change:test2 '); 65 | 66 | assert.equal(result.length, 2); 67 | assert.equal(result[0], 'change:test2'); 68 | assert.equal(result[1], 'change'); 69 | 70 | result = dummyObject._getNameSpaces(' change:test2:attribute'); 71 | 72 | assert.equal(result.length, 3); 73 | assert.equal(result[0], 'change:test2:attribute'); 74 | assert.equal(result[1], 'change:test2'); 75 | assert.equal(result[2], 'change'); 76 | done(); 77 | }); 78 | 79 | }); 80 | }); 81 | }); 82 | 83 | 84 | describe('Simple trigger', function() { 85 | describe('#on and #trigger', function() { 86 | it('a trigger must be listened', function(done) { 87 | promise.then(function(value) { 88 | var DummyClass = value.default; 89 | var dummyObject = new DummyClass(); 90 | 91 | dummyObject.on('change', function(){ 92 | assert.ok(true); 93 | }); 94 | 95 | dummyObject.trigger('change'); 96 | done(); 97 | }); 98 | 99 | }); 100 | 101 | it('this object must be the dispatcher', function(done) { 102 | promise.then(function(value) { 103 | var DummyClass = value.default; 104 | var dummyObject = new DummyClass(); 105 | 106 | dummyObject.on('change', function(){ 107 | assert.equal(this, dummyObject); 108 | }); 109 | 110 | dummyObject.trigger('change'); 111 | done(); 112 | }); 113 | 114 | }); 115 | 116 | it('data should be passed through the dispatched event', function(done) { 117 | promise.then(function(value) { 118 | var DummyClass = value.default; 119 | var dummyObject = new DummyClass(); 120 | 121 | dummyObject.on('change', function(data){ 122 | assert.equal('test', data.test); 123 | }); 124 | 125 | dummyObject.trigger('change', { test: 'test'}); 126 | done(); 127 | }); 128 | 129 | }); 130 | 131 | it('two triggers must be listened', function(done) { 132 | promise.then(function(value) { 133 | var DummyClass = value.default; 134 | var dummyObject = new DummyClass(); 135 | var numAssertions = 0; 136 | 137 | dummyObject.on('change', function(){ 138 | assert.ok(true); 139 | numAssertions++; 140 | if(numAssertions === 2){ 141 | done(); 142 | } 143 | }); 144 | 145 | dummyObject.trigger('change'); 146 | dummyObject.trigger('change'); 147 | }); 148 | 149 | }); 150 | }); 151 | }); 152 | 153 | 154 | describe('Sub channel trigger', function() { 155 | describe('Test sub channel trigger', function() { 156 | it('a trigger on a sub channel must be listened by its parent channel', function(done) { 157 | promise.then(function(value) { 158 | var DummyClass = value.default; 159 | var dummyObject = new DummyClass(); 160 | 161 | dummyObject.on('change', function(){ 162 | assert.ok(true); 163 | }); 164 | 165 | dummyObject.trigger('change:object'); 166 | done(); 167 | }); 168 | 169 | }); 170 | it('a trigger on a channel must not be listened by a child channel', function(done) { 171 | promise.then(function(value) { 172 | var DummyClass = value.default; 173 | var dummyObject = new DummyClass(); 174 | 175 | dummyObject.on('change:object', function(){ 176 | assert.ok(false); 177 | }); 178 | 179 | dummyObject.on('change', function(){ 180 | assert.ok(true); 181 | }); 182 | 183 | dummyObject.trigger('change'); 184 | done(); 185 | }); 186 | 187 | }); 188 | }); 189 | }); 190 | 191 | 192 | describe('Sub sub channel trigger', function() { 193 | describe('Test sub sub channel trigger', function() { 194 | it('a trigger on a sub sub channel must be listened by its grand parent channel', function(done) { 195 | promise.then(function(value) { 196 | var DummyClass = value.default; 197 | var dummyObject = new DummyClass(); 198 | 199 | dummyObject.on('change', function(){ 200 | assert.ok(true); 201 | }); 202 | 203 | dummyObject.trigger('change:object:attribute'); 204 | done(); 205 | }); 206 | 207 | }); 208 | it('a trigger on a sub channel must not be listened by a sibling channel', function(done) { 209 | promise.then(function(value) { 210 | var DummyClass = value.default; 211 | var dummyObject = new DummyClass(); 212 | 213 | dummyObject.on('change:object:attribute', function(){ 214 | assert.ok(false); 215 | }); 216 | 217 | dummyObject.on('change', function(){ 218 | assert.ok(true); 219 | }); 220 | 221 | 222 | dummyObject.trigger('change:object'); 223 | dummyObject.trigger('change:attribute'); 224 | done(); 225 | }); 226 | 227 | }); 228 | }); 229 | }); 230 | 231 | describe('Remove listener', function() { 232 | describe('#off', function() { 233 | it('an added and removed event must not be listened anymore', function(done) { 234 | promise.then(function(value) { 235 | var DummyClass = value.default; 236 | var dummyObject = new DummyClass(); 237 | 238 | function namedCallback(){ 239 | assert.ok(false); 240 | } 241 | 242 | dummyObject.on('change', namedCallback); 243 | dummyObject.off('change', namedCallback); 244 | 245 | dummyObject.trigger('change'); 246 | done(); 247 | }); 248 | 249 | }); 250 | it('an added and removed namespaced event must not be listened anymore', function(done) { 251 | promise.then(function(value) { 252 | var DummyClass = value.default; 253 | var dummyObject = new DummyClass(); 254 | 255 | function namedCallback(){ 256 | assert.notOk(true); 257 | } 258 | 259 | dummyObject.on('change:object', namedCallback); 260 | dummyObject.off('change:object', namedCallback); 261 | 262 | dummyObject.trigger('change:object'); 263 | done(); 264 | }); 265 | 266 | }); 267 | }); 268 | }); 269 | 270 | 271 | describe('Once callback', function() { 272 | describe('#once', function() { 273 | it('a once callback must be called on a single trigger', function(done) { 274 | promise.then(function(value) { 275 | var DummyClass = value.default; 276 | var dummyObject = new DummyClass(); 277 | 278 | dummyObject.once('change', function(){ 279 | assert.ok(true); 280 | }); 281 | 282 | dummyObject.trigger('change'); 283 | done(); 284 | }); 285 | 286 | }); 287 | 288 | it('a once callback must be called a single time', function(done) { 289 | promise.then(function(value) { 290 | var DummyClass = value.default; 291 | var dummyObject = new DummyClass(); 292 | var numAssertions = 0; 293 | function namedCallback(){ 294 | if(numAssertions === 0){ 295 | assert.ok(true); 296 | }else{ 297 | assert.ok(false); 298 | } 299 | numAssertions++; 300 | } 301 | 302 | dummyObject.once('change', namedCallback); 303 | 304 | dummyObject.trigger('change'); 305 | dummyObject.trigger('change'); 306 | dummyObject.trigger('change'); 307 | 308 | done(); 309 | }); 310 | 311 | }); 312 | }); 313 | }); 314 | describe('Mutliple listeners', function() { 315 | it('coma separated events should listen to all registered events', function(done) { 316 | promise.then(function(value) { 317 | var DummyClass = value.default; 318 | var dummyObject = new DummyClass(); 319 | var numAssertions = 0; 320 | 321 | dummyObject.on('change, test', function(){ 322 | assert.ok(true); 323 | numAssertions++; 324 | }); 325 | 326 | dummyObject.trigger('change'); 327 | dummyObject.trigger('test'); 328 | assert.ok(numAssertions === 2); 329 | done(); 330 | }); 331 | 332 | }); 333 | it('space separated events should listen to all registered events', function(done) { 334 | promise.then(function(value) { 335 | var DummyClass = value.default; 336 | var dummyObject = new DummyClass(); 337 | var numAssertions = 0; 338 | 339 | dummyObject.on('change test', function(){ 340 | assert.ok(true); 341 | numAssertions++; 342 | }); 343 | 344 | dummyObject.trigger('change'); 345 | dummyObject.trigger('test'); 346 | assert.ok(numAssertions === 2); 347 | done(); 348 | }); 349 | 350 | }); 351 | 352 | it('sub channel coma separated events should listen to all registered events', function(done) { 353 | promise.then(function(value) { 354 | var DummyClass = value.default; 355 | var dummyObject = new DummyClass(); 356 | var numAssertions = 0; 357 | 358 | dummyObject.on('change:attr, test:value', function(){ 359 | assert.ok(true); 360 | numAssertions++; 361 | }); 362 | 363 | dummyObject.trigger('change:attr'); 364 | dummyObject.trigger('test:value'); 365 | assert.ok(numAssertions === 2); 366 | done(); 367 | }); 368 | 369 | }); 370 | 371 | it('sub channel space separated events should listen to all registered events', function(done) { 372 | promise.then(function(value) { 373 | var DummyClass = value.default; 374 | var dummyObject = new DummyClass(); 375 | var numAssertions = 0; 376 | 377 | dummyObject.on('change:attr test:value', function(){ 378 | assert.ok(true); 379 | numAssertions++; 380 | }); 381 | 382 | dummyObject.trigger('change:attr'); 383 | dummyObject.trigger('test:value'); 384 | assert.ok(numAssertions === 2); 385 | done(); 386 | }); 387 | }); 388 | 389 | it('sub channel space separated events should listen to all registered events (coma separated trigger)', function(done) { 390 | promise.then(function(value) { 391 | var DummyClass = value.default; 392 | var dummyObject = new DummyClass(); 393 | var numAssertions = 0; 394 | 395 | dummyObject.on('change:attr test:value', function(){ 396 | assert.ok(true); 397 | numAssertions++; 398 | }); 399 | 400 | dummyObject.trigger('change:attr, test:value'); 401 | assert.ok(numAssertions === 2); 402 | done(); 403 | }); 404 | }); 405 | 406 | it('sub channel space separated events should listen to all registered events (space separated trigger)', function(done) { 407 | promise.then(function(value) { 408 | var DummyClass = value.default; 409 | var dummyObject = new DummyClass(); 410 | var numAssertions = 0; 411 | 412 | dummyObject.on('change:attr test:value', function(){ 413 | assert.ok(true); 414 | numAssertions++; 415 | }); 416 | 417 | dummyObject.trigger('change:attr test:value'); 418 | assert.ok(numAssertions === 2); 419 | done(); 420 | }); 421 | }); 422 | }); --------------------------------------------------------------------------------