├── .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 |
--------------------------------------------------------------------------------