├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── benchmarks ├── trigger_with_arguments.js └── trigger_with_call.js ├── doc ├── API.md └── Benchmarks.md ├── index.js ├── package.json └── test ├── index_test.js └── mocha.opts /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules/ 2 | /*.tgz 3 | /tmp/ 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /*.tgz 2 | /tmp/ 3 | /.travis.yml 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - "node" 5 | - "0.10" 6 | - "0.11" 7 | - "0.12" 8 | - "4" 9 | - "5" 10 | - "6" 11 | 12 | notifications: 13 | email: ["andri@dot.ee"] 14 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.1.0 (May 27, 2015) 2 | - Allows binding a listener's `thisArg` to `null`. 3 | 4 | Using `null` will mean the event listener will be called in the `null` context. In [JavaScript's strict mode][strict] that means `this === null`. 5 | ```javascript 6 | model.on("change", onChange, null) 7 | ``` 8 | 9 | As before, using `undefined` or leaving the context out will mean the event 10 | listener will be called in the object's context. This is most useful when 11 | setting up listeners on the object's prototype as v2.0.0 featured: 12 | ```javascript 13 | Model.prototype.on("change", fn) 14 | ``` 15 | 16 | - Allows binding additional arguments: 17 | 18 | ```javascript 19 | model.on("change", onChange, thisArg, 1, 2, 3) 20 | ``` 21 | 22 | [strict]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Strict_mode 23 | 24 | ## 2.0.1 (Feb 15, 2015) 25 | - Fixes rebinding to events named like `Object.prototype`'s properties after 26 | unbinding all. 27 | - Fixes rebinding an inherited event that was previously unbound. 28 | - Deletes an empty listener array from `_events` when the last event is unbound. 29 | 30 | ## 2.0.0 (Feb 13, 2015) 31 | - You can now inherit from objects and change their event listeners without 32 | affecting object prototypes. 33 | 34 | All event listeners will initially be inherited and then copied only once you 35 | call `on`, `once` or `off` on the child instances. For most use-cases you can 36 | initialize your event listeners only once, e.g. on your class's prototype, and 37 | then rely on inheritance to make them available. Eliminates an awful lot of 38 | redundant computation. 39 | 40 | See the [README][] for more info on [inheritable 41 | observables](https://github.com/moll/js-concert#inheriting). 42 | 43 | - Throws `TypeError` when `on` or `once` called without a name. 44 | - Throws `TypeError` when `on` or `once` called without a listener function. 45 | 46 | [README]: https://github.com/moll/js-concert 47 | 48 | ## 1.2.0 (Sep 29, 2014) 49 | - `Concert.prototype.off` no longer deletes `this._events`, but sets it to 50 | `null`. 51 | This allows for easier inheriting from an object that has `_events` set --- 52 | parent's events won't be unshadowed by accident. 53 | 54 | ## 1.1.0 (Jul 16, 2014) 55 | - Calls event handlers in the context of the object by default if a context 56 | wasn't explicitly set. 57 | This matches how Backbone.js's, Node.js's EventEmitter and DOM event handlers 58 | work. 59 | 60 | ## 1.0.0 (Mar 21, 2014) 61 | - Defines the internal `_events` property as [non-enumerable][for-in]. 62 | 63 | [for-in]: http://www.ecma-international.org/ecma-262/5.1/#sec-12.6.4 64 | 65 | ## 0.1.338 (Jan 5, 2014) 66 | - Allows using event names that exist on `Object.prototype`. 67 | 68 | ## 0.1.337 (Oct 25, 2013) 69 | - First release. The show must go on! 70 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Concert.js 2 | Copyright (C) 2013 Andri Möll 3 | 4 | This program is free software: you can redistribute it and/or modify it under 5 | the terms of the GNU Affero General Public License as published by the Free 6 | Software Foundation, either version 3 of the License, or any later version. 7 | 8 | Additional permission under the GNU Affero GPL version 3 section 7: 9 | If you modify this Program, or any covered work, by linking or 10 | combining it with other code, such other code is not for that reason 11 | alone subject to any of the requirements of the GNU Affero GPL version 3. 12 | 13 | In summary: 14 | - You can use this program for no cost. 15 | - You can use this program for both personal and commercial reasons. 16 | - You do not have to share your own program's code which uses this program. 17 | - You have to share modifications (e.g bug-fixes) you've made to this program. 18 | 19 | For the full copy of the GNU Affero General Public License see: 20 | http://www.gnu.org/licenses. 21 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NODE = node 2 | NODE_OPTS = --use-strict 3 | TEST_OPTS = 4 | 5 | # NOTE: Sorry, mocumentation is not yet published. 6 | MOCUMENT = ~/Documents/Mocumentation/bin/mocument 7 | MOCUMENT_OPTS = --type yui --title Concert.js 8 | GITHUB_URL = https://github.com/moll/js-concert 9 | 10 | love: 11 | @echo "Feel like makin' love." 12 | 13 | test: 14 | @$(NODE) $(NODE_OPTS) ./node_modules/.bin/mocha -R dot $(TEST_OPTS) 15 | 16 | spec: 17 | @$(NODE) $(NODE_OPTS) ./node_modules/.bin/mocha -R spec $(TEST_OPTS) 18 | 19 | autotest: 20 | @$(NODE) $(NODE_OPTS) ./node_modules/.bin/mocha -R dot --watch $(TEST_OPTS) 21 | 22 | autospec: 23 | @$(NODE) $(NODE_OPTS) ./node_modules/.bin/mocha -R spec --watch $(TEST_OPTS) 24 | 25 | pack: 26 | @file=$$(npm pack); echo "$$file"; tar tf "$$file" 27 | 28 | publish: 29 | npm publish 30 | 31 | tag: 32 | git tag "v$$($(NODE) -e 'console.log(require("./package").version)')" 33 | 34 | doc: doc.json 35 | @mkdir -p doc 36 | @$(MOCUMENT) $(MOCUMENT_OPTS) tmp/doc/data.json > doc/API.md 37 | 38 | toc: doc.json 39 | @$(MOCUMENT) $(MOCUMENT_OPTS) \ 40 | --template toc \ 41 | --var api_url=$(GITHUB_URL)/blob/master/doc/API.md \ 42 | tmp/doc/data.json > tmp/TOC.md 43 | 44 | @echo '/^API$$/,/^License$$/{/^API$$/{r tmp/TOC.md\na\\\n\\\n\\\n\n};/^License/!d;}' |\ 45 | sed -i "" -f /dev/stdin README.md 46 | 47 | doc.json: 48 | @mkdir -p tmp 49 | @yuidoc --exclude test,node_modules --parse-only --outdir tmp/doc . 50 | 51 | clean: 52 | rm -f *.tgz tmp 53 | 54 | .PHONY: love 55 | .PHONY: test spec autotest autospec 56 | .PHONY: pack publish tag 57 | .PHONY: clean 58 | .PHONY: doc toc doc.json 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Concert.js 2 | ========== 3 | [![NPM version][npm-badge]](https://www.npmjs.com/package/concert) 4 | [![Build status][travis-badge]](https://travis-ci.org/moll/js-concert) 5 | 6 | Concert.js is an **event library** for JavaScript and Node.js that implements 7 | the **observer pattern** (a.k.a publish/subscribe). This is a useful pattern for 8 | creating decoupled architectures, event driven systems and is one key element in 9 | the [Model-View-Controller][mvc] pattern. Concert.js similar to Node's 10 | [EventEmitter][ee] and [Backbone.Events][bb-events], but **decoupled**, 11 | **minimal** and **light-weight**. Also supports **inherited listeners** for far better performance! 12 | 13 | [npm-badge]: https://img.shields.io/npm/v/concert.svg 14 | [travis-badge]: https://travis-ci.org/moll/js-concert.png?branch=master 15 | [ee]: http://nodejs.org/api/events.html 16 | [bb-events]: http://backbonejs.org/#Events 17 | [mvc]: https://en.wikipedia.org/wiki/Model_View_Controller 18 | 19 | ### Tour 20 | - **Simple** and **minimal** — just `on`, `once`, `off` and `trigger`. 21 | No unnecessary method pollution or large API surface area like with other 22 | libraries. No legacy method names either. 23 | - **Light-weight** with little code and **no external dependencies**. 24 | - **Inheritable** — You can inherit from you observables and add 25 | listeners later. 26 | All event listeners will initially be inherited and then copied only once you 27 | call `on`, `once` or `off` on the child instances. Eliminate an awful lot of 28 | computation by setting your event listeners on your class's prototype. Read 29 | more on [inheritable observables](#inheriting). 30 | - **Familiar** if you've ever used [Backbone.js][bb] or Node.js's 31 | [EventEmitter][ee]. 32 | - Comes with a built-in [`once`] function to listen to an event only once and 33 | then remove the listener automatically. 34 | - Set a **listener's context** and optionally **extra arguments**. 35 | - **Rename or alias** any function to a name of your choice. 36 | Handy if you need to present a compatible or legacy API: 37 | `obj.addEventListener = Concert.on`. 38 | - Special `all` event for catching all triggered events. 39 | Useful also for **delegating or proxying** all events from one object to 40 | another. 41 | - Add listeners for **multiple events at once** with object syntax: 42 | `obj.on({change: onChange, save: onSave})` 43 | - Works well with namespaced event names such as `change:name`. 44 | Because there's no limit to event names, you can easily create faux 45 | namespaces. 46 | - Supports **ECMAScript 6's [Symbol][symbol]** in case you need to create events 47 | a little more private. 48 | But really, that's an illusion. There's `Object.getOwnPropertySymbols`. 49 | - Thoroughly tested. 50 | 51 | [bb]: http://backbonejs.org 52 | [`once`]: https://github.com/moll/js-concert/blob/master/doc/API.md#Concert.once 53 | [symbol]: https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/Symbol 54 | 55 | 56 | Installing 57 | ---------- 58 | ### Installing on Node.js 59 | ``` 60 | npm install concert 61 | ``` 62 | 63 | ### Installing for the browser 64 | Concert.js doesn't yet have a build ready for the browser, but you might be able 65 | to use [Browserify][browserify] to have it run there till then. 66 | 67 | [browserify]: https://github.com/substack/node-browserify 68 | 69 | 70 | Using 71 | ----- 72 | To add events to any object of your choice, just mix `Concert`'s functions to 73 | your object: 74 | ```javascript 75 | var Concert = require("concert") 76 | var music = {} 77 | for (var name in Concert) music[name] = Concert[name] 78 | ``` 79 | 80 | Then use `on` and `trigger` to add listeners and trigger events: 81 | ```javascript 82 | music.on("cowbell", function() { console.log("Cluck!") }) 83 | music.trigger("cowbell") 84 | ``` 85 | 86 | If you're using [Underscore.js][underscore] or [Lo-dash][lodash], you can use 87 | `_.extend` to mix Concert in: 88 | ```javascript 89 | _.extend(music, Concert) 90 | ``` 91 | 92 | [underscore]: http://underscorejs.org 93 | [lodash]: http://lodash.com 94 | 95 | ### Enabling Concert on all instances 96 | Mix `Concert` in to your class's `prototype` to make each instance observable. 97 | ```javascript 98 | function Music() {} 99 | _.extend(Music.prototype, Concert) 100 | ``` 101 | 102 | Then you can listen to and trigger events on each instance. 103 | ```javascript 104 | var music = new Music 105 | music.on("cowbell", console.log) 106 | ``` 107 | 108 | ### Faux Namespaces 109 | Because there are no limits to event names, you can create faux namespaces by 110 | adding a separator, e.g `:`, to event names. Then trigger both the specific and 111 | general version in your application code and you're good to go. This happens to 112 | be also what [Backbone.Model][bb-model] does for its `change` events. 113 | 114 | ```javascript 115 | model.trigger("change:name", "John") 116 | model.trigger("change") 117 | ``` 118 | 119 | [bb-model]: http://backbonejs.org/#Model 120 | 121 | 122 | ### Inheritable Observables 123 | Concert.js supports inheriting from your observables without worrying you'll 124 | change the prototypes. Set up your event listeners once on your "class's" 125 | prototype and then, if need be, add or remove listeners. 126 | 127 | ```javascript 128 | function Music(song) { this.song = song } 129 | 130 | _.extend(Music.prototype, Concert) 131 | 132 | Music.prototype.play = function() { this.trigger("play", this.song) } 133 | 134 | Music.prototype.on("play", console.log.bind(null, "Playing %s.")) 135 | ``` 136 | 137 | Once you initialize your object, all of the event listeners will be ready 138 | without having to call a bunch of `on`s and `once`s in the constructor. This 139 | pattern saves you from an awful lot of unnecessary computation. 140 | 141 | Ensure the third argument, the listener's context, remains `undefined` when 142 | calling `Music.prototype.on`. The listener's context will then be set to 143 | any particular instance on which [`trigger`][] was called. 144 | 145 | ```javascript 146 | var music = new Music("On Broadway") 147 | music.play() // => Will log "Playing On Broadway.". 148 | ``` 149 | 150 | You can then add listeners without worrying you'll change every instance in the 151 | system (as you would when you'd use Node.js's EventEmitter or Backbone's 152 | Events). 153 | 154 | ```javascript 155 | var jam = new Music("The Way It Is") 156 | jam.off("play") 157 | jam.on("play", console.log.bind(null, "Jamming %s.")) 158 | music.play() // => Will log "Jamming The Way It Is.". 159 | 160 | var classic = new Music("Tubular Bells") 161 | classic.play() // => Will still log "Playing Tubular Bells.". 162 | ``` 163 | 164 | [`trigger`]: https://github.com/moll/js-concert/blob/master/doc/API.md#Concert.trigger 165 | 166 | ## Extra arguments 167 | If you'd like to use a single listener for multiple events, but need a way to 168 | still differentiate between events, make use of Concert.js's support for 169 | binding arguments: 170 | 171 | ```javascript 172 | var song = new Music("The Way It Is") 173 | song.on("play", onStartOrStop, undefined, "play") 174 | song.on("stop", onStartOrStop, undefined, "stop") 175 | ``` 176 | 177 | Your `onStartOrStop` function will then be called in the context of `song` with 178 | its first argument as either `"play"` or `"stop"`. Any additional arguments 179 | given to [`trigger`] will come after the bound arguments. 180 | 181 | 182 | API 183 | --- 184 | For extended documentation on all functions, please see the 185 | [Concert.js API Documentation][api]. 186 | 187 | [api]: https://github.com/moll/js-concert/blob/master/doc/API.md 188 | 189 | ### [Concert](https://github.com/moll/js-concert/blob/master/doc/API.md#Concert) 190 | - [off](https://github.com/moll/js-concert/blob/master/doc/API.md#Concert.off)(event, listener, [thisArg]) 191 | - [on](https://github.com/moll/js-concert/blob/master/doc/API.md#Concert.on)(event, listener, [thisArg], [arguments...]) 192 | - [once](https://github.com/moll/js-concert/blob/master/doc/API.md#Concert.once)(event, listener, [thisArg], [arguments...]) 193 | - [trigger](https://github.com/moll/js-concert/blob/master/doc/API.md#Concert.trigger)(event, [arguments...]) 194 | 195 | 196 | License 197 | ------- 198 | Concert.js is released under a *Lesser GNU Affero General Public License*, which 199 | in summary means: 200 | 201 | - You **can** use this program for **no cost**. 202 | - You **can** use this program for **both personal and commercial reasons**. 203 | - You **do not have to share your own program's code** which uses this program. 204 | - You **have to share modifications** (e.g bug-fixes) you've made to this 205 | program. 206 | 207 | For more convoluted language, see the `LICENSE` file. 208 | 209 | 210 | About 211 | ----- 212 | **[Andri Möll](http://themoll.com)** typed this and the code. 213 | [Monday Calendar](https://mondayapp.com) supported the engineering work. 214 | 215 | If you find Concert.js needs improving, please don't hesitate to type to me now 216 | at [andri@dot.ee][email] or [create an issue online][issues]. 217 | 218 | [email]: mailto:andri@dot.ee 219 | [issues]: https://github.com/moll/js-concert/issues 220 | -------------------------------------------------------------------------------- /benchmarks/trigger_with_arguments.js: -------------------------------------------------------------------------------- 1 | var Concert = require("..") 2 | var Benchmark = require("benchmark") 3 | var benchmark = new Benchmark.Suite 4 | var slice = Array.prototype.slice 5 | 6 | var concert = new Concert 7 | concert.on("all", noop) 8 | concert.on("all", noop) 9 | concert.on("all", noop) 10 | 11 | var concertWithArguments = (function() { 12 | var show = Object.create(concert) 13 | 14 | show.trigger = function(name) { 15 | var evs = this._events 16 | if (evs == null) return this 17 | if (evs[name] != null) apply(evs[name], this, slice.call(arguments, 1)) 18 | if (evs.all != null) apply(evs.all, this, arguments) 19 | return this 20 | } 21 | 22 | function apply(fns, thisArg, args) { 23 | for (var i = 0, l = fns.length; i < l; ++i) 24 | fns[i][0].apply(fns[i][1] || thisArg, args) 25 | } 26 | 27 | return show 28 | })() 29 | 30 | var concertWithSlice = (function() { 31 | var show = Object.create(concert) 32 | 33 | show.trigger = function(name) { 34 | var evs = this._events 35 | if (evs == null) return this 36 | if (evs[name] != null) apply(evs[name], this, slice.call(arguments, 1)) 37 | if (evs.all != null) apply(evs.all, this, slice.call(arguments)) 38 | return this 39 | } 40 | 41 | function apply(fns, thisArg, args) { 42 | for (var i = 0, l = fns.length; i < l; ++i) 43 | fns[i][0].apply(fns[i][1] || thisArg, args) 44 | } 45 | 46 | return show 47 | })() 48 | 49 | benchmark.add("Status quo", function() { 50 | concert.trigger("change", 1, 2, 3) 51 | }) 52 | 53 | benchmark.add("Passing arguments to apply function", function() { 54 | concertWithArguments.trigger("change", 1, 2, 3) 55 | }) 56 | 57 | benchmark.add("Converting arguments to an array before applying", function() { 58 | concertWithSlice.trigger("change", 1, 2, 3) 59 | }) 60 | 61 | benchmark.on("cycle", function(ev) { console.log(String(ev.target)) }) 62 | benchmark.run() 63 | 64 | function noop() {} 65 | -------------------------------------------------------------------------------- /benchmarks/trigger_with_call.js: -------------------------------------------------------------------------------- 1 | var Concert = require("..") 2 | var Benchmark = require("benchmark") 3 | var benchmark = new Benchmark.Suite 4 | var slice = Array.prototype.slice 5 | 6 | var concert = new Concert 7 | concert.on("change", noop) 8 | concert.on("change", noop) 9 | concert.on("change", noop) 10 | 11 | var concertWithApply = (function() { 12 | var show = Object.create(concert) 13 | 14 | show.trigger = function(name) { 15 | var evs = this._events 16 | if (evs == null) return this 17 | if (evs[name] != null) apply(evs[name], this, slice.call(arguments, 1)) 18 | if (evs.all != null) apply(evs.all, this, arguments) 19 | return this 20 | } 21 | 22 | function apply(fns, thisArg, args) { 23 | for (var i = 0, l = fns.length; i < l; ++i) 24 | fns[i][0].apply(fns[i][1] || thisArg, args) 25 | } 26 | 27 | return show 28 | })() 29 | 30 | var concertWithApplyOptimization = (function() { 31 | var show = Object.create(concert) 32 | 33 | show.trigger = function(name) { 34 | var evs = this._events 35 | if (evs == null) return this 36 | if (evs[name] != null) apply(evs[name], this, slice.call(arguments, 1)) 37 | if (evs.all != null) apply(evs.all, this, arguments) 38 | return this 39 | } 40 | 41 | function apply(fns, thisArg, args) { 42 | for (var i = 0, l = fns.length, fn; i < l; ++i) { 43 | (fn = fns[i])[0].apply(fn[1] || thisArg, args) 44 | } 45 | } 46 | 47 | return show 48 | })() 49 | 50 | var concertWithCall = (function() { 51 | var show = Object.create(concert) 52 | 53 | show.trigger = function(name) { 54 | var evs = this._events 55 | if (evs == null) return this 56 | if (evs[name] != null) apply(evs[name], this, slice.call(arguments, 1)) 57 | if (evs.all != null) apply(evs.all, this, slice.call(arguments)) 58 | return this 59 | } 60 | 61 | function apply(fns, thisArg, args) { 62 | var fn 63 | var i = -1 64 | var l = fns.length 65 | var a = args[0] 66 | var b = args[1] 67 | var c = args[2] 68 | 69 | switch (args.length) { 70 | case 0: 71 | while (++i < l) (fn = fns[i])[0].call(fn[1] || thisArg); break 72 | case 1: 73 | while (++i < l) (fn = fns[i])[0].call(fn[1] || thisArg, a); break 74 | case 2: 75 | while (++i < l) (fn = fns[i])[0].call(fn[1] || thisArg, a, b); break 76 | case 3: 77 | while (++i < l) (fn = fns[i])[0].call(fn[1] || thisArg, a, b, c); break 78 | default: 79 | while (++i < l) (fn = fns[i])[0].apply(fn[1] || thisArg, args) 80 | } 81 | } 82 | 83 | return show 84 | })() 85 | 86 | benchmark.add("Status quo", function() { 87 | concert.trigger("change", 1) 88 | concert.trigger("change", 1, 2) 89 | concert.trigger("change", 1, 2, 3) 90 | }) 91 | 92 | benchmark.add("Function.prototype.apply", function() { 93 | concertWithApply.trigger("change", 1) 94 | concertWithApply.trigger("change", 1, 2) 95 | concertWithApply.trigger("change", 1, 2, 3) 96 | }) 97 | 98 | benchmark.add("Function.prototype.apply with reference to element", function() { 99 | concertWithApplyOptimization.trigger("change", 1) 100 | concertWithApplyOptimization.trigger("change", 1, 2) 101 | concertWithApplyOptimization.trigger("change", 1, 2, 3) 102 | }) 103 | 104 | benchmark.add("Function.prototype.call for <= 3 arguments", function() { 105 | concertWithCall.trigger("change", 1) 106 | concertWithCall.trigger("change", 1, 2) 107 | concertWithCall.trigger("change", 1, 2, 3) 108 | }) 109 | 110 | benchmark.on("cycle", function(ev) { console.log(String(ev.target)) }) 111 | benchmark.run() 112 | 113 | function noop() {} 114 | -------------------------------------------------------------------------------- /doc/API.md: -------------------------------------------------------------------------------- 1 | Concert.js API Documentation 2 | ============================ 3 | ### [Concert](#Concert) 4 | - [.off](#Concert.off)(event, listener, [thisArg]) 5 | - [.on](#Concert.on)(event, listener, [thisArg], [arguments...]) 6 | - [.once](#Concert.once)(event, listener, [thisArg], [arguments...]) 7 | - [.trigger](#Concert.trigger)(event, [arguments...]) 8 | 9 | 10 | Concert() 11 | --------- 12 | Concert is the main object or *module* that you can inject into your own 13 | objects to make them observables (also known as *event emitters* or 14 | *dispatchers*). You can then listen to and trigger events on that object. 15 | 16 | ```javascript 17 | var Concert = require("concert") 18 | function Galaxy() {} 19 | for (var name in Concert) Galaxy.prototype[name] = Concert[name] 20 | 21 | var galaxy = new Galaxy() 22 | galaxy.on("secret", console.log) 23 | galaxy.trigger("secret", 42) 24 | ``` 25 | 26 | You can also create a new instance of `Concert` to get a blank object to 27 | use as an *event bus*. 28 | ```javascript 29 | var Concert = require("concert") 30 | var bus = new Concert 31 | bus.on("message", console.log) 32 | bus.trigger("message", "One giant man to step, one small...") 33 | ``` 34 | 35 | If you need, you can also rename any `Concert`'s function by just assigning 36 | them to new names on your object: 37 | ```javascript 38 | obj.addEventListener = Concert.on 39 | obj.emit = Concert.trigger 40 | obj.removeEventListener = Concert.off 41 | ``` 42 | 43 | ### Concert.off(event, listener, [thisArg]) 44 | Remove previously added listeners by event name, by listener, by `thisArg` 45 | or by any combination. 46 | Returns self. 47 | 48 | You can also specify **multiple events** at once by passing an object whose 49 | keys are event names and values functions. Pass `thisArg` then as the 2nd 50 | parameter. 51 | 52 | **Examples**: 53 | ```javascript 54 | // Remove all listeners: 55 | obj.off() 56 | // Remove listeners listening to event "foo": 57 | obj.off("foo") 58 | // Remove listeners fn of event "foo": 59 | obj.off("foo", fn) 60 | // Remove listener fn bound to thisArg listening to event "foo": 61 | obj.off("foo", fn, thisArg) 62 | // Remove all listeners bound to thisArg: 63 | obj.off(null, null, thisArg) 64 | // Remove all listeners fn listening to any event: 65 | obj.off(null, fn) 66 | // Remove multiple listeners together: 67 | obj.off({add: view.onAdd, remove: view.onRemove}, thisArg) 68 | ``` 69 | 70 | ### Concert.on(event, listener, [thisArg], [arguments...]) 71 | Add a `listener` for `event`. 72 | Optionally specify the listener's `this` value. Defaults to the object 73 | the event was triggered on when left undefined. 74 | Optionally specify additional arguments to be passed to the listener. 75 | Returns self. 76 | 77 | You can also specify **multiple events** at once by passing an object whose 78 | keys are event names and values functions. Pass the optional `this` then as 79 | the 2nd parameter. 80 | 81 | Listen to the special `all` event to be called when any event is triggered: 82 | ```javascript 83 | obj.on("all", function(event) {}) 84 | ``` 85 | 86 | The listener will be called with any arguments passed to 87 | [`trigger`](#Concert.trigger). 88 | 89 | **Examples**: 90 | ```javascript 91 | music.on("cowbell", function() { console.log("Cluck!") }) 92 | collection.on({add: view.onAdd, remove: view.onRemove}, view) 93 | model.on("change:name", view.onChange, view, "name") 94 | ``` 95 | 96 | ### Concert.once(event, listener, [thisArg], [arguments...]) 97 | Like [`on`](#Concert.on), but the listener is guaranteed to be called only 98 | once. 99 | Returns self. 100 | 101 | ### Concert.trigger(event, [arguments...]) 102 | Trigger `event` and optionally pass any extra arguments to the listeners. 103 | Returns self. 104 | 105 | Every event triggering also automatically triggers an `all` event with the 106 | event name prepended to other arguments. 107 | 108 | **Examples**: 109 | ```javascript 110 | obj.trigger("change", 42, {previous: 69}) 111 | ``` 112 | -------------------------------------------------------------------------------- /doc/Benchmarks.md: -------------------------------------------------------------------------------- 1 | Benchmarks 2 | ========== 3 | 4 | ## Concert.prototype.trigger with arguments 5 | Node v0.11.15 with Macbook Pro 2.4GHz Intel Core 2 Duo. 6 | ``` 7 | Status quo x 499,316 ops/sec ±0.64% (100 runs sampled) 8 | Passing arguments to apply function x 499,115 ops/sec ±0.64% (56 runs sampled) 9 | Converting arguments to an array before applying x 325,692 ops/sec ±1.53% (101 runs sampled) 10 | ``` 11 | 12 | ## Concert.prototype.trigger with call optimization 13 | Node v0.11.15 with Macbook Pro 2.4GHz Intel Core 2 Duo. 14 | ``` 15 | Status quo x 140,476 ops/sec ±1.39% (96 runs sampled) 16 | Function.prototype.apply x 141,547 ops/sec ±1.30% (93 runs sampled) 17 | Function.prototype.apply with reference to element x 136,582 ops/sec ±0.73% (96 runs sampled) 18 | Function.prototype.call for <= 3 arguments x 125,069 ops/sec ±1.02% (97 runs sampled) 19 | ``` 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var hasOwn = Function.call.bind(Object.hasOwnProperty) 2 | var slice = Function.call.bind(Array.prototype.slice) 3 | var concat = Array.prototype.concat.bind(Array.prototype) 4 | var NAME_TYPE_ERR = "Event name should be a string" 5 | var LISTENER_TYPE_ERR = "Event listener should be a function" 6 | module.exports = Concert 7 | 8 | /** 9 | * Concert is the main object or *module* that you can inject into your own 10 | * objects to make them observables (also known as *event emitters* or 11 | * *dispatchers*). You can then listen to and trigger events on that object. 12 | * 13 | * ```javascript 14 | * var Concert = require("concert") 15 | * function Galaxy() {} 16 | * for (var name in Concert) Galaxy.prototype[name] = Concert[name] 17 | * 18 | * var galaxy = new Galaxy() 19 | * galaxy.on("secret", console.log) 20 | * galaxy.trigger("secret", 42) 21 | * ``` 22 | * 23 | * You can also create a new instance of `Concert` to get a blank object to 24 | * use as an *event bus*. 25 | * ```javascript 26 | * var Concert = require("concert") 27 | * var bus = new Concert 28 | * bus.on("message", console.log) 29 | * bus.trigger("message", "One giant man to step, one small...") 30 | * ``` 31 | * 32 | * If you need, you can also rename any `Concert`'s function by just assigning 33 | * them to new names on your object: 34 | * ```javascript 35 | * obj.addEventListener = Concert.on 36 | * obj.emit = Concert.trigger 37 | * obj.removeEventListener = Concert.off 38 | * ``` 39 | * 40 | * @class Concert 41 | * @constructor 42 | */ 43 | function Concert() {} 44 | 45 | /** 46 | * Add a `listener` for `event`. 47 | * Optionally specify the listener's `this` value. Defaults to the object 48 | * the event was triggered on when left undefined. 49 | * Optionally specify additional arguments to be passed to the listener. 50 | * Returns self. 51 | * 52 | * You can also specify **multiple events** at once by passing an object whose 53 | * keys are event names and values functions. Pass the optional `this` then as 54 | * the 2nd parameter. 55 | * 56 | * Listen to the special `all` event to be called when any event is triggered: 57 | * ```javascript 58 | * obj.on("all", function(event) {}) 59 | * ``` 60 | * 61 | * The listener will be called with any arguments passed to 62 | * [`trigger`](#Concert.trigger). 63 | * 64 | * @example 65 | * music.on("cowbell", function() { console.log("Cluck!") }) 66 | * collection.on({add: view.onAdd, remove: view.onRemove}, view) 67 | * model.on("change:name", view.onChange, view, "name") 68 | * 69 | * @static 70 | * @method on 71 | * @param event 72 | * @param listener 73 | * @param [thisArg] 74 | * @param [arguments...] 75 | */ 76 | Concert.prototype.on = function on(name, fn, thisArg) { 77 | if (name == null) throw new TypeError(NAME_TYPE_ERR) 78 | if (unpack(on, this, arguments)) return this 79 | if (fn == null) throw new TypeError(LISTENER_TYPE_ERR) 80 | 81 | var events = this._events 82 | if (events == null || !hasOwn(this, "_events")) 83 | events = create(this, "_events", events) 84 | 85 | var fns = events[name] 86 | if (fns == null) fns = events[name] = [] 87 | else if (!hasOwn(events, name)) fns = events[name] = fns.slice() 88 | fns.push(slice(arguments, 1)) 89 | 90 | return this 91 | } 92 | 93 | /** 94 | * Like [`on`](#Concert.on), but the listener is guaranteed to be called only 95 | * once. 96 | * Returns self. 97 | * 98 | * @static 99 | * @method once 100 | * @param event 101 | * @param listener 102 | * @param [thisArg] 103 | * @param [arguments...] 104 | */ 105 | Concert.prototype.once = function once(name, fn, thisArg) { 106 | if (name == null) throw new TypeError(NAME_TYPE_ERR) 107 | if (unpack(once, this, arguments)) return this 108 | if (fn == null) throw new TypeError(LISTENER_TYPE_ERR) 109 | 110 | function fnOnce() { 111 | "use strict" 112 | Concert.prototype.off.call(this, name, fn) 113 | fn.apply(thisArg === undefined ? this : thisArg, arguments) 114 | } 115 | 116 | fnOnce.__func = fn 117 | fnOnce.__this = thisArg 118 | 119 | var args = [name, fnOnce, undefined] 120 | if (arguments.length > 3) args.push.apply(args, slice(arguments, 3)) 121 | return Concert.prototype.on.apply(this, args) 122 | } 123 | 124 | /** 125 | * Remove previously added listeners by event name, by listener, by `thisArg` 126 | * or by any combination. 127 | * Returns self. 128 | * 129 | * You can also specify **multiple events** at once by passing an object whose 130 | * keys are event names and values functions. Pass `thisArg` then as the 2nd 131 | * parameter. 132 | * 133 | * @example 134 | * // Remove all listeners: 135 | * obj.off() 136 | * // Remove listeners listening to event "foo": 137 | * obj.off("foo") 138 | * // Remove listeners fn of event "foo": 139 | * obj.off("foo", fn) 140 | * // Remove listener fn bound to thisArg listening to event "foo": 141 | * obj.off("foo", fn, thisArg) 142 | * // Remove all listeners bound to thisArg: 143 | * obj.off(null, null, thisArg) 144 | * // Remove all listeners fn listening to any event: 145 | * obj.off(null, fn) 146 | * // Remove multiple listeners together: 147 | * obj.off({add: view.onAdd, remove: view.onRemove}, thisArg) 148 | * 149 | * @static 150 | * @method off 151 | * @param event 152 | * @param listener 153 | * @param [thisArg] 154 | */ 155 | Concert.prototype.off = function off(name, fn, thisArg) { 156 | var events = this._events 157 | if (events == null) return this 158 | if (unpack(off, this, arguments)) return this 159 | 160 | if (fn == null && thisArg === undefined) { 161 | if (name == null) 162 | // When unbinding everything, just set a new _events. Stops the one in 163 | // the prototype. 164 | define(this, "_events", null) 165 | else if (events[name] != null) { 166 | if (!hasOwn(this, "_events")) events = create(this, "_events", events) 167 | else delete events[name] 168 | // If there are inherited events, mask them with null. 169 | if (name in events) events[name] = null 170 | } 171 | } 172 | else { 173 | if (!hasOwn(this, "_events")) events = create(this, "_events", events) 174 | if (name != null) filter(events, name, fn, thisArg) 175 | else for (name in events) filter(events, name, fn, thisArg) 176 | } 177 | 178 | return this 179 | } 180 | 181 | /** 182 | * Trigger `event` and optionally pass any extra arguments to the listeners. 183 | * Returns self. 184 | * 185 | * Every event triggering also automatically triggers an `all` event with the 186 | * event name prepended to other arguments. 187 | * 188 | * @example 189 | * obj.trigger("change", 42, {previous: 69}) 190 | * 191 | * @static 192 | * @method trigger 193 | * @param event 194 | * @param [arguments...] 195 | */ 196 | Concert.prototype.trigger = function trigger(name) { 197 | var events = this._events 198 | if (events == null) return this 199 | if (events[name] != null) apply(events[name], this, slice(arguments, 1)) 200 | if (events.all != null) apply(events.all, this, arguments) 201 | return this 202 | } 203 | 204 | Concert.on = Concert.prototype.on 205 | Concert.once = Concert.prototype.once 206 | Concert.off = Concert.prototype.off 207 | Concert.trigger = Concert.prototype.trigger 208 | 209 | function unpack(on, self, args) { 210 | var obj = args[0] 211 | if (obj == null || typeof obj != "object") return false 212 | 213 | var name 214 | if (args.length > 2) { 215 | var argsArray = slice(args, 1) 216 | for (name in obj) on.apply(self, concat(name, obj[name], argsArray)) 217 | } 218 | else for (name in obj) on.call(self, name, obj[name], args[1]) 219 | 220 | return true 221 | } 222 | 223 | function define(obj, name, value) { 224 | Object.defineProperty(obj, name, {value: value, configurable: 1, writable: 1}) 225 | return value 226 | } 227 | 228 | function create(obj, name, prototype) { 229 | return define(obj, name, Object.create(prototype || null)) 230 | } 231 | 232 | function filter(events, name, fn, thisArg) { 233 | var fns = events[name] 234 | if (fns == null) return 235 | 236 | fns = fns.filter(function(args) { 237 | if (fn != null && !hasFunction(args, fn)) return true 238 | if (thisArg !== undefined && !hasThis(args, thisArg)) return true 239 | return false 240 | }) 241 | 242 | if (fns.length == 0) { 243 | delete events[name] 244 | if (name in events) events[name] = null 245 | } 246 | else events[name] = fns 247 | } 248 | 249 | function hasFunction(args, fn) { 250 | return args[0] === fn || args[0].__func === fn 251 | } 252 | 253 | function hasThis(args, thisArg) { 254 | return (args[0].__this || args[1]) === thisArg 255 | } 256 | 257 | function apply(fns, thisArg, args) { 258 | for (var i = 0, l = fns.length; i < l; ++i) { 259 | var fn = fns[i] 260 | 261 | var argsArray = fn.length > 2 ? fn.slice(2) : null 262 | if (argsArray == null) argsArray = args 263 | else if (args.length > 0) argsArray.push.apply(argsArray, args) 264 | 265 | fn[0].apply(fn[1] === undefined ? thisArg : fn[1], argsArray) 266 | } 267 | } 268 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "concert", 3 | "version": "2.1.0", 4 | "description": "An event library that implements the observer pattern (a.k.a publish/subscribe). Similar to Node's EventEmitter and Backbone.Events, but decoupled, minimal and light-weight. Supports inherited listeners.", 5 | "keywords": [ 6 | "bind", 7 | "emitter", 8 | "event", 9 | "eventemitter", 10 | "events", 11 | "observer", 12 | "pubsub", 13 | "trigger" 14 | ], 15 | "homepage": "https://github.com/moll/js-concert", 16 | "bugs": "https://github.com/moll/js-concert/issues", 17 | 18 | "author": { 19 | "name": "Andri Möll", 20 | "email": "andri@dot.ee", 21 | "url": "http://themoll.com" 22 | }, 23 | 24 | "repository": { 25 | "type": "git", 26 | "url": "git://github.com/moll/js-concert.git" 27 | }, 28 | 29 | "licenses": [{ 30 | "type": "LAGPL", 31 | "url": "https://github.com/moll/js-concert/blob/master/LICENSE" 32 | }], 33 | 34 | "main": "index.js", 35 | "scripts": {"test": "make test"}, 36 | 37 | "devDependencies": { 38 | "mocha": ">= 2.5.3 < 3", 39 | "must": ">= 0.13.2 < 0.14", 40 | "sinon": ">= 1.7.3 < 2", 41 | "underscore": ">= 1.5.2 < 2", 42 | "benchmark": ">= 1.0.0 < 2" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /test/index_test.js: -------------------------------------------------------------------------------- 1 | var _ = require("underscore") 2 | var Sinon = require("sinon") 3 | var Concert = require("..") 4 | 5 | describe("Concert", function() { 6 | function create() { return _.extend({}, Concert) } 7 | 8 | it("must only have public API as enumerable properties", function() { 9 | Concert.must.have.keys(["on", "once", "off", "trigger"]) 10 | }) 11 | 12 | describe(".on", function() { 13 | it("must be renameable", function() { 14 | var obj = create() 15 | obj.addEventListener = obj.on 16 | delete obj.on 17 | 18 | var fn = Sinon.spy(), context = {} 19 | obj.addEventListener("foo", fn, context) 20 | obj.trigger("foo") 21 | fn.firstCall.thisValue.must.equal(context) 22 | }) 23 | 24 | // This was a bug in v0.1.337. 25 | it("must allow names of Object.prototype's properties", function() { 26 | var obj = create() 27 | var fn = Sinon.spy() 28 | obj.on("hasOwnProperty", fn) 29 | obj.trigger("hasOwnProperty") 30 | fn.callCount.must.equal(1) 31 | }) 32 | 33 | it("must inherit parent's events without changing parent", function() { 34 | var obj = create() 35 | var child = Object.create(obj) 36 | 37 | var a = Sinon.spy() 38 | obj.on("foo", a) 39 | var b = Sinon.spy() 40 | child.on("foo", b) 41 | 42 | child.trigger("foo") 43 | a.callCount.must.equal(1) 44 | b.callCount.must.equal(1) 45 | 46 | obj.trigger("foo") 47 | a.callCount.must.equal(2) 48 | b.callCount.must.equal(1) 49 | }) 50 | 51 | it("must bind to inherited object by default", function() { 52 | var obj = create() 53 | var child = Object.create(obj) 54 | 55 | var a = Sinon.spy() 56 | obj.on("foo", a) 57 | var b = Sinon.spy() 58 | child.on("foo", b) 59 | 60 | child.trigger("foo") 61 | a.thisValues[0].must.equal(child) 62 | b.thisValues[0].must.equal(child) 63 | }) 64 | 65 | it("must not inherit parent's events after calling off", function() { 66 | var obj = create() 67 | var child = Object.create(obj) 68 | 69 | var a = Sinon.spy() 70 | obj.on("foo", a) 71 | var b = Sinon.spy() 72 | child.on("foo", b) 73 | child.off() 74 | 75 | child.on("foo", b) 76 | child.trigger("foo") 77 | a.callCount.must.equal(0) 78 | b.callCount.must.equal(1) 79 | }) 80 | 81 | it("must create non-enumerable this._events", function() { 82 | var obj = create() 83 | obj.on("foo", function() {}) 84 | obj.must.have.nonenumerable("_events") 85 | }) 86 | 87 | // This was a bug on Feb 15, 2015 related to setting _events to an object 88 | // inheriting from Object.prototype when it already existed on the object, 89 | // rather than null as usual. 90 | it("must bind to Object.prototype's property after unbinding all", 91 | function(){ 92 | var obj = create() 93 | obj.on("foo", function() {}) 94 | obj.off() 95 | 96 | var fn = Sinon.spy() 97 | obj.on("hasOwnProperty", fn) 98 | obj.trigger("hasOwnProperty") 99 | fn.callCount.must.equal(1) 100 | }) 101 | 102 | it("must bind after unbinding an inherited event", function(){ 103 | var obj = create() 104 | function onFoo() {} 105 | obj.on("foo", onFoo) 106 | 107 | var child = Object.create(obj) 108 | child.off("foo", onFoo) 109 | 110 | var fn = Sinon.spy() 111 | child.on("foo", fn) 112 | child.trigger("foo") 113 | fn.callCount.must.equal(1) 114 | }) 115 | 116 | it("must bind after unbinding an inherited event and function", function(){ 117 | var obj = create() 118 | obj.on("foo", function() {}) 119 | 120 | var child = Object.create(obj) 121 | child.off("foo", fn) 122 | 123 | var fn = Sinon.spy() 124 | child.on("foo", fn) 125 | child.trigger("foo") 126 | fn.callCount.must.equal(1) 127 | }) 128 | 129 | describe("given nothing", function() { 130 | it("must throw TypeError", function() { 131 | var obj = create() 132 | var err 133 | try { obj.on() } catch (ex) { err = ex } 134 | err.must.be.an.instanceof(TypeError) 135 | err.message.must.match(/name/i) 136 | obj.must.not.have.property("_events") 137 | }) 138 | }) 139 | 140 | describe("given only name", function() { 141 | it("must throw TypeError", function() { 142 | var obj = create() 143 | var err 144 | try { obj.on("foo") } catch (ex) { err = ex } 145 | err.must.be.an.instanceof(TypeError) 146 | err.message.must.match(/function/i) 147 | }) 148 | }) 149 | 150 | describe("given name and function", function() { 151 | it("must return self", function() { 152 | var obj = create() 153 | obj.on("foo", function() {}).must.equal(obj) 154 | }) 155 | 156 | it("must bind", function() { 157 | var obj = create() 158 | var fn = Sinon.spy() 159 | obj.on("foo", fn) 160 | obj.trigger("foo") 161 | fn.callCount.must.equal(1) 162 | }) 163 | 164 | it("must bind to object context by default", function() { 165 | var obj = create() 166 | var fn = Sinon.spy() 167 | obj.on("foo", fn).trigger("foo") 168 | fn.firstCall.thisValue.must.equal(obj) 169 | }) 170 | 171 | it("must bind twice if called again", function() { 172 | var obj = create() 173 | var fn = Sinon.spy() 174 | obj.on("foo", fn) 175 | obj.on("foo", fn) 176 | obj.trigger("foo") 177 | fn.callCount.must.equal(2) 178 | }) 179 | 180 | it("must bind given 0 name", function() { 181 | var obj = create() 182 | var fn = Sinon.spy() 183 | obj.on(0, fn) 184 | obj.trigger("0") 185 | fn.callCount.must.equal(1) 186 | }) 187 | 188 | if (typeof Symbol != "undefined") 189 | it("must bind to symbol", function() { 190 | var obj = create() 191 | var symbol = Symbol() 192 | var fn = Sinon.spy() 193 | obj.on(symbol, fn) 194 | obj.trigger(symbol) 195 | fn.callCount.must.equal(1) 196 | }) 197 | 198 | if (typeof Symbol != "undefined") 199 | it("must differentiate between two symbols", function() { 200 | var obj = create() 201 | 202 | var a = Symbol() 203 | var b = Symbol() 204 | var onA = Sinon.spy() 205 | var onB = Sinon.spy() 206 | obj.on(a, onA) 207 | obj.on(b, onB) 208 | 209 | obj.trigger(a) 210 | onA.callCount.must.equal(1) 211 | onB.callCount.must.equal(0) 212 | }) 213 | }) 214 | 215 | describe("given name, function and context", function() { 216 | it("must bind", function() { 217 | var obj = create() 218 | var context = {} 219 | var fn = Sinon.spy() 220 | obj.on("foo", fn, context) 221 | obj.trigger("foo") 222 | fn.firstCall.thisValue.must.equal(context) 223 | }) 224 | 225 | it("must bind to object context given undefined context", function() { 226 | var obj = create() 227 | var fn = Sinon.spy() 228 | obj.on("foo", fn, undefined).trigger("foo") 229 | fn.firstCall.thisValue.must.equal(obj) 230 | }) 231 | 232 | it("must bind to null given null context", function() { 233 | var obj = create() 234 | var fn = Sinon.spy() 235 | obj.on("foo", fn, null).trigger("foo") 236 | fn.firstCall.must.have.property("thisValue", null) 237 | }) 238 | 239 | it("must bind with argument", function() { 240 | var obj = create() 241 | var fn = Sinon.spy() 242 | obj.on("foo", fn, null, 1).trigger("foo") 243 | fn.firstCall.args.must.eql([1]) 244 | }) 245 | 246 | it("must bind with arguments", function() { 247 | var obj = create() 248 | var fn = Sinon.spy() 249 | obj.on("foo", fn, null, 1, 2, 3).trigger("foo") 250 | fn.firstCall.args.must.eql([1, 2, 3]) 251 | }) 252 | }) 253 | 254 | describe("given object", function() { 255 | it("must return self", function() { 256 | var obj = create() 257 | obj.on({foo: function() {}}).must.equal(obj) 258 | }) 259 | 260 | it("must bind all given", function() { 261 | var obj = create() 262 | var foo = Sinon.spy(), bar = Sinon.spy() 263 | obj.on({foo: foo, bar: bar}) 264 | obj.trigger("foo") 265 | foo.callCount.must.equal(1) 266 | bar.callCount.must.equal(0) 267 | obj.trigger("bar") 268 | foo.callCount.must.equal(1) 269 | bar.callCount.must.equal(1) 270 | }) 271 | 272 | it("must bind to object context by default", function() { 273 | var obj = create() 274 | var fn = Sinon.spy() 275 | obj.on({foo: fn}).trigger("foo") 276 | fn.firstCall.thisValue.must.equal(obj) 277 | }) 278 | 279 | it("must throw TypeError given null for a function", function() { 280 | var obj = create() 281 | var err 282 | try { obj.on({foo: null}) } catch (ex) { err = ex } 283 | err.must.be.an.instanceof(TypeError) 284 | err.message.must.match(/function/i) 285 | obj.must.not.have.property("_events") 286 | }) 287 | }) 288 | 289 | describe("given object and context", function() { 290 | it("must bind", function() { 291 | var obj = create() 292 | var context = {} 293 | var fn = Sinon.spy() 294 | obj.on({foo: fn}, context) 295 | obj.trigger("foo") 296 | fn.firstCall.thisValue.must.equal(context) 297 | }) 298 | 299 | it("must bind to object context given undefined context", function() { 300 | var obj = create() 301 | var fn = Sinon.spy() 302 | obj.on({foo: fn}, undefined).trigger("foo") 303 | fn.firstCall.thisValue.must.equal(obj) 304 | }) 305 | 306 | it("must bind to null given null context", function() { 307 | var obj = create() 308 | var fn = Sinon.spy() 309 | obj.on({foo: fn}, null).trigger("foo") 310 | fn.firstCall.must.have.property("thisValue", null) 311 | }) 312 | 313 | it("must bind with argument", function() { 314 | var obj = create() 315 | var fn = Sinon.spy() 316 | obj.on({foo: fn}, null, 1).trigger("foo") 317 | fn.firstCall.args.must.eql([1]) 318 | }) 319 | 320 | it("must bind with arguments", function() { 321 | var obj = create() 322 | var fn = Sinon.spy() 323 | obj.on({foo: fn}, null, 1, 2, 3).trigger("foo") 324 | fn.firstCall.args.must.eql([1, 2, 3]) 325 | }) 326 | }) 327 | }) 328 | 329 | describe(".once", function() { 330 | // Not working as expected yet with the inheritance support. 331 | xit("must not call twice when event triggered from handler", function() { 332 | var obj = create() 333 | var fn = Sinon.spy() 334 | obj.on("foo", _.once(function() { obj.trigger("foo") })) 335 | obj.once("foo", fn) 336 | obj.trigger("foo") 337 | fn.callCount.must.equal(1) 338 | }) 339 | 340 | describe("given nothing", function() { 341 | it("must throw TypeError", function() { 342 | var obj = create() 343 | var err 344 | try { obj.once() } catch (ex) { err = ex } 345 | err.must.be.an.instanceof(TypeError) 346 | err.message.must.match(/name/i) 347 | obj.must.not.have.property("_events") 348 | }) 349 | }) 350 | 351 | describe("given only name", function() { 352 | it("must throw TypeError", function() { 353 | var obj = create() 354 | var err 355 | try { obj.once("foo") } catch (ex) { err = ex } 356 | err.must.be.an.instanceof(TypeError) 357 | err.message.must.match(/function/i) 358 | obj.must.not.have.property("_events") 359 | }) 360 | }) 361 | 362 | describe("given name and function", function() { 363 | it("must return self", function() { 364 | var obj = create() 365 | obj.once("foo", function() {}).must.equal(obj) 366 | }) 367 | 368 | it("must bind once", function() { 369 | var obj = create() 370 | var fn = Sinon.spy() 371 | obj.once("foo", fn) 372 | obj.trigger("foo") 373 | obj.trigger("foo") 374 | fn.callCount.must.equal(1) 375 | }) 376 | 377 | it("must bind to object context by default", function() { 378 | var obj = create() 379 | var fn = Sinon.spy() 380 | obj.once("foo", fn) 381 | obj.trigger("foo") 382 | fn.firstCall.thisValue.must.equal(obj) 383 | }) 384 | 385 | it("must bind to object context by default when inherited", function() { 386 | var obj = create() 387 | var fn = Sinon.spy() 388 | obj.once("foo", fn) 389 | var child = Object.create(obj) 390 | child.trigger("foo") 391 | fn.firstCall.thisValue.must.equal(child) 392 | }) 393 | 394 | it("must bind once when inherited", function() { 395 | var obj = create() 396 | var fn = Sinon.spy() 397 | obj.once("foo", fn) 398 | 399 | var child = Object.create(obj) 400 | child.trigger("foo") 401 | child.trigger("foo") 402 | fn.callCount.must.equal(1) 403 | }) 404 | 405 | it("must not change the parent when called on child", function() { 406 | var obj = create() 407 | var fn = Sinon.spy() 408 | obj.once("foo", fn) 409 | 410 | var child = Object.create(obj) 411 | child.trigger("foo") 412 | 413 | obj.trigger("foo") 414 | fn.callCount.must.equal(2) 415 | }) 416 | 417 | it("must bind twice if called again", function() { 418 | var obj = create() 419 | var fn = Sinon.spy() 420 | obj.once("foo", fn) 421 | obj.once("foo", fn) 422 | obj.trigger("foo") 423 | fn.callCount.must.equal(2) 424 | }) 425 | 426 | it("must not affect other events by same name", function() { 427 | var obj = create() 428 | var fn = Sinon.spy() 429 | var other = Sinon.spy() 430 | obj.once("foo", fn) 431 | obj.on("foo", other) 432 | obj.trigger("foo") 433 | obj.trigger("foo") 434 | other.callCount.must.equal(2) 435 | }) 436 | }) 437 | 438 | describe("given name, function and context", function() { 439 | it("must bind", function() { 440 | var obj = create() 441 | var context = {} 442 | var fn = Sinon.spy() 443 | obj.once("foo", fn, context) 444 | obj.trigger("foo") 445 | fn.firstCall.thisValue.must.equal(context) 446 | }) 447 | 448 | it("must bind once", function() { 449 | var obj = create() 450 | var context = {} 451 | var fn = Sinon.spy() 452 | obj.once("foo", fn, context) 453 | obj.trigger("foo") 454 | obj.trigger("foo") 455 | fn.callCount.must.equal(1) 456 | }) 457 | 458 | it("must bind to object context given undefined context", function() { 459 | var obj = create() 460 | var fn = Sinon.spy() 461 | obj.once("foo", fn, undefined).trigger("foo") 462 | fn.firstCall.thisValue.must.equal(obj) 463 | }) 464 | 465 | it("must bind to null given null context", function() { 466 | var obj = create() 467 | var fn = Sinon.spy() 468 | obj.once("foo", fn, null).trigger("foo") 469 | fn.firstCall.must.have.property("thisValue", null) 470 | }) 471 | 472 | it("must bind with argument", function() { 473 | var obj = create() 474 | var fn = Sinon.spy() 475 | obj.once("foo", fn, null, 1).trigger("foo") 476 | fn.firstCall.args.must.eql([1]) 477 | }) 478 | 479 | it("must bind with arguments", function() { 480 | var obj = create() 481 | var fn = Sinon.spy() 482 | obj.once("foo", fn, null, 1, 2, 3).trigger("foo") 483 | fn.firstCall.args.must.eql([1, 2, 3]) 484 | }) 485 | }) 486 | 487 | describe("given object", function() { 488 | it("must return self", function() { 489 | var obj = create() 490 | obj.once({foo: function() {}}).must.equal(obj) 491 | }) 492 | 493 | it("must bind all given", function() { 494 | var obj = create() 495 | var foo = Sinon.spy(), bar = Sinon.spy() 496 | obj.once({foo: foo, bar: bar}) 497 | obj.trigger("foo") 498 | foo.callCount.must.equal(1) 499 | bar.callCount.must.equal(0) 500 | obj.trigger("bar") 501 | foo.callCount.must.equal(1) 502 | bar.callCount.must.equal(1) 503 | }) 504 | 505 | it("must bind once", function() { 506 | var obj = create() 507 | var foo = Sinon.spy(), bar = Sinon.spy() 508 | obj.once({foo: foo, bar: bar}) 509 | obj.trigger("foo") 510 | obj.trigger("foo") 511 | obj.trigger("bar") 512 | obj.trigger("bar") 513 | foo.callCount.must.equal(1) 514 | bar.callCount.must.equal(1) 515 | }) 516 | 517 | it("must bind to object context by default", function() { 518 | var obj = create() 519 | obj.once({foo: function() { this.must.equal(obj) }}).trigger("foo") 520 | }) 521 | 522 | it("must throw TypeError given null for a function", function() { 523 | var obj = create() 524 | var err 525 | try { obj.once({foo: null}) } catch (ex) { err = ex } 526 | err.must.be.an.instanceof(TypeError) 527 | err.message.must.match(/function/i) 528 | obj.must.not.have.property("_events") 529 | }) 530 | }) 531 | 532 | describe("given object and context", function() { 533 | it("must bind", function() { 534 | var obj = create() 535 | var context = {} 536 | var fn = Sinon.spy() 537 | obj.once({foo: fn}, context) 538 | obj.trigger("foo") 539 | fn.firstCall.thisValue.must.equal(context) 540 | }) 541 | 542 | it("must bind to object context given undefined context", function() { 543 | var obj = create() 544 | var fn = Sinon.spy() 545 | obj.once({foo: fn}, undefined).trigger("foo") 546 | fn.firstCall.thisValue.must.equal(obj) 547 | }) 548 | 549 | it("must bind to null given null context", function() { 550 | var obj = create() 551 | var fn = Sinon.spy() 552 | obj.once({foo: fn}, null).trigger("foo") 553 | fn.firstCall.must.have.property("thisValue", null) 554 | }) 555 | 556 | it("must bind with argument", function() { 557 | var obj = create() 558 | var fn = Sinon.spy() 559 | obj.once({foo: fn}, null, 1).trigger("foo") 560 | fn.firstCall.args.must.eql([1]) 561 | }) 562 | 563 | it("must bind with arguments", function() { 564 | var obj = create() 565 | var fn = Sinon.spy() 566 | obj.once({foo: fn}, null, 1, 2, 3).trigger("foo") 567 | fn.firstCall.args.must.eql([1, 2, 3]) 568 | }) 569 | }) 570 | 571 | it("must be renameable", function() { 572 | var obj = create() 573 | obj.addEventListenerOnce = obj.once 574 | delete obj.once 575 | 576 | var fn = Sinon.spy() 577 | obj.addEventListenerOnce("foo", fn) 578 | obj.trigger("foo") 579 | obj.trigger("foo") 580 | fn.callCount.must.equal(1) 581 | }) 582 | 583 | it("must be renameable with both Concert.on and Concert.off", function() { 584 | var obj = create() 585 | obj.addEventListener = obj.on 586 | delete obj.on 587 | obj.addEventListenerOnce = obj.once 588 | delete obj.once 589 | obj.removeEventListener = obj.off 590 | delete obj.off 591 | 592 | var fn = Sinon.spy() 593 | obj.addEventListenerOnce("foo", fn) 594 | obj.trigger("foo") 595 | obj.trigger("foo") 596 | fn.callCount.must.equal(1) 597 | }) 598 | }) 599 | 600 | describe(".off", function() { 601 | describe("given nothing", function() { 602 | it("must return self", function() { 603 | var obj = create() 604 | obj.off().must.equal(obj) 605 | }) 606 | 607 | it("must not create this._events if it didn't exist", function() { 608 | var obj = create() 609 | obj.off() 610 | obj.must.not.have.property("_events") 611 | }) 612 | 613 | it("must unbind all", function() { 614 | var obj = create() 615 | var fn1 = Sinon.spy(), fn2 = Sinon.spy() 616 | obj.on("foo", fn1) 617 | obj.on("bar", fn2) 618 | obj.off() 619 | obj.trigger("foo") 620 | obj.trigger("bar") 621 | fn1.callCount.must.equal(0) 622 | fn2.callCount.must.equal(0) 623 | }) 624 | 625 | it("must unbind inherited events without changing parent", function() { 626 | var obj = create() 627 | var fn = Sinon.spy() 628 | obj.on("foo", fn) 629 | 630 | var child = Object.create(obj) 631 | child.off() 632 | child.trigger("foo") 633 | fn.callCount.must.equal(0) 634 | 635 | obj.trigger("foo") 636 | fn.callCount.must.equal(1) 637 | }) 638 | 639 | it("must unbind inherited and own events without changing parent", 640 | function() { 641 | var obj = create() 642 | var child = Object.create(obj) 643 | 644 | var a = Sinon.spy() 645 | obj.on("foo", a) 646 | var b = Sinon.spy() 647 | child.on("foo", b) 648 | 649 | child.off() 650 | child.trigger("foo") 651 | a.callCount.must.equal(0) 652 | b.callCount.must.equal(0) 653 | 654 | obj.trigger("foo") 655 | a.callCount.must.equal(1) 656 | }) 657 | 658 | it("must create non-enumerable this._events given inherited events", 659 | function() { 660 | var obj = create() 661 | obj.on("foo", function() {}) 662 | var child = Object.create(obj) 663 | child.off() 664 | child.must.have.nonenumerable("_events") 665 | }) 666 | }) 667 | 668 | describe("given name", function() { 669 | it("must return self", function() { 670 | var obj = create() 671 | obj.off("foo").must.equal(obj) 672 | }) 673 | 674 | it("must not create this._events if it didn't exist", function() { 675 | var obj = create() 676 | obj.off("foo") 677 | obj.must.not.have.property("_events") 678 | }) 679 | 680 | it("must unbind", function() { 681 | var obj = create() 682 | var fn = Sinon.spy() 683 | obj.on("foo", fn) 684 | obj.off("foo") 685 | obj.trigger("foo") 686 | fn.callCount.must.equal(0) 687 | }) 688 | 689 | it("must not unbind others with different names", function() { 690 | var obj = create() 691 | var fn = Sinon.spy() 692 | obj.on("foo", fn) 693 | obj.off("bar") 694 | obj.trigger("foo") 695 | fn.callCount.must.equal(1) 696 | }) 697 | 698 | it("must not unbind others with different names given 0", function() { 699 | var obj = create() 700 | var fn = Sinon.spy() 701 | obj.on("foo", fn) 702 | obj.off(0) 703 | obj.trigger("foo") 704 | fn.callCount.must.equal(1) 705 | }) 706 | 707 | it("must unbind inherited events without changing parent", function() { 708 | var obj = create() 709 | var fn = Sinon.spy() 710 | obj.on("foo", fn) 711 | 712 | var child = Object.create(obj) 713 | child.off("foo") 714 | child.trigger("foo") 715 | fn.callCount.must.equal(0) 716 | 717 | obj.trigger("foo") 718 | fn.callCount.must.equal(1) 719 | }) 720 | 721 | it("must unbind inherited and own events without changing parent", 722 | function() { 723 | var obj = create() 724 | var child = Object.create(obj) 725 | 726 | var a = Sinon.spy() 727 | obj.on("foo", a) 728 | var b = Sinon.spy() 729 | child.on("foo", b) 730 | 731 | child.off("foo") 732 | child.trigger("foo") 733 | a.callCount.must.equal(0) 734 | b.callCount.must.equal(0) 735 | 736 | obj.trigger("foo") 737 | a.callCount.must.equal(1) 738 | }) 739 | 740 | it("must create non-enumerable this._events given inherited events", 741 | function() { 742 | var obj = create() 743 | obj.on("foo", function() {}) 744 | var child = Object.create(obj) 745 | child.off("foo") 746 | child.must.have.nonenumerable("_events") 747 | }) 748 | }) 749 | 750 | describe("given name and function", function() { 751 | it("must return self", function() { 752 | var obj = create() 753 | obj.off("foo", function() {}).must.equal(obj) 754 | }) 755 | 756 | it("must not create this._events if it didn't exist", function() { 757 | var obj = create() 758 | obj.off("foo", function() {}) 759 | obj.must.not.have.property("_events") 760 | }) 761 | 762 | it("must unbind", function() { 763 | var obj = create() 764 | var fn = Sinon.spy() 765 | obj.on("foo", fn) 766 | obj.off("foo", fn) 767 | obj.trigger("foo") 768 | fn.callCount.must.equal(0) 769 | }) 770 | 771 | it("must not unbind others with different functions", function() { 772 | var obj = create() 773 | var fn = Sinon.spy() 774 | obj.on("foo", fn) 775 | obj.off("foo", function() {}) 776 | obj.trigger("foo") 777 | fn.callCount.must.equal(1) 778 | }) 779 | 780 | it("must not unbind others with different names", function() { 781 | var obj = create() 782 | var fn = Sinon.spy() 783 | obj.on("foo", fn) 784 | obj.off("bar", fn) 785 | obj.trigger("foo") 786 | fn.callCount.must.equal(1) 787 | }) 788 | 789 | it("must not unbind others with different names given 0", function() { 790 | var obj = create() 791 | var fn = Sinon.spy() 792 | obj.on("foo", fn) 793 | obj.off(0, fn) 794 | obj.trigger("foo") 795 | fn.callCount.must.equal(1) 796 | }) 797 | 798 | it("must unbind functions bound with \"once\"", function() { 799 | var obj = create() 800 | var fn = Sinon.spy() 801 | obj.once("foo", fn) 802 | obj.off("foo", fn) 803 | obj.trigger("foo") 804 | fn.callCount.must.equal(0) 805 | }) 806 | 807 | it("must unbind functions bound both with \"on\" & \"once\"", function() { 808 | var obj = create() 809 | var fn = Sinon.spy() 810 | obj.on("foo", fn) 811 | obj.once("foo", fn) 812 | obj.off("foo", fn) 813 | obj.trigger("foo") 814 | fn.callCount.must.equal(0) 815 | }) 816 | 817 | it("must unbind inherited events without changing parent", function() { 818 | var obj = create() 819 | var fn = Sinon.spy() 820 | obj.on("foo", fn) 821 | 822 | var child = Object.create(obj) 823 | child.off("foo", fn) 824 | child.trigger("foo") 825 | fn.callCount.must.equal(0) 826 | 827 | obj.trigger("foo") 828 | fn.callCount.must.equal(1) 829 | }) 830 | 831 | it("must unbind inherited and own events without changing parent", 832 | function() { 833 | var obj = create() 834 | var child = Object.create(obj) 835 | 836 | var fn = Sinon.spy() 837 | obj.on("foo", fn) 838 | child.on("foo", fn) 839 | 840 | child.off("foo", fn) 841 | child.trigger("foo") 842 | fn.callCount.must.equal(0) 843 | 844 | obj.trigger("foo") 845 | fn.callCount.must.equal(1) 846 | }) 847 | }) 848 | 849 | describe("given name and context", function() { 850 | it("must unbind", function() { 851 | var obj = create() 852 | var context = {} 853 | var fn = Sinon.spy() 854 | obj.on("foo", function() {}, context) 855 | obj.off("foo", null, context) 856 | obj.trigger("foo") 857 | fn.callCount.must.equal(0) 858 | }) 859 | 860 | it("must not unbind others with different contexts", function() { 861 | var obj = create() 862 | var fn = Sinon.spy() 863 | obj.on("foo", fn, {}) 864 | obj.off("foo", null, {}) 865 | obj.trigger("foo") 866 | fn.callCount.must.equal(1) 867 | }) 868 | 869 | it("must not unbind others with different names", function() { 870 | var obj = create() 871 | var context = {} 872 | var fn = Sinon.spy() 873 | obj.on("foo", fn, context) 874 | obj.off("bar", null, context) 875 | obj.trigger("foo") 876 | fn.callCount.must.equal(1) 877 | }) 878 | 879 | it("must unbind given undefined context", function() { 880 | var obj = create() 881 | var fn = Sinon.spy() 882 | obj.on("foo", fn, undefined) 883 | obj.off("foo", null, undefined) 884 | obj.trigger("foo") 885 | fn.callCount.must.equal(0) 886 | }) 887 | 888 | it("must unbind given null context", function() { 889 | var obj = create() 890 | var fn = Sinon.spy() 891 | obj.on("foo", fn, null) 892 | obj.off("foo", null, null) 893 | obj.trigger("foo") 894 | fn.callCount.must.equal(0) 895 | }) 896 | 897 | it("must not unbind undefined contexts given null context", function() { 898 | var obj = create() 899 | var fn = Sinon.spy() 900 | obj.on("foo", fn, undefined) 901 | obj.off("foo", null, null) 902 | obj.trigger("foo") 903 | fn.callCount.must.equal(1) 904 | }) 905 | 906 | it("must unbind null contexts given undefined context", function() { 907 | var obj = create() 908 | var fn = Sinon.spy() 909 | obj.on("foo", fn, null) 910 | obj.off("foo", null, undefined) 911 | obj.trigger("foo") 912 | fn.callCount.must.equal(0) 913 | }) 914 | }) 915 | 916 | describe("given name, function and context", function() { 917 | it("must return self", function() { 918 | var obj = create() 919 | obj.off("foo", function() {}, {}).must.equal(obj) 920 | }) 921 | 922 | it("must unbind", function() { 923 | var obj = create() 924 | var context = {} 925 | var fn = Sinon.spy() 926 | obj.on("foo", fn, context) 927 | obj.off("foo", fn, context) 928 | obj.trigger("foo") 929 | fn.callCount.must.equal(0) 930 | }) 931 | 932 | it("must unbind functions bound with \"once\"", function() { 933 | var obj = create() 934 | var context = {} 935 | var fn = Sinon.spy() 936 | obj.once("foo", fn, context) 937 | obj.off("foo", fn, context) 938 | obj.trigger("foo") 939 | fn.callCount.must.equal(0) 940 | }) 941 | 942 | it("must not unbind others with different functions", function() { 943 | var obj = create() 944 | var context = {} 945 | var fn = Sinon.spy() 946 | obj.on("foo", fn, context) 947 | obj.off("foo", function() {}, context) 948 | obj.trigger("foo") 949 | fn.callCount.must.equal(1) 950 | }) 951 | 952 | it("must not unbind others with different contexts", function() { 953 | var obj = create() 954 | var fn = Sinon.spy() 955 | obj.on("foo", fn, {}) 956 | obj.off("foo", fn, {}) 957 | obj.trigger("foo") 958 | fn.callCount.must.equal(1) 959 | }) 960 | }) 961 | 962 | describe("given function", function() { 963 | it("must not unbind undefined context if given null", function() { 964 | var obj = create() 965 | var fn = Sinon.spy() 966 | obj.on("foo", fn, undefined) 967 | obj.off(null, fn, null) 968 | obj.trigger("foo") 969 | fn.callCount.must.equal(1) 970 | }) 971 | 972 | it("must unbind null context if given undefined", function() { 973 | var obj = create() 974 | var fn = Sinon.spy() 975 | obj.on("foo", fn, null) 976 | obj.off(null, fn, undefined) 977 | obj.trigger("foo") 978 | fn.callCount.must.equal(0) 979 | }) 980 | 981 | it("must unbind functions bound with \"once\"", function() { 982 | var obj = create() 983 | var fn = Sinon.spy() 984 | obj.once("foo", fn) 985 | obj.off(null, fn) 986 | obj.trigger("foo") 987 | fn.callCount.must.equal(0) 988 | }) 989 | 990 | it("must unbind inherited events without changing parent", function() { 991 | var obj = create() 992 | var fn = Sinon.spy() 993 | obj.on("foo", fn) 994 | 995 | var child = Object.create(obj) 996 | child.off(null, fn) 997 | child.trigger("foo") 998 | fn.callCount.must.equal(0) 999 | 1000 | obj.trigger("foo") 1001 | fn.callCount.must.equal(1) 1002 | }) 1003 | 1004 | it("must unbind inherited and own events without changing parent", 1005 | function() { 1006 | var obj = create() 1007 | var child = Object.create(obj) 1008 | 1009 | var fn = Sinon.spy() 1010 | obj.on("foo", fn) 1011 | child.on("foo", fn) 1012 | 1013 | child.off(null, fn) 1014 | child.trigger("foo") 1015 | fn.callCount.must.equal(0) 1016 | 1017 | obj.trigger("foo") 1018 | fn.callCount.must.equal(1) 1019 | }) 1020 | }) 1021 | 1022 | describe("given function and context", function() { 1023 | it("must unbind", function() { 1024 | var obj = create() 1025 | var context = {} 1026 | var fn = Sinon.spy() 1027 | obj.on("foo", fn, context) 1028 | obj.off(null, fn, context) 1029 | obj.trigger("foo") 1030 | fn.callCount.must.equal(0) 1031 | }) 1032 | 1033 | it("must unbind functions bound with \"once\"", function() { 1034 | var obj = create() 1035 | var context = {} 1036 | var fn = Sinon.spy() 1037 | obj.once("foo", fn, context) 1038 | obj.off(null, fn, context) 1039 | obj.trigger("foo") 1040 | fn.callCount.must.equal(0) 1041 | }) 1042 | 1043 | it("must not unbind others with different functions", function() { 1044 | var obj = create() 1045 | var context = {} 1046 | var fn = Sinon.spy() 1047 | obj.on("foo", fn, context) 1048 | obj.off(null, function() {}, context) 1049 | obj.trigger("foo") 1050 | fn.callCount.must.equal(1) 1051 | }) 1052 | 1053 | it("must not unbind others with different contexts", function() { 1054 | var obj = create() 1055 | var fn = Sinon.spy() 1056 | obj.on("foo", fn, {}) 1057 | obj.off(null, fn, {}) 1058 | obj.trigger("foo") 1059 | fn.callCount.must.equal(1) 1060 | }) 1061 | }) 1062 | 1063 | describe("given object", function() { 1064 | it("must return self", function() { 1065 | var obj = create() 1066 | obj.off({foo: function() {}}).must.equal(obj) 1067 | }) 1068 | 1069 | it("must unbind all given", function() { 1070 | var obj = create() 1071 | var foo = Sinon.spy(), bar = Sinon.spy() 1072 | obj.on({foo: foo, bar: bar}) 1073 | obj.off({foo: foo, bar: bar}) 1074 | obj.trigger("foo") 1075 | obj.trigger("bar") 1076 | foo.callCount.must.equal(0) 1077 | bar.callCount.must.equal(0) 1078 | }) 1079 | 1080 | it("must not unbind others with different functions", function() { 1081 | var obj = create() 1082 | var fn = Sinon.spy() 1083 | obj.on({foo: fn}) 1084 | obj.off({foo: function() {}}) 1085 | obj.trigger("foo") 1086 | fn.callCount.must.equal(1) 1087 | }) 1088 | 1089 | it("must not unbind others with different names", function() { 1090 | var obj = create() 1091 | var fn = Sinon.spy() 1092 | obj.on({foo: fn}) 1093 | obj.off({bar: fn}) 1094 | obj.trigger("foo") 1095 | fn.callCount.must.equal(1) 1096 | }) 1097 | }) 1098 | 1099 | describe("given object and context", function() { 1100 | it("must unbind all given", function() { 1101 | var obj = create() 1102 | var context = {} 1103 | var foo = Sinon.spy(), bar = Sinon.spy() 1104 | obj.on({foo: foo, bar: bar}, context) 1105 | obj.off({foo: foo, bar: bar}, context) 1106 | obj.trigger("foo") 1107 | obj.trigger("bar") 1108 | foo.callCount.must.equal(0) 1109 | bar.callCount.must.equal(0) 1110 | }) 1111 | 1112 | it("must not unbind others with different functions", function() { 1113 | var obj = create() 1114 | var context = {} 1115 | var fn = Sinon.spy() 1116 | obj.on({foo: fn}, context) 1117 | obj.off({foo: function() {}}, context) 1118 | obj.trigger("foo") 1119 | fn.callCount.must.equal(1) 1120 | }) 1121 | 1122 | it("must not unbind others with different names", function() { 1123 | var obj = create() 1124 | var context = {} 1125 | var fn = Sinon.spy() 1126 | obj.on({foo: fn}, context) 1127 | obj.off({bar: fn}, context) 1128 | obj.trigger("foo") 1129 | fn.callCount.must.equal(1) 1130 | }) 1131 | 1132 | it("must not unbind others with different contexts", function() { 1133 | var obj = create() 1134 | var fn = Sinon.spy() 1135 | obj.on({foo: fn}, {}) 1136 | obj.off({foo: fn}, {}) 1137 | obj.trigger("foo") 1138 | fn.callCount.must.equal(1) 1139 | }) 1140 | }) 1141 | 1142 | describe("given context", function() { 1143 | it("must unbind", function() { 1144 | var obj = create() 1145 | var context = {} 1146 | var fn = Sinon.spy() 1147 | obj.on("foo", fn, context) 1148 | obj.off(null, null, context) 1149 | obj.trigger("foo") 1150 | fn.callCount.must.equal(0) 1151 | }) 1152 | 1153 | it("must unbind functions bound with \"once\"", function() { 1154 | var obj = create() 1155 | var context = {} 1156 | var fn = Sinon.spy() 1157 | obj.once("foo", fn, context) 1158 | obj.off(null, null, context) 1159 | obj.trigger("foo") 1160 | fn.callCount.must.equal(0) 1161 | }) 1162 | 1163 | it("must not unbind others with different contexts", function() { 1164 | var obj = create() 1165 | var fn = Sinon.spy() 1166 | obj.on("foo", fn, {}) 1167 | obj.off(null, null, {}) 1168 | obj.trigger("foo") 1169 | fn.callCount.must.equal(1) 1170 | }) 1171 | }) 1172 | 1173 | it("must be renameable", function() { 1174 | var obj = create() 1175 | obj.removeEventListener = obj.off 1176 | delete obj.off 1177 | 1178 | var fn = Sinon.spy() 1179 | obj.on("foo", fn) 1180 | obj.removeEventListener("foo") 1181 | obj.trigger("foo") 1182 | fn.callCount.must.equal(0) 1183 | }) 1184 | 1185 | it("must unbind bound name of Object.prototype's properties", function() { 1186 | var obj = create() 1187 | var fn = Sinon.spy() 1188 | obj.on("hasOwnProperty", fn) 1189 | obj.off("hasOwnProperty", fn) 1190 | obj.trigger("hasOwnProperty") 1191 | fn.callCount.must.equal(0) 1192 | }) 1193 | 1194 | it("must ignore names of Object.prototype's properties", function() { 1195 | var obj = create() 1196 | obj.off.bind(obj, "hasOwnProperty", function() {}).must.not.throw() 1197 | }) 1198 | }) 1199 | 1200 | describe(".trigger", function() { 1201 | it("must return self", function() { 1202 | var obj = create() 1203 | obj.on("foo", function() {}) 1204 | obj.trigger("foo").must.equal(obj) 1205 | }) 1206 | 1207 | it("must return self even if this._events doesn't exist", function() { 1208 | var obj = create() 1209 | obj.trigger("foo").must.equal(obj) 1210 | }) 1211 | 1212 | it("must return self if no matching event", function() { 1213 | var obj = create() 1214 | obj.on("bar", function() {}) 1215 | obj.trigger("foo").must.equal(obj) 1216 | }) 1217 | 1218 | it("must trigger event", function() { 1219 | var obj = create() 1220 | var fn = Sinon.spy() 1221 | obj.on("foo", fn) 1222 | obj.trigger("foo") 1223 | fn.callCount.must.equal(1) 1224 | fn.firstCall.args.must.be.empty() 1225 | }) 1226 | 1227 | it("must trigger inherited event", function() { 1228 | var obj = create() 1229 | var fn = Sinon.spy() 1230 | obj.on("foo", fn) 1231 | Object.create(obj).trigger("foo") 1232 | fn.callCount.must.equal(1) 1233 | }) 1234 | 1235 | if (typeof Symbol != "undefined") 1236 | it("must trigger inherited event given symbol", function() { 1237 | var obj = create() 1238 | var symbol = Symbol() 1239 | var fn = Sinon.spy() 1240 | obj.on(symbol, fn) 1241 | Object.create(obj).trigger(symbol) 1242 | fn.callCount.must.equal(1) 1243 | }) 1244 | 1245 | it("must not trigger other events", function() { 1246 | var obj = create() 1247 | var fn = Sinon.spy() 1248 | var other = Sinon.spy() 1249 | obj.on("foo", fn) 1250 | obj.on("bar", other) 1251 | obj.trigger("foo") 1252 | other.callCount.must.equal(0) 1253 | }) 1254 | 1255 | it("must trigger event named 0", function() { 1256 | var obj = create() 1257 | var fn = Sinon.spy() 1258 | obj.on(0, fn) 1259 | obj.trigger(0) 1260 | fn.callCount.must.equal(1) 1261 | }) 1262 | 1263 | it("must trigger event with given argument", function() { 1264 | var obj = create() 1265 | var fn = Sinon.spy() 1266 | obj.on("foo", fn) 1267 | obj.trigger("foo", 1) 1268 | fn.callCount.must.equal(1) 1269 | fn.firstCall.args.must.eql([1]) 1270 | }) 1271 | 1272 | it("must trigger event with given arguments", function() { 1273 | var obj = create() 1274 | var fn = Sinon.spy() 1275 | obj.on("foo", fn) 1276 | obj.trigger("foo", 1, 2, 3) 1277 | fn.callCount.must.equal(1) 1278 | fn.firstCall.args.must.eql([1, 2, 3]) 1279 | }) 1280 | 1281 | it("must trigger event with bound and given argument", function() { 1282 | var obj = create() 1283 | var fn = Sinon.spy() 1284 | obj.on("foo", fn, null, 1) 1285 | obj.trigger("foo", 2) 1286 | fn.callCount.must.equal(1) 1287 | fn.firstCall.args.must.eql([1, 2]) 1288 | }) 1289 | 1290 | it("must trigger event with bound and given arguments", function() { 1291 | var obj = create() 1292 | var fn = Sinon.spy() 1293 | obj.on("foo", fn, null, 1, 2, 3) 1294 | obj.trigger("foo", 4, 5, 6) 1295 | fn.callCount.must.equal(1) 1296 | fn.firstCall.args.must.eql([1, 2, 3, 4, 5, 6]) 1297 | }) 1298 | 1299 | it("must trigger \"all\" event with event name", function() { 1300 | var obj = create() 1301 | var fn = Sinon.spy() 1302 | obj.on("all", fn) 1303 | obj.trigger("foo") 1304 | fn.callCount.must.equal(1) 1305 | fn.firstCall.args.must.eql(["foo"]) 1306 | }) 1307 | 1308 | it("must trigger \"all\" event with given argument", function() { 1309 | var obj = create() 1310 | var fn = Sinon.spy() 1311 | obj.on("all", fn) 1312 | obj.trigger("foo", 1) 1313 | fn.callCount.must.equal(1) 1314 | fn.firstCall.args.must.eql(["foo", 1]) 1315 | }) 1316 | 1317 | it("must trigger \"all\" event with given array argument", function() { 1318 | var obj = create() 1319 | var fn = Sinon.spy() 1320 | obj.on("all", fn) 1321 | obj.trigger("foo", [1]) 1322 | fn.callCount.must.equal(1) 1323 | fn.firstCall.args.must.eql(["foo", [1]]) 1324 | }) 1325 | 1326 | it("must trigger \"all\" event with given arguments", function() { 1327 | var obj = create() 1328 | var fn = Sinon.spy() 1329 | obj.on("all", fn) 1330 | obj.trigger("foo", 1, 2, 3) 1331 | fn.callCount.must.equal(1) 1332 | fn.firstCall.args.must.eql(["foo", 1, 2, 3]) 1333 | }) 1334 | 1335 | it("must trigger \"all\" event with bound argument", function() { 1336 | var obj = create() 1337 | var fn = Sinon.spy() 1338 | obj.on("all", fn, null, 1) 1339 | obj.trigger("foo") 1340 | fn.callCount.must.equal(1) 1341 | fn.firstCall.args.must.eql([1, "foo"]) 1342 | }) 1343 | 1344 | it("must trigger \"all\" event with bound arguments", function() { 1345 | var obj = create() 1346 | var fn = Sinon.spy() 1347 | obj.on("all", fn, null, 1, 2, 3) 1348 | obj.trigger("foo") 1349 | fn.callCount.must.equal(1) 1350 | fn.firstCall.args.must.eql([1, 2, 3, "foo"]) 1351 | }) 1352 | 1353 | it("must trigger \"all\" event with bound and given argument", function() { 1354 | var obj = create() 1355 | var fn = Sinon.spy() 1356 | obj.on("all", fn, null, 1) 1357 | obj.trigger("foo", 2) 1358 | fn.callCount.must.equal(1) 1359 | fn.firstCall.args.must.eql([1, "foo", 2]) 1360 | }) 1361 | 1362 | it("must trigger \"all\" event with bound and given arguments", function() { 1363 | var obj = create() 1364 | var fn = Sinon.spy() 1365 | obj.on("all", fn, null, 1, 2, 3) 1366 | obj.trigger("foo", 4, 5, 6) 1367 | fn.callCount.must.equal(1) 1368 | fn.firstCall.args.must.eql([1, 2, 3, "foo", 4, 5, 6]) 1369 | }) 1370 | 1371 | it("must trigger \"all\" for inherited events", function() { 1372 | var obj = create() 1373 | var fn = Sinon.spy() 1374 | obj.on("all", fn) 1375 | Object.create(obj).trigger("foo") 1376 | fn.callCount.must.equal(1) 1377 | }) 1378 | 1379 | it("must trigger listeners of Object.prototype's properties", function() { 1380 | var obj = create() 1381 | var fn = Sinon.spy() 1382 | obj.on("hasOwnProperty", fn) 1383 | obj.trigger("hasOwnProperty") 1384 | fn.callCount.must.equal(1) 1385 | }) 1386 | 1387 | it("must trigger inherited listeners for Object.prototype's properties", 1388 | function() { 1389 | var obj = create() 1390 | var hasOwnProperty = Sinon.spy() 1391 | obj.on("hasOwnProperty", hasOwnProperty) 1392 | 1393 | var child = Object.create(obj) 1394 | var toString = Sinon.spy() 1395 | child.on("toString", toString) 1396 | 1397 | child.trigger("hasOwnProperty") 1398 | hasOwnProperty.callCount.must.equal(1) 1399 | child.trigger("toString") 1400 | toString.callCount.must.equal(1) 1401 | }) 1402 | 1403 | it("must not trigger if event name only in Object.prototype", function() { 1404 | var obj = create() 1405 | obj.on("foo", function() {}) // Let this._events be created. 1406 | obj.trigger("hasOwnProperty") 1407 | }) 1408 | 1409 | it("must be renameable", function() { 1410 | var obj = create() 1411 | obj.emit = obj.trigger 1412 | 1413 | var fn = Sinon.spy() 1414 | obj.on("foo", fn) 1415 | obj.emit("foo") 1416 | fn.callCount.must.equal(1) 1417 | }) 1418 | }) 1419 | }) 1420 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --recursive 2 | --require must 3 | --------------------------------------------------------------------------------