├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── api.md ├── bower.json ├── build.js ├── index.js ├── package.json ├── react-events.js ├── react-events.min.js ├── release-notes.md └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | .generator-release 28 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | // JSHint Default Configuration File (as on JSHint website) 3 | // See http://jshint.com/docs/ for more details 4 | 5 | "maxerr" : 50, // {int} Maximum error before stopping 6 | 7 | // Enforcing 8 | "bitwise" : true, // true: Prohibit bitwise operators (&, |, ^, etc.) 9 | "camelcase" : false, // true: Identifiers must be in camelCase 10 | "curly" : true, // true: Require {} for every new block or scope 11 | "eqeqeq" : true, // true: Require triple equals (===) for comparison 12 | "forin" : true, // true: Require filtering for..in loops with obj.hasOwnProperty() 13 | "freeze" : true, // true: prohibits overwriting prototypes of native objects such as Array, Date etc. 14 | "immed" : false, // true: Require immediate invocations to be wrapped in parens e.g. `(function () { } ());` 15 | "indent" : 4, // {int} Number of spaces to use for indentation 16 | "latedef" : false, // true: Require variables/functions to be defined before being used 17 | "newcap" : false, // true: Require capitalization of all constructor functions e.g. `new F()` 18 | "noarg" : true, // true: Prohibit use of `arguments.caller` and `arguments.callee` 19 | "noempty" : true, // true: Prohibit use of empty blocks 20 | "nonbsp" : true, // true: Prohibit "non-breaking whitespace" characters. 21 | "nonew" : false, // true: Prohibit use of constructors for side-effects (without assignment) 22 | "plusplus" : false, // true: Prohibit use of `++` & `--` 23 | "quotmark" : false, // Quotation mark consistency: 24 | // false : do nothing (default) 25 | // true : ensure whatever is used is consistent 26 | // "single" : require single quotes 27 | // "double" : require double quotes 28 | "undef" : true, // true: Require all non-global variables to be declared (prevents global leaks) 29 | "unused" : true, // true: Require all defined variables be used 30 | "strict" : false, // true: Requires all functions run in ES5 Strict Mode 31 | "maxparams" : false, // {int} Max number of formal params allowed per function 32 | "maxdepth" : false, // {int} Max depth of nested blocks (within functions) 33 | "maxstatements" : false, // {int} Max number statements per function 34 | "maxcomplexity" : false, // {int} Max cyclomatic complexity per function 35 | "maxlen" : false, // {int} Max number of characters per line 36 | 37 | // Relaxing 38 | "asi" : false, // true: Tolerate Automatic Semicolon Insertion (no semicolons) 39 | "boss" : false, // true: Tolerate assignments where comparisons would be expected 40 | "debug" : false, // true: Allow debugger statements e.g. browser breakpoints. 41 | "eqnull" : false, // true: Tolerate use of `== null` 42 | "es5" : false, // true: Allow ES5 syntax (ex: getters and setters) 43 | "esnext" : false, // true: Allow ES.next (ES6) syntax (ex: `const`) 44 | "moz" : false, // true: Allow Mozilla specific syntax (extends and overrides esnext features) 45 | // (ex: `for each`, multiple try/catch, function expression…) 46 | "evil" : false, // true: Tolerate use of `eval` and `new Function()` 47 | "expr" : false, // true: Tolerate `ExpressionStatement` as Programs 48 | "funcscope" : false, // true: Tolerate defining variables inside control statements 49 | "globalstrict" : false, // true: Allow global "use strict" (also enables 'strict') 50 | "iterator" : false, // true: Tolerate using the `__iterator__` property 51 | "lastsemic" : false, // true: Tolerate omitting a semicolon for the last statement of a 1-line block 52 | "laxbreak" : false, // true: Tolerate possibly unsafe line breakings 53 | "laxcomma" : false, // true: Tolerate comma-first style coding 54 | "loopfunc" : false, // true: Tolerate functions being defined in loops 55 | "multistr" : false, // true: Tolerate multi-line strings 56 | "noyield" : false, // true: Tolerate generator functions with no yield statement in them. 57 | "notypeof" : false, // true: Tolerate invalid typeof operator values 58 | "proto" : false, // true: Tolerate using the `__proto__` property 59 | "scripturl" : false, // true: Tolerate script-targeted URLs 60 | "shadow" : false, // true: Allows re-define variables later in code e.g. `var x=1; x=2;` 61 | "sub" : false, // true: Tolerate using `[]` notation when it can still be expressed in dot notation 62 | "supernew" : false, // true: Tolerate `new function () { ... };` and `new Object;` 63 | "validthis" : false, // true: Tolerate using this in a non-constructor function 64 | 65 | // Environments 66 | "browser" : true, // Web Browser (window, document, etc) 67 | "browserify" : false, // Browserify (node.js code in the browser) 68 | "couch" : false, // CouchDB 69 | "devel" : true, // Development/debugging (alert, confirm, etc) 70 | "dojo" : false, // Dojo Toolkit 71 | "jasmine" : false, // Jasmine 72 | "jquery" : false, // jQuery 73 | "mocha" : true, // Mocha 74 | "mootools" : false, // MooTools 75 | "node" : false, // Node.js 76 | "nonstandard" : false, // Widely adopted globals (escape, unescape, etc) 77 | "prototypejs" : false, // Prototype and Scriptaculous 78 | "qunit" : false, // QUnit 79 | "rhino" : false, // Rhino 80 | "shelljs" : false, // ShellJS 81 | "worker" : false, // Web Workers 82 | "wsh" : false, // Windows Scripting Host 83 | "yui" : false, // Yahoo User Interface 84 | 85 | // Custom Globals 86 | "globals" : { 87 | "define" : true, 88 | "module" : true, 89 | "React" : true 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | email: 5 | on_failure: change 6 | on_success: never 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Joe Hudson 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | react-events 2 | ============ 3 | Declarative managed event bindings for [React](http://facebook.github.io/react/) components 4 | 5 | * No manual event cleanup 6 | * All events are declared in a single place for easier readability 7 | * Provided ```listenTo``` API 8 | * Pluggable event definitions with many supported types out of the box (refs, props, window, repeat) 9 | 10 | This project indludes/depends on [jhudson8/react-mixin-manager](https://github.com/jhudson8/react-mixin-manager) 11 | 12 | [View the installation and API docs](http://jhudson8.github.io/fancydocs/index.html#project/jhudson8/react-events) 13 | 14 | 15 | ### Other React projects that may interest you 16 | 17 | * [jhudson8/react-backbone](https://github.com/jhudson8/react-backbone) 18 | * [jhudson8/react-chartjs](https://github.com/jhudson8/react-chartjs) 19 | 20 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | react-events 2 | ============ 3 | Declarative managed event bindings for [React](http://facebook.github.io/react/) components 4 | 5 | * No manual event cleanup 6 | * All events are declared in 1 place for easier readability 7 | * Provided ```listenTo``` API 8 | * Pluggable event definitions with many supported types out of the box (refs, props, window, repeat) 9 | 10 | Dependencies 11 | -------------- 12 | * [react-mixin-manager](https://github.com/jhudson8/react-mixin-manager) 13 | 14 | Installation 15 | -------------- 16 | Browser: 17 | 18 | ``` 19 | 20 | 21 | 22 | ``` 23 | 24 | CommonJS 25 | 26 | ``` 27 | var ReactEvents = require('react-events'); 28 | ``` 29 | 30 | AMD 31 | 32 | ``` 33 | require( 34 | ['react-events'], function(ReactEvents) { 35 | ... 36 | }); 37 | ``` 38 | 39 | 40 | API: Event Binding Definitions 41 | -------------- 42 | Event listeners are declared using the ```events``` attribute. To add this support the ```events``` mixin ***must*** be included with your component mixins. 43 | 44 | ```javascript 45 | React.createClass({ 46 | events: { 47 | '{type}:{path}': (callback function or attribute name identifying a callback function) 48 | }, 49 | mixins: ['events'] // or ['react-events.events'] 50 | }) 51 | ``` 52 | 53 | The ```type``` and ```path``` values are specific to different event handlers. 54 | 55 | ### window events 56 | Monitor window events (requires a global "window" variable). 57 | 58 | Syntax 59 | 60 | ```javascript 61 | window:{window event} 62 | ``` 63 | 64 | Example 65 | 66 | ```javascript 67 | React.createClass({ 68 | mixins: ['events'], // or ['react-events.events'] 69 | events: { 70 | 'window:scroll': 'onScroll' 71 | }, 72 | onScroll: function() { 73 | // will fire when a window scroll event has been triggered and "this" is the parent component 74 | } 75 | }); 76 | ``` 77 | 78 | ### repeat events 79 | Execute the callback every n milis 80 | 81 | Event signature 82 | 83 | ```javascript 84 | // repeat every * interval 85 | repeat:{duration in millis} 86 | // repeat every * interval (only when the browser tab is active) 87 | !repeat:{duration in millis} 88 | ``` 89 | 90 | Example 91 | 92 | ```javascript 93 | React.createClass({ 94 | mixins: ['events'], // or ['react-events.events'] 95 | events: { 96 | 'repeat:3000': function() { 97 | // this will be called every 3 seconds only when the component is mounted 98 | }, 99 | '!repeat:3000': function() { 100 | // same as above but will *only* be called when this web page is the active page (requestAnimationFrame) 101 | }, 102 | } 103 | }); 104 | ``` 105 | 106 | ### component by ref events 107 | Execute the callback when events are triggered on the components identified by the [this](http://facebook.github.io/react/docs/more-about-refs.html) value. 108 | 109 | Event signature 110 | 111 | ```javascript 112 | ref:{ref name}:{event name} 113 | ``` 114 | 115 | Example 116 | 117 | ```javascript 118 | React.createClass({ 119 | mixins: ['events'], // or ['react-events.events'] 120 | events: { 121 | 'ref:someComponent:something-happened': 'onSomethingHappened' 122 | }, 123 | onSomethingHappened: function() { 124 | // "someComponent" triggered the "something-happened" event and "this" is the parent component 125 | }, 126 | render: function() { 127 | return
; 128 | } 129 | }); 130 | ``` 131 | 132 | 133 | ### object by prop key events 134 | Execute the callback when events are triggered on the objects identified by the property value. 135 | 136 | Event signature 137 | 138 | ```javascript 139 | prop:{ref name}:{event name} 140 | ``` 141 | 142 | Example 143 | 144 | ```javascript 145 | var MyComponent = React.createClass({ 146 | mixins: ['events'], // or ['react-events.events'] 147 | events: { 148 | 'prop:someProp:something-happened': 'onSomethingHappened' 149 | }, 150 | onSomethingHappened: function() { 151 | // "someProp" triggered the "something-happened" event and "this" is the parent component 152 | } 153 | }); 154 | ... 155 | 156 | ``` 157 | 158 | ### DOM events 159 | To avoid a a jquery dependency, this is not provided with react-events. However, if you wish to implement DOM event 160 | support, copy/paste the code below 161 | 162 | Event signature 163 | 164 | ```javascript 165 | dom:{DOM events separated by space}:{query path} 166 | ``` 167 | 168 | Copy/Paste 169 | 170 | ```javascript 171 | /** 172 | * Bind to DOM element events (recommended solution is to use React "on..." attributes) 173 | * format: "dom:{event names separated with space}:{element selector}" 174 | * example: events: { 'dom:click:a': 'onAClick' } 175 | */ 176 | require('react-events').handle('dom', function(options, callback) { 177 | var parts = options.path.match(splitter); 178 | return { 179 | on: function() { 180 | $(this.getDOMNode()).on(parts[1], parts[2], callback); 181 | }, 182 | off: function() { 183 | $(this.getDOMNode()).off(parts[1], parts[2], callback); 184 | } 185 | }; 186 | }); 187 | ``` 188 | 189 | Example 190 | 191 | ```javascript 192 | React.createClass({ 193 | events: { 194 | 'dom:click:button': 'onClick' 195 | }, 196 | mixins: ['events'], // or ['react-events.events'] 197 | onClick: function() { 198 | // will fire when the button is clicked and "this" is the parent component 199 | } 200 | }); 201 | ``` 202 | 203 | 204 | ### application events 205 | If you want to provide declaritive event support for a custom global application event handler (that implements ```on```/```off```), you can copy/paste the code below. 206 | 207 | ```javascript 208 | require('react-events').handle('app', { 209 | target: myGlobalEventHandler 210 | }); 211 | ``` 212 | 213 | Example 214 | 215 | ```javascript 216 | events: { 217 | 'app:some-event': 'onSomeEvent' 218 | } 219 | ``` 220 | 221 | 222 | API: Mixins 223 | --------- 224 | ### events 225 | 226 | This mixin is required if you want to be able to use declaritive event definitions. 227 | 228 | For example 229 | 230 | ```javascript 231 | React.createClass({ 232 | mixins: ['events'], // or ['react-events.events'] 233 | events: { 234 | 'window:scroll': 'onScroll' 235 | }, 236 | ... 237 | onScroll: function() { ... } 238 | }); 239 | ``` 240 | 241 | In addition, it also includes component state binding for the event handler implementation (not included). 242 | 243 | The event handler implementation is included with [react-backbone](https://github.com/jhudson8/react-backbone) or can be specified by setting ```require('react-events').mixin```. The event handler is simply an object that contains method implementations for 244 | 245 | * trigger 246 | * on 247 | * off 248 | 249 | ```javascript 250 | require('react-events').mixin = myObjectThatSupportsEventMethods; 251 | ``` 252 | 253 | #### triggerWith(event[, parameters...]) 254 | * ***event***: the event name 255 | * ***parameters***: any additional parameters that should be added to the trigger 256 | 257 | A convienance method which allows for easy closure binding of component event triggering when React events occur. 258 | 259 | ```javascript 260 | React.createClass({ 261 | mixins: ['events'], // or ['react-events.events'] 262 | render: function() { 263 | 264 | // when the button is clicked, the parent component will have 'button-clicked' triggered with the provided parameters 265 | return 266 | } 267 | }); 268 | ``` 269 | 270 | You can also pass in a target object as the first parameter (this object must implement the ```trigger``` method). 271 | 272 | ```javascript 273 | React.createClass({ 274 | mixins: ['events'], // or ['react-events.events'] 275 | render: function() { 276 | 277 | // when the button is clicked, the parent component will have 'button-clicked' triggered with the provided parameters 278 | return 279 | } 280 | }); 281 | ``` 282 | 283 | Remember that you *must* (if not using [react-backbone]()) set the event handler mixin (the thing that implements on/off/trigger). 284 | 285 | ```javascript 286 | require('react-events').mixin = OnOffTriggerImpl; 287 | ``` 288 | 289 | #### callWith(func[, parameters...]) 290 | * ***func***: the event name 291 | * ***parameters***: any additional parameters that should be used as arguments to the provided callback function 292 | 293 | A convienance method which allows for easy closure binding of a callback function with arguments 294 | 295 | ```javascript 296 | React.createClass({ 297 | mixins: ['events'], // or ['react-events.events'] 298 | render: function() { 299 | 300 | // when the button is clicked, the parent component will have 'button-clicked' triggered with the provided parameters 301 | for (var i=0; iClick me 303 | } 304 | } 305 | }); 306 | ``` 307 | 308 | 309 | ### manageEvents (events) 310 | * ***events***: the events has that you would see on a React component 311 | 312 | This method can be used to the same functionality that a React component can use with the ```events``` hash. This allows mixins to use all of the managed behavior and event callbacks provided with this project. 313 | 314 | ```javascript 315 | var MyMixin = { 316 | mixins: ['events'], // or ['react-events.events'] 317 | 318 | getInitialState: function() { 319 | 320 | this.manageEvents({ 321 | '*throttle(300)->window:resize': function() { 322 | // this will be called (throttled) whenever the window resizes 323 | } 324 | }); 325 | 326 | return null; 327 | } 328 | } 329 | ``` 330 | 331 | 332 | ### listen 333 | 334 | Utility mixin to expose managed Backbone.Events binding functions which are cleaned up when the component is unmounted. 335 | This is similar to the "modelEventAware" mixin but is not model specific. 336 | 337 | ```javascript 338 | var MyClass React.createClass({ 339 | mixins: ['listen'], // or ['react-events.listen'] 340 | 341 | getInitialState: function() { 342 | this.listenTo(this.props.someObject, 'change', this.onChange); 343 | return null; 344 | }, 345 | onChange: function() { ... } 346 | }); 347 | ``` 348 | 349 | 350 | #### listenTo(target, eventName, callback[, context]) 351 | * ***target***: the source object to bind to 352 | * ***eventName***: the event name 353 | * ***callback***: the event callback function 354 | * ***context***: the callback context 355 | 356 | Managed event binding for ```target.on```. 357 | 358 | 359 | #### listenToOnce(target, eventName, callback[, context]) 360 | * ***target***: the source object to bind to 361 | * ***eventName***: the event name 362 | * ***callback***: the event callback function 363 | * ***context***: the callback context 364 | 365 | Managed event binding for ```target.once```. 366 | 367 | 368 | #### stopListening(eventName, callback[, context]) 369 | * ***target***: the source object to bind to 370 | * ***eventName***: the event name 371 | * ***callback***: the event callback function 372 | * ***context***: the callback context 373 | 374 | Unbind event handler created with ```listenTo``` or ```listenToOnce``` 375 | 376 | 377 | API 378 | -------- 379 | ### react-events 380 | 381 | #### handle (identifier, options) or (identifier, handler) 382 | * ***identifier***: *{string or regular expression}* the event type (first part of event definition) 383 | * ***options***: will use a predefined "standard" handler; this assumes the event format of "{handler identifier}:{target identifier}:{event name}" 384 | * ***target***: {object or function(targetIdentifier, eventName)} the target to bind/unbind from or the functions which retuns this target 385 | * ***onKey***: {string} the attribute which identifies the event binding function on the target (default is "on") 386 | * ***offKey***: {string} the attribute which identifies the event un-binding function on the target (default is "off") 387 | * ***handler***: {function(handlerOptions, handlerCallback)} which returns the object used as the event handler. 388 | * ***handlerOptions***: {object} will contain a *path* attribute - the event key (without the handler key prefix). if the custom handler was registered as "foo" and events hash was { "foo:abc": "..." }, the path is "abc" 389 | * ***handlerCallback***: {function} the callback function to be bound to the event 390 | 391 | For example, the following are the implementations of the event handlers provided by default: 392 | 393 | ***window events (standard event handler type with custom on/off methods and static target)*** 394 | 395 | ```javascript 396 | require('react-events').handle('window', { 397 | target: window, 398 | onKey: 'addEventListener', 399 | offKey: 'removeEventListener' 400 | }); 401 | ``` 402 | 403 | ```javascript 404 | // this will match any key that starts with custom- 405 | require('react-events').handle(/custom-.*/, function(options, callback) { 406 | // if the event declaration was "custom-foo:bar" 407 | var key = options.key; // customm-foo 408 | var path = options.path; // bar 409 | ... 410 | } 411 | ``` 412 | 413 | ***DOM events (custom handler which must return an object with on/off methods)*** 414 | 415 | ```javascript 416 | require('react-events').handle('dom', function(options, callback) { 417 | var parts = options.path.match(splitter); 418 | return { 419 | on: function() { 420 | $(this.getDOMNode()).on(parts[1], parts[2], callback); 421 | }, 422 | off: function() { 423 | $(this.getDOMNode()).off(parts[1], parts[2], callback); 424 | } 425 | }; 426 | }); 427 | ``` 428 | 429 | 430 | Sections 431 | ---------------- 432 | 433 | ### React Component Events 434 | 435 | When using the ```ref``` event handler, the component should support the on/off methods. While this script does not include the implementation of that, it does provide a hook for including your own impl when the ```events``` mixin is included using ```require('react-events').mixin```. 436 | 437 | ```javascript 438 | require('react-events').mixin = objectThatHasOnOffMethods; 439 | ``` 440 | 441 | If you include [react-backbone](https://github.com/jhudson8/react-backbone) this will be set automatically for you as well as ```model``` event bindings. 442 | 443 | You will the have the ability to do the following: 444 | 445 | ```javascript 446 | var ChildComponent = React.createClass({ 447 | mixins: ['events'], // or ['react-events.events'] 448 | ... 449 | onSomethingHappened: function() { 450 | this.trigger('something-happened'); 451 | } 452 | }); 453 | ... 454 | 455 | var ParentComponent = React.createClass({ 456 | mixins: ['events', 'modelEventBinder'], 457 | events: { 458 | 'model:onChange': 'onModelChange', 459 | 'ref:myComponent:something-happened': 'onSomethingHappened' 460 | }, 461 | render: function() { 462 | return
; 463 | }, 464 | onSomethingHappened: function() { ... }, 465 | onModelChange: function() { ... } 466 | }); 467 | ``` 468 | 469 | ### Advanced Features 470 | 471 | #### Declaritive event tree 472 | 473 | The event bindings can be declared as a tree structure. Each element in the tree will be appended 474 | to the parent element using the ```:``` separator. For example 475 | 476 | ```javascript 477 | events: { 478 | prop: { 479 | 'foo:test1': 'test1', 480 | foo: { 481 | test2: 'test2', 482 | test3: 'test3' 483 | } 484 | }, 485 | 'prop:bar:test4': 'test4' 486 | } 487 | ``` 488 | will be converted to 489 | ``` 490 | events: { 491 | 'prop:foo:test1': 'test1', 492 | 'prop:foo:test2': 'test2', 493 | 'prop:foo:test3': 'test3', 494 | 'prop:bar:test4': 'test4' 495 | } 496 | ``` 497 | 498 | #### Instance References 499 | 500 | If you need to reference ```this``` when declaring your event handler, you can use an object with a ```callback``` object. 501 | 502 | ```javascript 503 | var MyClass = React.createClass({ 504 | mixins: ['events'], 505 | events: { 506 | 'window:resize': { 507 | callback: function() { 508 | // return the callback function; executed after the instance has been created 509 | // so "this" can be referenced as the react component instance 510 | } 511 | } 512 | } 513 | }); 514 | ``` 515 | 516 | 517 | #### Callback Wrappers 518 | 519 | It is sometimes useful to wrap callback methods for throttling, cacheing or other purposes. Because an instance is required for this, the previously described instance reference ```callback``` can be used but can be verbose. Special callback wrappers can be used to accomplish this. If the event name is prefixed with ```*someSpecialName(args)->...``` the ```someSpecialName``` callback wrapper will be invoked. 520 | 521 | This is best described with an example 522 | 523 | ```javascript 524 | events: { 525 | '*throttle(300)->window:resize': 'forceUpdate' 526 | } 527 | ``` 528 | 529 | To implement your own special handler, just reference a wrapper function by name on ```require('react-events').specials```. For example: 530 | 531 | ```javascript 532 | // callback is the runtime event callback and args are the special definition arguments 533 | require('react-events').specials.throttle = function(callback, args) { 534 | // the arguments provided here are the runtime event arguments 535 | return function() { 536 | var throttled = this.throttled || _.throttle(callback, args[0]); 537 | throttled.apply(this, arguments); 538 | } 539 | } 540 | ``` 541 | 542 | If the runtime event was triggered triggered with arguments ("foo"), the actual parameters would look like this 543 | 544 | ```javascript 545 | require('react-events').specials.throttle = function(callback, [3000]) { 546 | // the arguments provided here are the runtime event arguments 547 | return function("foo") { 548 | // "this" will be an object unique to this special definition and remain consistent through multiple callbacks 549 | var throttled = this.throttled || _.throttle(callback, 3000); 550 | throttled.apply(this, arguments); 551 | } 552 | } 553 | ``` 554 | 555 | While no special handlers are implemented by default, by including [react-backbone](https://github.com/jhudson8/react-backbone), the following special handlers are available (see [underscore](http://underscorejs.org) for more details) 556 | 557 | * memoize 558 | * delay 559 | * defer 560 | * throttle 561 | * debounce 562 | * once 563 | 564 | 565 | #### Custom Event Handlers 566 | 567 | All events supported by default use the same API as the custom event handler. Using ```require('react-events').handle```, you can add support for a custom event handler. This could be useful for adding an application specific global event bus for example. 568 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-events", 3 | "main": "react-events.js", 4 | "version": "1.0.1", 5 | "homepage": "https://github.com/jhudson8/react-events", 6 | "authors": [ 7 | "Joe Hudson " 8 | ], 9 | "description": "Declarative managed event bindings for react components", 10 | "moduleType": [ 11 | "amd", 12 | "globals" 13 | ], 14 | "keywords": [ 15 | "react", 16 | "react-component", 17 | "react-mixin-manager", 18 | "events" 19 | ], 20 | "license": "MIT", 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "test", 26 | "tests" 27 | ], 28 | "dependencies": { 29 | "react-mixin-manager": ">=0.5.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /build.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | UglifyJS = require('uglify-js'); 3 | 4 | var packageInfo = JSON.parse(fs.readFileSync('./package.json', {encoding: 'utf-8'})), 5 | name = packageInfo.name, 6 | version = packageInfo.version, 7 | file = './' + name + '.js', 8 | minimizedFile = './' + name + '.min.js', 9 | repo = 'https://github.com/jhudson8/' + name, 10 | content = fs.readFileSync(file, {encoding: 'utf8'}), 11 | versionMatcher = new RegExp(name + ' v[0-9\.]+'); 12 | 13 | content = content.replace(versionMatcher, name + ' v' + version); 14 | fs.writeFileSync(file, content, {encoding: 'utf8'}); 15 | 16 | var minimized = UglifyJS.minify(file); 17 | var minimizedHeader = '/*!\n * [' + name + '](' + repo + ') v' + version + '; MIT license; Joe Hudson\n */\n'; 18 | fs.writeFileSync(minimizedFile, minimizedHeader + minimized.code, {encoding: 'utf8'}); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./react-events'); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-events", 3 | "version": "1.0.1", 4 | "author": "Joe Hudson ", 5 | "description": "Declarative managed event bindings for React components", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/jhudson8/react-events.git" 9 | }, 10 | "scripts": { 11 | "test": "mocha" 12 | }, 13 | "keywords": [ 14 | "react", 15 | "react-component", 16 | "react-mixin-manager", 17 | "events" 18 | ], 19 | "peerDependencies": { 20 | "react-mixin-manager": ">=1.0.0" 21 | }, 22 | "devDependencies": { 23 | "underscore": "~1.7", 24 | "react-mixin-manager": ">=1.0.0", 25 | "react": "~0.12", 26 | "backbone": "~1.1", 27 | "chai": "~1.10", 28 | "mocha": "~2.0", 29 | "sinon": "~1.12", 30 | "sinon-chai": "~2.6", 31 | "uglify-js": "~2.4" 32 | }, 33 | "license": "MIT" 34 | } 35 | -------------------------------------------------------------------------------- /react-events.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * react-events v1.0.1 3 | * https://github.com/jhudson8/react-events 4 | * 5 | * 6 | * Copyright (c) 2014 Joe Hudson 7 | * 8 | * Permission is hereby granted, free of charge, to any person obtaining a copy 9 | * of this software and associated documentation files (the "Software"), to deal 10 | * in the Software without restriction, including without limitation the rights 11 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 12 | * copies of the Software, and to permit persons to whom the Software is 13 | * furnished to do so, subject to the following conditions: 14 | * 15 | * The above copyright notice and this permission notice shall be included in 16 | * all copies or substantial portions of the Software. 17 | * 18 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 19 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 20 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 21 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 22 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 23 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 24 | * THE SOFTWARE. 25 | */ 26 | (function(main) { 27 | if (typeof define === 'function' && define.amd) { 28 | define(['react-mixin-manager'], function(ReactMixinManager) { 29 | // AMD 30 | return main(ReactMixinManager); 31 | }); 32 | } else if (typeof exports !== 'undefined' && typeof require !== 'undefined') { 33 | // CommonJS 34 | module.exports = main(require('react-mixin-manager')); 35 | } else { 36 | // browser 37 | ReactEvents = main(ReactMixinManager); 38 | } 39 | })(function(ReactMixinManager) { 40 | 41 | var handlers = {}, 42 | patternHandlers = [], 43 | splitter = /^([^:]+):?(.*)/, 44 | specialWrapper = /^\*([^\(]+)\(([^)]*)\)[->:]*(.*)/, 45 | noArgMethods = ['forceUpdate'], 46 | setState = ReactMixinManager.setState, 47 | getState = ReactMixinManager.getState, 48 | namespace = 'react-events' + '.'; 49 | 50 | /** 51 | * Allow events to be referenced in a hierarchical structure. All parts in the 52 | * hierarchy will be appended together using ":" as the separator 53 | * window: { 54 | * scroll: 'onScroll', 55 | * resize: 'onResize' 56 | * } 57 | * will return as 58 | * { 59 | * 'window:scroll': 'onScroll', 60 | * 'window:resize': 'onResize' 61 | * } 62 | } 63 | */ 64 | function normalizeEvents(events, rtn, prefix) { 65 | rtn = rtn || {}; 66 | if (prefix) { 67 | prefix += ':'; 68 | } else { 69 | prefix = ''; 70 | } 71 | var value, valueType; 72 | for (var key in events) { 73 | if (events.hasOwnProperty(key)) { 74 | value = events[key]; 75 | valueType = typeof value; 76 | if (valueType === 'string' || valueType === 'function') { 77 | rtn[prefix + key] = value; 78 | } else if (value) { 79 | normalizeEvents(value, rtn, prefix + key); 80 | } 81 | } 82 | } 83 | return rtn; 84 | } 85 | 86 | /** 87 | * Internal model event binding handler 88 | * (type(on|once|off), {event, callback, context, target}) 89 | */ 90 | function manageEvent(type, data) { 91 | /*jshint validthis:true */ 92 | var _data = { 93 | type: type 94 | }; 95 | for (var name in data) { 96 | if (data.hasOwnProperty(name)) { 97 | _data[name] = data[name]; 98 | } 99 | } 100 | var watchedEvents = getState('__watchedEvents', this); 101 | if (!watchedEvents) { 102 | watchedEvents = []; 103 | setState({ 104 | __watchedEvents: watchedEvents 105 | }, this); 106 | } 107 | _data.context = _data.context || this; 108 | watchedEvents.push(_data); 109 | 110 | // bind now if we are already mounted (as the mount function won't be called) 111 | var target = getTarget(_data.target, this); 112 | if (this.isMounted()) { 113 | if (target) { 114 | target[_data.type](_data.event, _data.callback, _data.context); 115 | } 116 | } 117 | if (type === 'off') { 118 | var watchedEvent; 119 | for (var i = 0; i < watchedEvents.length; i++) { 120 | watchedEvent = watchedEvents[i]; 121 | if (watchedEvent.event === data.event && 122 | watchedEvent.callback === data.callback && 123 | getTarget(watchedEvent.target, this) === target) { 124 | watchedEvents.splice(i, 1); 125 | } 126 | } 127 | } 128 | } 129 | 130 | // bind all registered events to the model 131 | function _watchedEventsBindAll(context) { 132 | var watchedEvents = getState('__watchedEvents', context); 133 | if (watchedEvents) { 134 | var data; 135 | for (var name in watchedEvents) { 136 | if (watchedEvents.hasOwnProperty(name)) { 137 | data = watchedEvents[name]; 138 | var target = getTarget(data.target, context); 139 | if (target) { 140 | target[data.type](data.event, data.callback, data.context); 141 | } 142 | } 143 | } 144 | } 145 | } 146 | 147 | // unbind all registered events from the model 148 | function _watchedEventsUnbindAll(keepRegisteredEvents, context) { 149 | var watchedEvents = getState('__watchedEvents', context); 150 | if (watchedEvents) { 151 | var data; 152 | for (var name in watchedEvents) { 153 | if (watchedEvents.hasOwnProperty(name)) { 154 | data = watchedEvents[name]; 155 | var target = getTarget(data.target, context); 156 | if (target) { 157 | target.off(data.event, data.callback, data.context); 158 | } 159 | } 160 | } 161 | if (!keepRegisteredEvents) { 162 | setState({ 163 | __watchedEvents: [] 164 | }, context); 165 | } 166 | } 167 | } 168 | 169 | function getTarget(target, context) { 170 | if (typeof target === 'function') { 171 | return target.call(context); 172 | } 173 | return target; 174 | } 175 | 176 | /* 177 | * wrapper for event implementations - includes on/off methods 178 | */ 179 | function createHandler(event, callback, context, dontWrapCallback) { 180 | if (!dontWrapCallback) { 181 | var _callback = callback, 182 | noArg; 183 | if (typeof callback === 'object') { 184 | // use the "callback" attribute to get the callback function. useful if you need to reference the component as "this" 185 | /*jshint validthis:true */ 186 | _callback = callback.callback.call(this); 187 | } 188 | if (typeof callback === 'string') { 189 | noArg = (noArgMethods.indexOf(callback) >= 0); 190 | _callback = context[callback]; 191 | } 192 | if (!_callback) { 193 | throw 'no callback function exists for "' + callback + '"'; 194 | } 195 | callback = function() { 196 | return _callback.apply(context, noArg ? [] : arguments); 197 | }; 198 | } 199 | 200 | // check for special wrapper function 201 | var match = event.match(specialWrapper); 202 | if (match) { 203 | var specialMethodName = match[1], 204 | /*jshint evil: true */ 205 | args = eval('[' + match[2] + ']'), 206 | rest = match[3], 207 | specialHandler = eventManager.specials[specialMethodName]; 208 | if (specialHandler) { 209 | if (args.length === 1 && args[0] === '') { 210 | args = []; 211 | } 212 | callback = specialHandler.call(context, callback, args); 213 | return createHandler(rest, callback, context, true); 214 | } else { 215 | throw new Error('invalid special event handler "' + specialMethodName + "'"); 216 | } 217 | } 218 | 219 | var parts = event.match(splitter), 220 | handlerName = parts[1], 221 | path = parts[2], 222 | handler = handlers[handlerName]; 223 | 224 | // check pattern handlers if no match 225 | for (var i = 0; !handler && i < patternHandlers.length; i++) { 226 | if (handlerName.match(patternHandlers[i].pattern)) { 227 | handler = patternHandlers[i].handler; 228 | } 229 | } 230 | if (!handler) { 231 | throw new Error('no handler registered for "' + event + '"'); 232 | } 233 | 234 | return handler.call(context, { 235 | key: handlerName, 236 | path: path 237 | }, callback); 238 | } 239 | 240 | // predefined templates of common handler types for simpler custom handling 241 | var handlerTemplates = { 242 | 243 | /** 244 | * Return a handler which will use a standard format of on(eventName, handlerFunction) and off(eventName, handlerFunction) 245 | * @param data {object} handler options 246 | * - target {object or function()}: the target to bind to or function(name, event) which returns this target ("this" is the React component) 247 | * - onKey {string}: the function attribute used to add the event binding (default is "on") 248 | * - offKey {string}: the function attribute used to add the event binding (default is "off") 249 | */ 250 | standard: function(data) { 251 | var accessors = { 252 | on: data.onKey || 'on', 253 | off: data.offKey || 'off' 254 | }, 255 | target = data.target; 256 | return function(options, callback) { 257 | var path = options.path; 258 | 259 | function checkTarget(type, context) { 260 | return function() { 261 | var _target = (typeof target === 'function') ? target.call(context, path) : target; 262 | if (_target) { 263 | // register the handler 264 | _target[accessors[type]](path, callback); 265 | } 266 | }; 267 | } 268 | 269 | return { 270 | on: checkTarget('on', this), 271 | off: checkTarget('off', this), 272 | initialize: data.initialize 273 | }; 274 | }; 275 | } 276 | }; 277 | 278 | var eventManager = { 279 | // placeholder for special methods 280 | specials: {}, 281 | 282 | /** 283 | * Register an event handler 284 | * @param identifier {string} the event type (first part of event definition) 285 | * @param handlerOrOptions {function(options, callback) *OR* options object} 286 | * 287 | * handlerOrOptions as function(options, callback) a function which returns the object used as the event handler. 288 | * @param options {object}: will contain a *path* attribute - the event key (without the handler key prefix). 289 | * if the custom handler was registered as "foo" and events hash was { "foo:abc": "..." }, the path is "abc" 290 | * @param callback {function}: the callback function to be bound to the event 291 | * 292 | * handlerOrOptions as options: will use a predefined "standard" handler; this assumes the event format of "{handler identifier}:{target identifier}:{event name}" 293 | * @param target {object or function(targetIdentifier, eventName)} the target to bind/unbind from or the functions which retuns this target 294 | * @param onKey {string} the attribute which identifies the event binding function on the target (default is "on") 295 | * @param offKey {string} the attribute which identifies the event un-binding function on the target (default is "off") 296 | */ 297 | handle: function(identifier, optionsOrHandler) { 298 | if (typeof optionsOrHandler !== 'function') { 299 | // it's options 300 | optionsOrHandler = handlerTemplates[optionsOrHandler.type || 'standard'](optionsOrHandler); 301 | } 302 | if (identifier instanceof RegExp) { 303 | patternHandlers.push({ 304 | pattern: identifier, 305 | handler: optionsOrHandler 306 | }); 307 | } else { 308 | handlers[identifier] = optionsOrHandler; 309 | } 310 | } 311 | }; 312 | 313 | //// REGISTER THE DEFAULT EVENT HANDLERS 314 | if (typeof window !== 'undefined') { 315 | /** 316 | * Bind to window events 317 | * format: "window:{event name}" 318 | * example: events: { 'window:scroll': 'onScroll' } 319 | */ 320 | eventManager.handle('window', { 321 | target: window, 322 | onKey: 'addEventListener', 323 | offKey: 'removeEventListener' 324 | }); 325 | } 326 | 327 | var objectHandlers = { 328 | /** 329 | * Bind to events on components that are given a [ref](http://facebook.github.io/react/docs/more-about-refs.html) 330 | * format: "ref:{ref name}:{event name}" 331 | * example: "ref:myComponent:something-happened": "onSomethingHappened" 332 | */ 333 | ref: function(refKey) { 334 | return this.refs[refKey]; 335 | }, 336 | 337 | /** 338 | * Bind to events on components that are provided as property values 339 | * format: "prop:{prop name}:{event name}" 340 | * example: "prop:componentProp:something-happened": "onSomethingHappened" 341 | */ 342 | prop: function(propKey) { 343 | return this.props[propKey]; 344 | } 345 | }; 346 | 347 | function registerObjectHandler(key, objectFactory) { 348 | eventManager.handle(key, function(options, callback) { 349 | var parts = options.path.match(splitter), 350 | objectKey = parts[1], 351 | ev = parts[2], 352 | bound, componentState; 353 | return { 354 | on: function() { 355 | var target = objectFactory.call(this, objectKey); 356 | if (target) { 357 | componentState = target.state || target; 358 | target.on(ev, callback); 359 | bound = target; 360 | } 361 | }, 362 | off: function() { 363 | if (bound) { 364 | bound.off(ev, callback); 365 | bound = undefined; 366 | componentState = undefined; 367 | } 368 | }, 369 | isStale: function() { 370 | if (bound) { 371 | var target = objectFactory.call(this, objectKey); 372 | if (!target || (target.state || target) !== componentState) { 373 | // if the target doesn't exist now and we were bound before or the target state has changed we are stale 374 | return true; 375 | } 376 | } else { 377 | // if we weren't bound before but the component exists now, we are stale 378 | return true; 379 | } 380 | } 381 | }; 382 | }); 383 | } 384 | 385 | for (var key in objectHandlers) { 386 | if (objectHandlers.hasOwnProperty(key)) { 387 | registerObjectHandler(key, objectHandlers[key]); 388 | } 389 | } 390 | 391 | /** 392 | * Allow binding to setInterval events 393 | * format: "repeat:{milis}" 394 | * example: events: { 'repeat:3000': 'onRepeat3Sec' } 395 | */ 396 | eventManager.handle('repeat', function(options, callback) { 397 | var delay = parseInt(options.path, 10), 398 | id; 399 | return { 400 | on: function() { 401 | id = setInterval(callback, delay); 402 | }, 403 | off: function() { 404 | id = !!clearInterval(id); 405 | } 406 | }; 407 | }); 408 | 409 | /** 410 | * Like setInterval events *but* will only fire when the user is actively viewing the web page 411 | * format: "!repeat:{milis}" 412 | * example: events: { '!repeat:3000': 'onRepeat3Sec' } 413 | */ 414 | eventManager.handle('!repeat', function(options, callback) { 415 | var delay = parseInt(options.path, 10), 416 | keepGoing; 417 | 418 | function doInterval(suppressCallback) { 419 | if (suppressCallback !== true) { 420 | callback(); 421 | } 422 | setTimeout(function() { 423 | if (keepGoing) { 424 | requestAnimationFrame(doInterval); 425 | } 426 | }, delay); 427 | } 428 | return { 429 | on: function() { 430 | keepGoing = true; 431 | doInterval(true); 432 | }, 433 | off: function() { 434 | keepGoing = false; 435 | } 436 | }; 437 | }); 438 | 439 | function handleEvents(events, context, initialize) { 440 | var handlers = getState('_eventHandlers', context) || [], handler; 441 | events = normalizeEvents(events); 442 | for (var ev in events) { 443 | if (events.hasOwnProperty(ev)) { 444 | handler = createHandler(ev, events[ev], context); 445 | if (handler.initialize) { 446 | handler.initialize.call(context); 447 | } 448 | handlers.push(handler); 449 | if (initialize && context.isMounted()) { 450 | handler.on.call(this); 451 | } 452 | } 453 | } 454 | return handlers; 455 | } 456 | 457 | //// REGISTER THE REACT MIXIN 458 | 459 | ReactMixinManager.add(namespace + 'events', function() { 460 | var rtn = [{ 461 | /** 462 | * Return a callback fundtion that will trigger an event on "this" when executed with the provided parameters 463 | */ 464 | triggerWith: function() { 465 | var args = Array.prototype.slice.call(arguments), 466 | target = this; 467 | 468 | // allow the first parameter to be the target 469 | if (typeof args[0] !== 'string') { 470 | target = args[0]; 471 | args.splice(0, 1); 472 | } 473 | 474 | return function() { 475 | target.trigger.apply(target, args); 476 | }; 477 | }, 478 | 479 | /** 480 | * Return a callback fundtion that will call the provided function with the provided arguments 481 | */ 482 | callWith: function(callback) { 483 | var args = Array.prototype.slice.call(arguments, 1), 484 | self = this; 485 | return function() { 486 | callback.apply(self, args); 487 | }; 488 | }, 489 | 490 | manageEvents: function(events) { 491 | setState({ 492 | '_eventHandlers': handleEvents(events, this, true) 493 | }, this); 494 | }, 495 | 496 | getInitialState: function() { 497 | return { 498 | _eventHandlers: handleEvents(this.events, this) 499 | }; 500 | }, 501 | 502 | componentDidUpdate: function() { 503 | var handlers = getState('_eventHandlers', this), 504 | handler; 505 | for (var i = 0; i < handlers.length; i++) { 506 | handler = handlers[i]; 507 | if (handler.isStale && handler.isStale.call(this)) { 508 | handler.off.call(this); 509 | handler.on.call(this); 510 | } 511 | } 512 | }, 513 | 514 | componentDidMount: function() { 515 | var handlers = getState('_eventHandlers', this); 516 | for (var i = 0; i < handlers.length; i++) { 517 | handlers[i].on.call(this); 518 | } 519 | }, 520 | 521 | componentWillUnmount: function() { 522 | var handlers = getState('_eventHandlers', this); 523 | for (var i = 0; i < handlers.length; i++) { 524 | handlers[i].off.call(this); 525 | } 526 | } 527 | }]; 528 | 529 | function bind(func, context) { 530 | return function() { 531 | func.apply(context, arguments); 532 | }; 533 | } 534 | var eventHandler = eventManager.mixin; 535 | if (eventHandler) { 536 | var eventHandlerMixin = {}, 537 | state = {}, 538 | key; 539 | var keys = ['on', 'once', 'off', 'trigger']; 540 | for (var i = 0; i < keys.length; i++) { 541 | key = keys[i]; 542 | if (eventHandler[key]) { 543 | eventHandlerMixin[key] = bind(eventHandler[key], state); 544 | } 545 | } 546 | eventHandlerMixin.getInitialState = function() { 547 | return { 548 | __events: state 549 | }; 550 | }; 551 | rtn.push(eventHandlerMixin); 552 | } 553 | // React.eventHandler.mixin should contain impl for "on" "off" and "trigger" 554 | return rtn; 555 | }, 'state'); 556 | 557 | /** 558 | * Allow for managed bindings to any object which supports on/off. 559 | */ 560 | ReactMixinManager.add(namespace + 'listen', { 561 | componentDidMount: function() { 562 | // sanity check to prevent duplicate binding 563 | _watchedEventsUnbindAll(true, this); 564 | _watchedEventsBindAll(this); 565 | }, 566 | 567 | componentWillUnmount: function() { 568 | _watchedEventsUnbindAll(true, this); 569 | }, 570 | 571 | // {event, callback, context, model} 572 | listenTo: function(target, ev, callback, context) { 573 | var data = ev ? { 574 | event: ev, 575 | callback: callback, 576 | target: target, 577 | context: context 578 | } : target; 579 | manageEvent.call(this, 'on', data); 580 | }, 581 | 582 | listenToOnce: function(target, ev, callback, context) { 583 | var data = { 584 | event: ev, 585 | callback: callback, 586 | target: target, 587 | context: context 588 | }; 589 | manageEvent.call(this, 'once', data); 590 | }, 591 | 592 | stopListening: function(target, ev, callback, context) { 593 | var data = { 594 | event: ev, 595 | callback: callback, 596 | target: target, 597 | context: context 598 | }; 599 | manageEvent.call(this, 'off', data); 600 | } 601 | }); 602 | 603 | return eventManager; 604 | }); 605 | -------------------------------------------------------------------------------- /react-events.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * [react-events](https://github.com/jhudson8/react-events) v1.0.1; MIT license; Joe Hudson 3 | */ 4 | !function(e){"function"==typeof define&&define.amd?define(["react-mixin-manager"],function(t){return e(t)}):"undefined"!=typeof exports&&"undefined"!=typeof require?module.exports=e(require("react-mixin-manager")):ReactEvents=e(ReactMixinManager)}(function(ReactMixinManager){function normalizeEvents(e,t,n){t=t||{},n?n+=":":n="";var a,r;for(var i in e)e.hasOwnProperty(i)&&(a=e[i],r=typeof a,"string"===r||"function"===r?t[n+i]=a:a&&normalizeEvents(a,t,n+i));return t}function manageEvent(e,t){var n={type:e};for(var a in t)t.hasOwnProperty(a)&&(n[a]=t[a]);var r=getState("__watchedEvents",this);r||(r=[],setState({__watchedEvents:r},this)),n.context=n.context||this,r.push(n);var i=getTarget(n.target,this);if(this.isMounted()&&i&&i[n.type](n.event,n.callback,n.context),"off"===e)for(var c,l=0;l=0,_callback=context[callback]),!_callback)throw'no callback function exists for "'+callback+'"';callback=function(){return _callback.apply(context,noArg?[]:arguments)}}var match=event.match(specialWrapper);if(match){var specialMethodName=match[1],args=eval("["+match[2]+"]"),rest=match[3],specialHandler=eventManager.specials[specialMethodName];if(specialHandler)return 1===args.length&&""===args[0]&&(args=[]),callback=specialHandler.call(context,callback,args),createHandler(rest,callback,context,!0);throw new Error('invalid special event handler "'+specialMethodName+"'")}for(var parts=event.match(splitter),handlerName=parts[1],path=parts[2],handler=handlers[handlerName],i=0;!handler&&i:]*(.*)/,noArgMethods=["forceUpdate"],setState=ReactMixinManager.setState,getState=ReactMixinManager.getState,namespace="react-events.",handlerTemplates={standard:function(e){var t={on:e.onKey||"on",off:e.offKey||"off"},n=e.target;return function(a,r){function i(e,a){return function(){var i="function"==typeof n?n.call(a,c):n;i&&i[t[e]](c,r)}}var c=a.path;return{on:i("on",this),off:i("off",this),initialize:e.initialize}}}},eventManager={specials:{},handle:function(e,t){"function"!=typeof t&&(t=handlerTemplates[t.type||"standard"](t)),e instanceof RegExp?patternHandlers.push({pattern:e,handler:t}):handlers[e]=t}};"undefined"!=typeof window&&eventManager.handle("window",{target:window,onKey:"addEventListener",offKey:"removeEventListener"});var objectHandlers={ref:function(e){return this.refs[e]},prop:function(e){return this.props[e]}};for(var key in objectHandlers)objectHandlers.hasOwnProperty(key)&®isterObjectHandler(key,objectHandlers[key]);return eventManager.handle("repeat",function(e,t){var n,a=parseInt(e.path,10);return{on:function(){n=setInterval(t,a)},off:function(){n=!!clearInterval(n)}}}),eventManager.handle("!repeat",function(e,t){function n(e){e!==!0&&t(),setTimeout(function(){a&&requestAnimationFrame(n)},r)}var a,r=parseInt(e.path,10);return{on:function(){a=!0,n(!0)},off:function(){a=!1}}}),ReactMixinManager.add(namespace+"events",function(){function e(e,t){return function(){e.apply(t,arguments)}}var t=[{triggerWith:function(){var e=Array.prototype.slice.call(arguments),t=this;return"string"!=typeof e[0]&&(t=e[0],e.splice(0,1)),function(){t.trigger.apply(t,e)}},callWith:function(e){var t=Array.prototype.slice.call(arguments,1),n=this;return function(){e.apply(n,t)}},manageEvents:function(e){setState({_eventHandlers:handleEvents(e,this,!0)},this)},getInitialState:function(){return{_eventHandlers:handleEvents(this.events,this)}},componentDidUpdate:function(){for(var e,t=getState("_eventHandlers",this),n=0;n= 0.12.0 33 | 34 | 35 | [Commits](https://github.com/jhudson8/react-events/compare/v0.8.1...v0.9.0) 36 | 37 | ## v0.8.1 - February 10th, 2015 38 | - additional support external target with the triggerWith mixin - 5445d93 39 | 40 | A convienance method which allows for easy closure binding of component event triggering when React events occur. 41 | 42 | ``` 43 | React.createClass({ 44 | mixins: ['triggerWith'], 45 | render: function() { 46 | 47 | // when the button is clicked, the parent component will have 'button-clicked' triggered with the provided parameters 48 | return 49 | } 50 | }) 51 | ``` 52 | 53 | You can also pass in a target object as the first parameter (this object must implement the ```trigger``` method). 54 | 55 | ``` 56 | React.createClass({ 57 | mixins: ['triggerWith'], 58 | render: function() { 59 | 60 | // when the button is clicked, the parent component will have 'button-clicked' triggered with the provided parameters 61 | return 62 | } 63 | }) 64 | ``` 65 | 66 | 67 | [Commits](https://github.com/jhudson8/react-events/compare/v0.8.0...v0.8.1) 68 | 69 | ## v0.8.0 - February 9th, 2015 70 | - add the manageEvents function to the "events" mixin - e8860f1 71 | 72 | This method can be used to the same functionality that a React component can use with the ```events``` hash. This allows mixins to use all of the managed behavior and event callbacks provided with this project. 73 | 74 | ``` 75 | var MyMixin = { 76 | mixins: ['events'], 77 | 78 | getInitialState: function() { 79 | 80 | this.manageEvents({ 81 | '*throttle(300)->window:resize': function() { 82 | // this will be called (throttled) whenever the window resizes 83 | } 84 | }); 85 | 86 | return null; 87 | } 88 | } 89 | ``` 90 | 91 | 92 | [Commits](https://github.com/jhudson8/react-events/compare/v0.7.9...v0.8.0) 93 | 94 | ## v0.7.9 - December 11th, 2014 95 | - code cleanup - ccd0527 96 | 97 | 98 | [Commits](https://github.com/jhudson8/react-events/compare/v0.7.8...v0.7.9) 99 | 100 | ## v0.7.8 - December 10th, 2014 101 | no functional code changes. There is just an additional comment that is used to create react-backbone/with-deps.js 102 | 103 | 104 | [Commits](https://github.com/jhudson8/react-events/compare/v0.7.7...v0.7.8) 105 | 106 | ## v0.7.7 - December 4th, 2014 107 | - [#2](https://github.com/jhudson8/react-events/issues/2) - Interaction Nirvana 108 | - add "callWith" method to the "events" mixin - 367bf06 109 | 110 | 111 | [Commits](https://github.com/jhudson8/react-events/compare/v0.7.6...v0.7.7) 112 | 113 | ## v0.7.6 - December 2nd, 2014 114 | - include "once" for React.events.mixin functions to be applied within the "events" mixin - 8c05d16 115 | 116 | 117 | [Commits](https://github.com/jhudson8/react-events/compare/v0.7.5...v0.7.6) 118 | 119 | ## v0.7.5 - December 1st, 2014 120 | - bug fix: only include on/off/trigger from event handler impl - 8a17425 121 | 122 | 123 | [Commits](https://github.com/jhudson8/react-events/compare/v0.7.4...v0.7.5) 124 | 125 | ## v0.7.4 - November 28th, 2014 126 | - add more visible -> as the special separator "specialName(...)->..." - 545f9a3 127 | 128 | 129 | [Commits](https://github.com/jhudson8/react-events/compare/v0.7.3...v0.7.4) 130 | 131 | ## v0.7.3 - November 28th, 2014 132 | - allow non-string special arguments - 39a920c 133 | 134 | 135 | [Commits](https://github.com/jhudson8/react-events/compare/v0.7.2...v0.7.3) 136 | 137 | ## v0.7.2 - November 26th, 2014 138 | - support event bindings declared as a tree structure - 47b5628 139 | For example 140 | ``` 141 | events: { 142 | prop: { 143 | 'foo:test1': 'test1', 144 | foo: { 145 | test2: 'test2', 146 | test3: 'test3' 147 | } 148 | }, 149 | 'prop:bar:test4': 'test4' 150 | } 151 | ``` 152 | will be converted to 153 | ``` 154 | events: { 155 | 'prop:foo:test1': 'test1', 156 | 'prop:foo:test2': 'test2', 157 | 'prop:foo:test3': 'test3', 158 | 'prop:bar:test4': 'test4' 159 | } 160 | ``` 161 | 162 | 163 | [Commits](https://github.com/jhudson8/react-events/compare/v0.7.1...v0.7.2) 164 | 165 | ## v0.7.1 - November 26th, 2014 166 | - for AMD, you must execute the function with params (see README AMD install instructions) - ecb83cc 167 | ``` 168 | require( 169 | ['react', react-events'], function(React, reactEvents) { 170 | reactEvents(React); 171 | }); 172 | ``` 173 | 174 | 175 | [Commits](https://github.com/jhudson8/react-events/compare/v0.7.0...v0.7.1) 176 | 177 | ## v0.7.0 - November 25th, 2014 178 | - add "prop" declarative event - 8a8bb53 179 | - add "listen" mixin - 44d737c 180 | - bug fix: ensure bound handlers are saved within state - 312d8ce 181 | 182 | 183 | [Commits](https://github.com/jhudson8/react-events/compare/v0.6.0...v0.7.0) 184 | 185 | ## v0.6.0 - November 14th, 2014 186 | - remove DOM event handler - cd6489f 187 | 188 | Compatibility notes: 189 | If you are using the "dom" event handler, you must now add that event handler in your own project (to remove the jquery dependency for this project) 190 | 191 | ``` 192 | React.events.handle('dom', function(options, callback) { 193 | var parts = options.path.match(splitter); 194 | return { 195 | on: function() { 196 | $(this.getDOMNode()).on(parts[1], parts[2], callback); 197 | }, 198 | off: function() { 199 | $(this.getDOMNode()).off(parts[1], parts[2], callback); 200 | } 201 | }; 202 | }); 203 | ``` 204 | 205 | [Commits](https://github.com/jhudson8/react-events/compare/v0.5.2...v0.6.0) 206 | 207 | ## v0.5.2 - November 2nd, 2014 208 | - [#1](https://github.com/jhudson8/react-events/issues/1) - Exception when using react-events with browserify 209 | - add package keyword "react-mixin-manager" - a70d493 210 | 211 | [Commits](https://github.com/jhudson8/react-events/compare/v0.5.1...v0.5.2) 212 | 213 | ## v0.5.1 - October 3rd, 2014 214 | - $ to be provided for commonJS - aaa0faf 215 | 216 | [Commits](https://github.com/jhudson8/react-events/compare/v0.5.0...v0.5.1) 217 | 218 | ## v0.5.0 - September 7th, 2014 219 | - add "react-component" keyword - 39f0ce1 220 | - remove the "triggerWith" mixin and move the triggerWith method into the "events" mixin - b489c2f 221 | - v0.4.3 - a694dac 222 | 223 | [Commits](https://github.com/jhudson8/react-events/compare/v0.4.3...v0.5.0) 224 | 225 | ## v0.4.3 - July 25th, 2014 226 | - fix ref event handlers in case the ref component changed during parent renders - cc934d8 227 | 228 | Compatibility notes: 229 | - TODO : What might have broken? 230 | 231 | [Commits](https://github.com/jhudson8/react-events/compare/v0.4.2...v0.4.3) 232 | 233 | ## v0.4.2 - June 17th, 2014 234 | - fix bower.json - 9ab5bbc 235 | - update dependencies docs - 1aa6bbc 236 | 237 | [Commits](https://github.com/jhudson8/react-events/compare/v0.4.1...v0.4.2) 238 | 239 | ## v0.4.1 - June 16th, 2014 240 | - update dependencies - bda8de9 241 | - allow for customer handlers to match the path by regular expression - 0bcdca0 242 | - add bower.json - 8372180 243 | 244 | [Commits](https://github.com/jhudson8/react-events/compare/v0.4.0...v0.4.1) 245 | 246 | ## v0.4.0 - June 12th, 2014 247 | - added special callback wrappers (useful for throttling, cacheing and other reusable callback wrappers) - 3085655 248 | - add object with "callback" parameter support with instance definitions to be able to refer to "this" as the component instance when declaring the callback function. - 55d5e2a 249 | 250 | [Commits](https://github.com/jhudson8/react-events/compare/v0.3.0...v0.4.0) 251 | 252 | ## v0.3.0 - June 4th, 2014 253 | - add 'repeat' and '!repeat' events - 9001bbf 254 | 255 | [Commits](https://github.com/jhudson8/react-events/compare/v0.2.1...v0.3.0) 256 | 257 | ## v0.2.1 - May 22nd, 2014 258 | - add custom events / forceUpdate unit tests - 30ae1dd 259 | - do not send any arguments if the handler event name is "forceUpdate" as that will throw an exception by React - b7122fe 260 | 261 | [Commits](https://github.com/jhudson8/react-events/compare/v0.2.0...v0.2.1) 262 | 263 | ## v0.2.0 - May 18th, 2014 264 | - expose a new mixin: "triggerWith" - b62497a, fc5eb13 265 | - bug fix: bind external "events" (on/off/trigger) object methods to state rather than just using "this" - f351712 266 | - change handler API (not backwards compatible) - 381a352 267 | 268 | [Commits](https://github.com/jhudson8/react-events/compare/v0.1.2...v0.2.0) 269 | 270 | ## v0.1.2 - May 16th, 2014 271 | - added window existence check before including window events - b71d259 272 | 273 | [Commits](https://github.com/jhudson8/react-events/compare/v0.1.1...v0.1.2) 274 | 275 | ## v0.1.1 - May 15th, 2014 276 | - Update README.md 277 | - fix author email address 278 | 279 | [Commits](https://github.com/jhudson8/react-events/compare/1341bec...v0.1.1) 280 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var sinon = require('sinon'), 2 | chai = require('chai'), 3 | sinonChai = require('sinon-chai'), 4 | expect = chai.expect, 5 | React = require('react'), 6 | Backbone = require('backbone'), // not a dependency - only used as an events impl 7 | _ = require('underscore'), 8 | $on = sinon.spy(), 9 | $off = sinon.spy(), 10 | $ = sinon.spy(function() { 11 | return { 12 | on: $on, 13 | off: $off 14 | }; 15 | }); 16 | chai.use(sinonChai); 17 | 18 | global.window = global.window || { 19 | addEventListener: sinon.spy(), 20 | removeEventListener: sinon.spy() 21 | }; 22 | 23 | // intitialize mixin-dependencies 24 | var ReactMixinManager = require('react-mixin-manager'); 25 | var ReactEvents = require('../react-events'); 26 | 27 | function newComponent(attributes, mixins) { 28 | mixins = mixins ? ReactMixinManager.get(mixins) : ReactMixinManager.get('events'); 29 | 30 | var obj = { 31 | getDOMNode: sinon.spy(), 32 | mount: function() { 33 | this._mounted = true; 34 | this.trigger('componentWillMount'); 35 | this.trigger('componentDidMount'); 36 | }, 37 | unmount: function() { 38 | this._mounted = false; 39 | this.trigger('componentWillUnmount'); 40 | this.trigger('componentDidUnmount'); 41 | }, 42 | 43 | isMounted: function() { 44 | return this._mounted; 45 | }, 46 | trigger: function(method) { 47 | var rtn = []; 48 | for (var i = 0; i < mixins.length; i++) { 49 | var func = mixins[i][method]; 50 | if (func) { 51 | rtn.push(func.apply(this, Array.prototype.slice.call(arguments, 1))); 52 | } 53 | } 54 | return rtn; 55 | } 56 | }; 57 | if (attributes) { 58 | for (var name in attributes) { 59 | obj[name] = attributes[name]; 60 | } 61 | } 62 | 63 | var state, aggregateState; 64 | 65 | for (var i = 0; i < mixins.length; i++) { 66 | var mixin = mixins[i]; 67 | _.defaults(obj, mixin); 68 | state = mixin.getInitialState && mixin.getInitialState.call(obj); 69 | if (state) { 70 | if (!aggregateState) aggregateState = {}; 71 | _.defaults(aggregateState, state); 72 | } 73 | } 74 | obj.state = aggregateState; 75 | return obj; 76 | } 77 | 78 | 79 | describe('#triggerWith', function() { 80 | it('should work with the default (self as target)', function() { 81 | var obj = newComponent({}); 82 | obj.mount(); 83 | sinon.stub(obj, 'trigger'); 84 | var callback = obj.triggerWith('foo', 'bar'); 85 | callback(); 86 | expect(obj.trigger.calledWith('foo', 'bar')).to.eql(true); 87 | }); 88 | 89 | it('should work with a specific object as target', function() { 90 | var obj = newComponent({}), 91 | obj2 = {trigger: sinon.spy()}; 92 | obj.mount(); 93 | 94 | sinon.stub(obj, 'trigger'); 95 | 96 | var callback = obj.triggerWith(obj2, 'foo', 'bar'); 97 | callback(); 98 | 99 | expect(!!obj.trigger.callcount).to.eql(false); 100 | expect(obj2.trigger.calledWith('foo', 'bar')).to.eql(true); 101 | }); 102 | }); 103 | 104 | 105 | describe('#callWith', function() { 106 | it('should work', function() { 107 | var obj = newComponent({}); 108 | obj.mount(); 109 | var spy = sinon.spy(); 110 | var callback = obj.callWith(spy, 'foo'); 111 | callback(); 112 | expect(spy.calledWith('foo')).to.eql(true); 113 | }) 114 | }); 115 | 116 | describe('window events', function() { 117 | 118 | it('should on and off window events', function() { 119 | 120 | var obj = newComponent({ 121 | events: { 122 | 'window:scroll': 'onScroll', 123 | 'window:resize': 'onResize', 124 | }, 125 | onScroll: sinon.spy(), 126 | onResize: sinon.spy(), 127 | }); 128 | obj.mount(); 129 | expect(window.addEventListener.callCount).to.eql(2); 130 | 131 | window.addEventListener.getCall(0).args[1]('foo'); 132 | expect(obj.onScroll.callCount).to.eql(1); 133 | expect(obj.onScroll.thisValues[0]).to.eql(obj); 134 | expect(obj.onScroll).to.have.been.calledWith('foo'); 135 | 136 | window.addEventListener.getCall(1).args[1]('bar'); 137 | expect(obj.onResize.callCount).to.eql(1); 138 | expect(obj.onResize.thisValues[0]).to.eql(obj); 139 | expect(obj.onResize).to.have.been.calledWith('bar'); 140 | expect(window.removeEventListener.callCount).to.eql(0); 141 | 142 | obj.unmount(); 143 | expect(window.removeEventListener.callCount).to.eql(2); 144 | }); 145 | }); 146 | 147 | 148 | describe('ref events', function() { 149 | it('should on and off ref events', function() { 150 | var eventSpy = function() { 151 | return { 152 | on: sinon.spy(), 153 | off: sinon.spy() 154 | }; 155 | }; 156 | var obj = newComponent({ 157 | events: { 158 | 'ref:foo:foo-event': 'onFooEvent', 159 | 'ref:bar:bar-event': 'onBarEvent' 160 | }, 161 | onFooEvent: sinon.spy(), 162 | onBarEvent: sinon.spy(), 163 | refs: { 164 | foo: { 165 | on: sinon.spy(), 166 | off: sinon.spy() 167 | }, 168 | bar: { 169 | on: sinon.spy(), 170 | off: sinon.spy() 171 | } 172 | } 173 | }); 174 | 175 | obj.mount(); 176 | expect(obj.refs.foo.on.callCount).to.eql(1); 177 | expect(obj.refs.foo.on).to.have.been.calledWith('foo-event'); 178 | expect(obj.refs.bar.on.callCount).to.eql(1); 179 | expect(obj.refs.bar.on).to.have.been.calledWith('bar-event'); 180 | 181 | obj.refs.foo.on.getCall(0).args[1]('foo'); 182 | expect(obj.onFooEvent.callCount).to.eql(1); 183 | expect(obj.onFooEvent).to.have.been.calledWith('foo'); 184 | expect(obj.onFooEvent.thisValues[0]).to.eql(obj); 185 | 186 | obj.refs.bar.on.getCall(0).args[1]('bar'); 187 | expect(obj.onBarEvent.callCount).to.eql(1); 188 | expect(obj.onBarEvent).to.have.been.calledWith('bar'); 189 | expect(obj.onBarEvent.thisValues[0]).to.eql(obj); 190 | }); 191 | }); 192 | 193 | describe('prop events', function() { 194 | it('should on and off prop events', function() { 195 | var eventSpy = function() { 196 | return { 197 | on: sinon.spy(), 198 | off: sinon.spy() 199 | }; 200 | }; 201 | var obj = newComponent({ 202 | events: { 203 | 'prop:foo:foo-event': 'onFooEvent', 204 | 'prop:bar:bar-event': 'onBarEvent' 205 | }, 206 | onFooEvent: sinon.spy(), 207 | onBarEvent: sinon.spy(), 208 | props: { 209 | foo: { 210 | on: sinon.spy(), 211 | off: sinon.spy() 212 | }, 213 | bar: { 214 | on: sinon.spy(), 215 | off: sinon.spy() 216 | } 217 | } 218 | }); 219 | 220 | obj.mount(); 221 | expect(obj.props.foo.on.callCount).to.eql(1); 222 | expect(obj.props.foo.on).to.have.been.calledWith('foo-event'); 223 | expect(obj.props.bar.on.callCount).to.eql(1); 224 | expect(obj.props.bar.on).to.have.been.calledWith('bar-event'); 225 | 226 | obj.props.foo.on.getCall(0).args[1]('foo'); 227 | expect(obj.onFooEvent.callCount).to.eql(1); 228 | expect(obj.onFooEvent).to.have.been.calledWith('foo'); 229 | expect(obj.onFooEvent.thisValues[0]).to.eql(obj); 230 | 231 | obj.props.bar.on.getCall(0).args[1]('bar'); 232 | expect(obj.onBarEvent.callCount).to.eql(1); 233 | expect(obj.onBarEvent).to.have.been.calledWith('bar'); 234 | expect(obj.onBarEvent.thisValues[0]).to.eql(obj); 235 | }); 236 | }); 237 | 238 | describe('custom event bindings', function() { 239 | var hander; 240 | beforeEach(function() { 241 | handler = {}; 242 | handler.on = sinon.spy(); 243 | handler.off = sinon.spy(); 244 | handler.onCustom = sinon.spy(); 245 | handler.offCustom = sinon.spy(); 246 | }); 247 | 248 | it('standard methods (on/off) and target callback (factory)', function() { 249 | var _handler = handler; 250 | ReactEvents.handle('custom1', { 251 | target: function() { 252 | return _handler; 253 | } 254 | }); 255 | 256 | var obj = newComponent({ 257 | events: { 258 | 'custom1:foo': 'onFoo', 259 | }, 260 | onFoo: sinon.spy() 261 | }); 262 | obj.mount(); 263 | expect(_handler.on.callCount).to.eql(1); 264 | expect(_handler.off.callCount).to.eql(0); 265 | 266 | obj.unmount(); 267 | expect(_handler.off.callCount).to.eql(1); 268 | }); 269 | 270 | it('custom methods (on/off) and static target', function() { 271 | var _handler = handler; 272 | ReactEvents.handle('custom2', { 273 | target: _handler, 274 | onKey: 'onCustom', 275 | offKey: 'offCustom' 276 | }); 277 | 278 | var obj = newComponent({ 279 | events: { 280 | 'custom2:foo': 'onFoo', 281 | }, 282 | onFoo: sinon.spy() 283 | }); 284 | obj.mount(); 285 | expect(_handler.onCustom.callCount).to.eql(1); 286 | expect(_handler.offCustom.callCount).to.eql(0); 287 | 288 | obj.unmount(); 289 | expect(_handler.offCustom.callCount).to.eql(1); 290 | }); 291 | 292 | it('should not provide any arguments if the handler method is "forceUpdate"', function() { 293 | var _handler = handler; 294 | ReactEvents.handle('custom3', { 295 | target: _handler 296 | }); 297 | 298 | var obj = newComponent({ 299 | events: { 300 | 'custom3:foo': 'forceUpdate', 301 | }, 302 | forceUpdate: sinon.spy() 303 | }); 304 | obj.mount(); 305 | expect(_handler.on.callCount).to.eql(1); 306 | }); 307 | 308 | it('should handle regular expression handlers', function() { 309 | var _handler = handler; 310 | ReactEvents.handle(/custom-.*/, { 311 | target: _handler, 312 | onKey: 'onCustom', 313 | offKey: 'offCustom' 314 | }); 315 | 316 | var obj = newComponent({ 317 | events: { 318 | 'custom-foo:foo': 'onFoo', 319 | }, 320 | onFoo: sinon.spy() 321 | }); 322 | obj.mount(); 323 | expect(_handler.onCustom.callCount).to.eql(1); 324 | expect(_handler.offCustom.callCount).to.eql(0); 325 | 326 | obj.unmount(); 327 | expect(_handler.offCustom.callCount).to.eql(1); 328 | }); 329 | }); 330 | 331 | describe('listen', function() { 332 | it('should start listening to a target', function() { 333 | var model = new Backbone.Model(), 334 | obj = newComponent({ 335 | props: { 336 | model: model 337 | } 338 | }, ['listen']), 339 | spy = sinon.spy(); 340 | 341 | obj.listenTo(model, 'foo', spy); 342 | model.trigger('foo'); 343 | // we shouldn't bind yet because we aren't mounted 344 | expect(spy.callCount).to.eql(0); 345 | 346 | obj.mount(); 347 | model.trigger('foo'); 348 | expect(spy.callCount).to.eql(1); 349 | 350 | // we shouldn't bind now because we will be unmounted 351 | obj.unmount(); 352 | model.trigger('foo'); 353 | expect(spy.callCount).to.eql(1); 354 | 355 | // mount again and ensure that we rebind 356 | obj.mount(); 357 | model.trigger('foo'); 358 | expect(spy.callCount).to.eql(2); 359 | obj.unmount(); 360 | model.trigger('foo'); 361 | expect(spy.callCount).to.eql(2); 362 | }); 363 | 364 | it('should listen to a target once', function() { 365 | var model = new Backbone.Model(), 366 | obj = newComponent({ 367 | props: { 368 | model: model 369 | } 370 | }, ['listen']), 371 | spy = sinon.spy(); 372 | 373 | obj.listenToOnce(model, 'foo', spy); 374 | model.trigger('foo'); 375 | // we shouldn't bind yet because we aren't mounted 376 | expect(spy.callCount).to.eql(0); 377 | 378 | obj.mount(); 379 | model.trigger('foo'); 380 | expect(spy.callCount).to.eql(1); 381 | 382 | model.trigger('foo'); 383 | expect(spy.callCount).to.eql(1); 384 | }); 385 | 386 | it('should stop listening to a target', function() { 387 | var model = new Backbone.Model(), 388 | obj = newComponent({ 389 | props: { 390 | model: model 391 | } 392 | }, ['listen']), 393 | spy = sinon.spy(); 394 | obj.listenTo(model, 'foo', spy); 395 | obj.mount(); 396 | model.trigger('foo'); 397 | expect(spy.callCount).to.eql(1); 398 | 399 | obj.stopListening(model, 'foo', spy); 400 | model.trigger('foo'); 401 | expect(spy.callCount).to.eql(1); 402 | }); 403 | }); 404 | 405 | describe('events defined as a hierarchy', function() { 406 | it('should create event handlers for allevents defined as a hierarchy', function() { 407 | var model1 = new Backbone.Model(), 408 | model2 = new Backbone.Model(), 409 | obj = newComponent({ 410 | props: { 411 | foo: model1, 412 | bar: model2 413 | }, 414 | events: { 415 | prop: { 416 | 'foo:test1': 'test1', 417 | foo: { 418 | test2: 'test2', 419 | test3: 'test3' 420 | } 421 | }, 422 | 'prop:bar:test4': 'test4' 423 | }, 424 | test1: sinon.spy(), 425 | test2: sinon.spy(), 426 | test3: sinon.spy(), 427 | test4: sinon.spy() 428 | }, ['events']); 429 | obj.mount(); 430 | 431 | model1.trigger('test1', 'a'); 432 | expect(obj.test1.callCount).to.eql(1); 433 | expect(obj.test1.calledWith('a')).to.eql(true); 434 | 435 | model1.trigger('test2', 'b'); 436 | expect(obj.test2.callCount).to.eql(1); 437 | expect(obj.test2.calledWith('b')).to.eql(true); 438 | 439 | model1.trigger('test3', 'c'); 440 | expect(obj.test3.callCount).to.eql(1); 441 | expect(obj.test3.calledWith('c')).to.eql(true); 442 | 443 | model2.trigger('test4', 'd'); 444 | expect(obj.test4.callCount).to.eql(1); 445 | expect(obj.test4.calledWith('d')).to.eql(true); 446 | }); 447 | }); 448 | 449 | describe('special callback wrappers', function() { 450 | // the special definition arguments 451 | var spy = sinon.spy(function(callback, args) { 452 | // the event call arguments 453 | return function(param1) { 454 | var _args = Array.prototype.slice.call(args, 0); 455 | _args.push(param1); 456 | callback.apply(this, _args); 457 | } 458 | }); 459 | ReactEvents.specials.test = spy; 460 | beforeEach(function() { 461 | spy.reset(); 462 | }); 463 | 464 | it('should use the special with 0 params (and still proxy the event params)', function() { 465 | var eventSpy = sinon.spy(); 466 | var model1 = new Backbone.Model(), 467 | obj = newComponent({ 468 | props: { 469 | foo: model1 470 | }, 471 | events: { 472 | '*test()prop:foo:test': eventSpy 473 | } 474 | }, ['events']); 475 | obj.mount(); 476 | 477 | model1.trigger('test', 'eventParam'); 478 | expect(spy.callCount).to.eql(1); 479 | expect(spy.getCall(0).args[1]).to.eql([]); 480 | expect(eventSpy.callCount).to.eql(1); 481 | expect(eventSpy.calledWith('eventParam')).to.eql(true); 482 | }); 483 | 484 | it('should use the special with multiple params or different types', function() { 485 | var eventSpy = sinon.spy(); 486 | var model1 = new Backbone.Model(), 487 | obj = newComponent({ 488 | props: { 489 | foo: model1 490 | }, 491 | events: { 492 | '*test("specialParam", 1, true)prop:foo:test': eventSpy 493 | } 494 | }, ['events']); 495 | obj.mount(); 496 | 497 | model1.trigger('test', 'eventParam'); 498 | expect(eventSpy.callCount).to.eql(1); 499 | expect(eventSpy.calledWith('specialParam', 1, true, 'eventParam')).to.eql(true); 500 | }); 501 | 502 | it('should support order format (additional ":" separator)', function() { 503 | var eventSpy = sinon.spy(); 504 | var model1 = new Backbone.Model(), 505 | obj = newComponent({ 506 | props: { 507 | foo: model1 508 | }, 509 | events: { 510 | '*test("specialParam", 1, true):prop:foo:test': eventSpy 511 | } 512 | }, ['events']); 513 | obj.mount(); 514 | 515 | model1.trigger('test', 'eventParam'); 516 | expect(eventSpy.callCount).to.eql(1); 517 | expect(eventSpy.calledWith('specialParam', 1, true, 'eventParam')).to.eql(true); 518 | }); 519 | 520 | it('should support pointer format (additional "->" separator)', function() { 521 | var eventSpy = sinon.spy(); 522 | var model1 = new Backbone.Model(), 523 | obj = newComponent({ 524 | props: { 525 | foo: model1 526 | }, 527 | events: { 528 | '*test("specialParam", 1, true)->prop:foo:test': eventSpy 529 | } 530 | }, ['events']); 531 | obj.mount(); 532 | 533 | model1.trigger('test', 'eventParam'); 534 | expect(eventSpy.callCount).to.eql(1); 535 | expect(eventSpy.calledWith('specialParam', 1, true, 'eventParam')).to.eql(true); 536 | }); 537 | 538 | describe('#manageEvents', function() { 539 | ReactMixinManager.add('managedEventTester', { 540 | mixins: ['events'], 541 | getInitialState: function() { 542 | this.manageEvents({ 543 | 'prop:foo:test': 'onTest' 544 | }); 545 | return null; 546 | } 547 | }); 548 | 549 | it('should work correctly before state has been created', function() { 550 | var model1 = new Backbone.Model(), 551 | obj = newComponent({ 552 | props: { 553 | foo: model1 554 | }, 555 | onTest: sinon.spy() 556 | }, ['managedEventTester']); 557 | obj.mount(); 558 | 559 | model1.trigger('test', 'eventParam'); 560 | expect(obj.onTest.callCount).to.eql(1); 561 | }); 562 | }); 563 | 564 | it('should work when using the namespace', function() { 565 | // this will throw an error if it fails 566 | newComponent({}, 'react-events.events'); 567 | }); 568 | }); 569 | --------------------------------------------------------------------------------