├── .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 | [](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 | });
--------------------------------------------------------------------------------