├── .editorconfig ├── .gitignore ├── .jshintrc ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── Gruntfile.js ├── LICENSE.md ├── README.md ├── bower.json ├── dist ├── reflux.js └── reflux.min.js ├── docs ├── README.md ├── actions │ └── README.md ├── advanced │ └── README.md ├── components │ └── README.md ├── other │ ├── examples.md │ └── reflux-vs-flux.md └── stores │ └── README.md ├── karma.conf.js ├── old-README.md ├── package.json ├── src ├── ListenerMixin.js ├── addES6.js ├── connect.js ├── connectFilter.js ├── defineReact.js ├── index.js ├── listenTo.js └── listenToMany.js └── test ├── .jshintrc ├── Reflux.Component.spec.js ├── Reflux.GlobalState.spec.js ├── Reflux.Store.spec.js ├── benchmarks ├── index.js ├── listeningToActions.js ├── listeningToJoins.js └── listeningToStores.js ├── listenTo.spec.js ├── listenToMany.spec.js ├── sauceLaunchers.js ├── shims └── phantomjs-shims.js ├── usingConnectFilterMixin.spec.js ├── usingConnectMixin.spec.js ├── usingListenerMethodsMixin.spec.js └── usingListenerMixin.spec.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_style = space 7 | trim_trailing_whitespace = true 8 | charset = utf-8 9 | 10 | [*.js] 11 | indent_size = 4 12 | 13 | [{package.json,.travis.yml,Gruntfile.js}] 14 | indent_size = 2 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Node.JS specifics 2 | 3 | lib-cov 4 | *.seed 5 | *.log 6 | *.csv 7 | *.dat 8 | *.out 9 | *.pid 10 | *.gz 11 | 12 | pids 13 | logs 14 | results 15 | 16 | npm-debug.log 17 | node_modules 18 | 19 | # MacOSX specifics 20 | .DS_Store 21 | 22 | # Additional tools 23 | .idea 24 | 25 | # Build artifacts 26 | tmp 27 | bower_components 28 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "maxerr": 50, 3 | 4 | "curly": true, 5 | "unused": true, 6 | "undef": true, 7 | 8 | "browser": true, 9 | "devel": true, 10 | "node": true, 11 | 12 | "globals": { 13 | "Promise":true 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | test/ 3 | tmp/ 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4.1.2" 5 | 6 | env: 7 | global: 8 | - secure: B0emLHNPO7BVpq2lT/KAPW1miUOhgODa/qoVqYbFspcdoBVfJmO1FpcgEppE8fuEecaOv9QfSI3hwGOUkzm9kp8HFde8ZenR5beauhfploXMQB/bbNjd0cBz0ORRMwzsTFg9GeWcnStr+9H05833W84K3N1oy3ShT68kYRWObh4= 9 | - secure: LZNuke26p3RYq5jbSB1oDlG8AeCY1fm8uBoVWmCdKKmmgV/+nKXDgdYygE/yeAPm39XnZ8jGH64ctHVixpgc8TQDa9ZCwOr1q3SWVL+dVAwhKm7WQ9/v2yW/c/zOEuYzvGqr1ylAC8PfwqYA6ShGUnSWAua7TIOeYqdtAjGbU1k= 10 | matrix: 11 | - REACT_VERSION="15" 12 | 13 | before_install: 14 | - "export DISPLAY=:99.0" 15 | - "sh -e /etc/init.d/xvfb start" 16 | 17 | before_script: 18 | - "npm install react@$REACT_VERSION" 19 | 20 | script: 21 | - npm test 22 | - if [ "$SAUCE_USERNAME" ]; then npm run test:sauce; fi 23 | 24 | addons: 25 | sauce_connect: true 26 | 27 | sudo: false 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | Check for latest changes on the [milestones page](https://github.com/spoike/refluxjs/milestones). 4 | 5 | For updates on reflux core methods, check the [`CHANGELOG`](https://github.com/reflux/reflux-core/blob/master/CHANGELOG.md) at `reflux-core` project. 6 | 7 | ## v6.4.1 8 | 9 | * NOTE: New `MyStore.state` shortcut (from 6.4.0) is not available in IE10 and below, use accordingly in your projects. 10 | * Remove test for it so that it doesn't fail, and is undocumented feature for now, until the day we can drop IE9 and IE10. 11 | 12 | ## v6.4.0 13 | 14 | * Improved on class extending function used internally. 15 | * Made that extending function available externally at `Reflux.utils.inherits(NewClass, InheritsFrom)` so that it can be used for testing. 16 | * Made `MyStore.state` work as shortcut access to `MyStore.singleton.state` (not available in IE9 and IE10, plan usage accordingly for your project). 17 | 18 | ## v6.3.0 19 | 20 | * Added Reflux.PureComponent which extends from React.PureComponent instead of React.Component. 21 | 22 | ## v6.2.0 23 | 24 | * React.Component constructor arguments for `context` and `updater` added to Reflux.Component contructor. 25 | 26 | ## v6.1.0 27 | 28 | * Documentation about Action listening/unsubscribing added. 29 | * Reflux.serverMode added, only adds listeners when hooking store to component if not in server mode, eliminating memory leak. 30 | * Bugfix with unmounting of reflux components that did not have any store attached to them. 31 | 32 | ## v6.0.0 33 | 34 | * Switch to new docs. 35 | * Updated to `reflux-core` v1.0.0. 36 | * Allow complex definitions for child actions instead of just string for action name. 37 | * Breaking change: actions are sync by default unless child actions are involved. 38 | 39 | ## v5.0.4 40 | 41 | * Workaround to babel __proto__ issue with old browsers. 42 | * Continued documentation refactoring (viewable in new-README.md). 43 | * Minor doc fixes. 44 | 45 | ## v5.0.3 46 | 47 | * Implemented `Reflux.initStore` (basically clone of `Reflux.initializeGlobalStore` but can be used more broadly). 48 | * Deprecate `Reflux.initializeGlobalStore` in favor of more broadly usable `Reflux.initStore`. 49 | * Major documentation refactor started, but not implemented yet (viewable in new-README.md). 50 | * Updated to run on `reflux-core` v0.4.2, which has more broad importing support. 51 | 52 | ## v5.0.2 53 | 54 | * Reduced the need for `defineReact` by better detecting of React. 55 | * Updated to run on `reflux-core` v0.4.1, which solves some memory leaks. 56 | 57 | ## v5.0.1 58 | 59 | * Adding of full ES6 API, most notably `Reflux.Component` and `Reflux.Store` (and their sub-methods/properties). Also `defineReact`, `initializeGlobalStore`, `GlobalState`, `getGlobalState`, `setGlobalState`. 60 | 61 | ## v0.4.x 62 | 63 | * ?? 64 | 65 | ## v0.3.0 66 | 67 | * Bumped up along with breaking changes in [`reflux-core`](https://github.com/reflux/reflux-core). 68 | 69 | ## v0.2.13 70 | 71 | * Fixed React dependency [#425](https://github.com/reflux/refluxjs/pull/425) 72 | * Example on README corrected [#406](https://github.com/reflux/refluxjs/pull/406) 73 | 74 | ## v0.2.12 75 | 76 | * Integrated [`reflux-core`](https://github.com/reflux/reflux-core) v0.2.1, see [#380](https://github.com/reflux/refluxjs/issues/380). 77 | 78 | ## v0.2.11 79 | 80 | * Using setImmediate if available. Fixes issue with IE10+, see [#374](https://github.com/spoike/refluxjs/issues/374). 81 | 82 | ## v0.2.10 83 | 84 | * Exposing Reflux.utils [#253](https://github.com/spoike/refluxjs/issues/253) 85 | * Fixing connectFilter [#371](https://github.com/spoike/refluxjs/pull/371) 86 | * Added links in README to extensions and plugins 87 | 88 | ### Breaking changes to undocumented features 89 | 90 | * Fixed circular referencing to the index js file. Should fix issues building with bower package. This means you can't replace the ActionMethods and StoreMethods objects (as the previous unit tests did), you have to add properties to them. 91 | 92 | * Removed `native-promise-only` dependency. Reflux does an initial check for Promises and will instead create async actions using `triggerAsync` instead of `triggerPromise` if `Promise` is missing from the environment. Do use a Promise polyfill or your own Promise library if you plan to use this behavior. [#302](https://github.com/spoike/refluxjs/issues/302) 93 | 94 | ## v0.2.9 95 | 96 | * Fixes issue with a join check [#272](https://github.com/spoike/refluxjs/pull/272). 97 | * Added links to README. See [#341](https://github.com/spoike/refluxjs/pull/341), [#361](https://github.com/spoike/refluxjs/pull/361) 98 | 99 | ## v0.2.8 100 | 101 | See [#345](https://github.com/spoike/refluxjs/pull/345). 102 | 103 | * Fixes [#239](https://github.com/spoike/refluxjs/pull/239), checks for component mounted state before trying to mount component 104 | * Adds more example apps to README 105 | * Fixes some typo's in the readme 106 | * Improves documentation on action handlers in stores 107 | * Updates dependencies 108 | * Specifies react compatibility via peerDependencies 109 | 110 | ## v0.2.7 111 | 112 | * Fixed IE8 errors and warnings [#286](https://github.com/spoike/refluxjs/pull/286), [#270](https://github.com/spoike/refluxjs/pull/270) 113 | * Ensure triggerPromise original promise [#229](https://github.com/spoike/refluxjs/pull/229) 114 | * Fixed aborted callbacks [#227](https://github.com/spoike/refluxjs/pull/227) 115 | 116 | ## v0.2.6 117 | 118 | * Fixed catch call in Promises [#267](https://github.com/spoike/refluxjs/pull/267) 119 | * Promise and EventEmitter is now properly exported [#258](https://github.com/spoike/refluxjs/pull/258), [#253](https://github.com/spoike/refluxjs/pull/253) 120 | * Getters in stores were accidentally invoked [#231](https://github.com/spoike/refluxjs/pull/231), [#230](https://github.com/spoike/refluxjs/pull/230) 121 | * Asynchronous actions will now return promises [#223](https://github.com/spoike/refluxjs/pull/223), [#216](https://github.com/spoike/refluxjs/issues/216), [#259](https://github.com/spoike/refluxjs/issues/259) 122 | * `dist` folder is now available again in npm package [#266](https://github.com/spoike/refluxjs/pull/266) 123 | * Fixes to README file [#260](https://github.com/spoike/refluxjs/pull/260), [#247](https://github.com/spoike/refluxjs/pull/247), [#244](https://github.com/spoike/refluxjs/pull/244), [#240](https://github.com/spoike/refluxjs/pull/240), [#236](https://github.com/spoike/refluxjs/pull/236), [#235](https://github.com/spoike/refluxjs/pull/235), [#234](https://github.com/spoike/refluxjs/pull/234) 124 | 125 | ## v0.2.5 126 | 127 | * Added `connectFilter` [#222](https://github.com/spoike/refluxjs/pull/222) 128 | * A lot of clarifications and typo fixes in documentation. [#147](https://github.com/spoike/refluxjs/pull/147), [#207](https://github.com/spoike/refluxjs/pull/207), [#208](https://github.com/spoike/refluxjs/pull/208), [#209](https://github.com/spoike/refluxjs/pull/209), [#211](https://github.com/spoike/refluxjs/pull/211), and [#214](https://github.com/spoike/refluxjs/pull/214) 129 | 130 | ## v0.2.4 131 | 132 | * Promisable actions [#185](https://github.com/spoike/refluxjs/issues/185) 133 | * Fixed IE8 bug [#202](https://github.com/spoike/refluxjs/issues/202), [#187](https://github.com/spoike/refluxjs/issues/187) 134 | * Plus other various fixes: [#201](https://github.com/spoike/refluxjs/issues/201), [#200](https://github.com/spoike/refluxjs/issues/202), [#183](https://github.com/spoike/refluxjs/issues/183), and [#182](https://github.com/spoike/refluxjs/issues/182) 135 | 136 | ## v0.2.3 137 | 138 | * Store mixins [#124](https://github.com/spoike/refluxjs/pull/124) 139 | 140 | ## v0.2.2 141 | 142 | * Fixed circular dependency issue that caused browserify builds not to work as expected [#129](https://github.com/spoike/refluxjs/issues/129) [#138](https://github.com/spoike/refluxjs/issues/138) 143 | * Bind store methods before init() method executes. [#168](https://github.com/spoike/refluxjs/issues/168) 144 | * Clarify the meaning of "FRP". [#161](https://github.com/spoike/refluxjs/issues/161) 145 | * Child (async) actions and promise handling [#140](https://github.com/spoike/refluxjs/issues/140) 146 | 147 | ## v0.2.1 148 | 149 | * IE8 trailing comma bug fix [#145](https://github.com/spoike/refluxjs/pull/145) 150 | * Multiple use of Reflux.connect bug fix [#142](https://github.com/spoike/refluxjs/issues/142), [#143](https://github.com/spoike/refluxjs/pull/143) 151 | * Added .npmignore file, removing non-essential files from `npm install` [#125](https://github.com/spoike/refluxjs/issues/125) 152 | 153 | ## v0.2.0 154 | 155 | * Breaking change: Set initial state before componentDidMount (in `Reflux.connect`) [#117](https://github.com/spoike/refluxjs/pull/117) 156 | * Allow extension of actions and stores (with `Reflux.ActionMethods` and `Reflux.StoreMethods`) [#121](https://github.com/spoike/refluxjs/pull/121) 157 | * Automatically bind store methods [#100](https://github.com/spoike/refluxjs/pull/100) 158 | * Bugfix: Connect and listenermixin combo [#131](https://github.com/spoike/refluxjs/pull/131) 159 | 160 | ## v0.1.14, v0.1.15 161 | 162 | * You may now stop listening to joined listenables individually [#96](https://github.com/spoike/refluxjs/pull/96). 163 | * Reflux will now throw an error if you attempt to join less than two listenables [#97](https://github.com/spoike/refluxjs/pull/97). 164 | 165 | ## v0.1.13 166 | 167 | * Added more join methods, i.e. `listener.joinLeading`, `listener.joinTrailing`, `listener.joinConcat` and `listener.joinStrict` 168 | [#92](https://github.com/spoike/refluxjs/pull/92). 169 | * Actions can be set to sync or async trigger [#93](https://github.com/spoike/refluxjs/pull/93). 170 | * And various bug fixes. Check the [milestone page](https://github.com/spoike/refluxjs/issues?q=milestone%3A0.1.13+is%3Aclosed). 171 | 172 | ## v0.1.12 173 | 174 | * Bug fixes. Check the [milestone page](https://github.com/spoike/refluxjs/issues?q=milestone%3A0.1.12+is%3Aclosed). 175 | 176 | ## v0.1.9, v0.1.10, v0.1.11 177 | 178 | * Critical bug fixes. See [#80](https://github.com/spoike/refluxjs/issues/80), [#81](https://github.com/spoike/refluxjs/issues/81), and [#82](https://github.com/spoike/refluxjs/issues/82). 179 | 180 | ## v0.1.8 181 | 182 | * Added `Reflux.connect`, `Reflux.listenTo`, `listenToMany` conveniences. See [#63](https://github.com/spoike/refluxjs/pull/63) and [#75](https://github.com/spoike/refluxjs/pull/75) 183 | * Stores may now use a `listenables` prop [#63](https://github.com/spoike/refluxjs/pull/63) to automatically register actions to callbacks 184 | * `preEmit` may now map or transform the action payload by returning something. See [58](https://github.com/spoike/refluxjs/issues/58) and [#78](https://github.com/spoike/refluxjs/pull/78) 185 | * Reflux now exposes a `keep` for easier introspection on actions and stores [#56](https://github.com/spoike/refluxjs/issues/56) 186 | * Added mixin interfaces `ListenerMethods` and `PublisherMethods` making it possible for users to extend Reflux's actions and stores. See [#45](https://github.com/spoike/refluxjs/issues/45) 187 | 188 | ## v0.1.7 189 | 190 | * Added support for initial data handling [#49](https://github.com/spoike/refluxjs/pull/49) 191 | * Added CHANGELOG.md [#50](https://github.com/spoike/refluxjs/issues/50) 192 | * Bug: Unregistered actions could not be reregistered [#47](https://github.com/spoike/refluxjs/pull/47) 193 | 194 | ## v0.1.6 195 | 196 | * Added possibility to join actions and stores with `Reflux.all` [#27](https://github.com/spoike/refluxjs/issues/27), [#28](https://github.com/spoike/refluxjs/pull/28) 197 | * Added circular dependency check [#26](https://github.com/spoike/refluxjs/issues/26) 198 | 199 | ## v0.1.5 200 | 201 | * Bug fix 202 | 203 | ## v0.1.4 204 | 205 | * Action functors are now deferred [#22](https://github.com/spoike/refluxjs/issues/22), [#23](https://github.com/spoike/refluxjs/pull/23) 206 | * Added web tests using testling.ci [#20](https://github.com/spoike/refluxjs/pull/20) 207 | 208 | ## v0.1.3 209 | 210 | * Added hooks `preEmit` and `shouldEmit` for actions [#16](https://github.com/spoike/refluxjs/issues/16) 211 | * Various bug fixes and `.jshintrc` file created for grunt build 212 | 213 | ## v0.1.2 214 | 215 | * Added `ListenerMixin` useful for React components [#7](https://github.com/spoike/refluxjs/issues/7) 216 | * Using `eventemitter3` instead [#4](https://github.com/spoike/refluxjs/issues/4) 217 | 218 | ## v0.1.1 219 | 220 | * Added convenience function to create multiple actions [#6](https://github.com/spoike/refluxjs/issues/6) 221 | * Bug: createStore's unsubscribe function was broken [#5](https://github.com/spoike/refluxjs/issues/5) 222 | 223 | ## v0.1.0 224 | 225 | * Removed lodash dependency [#1](https://github.com/spoike/refluxjs/issues/1) 226 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | require('es6-promise').polyfill(); 2 | 3 | module.exports = function(grunt) { 4 | 5 | var sauceLaunchers = require('./test/sauceLaunchers'); 6 | 7 | grunt.initConfig({ 8 | pkg: grunt.file.readJSON('package.json'), 9 | jshint: { 10 | files: ['Gruntfile.js', 'src/**/*.js', 'test/**/*.js'], 11 | options: { 12 | jshintrc: true 13 | } 14 | }, 15 | mochaTest: { 16 | test: { 17 | src: ['test/**/*.spec.js'] 18 | } 19 | }, 20 | browserify: { 21 | dist: { 22 | src: ['src/index.js'], 23 | dest: 'dist/<%= pkg.name %>.js', 24 | options: { 25 | exclude: ['react'], 26 | browserifyOptions: { 27 | standalone: 'Reflux' 28 | } 29 | }, 30 | } 31 | }, 32 | uglify: { 33 | dist: { 34 | src: 'dist/reflux.js', 35 | dest: 'dist/reflux.min.js' 36 | } 37 | }, 38 | watch: { 39 | files: ['<%= jshint.files %>'], 40 | tasks: ['build'] 41 | }, 42 | karma: { 43 | local: { 44 | configFile: 'karma.conf.js', 45 | options: { 46 | browsers: ['PhantomJS'] 47 | } 48 | }, 49 | devsauce: { 50 | configFile: 'karma.conf.js', 51 | options: { 52 | reporters: ['saucelabs', 'dots'], 53 | sauceLabs: { 54 | "public": "team", 55 | testName: 'RefluxJS Karma Tests (Dev)', 56 | recordVideo: false, 57 | recordScreenshot: false, 58 | tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, 59 | }, 60 | customLaunchers: sauceLaunchers, 61 | browsers: Object.keys(sauceLaunchers), 62 | captureTimeout: 0 63 | }, 64 | }, 65 | sauce: { 66 | configFile: 'karma.conf.js', 67 | options: { 68 | reporters: ['saucelabs', 'dots'], 69 | sauceLabs: { 70 | "public": "public", 71 | testName: 'RefluxJS Karma Tests (Travis)', 72 | recordVideo: false, 73 | recordScreenshot: false, 74 | startConnect: false, 75 | tunnelIdentifier: process.env.TRAVIS_JOB_NUMBER, 76 | connectOptions: { 77 | port: 5757, 78 | logfile: "sauce_connect.log" 79 | } 80 | }, 81 | customLaunchers: sauceLaunchers, 82 | browsers: Object.keys(sauceLaunchers), 83 | singleRun: true, 84 | captureTimeout: 0 85 | }, 86 | } 87 | } 88 | }); 89 | 90 | require('matchdep').filterDev('grunt-*').forEach(grunt.loadNpmTasks); 91 | 92 | grunt.registerTask('test', ['jshint', 'mochaTest', 'karma:local']); 93 | 94 | grunt.registerTask('build', ['test', 'browserify', 'uglify']); 95 | 96 | grunt.registerTask('default', ['watch']); 97 | 98 | }; 99 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014, Mikael Brassman 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 5 | 6 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 7 | 8 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 9 | 10 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 11 | 12 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # RefluxJS 3 | 4 | A simple library for unidirectional dataflow architecture inspired by ReactJS [Flux](http://facebook.github.io/react/blog/2014/05/06/flux.html). 5 | 6 | [![NPM Version][npm-image]][npm-url] 7 | [![Bower Version][bower-image]][bower-url] 8 | [![Build Status][travis-image]][travis-url] 9 | [![NPM Downloads][downloads-image]][npm-url] 10 | 11 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/refluxjs.svg)](https://saucelabs.com/u/refluxjs) 12 | 13 | -------------------- 14 | 15 | ## Installation 16 | 17 | You can currently install the package as a npm package, a bower component, or import it from a CDN. 18 | 19 | #### NPM 20 | 21 | The following command installs RefluxJS as a npm package: 22 | 23 | npm install reflux 24 | 25 | Then, in your script, you can gain a reference to RefluxJS like so: `var Reflux = require('reflux');` 26 | 27 | #### Bower 28 | 29 | The following command installs reflux as a bower component that can be used in the browser: 30 | 31 | bower install reflux 32 | 33 | Then the files may be imported into your html file via `bower_components/reflux/dist/reflux.js` or `bower_components/reflux/dist/reflux.min.js`. At that point a `Reflux` variable will be globally available to you. It is suggested that you import RefluxJS after React. 34 | 35 | #### CDN 36 | 37 | RefluxJS is available at [jsdelivr](http://www.jsdelivr.com/#!refluxjs). 38 | 39 | You may import the CDN files directly through a script tag. At that point a `Reflux` variable will be globally available to you. It is suggested that you import RefluxJS after React. 40 | 41 | -------------------------------- 42 | 43 | ## Overview 44 | 45 | The main function of Reflux is to introduce a more functional programming style architecture by eschewing MVC like pattern and adopting a single data flow pattern. 46 | 47 | ``` 48 | +---------+ +--------+ +-----------------+ 49 | ¦ Actions ¦------>¦ Stores ¦------>¦ View Components ¦ 50 | +---------+ +--------+ +-----------------+ 51 | ^ ¦ 52 | +--------------------------------------+ 53 | 54 | ``` 55 | 56 | The pattern is composed of actions and data stores, where actions initiate new data to pass through data stores before coming back to the view components again. If a view component has an event that needs to make a change in the application's data stores, they need to do so by signaling to the stores through the actions available. 57 | 58 | -------------------------------- 59 | 60 | ## Usage 61 | 62 | For usage, you need to create actions which can be called from React components. Those actions are listened to by stores which hold and update data. In turn those stores are hooked up to React components and set state within them as it is updated within the store. 63 | 64 | Therefore the 3 main concepts to know are: 65 | 66 | 1. [creating actions](#creating-actions) 67 | 2. [creating stores](#creating-stores) 68 | 3. [hooking stores to React components](#hooking-stores-to-components) 69 | 70 | -------------------------------- 71 | 72 | ## Creating Actions 73 | 74 | Create an action by calling `Reflux.createAction` with an optional options object. 75 | 76 | ```javascript 77 | var statusUpdate = Reflux.createAction(); 78 | ``` 79 | 80 | An action is a [function object](http://en.wikipedia.org/wiki/Function_object) that can be invoked like any other function. 81 | 82 | ```javascript 83 | statusUpdate(data); // Invokes the action statusUpdate 84 | ``` 85 | 86 | There is also a convenience function for creating multiple actions. 87 | 88 | ```javascript 89 | var Actions = Reflux.createActions([ 90 | "statusUpdate", 91 | "statusEdited", 92 | "statusAdded" 93 | ]); 94 | 95 | // Actions object now contains the actions 96 | // with the names given in the array above 97 | // that may be invoked as usual 98 | 99 | Actions.statusUpdate(); 100 | ``` 101 | 102 | #### More on Actions: 103 | Actions can also: 104 | - load files asynchronously with child actions 105 | - do preEmit and shouldEmit checking 106 | - have many shortcuts for easy usage 107 | 108 | See [Reflux Action Documentation](docs/actions/) for more. 109 | 110 | ------------------ 111 | 112 | ## Creating Stores 113 | 114 | Create a data store much like ReactJS's own `React.Component` by creating a class extending `Reflux.Store`. The store has a `state` property much like a component, and uses `setState` like a component as well. You may set up all action listeners in the `constructor` and register them by calling the store's own `listenTo` function. 115 | 116 | ```javascript 117 | class StatusStore extends Reflux.Store 118 | { 119 | constructor() 120 | { 121 | super(); 122 | this.state = {flag:'OFFLINE'}; // <- set store's default state much like in React 123 | this.listenTo(statusUpdate, this.onStatusUpdate); // listen to the statusUpdate action 124 | } 125 | 126 | onStatusUpdate(status) 127 | { 128 | var newFlag = status ? 'ONLINE' : 'OFFLINE'; 129 | this.setState({flag:newFlag}); 130 | } 131 | } 132 | ``` 133 | 134 | In the above example, whenever the action `statusUpdate` is called, the store's `onStatusUpdate` callback will be called with whatever parameters were sent in the action. E.g. if the action is called as `statusUpdate(true)` then the `status` argument in the `onStatusUpdate` function is `true`. 135 | 136 | Stores also integrate easily with sets of actions via things like `this.listenables`. When an actions object (or an Array of multiple actions objects) is applied to `this.listenables` you may automatically add listeners simply by naming convention. Just name the functions either after the action name (such as `actionName`, or the camelcased action name preceded with "on", (such as `onActionName`). 137 | 138 | ```javascript 139 | var Actions = Reflux.createActions(['firstAction', 'secondAction']); 140 | 141 | class StatusStore extends Reflux.Store 142 | { 143 | constructor() 144 | { 145 | super(); 146 | this.listenables = Actions; 147 | } 148 | 149 | onFirstAction() 150 | { 151 | // calls on Actions.firstAction(); 152 | } 153 | 154 | onSecondAction() 155 | { 156 | // calls on Actions.secondAction(); 157 | } 158 | } 159 | ``` 160 | 161 | #### More on Stores: 162 | 163 | Reflux stores are very powerful. They can even do things like contribute to a global state that can be read and set for partial or full-state time-travel, debugging, etc. 164 | 165 | See [Reflux Store Documentation](docs/stores/) to learn more about stores. 166 | 167 | -------------------------- 168 | 169 | ## Hooking Stores to Components 170 | 171 | Once you've created actions and stores, now the last step in working RefluxJS is to hook those stores to a React component. 172 | 173 | This is done as simply as extending `Reflux.Component` instead of `React.Component` and setting the store(s) to use. `Reflux.Component` itself extends `React.Component`, so you use them the exact same way. The only difference is that `Reflux.Component` allows you to set stores for the component to get state from: 174 | 175 | ```javascript 176 | class MyComponent extends Reflux.Component 177 | { 178 | constructor(props) 179 | { 180 | super(props); 181 | this.state = {}; // our store will add its own state to the component's 182 | this.store = StatusStore; // <- just assign the store class itself 183 | } 184 | 185 | render() 186 | { 187 | var flag = this.state.flag; // <- flag is mixed in from the StatusStore 188 | return
User is {flag}
189 | } 190 | } 191 | ``` 192 | 193 | When the component mounts it will either create a singleton instance of `StatusStore` (if one isn't already made) or use an already made singleton (if it was already created by another component that uses the store). 194 | 195 | Of important note is that you can: 196 | 197 | 1. Set multiple stores by setting `this.stores` (the plural) and setting it to an Array of store classes. 198 | 2. Set a `this.storeKeys` Array to restrict only certain parts of the store being mixed into the component state. 199 | 200 | There is also a `mapStoreToState` method in the documentation for those that want absolute control over how a store's state is mapped to a component. 201 | 202 | ```javascript 203 | class MyComponent extends Reflux.Component 204 | { 205 | constructor(props) 206 | { 207 | super(props); 208 | this.state = {type:'admin'}; // <- note that we can still use normal state 209 | this.stores = [StatusStore, AnotherStore]; 210 | this.storeKeys = ['flag', 'info']; 211 | } 212 | 213 | render() 214 | { 215 | var flag = this.state.flag; 216 | var info = this.state.info; 217 | var type = this.state.type; 218 | return
User is {flag}, info: {info}, type: {type}
219 | } 220 | } 221 | ``` 222 | 223 | The above will mix in properties from the state of both `StatusStore` and `AnotherStore`. However, because of `this.storeKeys` it will only mix in the properties `flag` and `info` from them. So any other properties within those stores will not get mixed in. So even if a store contained a `type` property in its state it would not get mixed in, and the `type` value we set as a normal part of the component state is safe. 224 | 225 | #### More on using Reflux style components: 226 | 227 | Reflux's simple and intuitive way of integrating stores into components is easy and powerful. You can aggregate stores together on a component-by-component basis, filter which parts of the stores come through and which don't, or even do a detailed manual mapping of exactly how you want the state from stores to map to the state in a particular component. 228 | 229 | See [Reflux Style Component Documentation](docs/components/) to learn more. 230 | 231 | ----------------------------------------- 232 | 233 | ## Documentation 234 | 235 | What you've just read is a "view from 10,000 feet" type overview of getting started with RefluxJS. For serious learning see the [documentation](docs/). 236 | 237 | [npm-image]: http://img.shields.io/npm/v/reflux.svg 238 | [downloads-image]: http://img.shields.io/npm/dm/reflux.svg 239 | [dependencies-image]: http://img.shields.io/david/reflux/refluxjs.svg 240 | [npm-url]: https://www.npmjs.org/package/reflux 241 | [bower-image]: http://img.shields.io/bower/v/reflux.svg 242 | [bower-url]: http://bower.io/search/?q=reflux 243 | [travis-image]: http://img.shields.io/travis/reflux/refluxjs/master.svg 244 | [travis-url]: https://travis-ci.org/reflux/refluxjs 245 | [gratipay-image]: http://img.shields.io/gratipay/spoike.svg 246 | [gratipay-url]: https://gratipay.com/spoike/ 247 | [thinkful-image]: https://tf-assets-staging.s3.amazonaws.com/badges/thinkful_repo_badge.svg 248 | [thinkful-url]: http://start.thinkful.com/react/?utm_source=github&utm_medium=badge&utm_campaign=reflux 249 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reflux", 3 | "version": "6.4.1", 4 | "homepage": "https://github.com/spoike/reflux", 5 | "authors": [ 6 | "Mikael Brassman " 7 | ], 8 | "description": "A simple library for uni-directional dataflow architecture inspired by ReactJS Flux", 9 | "main": "dist/reflux.js", 10 | "keywords": [ 11 | "action", 12 | "event", 13 | "data", 14 | "flux", 15 | "reactjs" 16 | ], 17 | "license": "BSD-3-Clause", 18 | "ignore": [ 19 | "**/.*", 20 | "node_modules", 21 | "bower_components", 22 | "test", 23 | "src", 24 | "Gruntfile.js", 25 | "karma.conf.js", 26 | "package.json" 27 | ], 28 | "dependencies": { 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /dist/reflux.min.js: -------------------------------------------------------------------------------- 1 | !function(a){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=a();else if("function"==typeof define&&define.amd)define([],a);else{var b;b="undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this,b.Reflux=a()}}(function(){return function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;ge;e++)h[e]=d[e].fn;return h},e.prototype.emit=function(a,b,c,d,e,f){var h=g?g+a:a;if(!this._events||!this._events[h])return!1;var i,j,k=this._events[h],l=arguments.length;if("function"==typeof k.fn){switch(k.once&&this.removeListener(a,k.fn,void 0,!0),l){case 1:return k.fn.call(k.context),!0;case 2:return k.fn.call(k.context,b),!0;case 3:return k.fn.call(k.context,b,c),!0;case 4:return k.fn.call(k.context,b,c,d),!0;case 5:return k.fn.call(k.context,b,c,d,e),!0;case 6:return k.fn.call(k.context,b,c,d,e,f),!0}for(j=1,i=new Array(l-1);l>j;j++)i[j-1]=arguments[j];k.fn.apply(k.context,i)}else{var m,n=k.length;for(j=0;n>j;j++)switch(k[j].once&&this.removeListener(a,k[j].fn,void 0,!0),l){case 1:k[j].fn.call(k[j].context);break;case 2:k[j].fn.call(k[j].context,b);break;case 3:k[j].fn.call(k[j].context,b,c);break;default:if(!i)for(m=1,i=new Array(l-1);l>m;m++)i[m-1]=arguments[m];k[j].fn.apply(k[j].context,i)}}return!0},e.prototype.on=function(a,b,c){var e=new d(b,c||this),f=g?g+a:a;return this._events||(this._events=g?{}:Object.create(null)),this._events[f]?this._events[f].fn?this._events[f]=[this._events[f],e]:this._events[f].push(e):this._events[f]=e,this},e.prototype.once=function(a,b,c){var e=new d(b,c||this,!0),f=g?g+a:a;return this._events||(this._events=g?{}:Object.create(null)),this._events[f]?this._events[f].fn?this._events[f]=[this._events[f],e]:this._events[f].push(e):this._events[f]=e,this},e.prototype.removeListener=function(a,b,c,d){var e=g?g+a:a;if(!this._events||!this._events[e])return this;var f=this._events[e],h=[];if(b)if(f.fn)(f.fn!==b||d&&!f.once||c&&f.context!==c)&&h.push(f);else for(var i=0,j=f.length;j>i;i++)(f[i].fn!==b||d&&!f[i].once||c&&f[i].context!==c)&&h.push(f[i]);return h.length?this._events[e]=1===h.length?h[0]:h:delete this._events[e],this},e.prototype.removeAllListeners=function(a){return this._events?(a?delete this._events[g?g+a:a]:this._events=g?{}:Object.create(null),this):this},e.prototype.off=e.prototype.removeListener,e.prototype.addListener=e.prototype.on,e.prototype.setMaxListeners=function(){return this},e.prefixed=g,"undefined"!=typeof b&&(b.exports=e)},{}],2:[function(a,b,c){"use strict";Object.defineProperty(c,"__esModule",{value:!0})},{}],3:[function(a,b,c){"use strict";function d(){var a=arguments.length>0&&void 0!==arguments[0]?arguments[0]:!0;h=a}function e(a){h&&i.push(a)}function f(a){h&&j.push(a)}function g(){for(;i.length;)i.pop();for(;j.length;)j.pop()}Object.defineProperty(c,"__esModule",{value:!0});var h=!1,i=[],j=[];c.useKeep=d,c.addStore=e,c.addAction=f,c.createdStores=i,c.createdActions=j,c.reset=g},{}],4:[function(a,b,c){"use strict";function d(a){if(a&&a.__esModule)return a;var b={};if(null!=a)for(var c in a)Object.prototype.hasOwnProperty.call(a,c)&&(b[c]=a[c]);return b["default"]=a,b}Object.defineProperty(c,"__esModule",{value:!0}),c.joinStrict=c.joinConcat=c.joinLeading=c.joinTrailing=c.fetchInitialState=c.stopListeningToAll=c.stopListeningTo=c.listenTo=c.validateListening=c.listenToMany=c.hasListener=void 0;var e=a("./utils"),f=d(e),g=a("./joins"),h=function(a){for(var b,c=0,d={};c<(a.children||[]).length;++c)b=a.children[c],a[b]&&(d[b]=a[b]);return d},i=function j(a){var b={};for(var c in a){var d=a[c],e=h(d),g=j(e);b[c]=d;for(var i in g){var k=g[i];b[c+f.capitalize(i)]=k}}return b};c.hasListener=function(a){for(var b,c,d,e=0;e<(this.subscriptions||[]).length;++e)for(d=[].concat(this.subscriptions[e].listenable),b=0;bb;b++)m.throwIf(this.validateListening(d[b]));for(b=0;f>b;b++)k.push(d[b].listen(i(b,j),this));return h(j),c={listenable:d},c.stop=g(c,k,this),this.subscriptions=(this.subscriptions||[]).concat(c),c}}function g(a,b,c){return function(){var d,e=c.subscriptions,f=e?e.indexOf(a):-1;for(m.throwIf(-1===f,"Tried to remove join already gone from subscriptions list!"),d=0;d1&&(c.init=function(){var a=arguments;b.init.forEach(function(b){b.apply(this,a)},this)}),b.preEmit.length>1&&(c.preEmit=function(){return b.preEmit.reduce(function(a,b){var c=b.apply(this,a);return void 0===c?a:[c]}.bind(this),arguments)}),b.shouldEmit.length>1&&(c.shouldEmit=function(){var a=arguments;return!b.shouldEmit.some(function(b){return!b.apply(this,a)},this)}),Object.keys(b).forEach(function(a){1===b[a].length&&(c[a]=b[a][0])}),c}Object.defineProperty(c,"__esModule",{value:!0}),c.mix=e;var f=a("./utils"),g=d(f)},{"./utils":13}],13:[function(a,b,c){"use strict";function d(a){return a.charAt(0).toUpperCase()+a.slice(1)}function e(a,b){return b=b||"on",b+c.capitalize(a)}function f(a){var b="undefined"==typeof a?"undefined":m(a);return"function"===b||"object"===b&&!!a}function g(a){if(!f(a))return a;for(var b,c,d,e=1,g=arguments.length;g>e;e++){b=arguments[e],c=Object.keys(b);for(var h=0;hc;c++){var f=this.stores[c];if("function"==typeof f){var g=f.id;if(f.singleton||(f.singleton=new f,g&&(h.stores[g]=f.singleton)),this.stores[c]=f=f.singleton,f.id=g,g&&h.GlobalState[g]){for(var i in h.GlobalState[g])f.state[i]=h.GlobalState[g][i];h.GlobalState[g]=f.state}else g&&(h.GlobalState[g]=f.state)}h.serverMode||this.__storeunsubscribes__.push(f.listen(b));var j=e(this.storeKeys,f.state);j&&this.setState(j)}}this.__readytomap__=!0;var k=this.__delayedmaps__;if(k)for(var l=0,m=k.length;m>l;l++)k[l].func(k[l].state);this.__delayedmaps__=null},j.componentWillUnmount=function(){if(this.__storeunsubscribes__)for(var a=0,b=this.__storeunsubscribes__.length;b>a;a++)this.__storeunsubscribes__[a]();this.__readytomap__=!1},j.mapStoreToState=function(a,b){function c(a){var c=b.call(d,a);if(c){var e=!1;for(var f in c){e=!0;break}e&&d.setState(c)}}"function"==typeof a&&(a=a.singleton?a.singleton:h.initStore(a));var d=this;this.__storeunsubscribes__=this.__storeunsubscribes__||[],this.__storeunsubscribes__.push(a.listen(c)),this.__readytomap__?c(a.state):(this.__delayedmaps__=this.__delayedmaps__||[],this.__delayedmaps__.push({func:c,state:a.state}))},m.extend=function(a){return d(null,null,a)},c)return m;h.Component=m,g.PureComponent&&(h.PureComponent=m.extend(g.PureComponent));var n=function(){this.__store__=h.createStore(),this.state={};var a=this;for(var b in this.__store__)!function(b){Object.defineProperty(a,b,{get:function(){return a.__store__[b]},set:function(c){a.__store__[b]=c}})}(b)};j=n.prototype,Object.defineProperty(j,"listenables",{get:function(){return this.__listenables__},set:function(a){var b={};Array.isArray(a)?a.forEach(function(a){for(var c in a)b[c]=a[c]}):b=a,this.__listenables__=b,this.listenToMany(b)},enumerable:!0,configurable:!0}),j.setState=function(a){for(var b in a)this.state[b]=a[b];this.id&&(h.GlobalState[this.id]=this.state),this.trigger(a)},Object.defineProperty(n,"isES6Store",{get:function(){return!0},enumerable:!0,configurable:!0}),Object.defineProperty(n,"state",{get:function(){if(!this.singleton)throw new Error("Reflux.Store.state is inaccessible before the store has been initialized.");return this.singleton.state},enumerable:!0,configurable:!0}),h.Store=n,h.GlobalState=h.GlobalState||{},h.stores={},h.getGlobalState=function(){return f(h.GlobalState)},h.setGlobalState=function(a){for(var b in a)h.stores[b]?h.stores[b].setState(a[b]):h.GlobalState[b]=a[b]},h.initializeGlobalStore=h.initStore=function(a){var b=a.id;if(a.singleton)return a.singleton;if(!b)return a.singleton=new a,a.singleton;var c=a.singleton=new a;if(h.stores[b]=c,c.id=b,h.GlobalState[b]){for(var d in h.GlobalState[b])c.state[d]=h.GlobalState[b][d];h.GlobalState[b]=c.state}else h.GlobalState[b]=c.state;return c},i=!0}}function e(a,b){if(!a)return b;for(var c=!1,d={},e=0,f=a.length;f>e;e++){var g=a[e];b.hasOwnProperty(g)&&(c=!0,d[g]=b[g])}return c?d:!1}function f(a,b){if(null===a||"object"!=typeof a)return a;if(a.constructor!==Object&&a.constructor!==Array)return a;if(a.constructor===Date||a.constructor===RegExp||a.constructor===Function||a.constructor===String||a.constructor===Number||a.constructor===Boolean)return new a.constructor(a);b=b||new a.constructor;for(var c in a)b[c]="undefined"==typeof b[c]?f(a[c],null):b[c];return b}var g,h=a("reflux-core"),i=!1;b.exports=d},{"reflux-core":10}],19:[function(a,b,c){var d=a("reflux-core");d.serverMode="object"!=typeof window,d.connect=a("./connect"),d.connectFilter=a("./connectFilter"),d.ListenerMixin=a("./ListenerMixin"),d.listenTo=a("./listenTo"),d.listenToMany=a("./listenToMany"),a("./addES6"),b.exports=d},{"./ListenerMixin":14,"./addES6":15,"./connect":16,"./connectFilter":17,"./listenTo":20,"./listenToMany":21,"reflux-core":10}],20:[function(a,b,c){var d=a("reflux-core/lib/ListenerMethods");b.exports=function(a,b,c){return{componentDidMount:function(){for(var e in d)if(this[e]!==d[e]){if(this[e])throw"Can't have other property '"+e+"' when using Reflux.listenTo!";this[e]=d[e]}this.listenTo(a,b,c)},componentWillUnmount:d.stopListeningToAll}}},{"reflux-core/lib/ListenerMethods":4}],21:[function(a,b,c){var d=a("reflux-core/lib/ListenerMethods");b.exports=function(a){return{componentDidMount:function(){for(var b in d)if(this[b]!==d[b]){if(this[b])throw"Can't have other property '"+b+"' when using Reflux.listenToMany!";this[b]=d[b]}this.listenToMany(a)},componentWillUnmount:d.stopListeningToAll}}},{"reflux-core/lib/ListenerMethods":4}]},{},[19])(19)}); -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/reflux/refluxjs/24a476defcf3f1dc79a3f1f29bb7b46e0a51d327/docs/README.md -------------------------------------------------------------------------------- /docs/actions/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Reflux Action Documentation 3 | 4 | ## Overview 5 | 6 | Actions are a type of function that a Reflux store can listen for. They are similar to dispatching an event in that many stores can listen for a single action, but they are called like a simple function. 7 | 8 | ### Creating Actions 9 | 10 | There are multiple ways to create actions in RefluxJS. The two main functions are `Reflux.createAction` (for making one) and `Reflux.createActions` (for making multiple actions at once). 11 | 12 | `Reflux.createAction` takes an optional definition object as an argument and returns the action itself. 13 | 14 | `Reflux.createActions` takes an array of definition objects. It returns an object where each created action is a property, and the property name is the action's name. 15 | 16 | `Reflux.createActions` also has a shortcut available where you give it an object instead of an array. It builds actions for each object property and names those actions after the property name. 17 | 18 | Simple usage for these looks like this: 19 | 20 | ```javascript 21 | var singleAction = Reflux.createAction(); 22 | var ManyActions = Reflux.createActions(['action1', 'action2']); 23 | var MoreActions = Reflux.createActions({anAction:{...}}); 24 | 25 | singleAction(); // calls the singleAction with no arguments 26 | ManyActions.action1(); // calls one of the actions made within the ManyActions object 27 | MoreActions.anAction(); // calls the action made within the MoreActions object 28 | ``` 29 | 30 | ### Action Definitions 31 | 32 | Action definitions are passed to the action creation functions (either directly to `createAction` or as an Array of them to `createActions`). As an example, a definition object may take the following format: 33 | 34 | ```javascript 35 | { 36 | actionName: 'myActionName', // <- the name of the action 37 | children: ['childAction'], // <- Array of child action names for async operations 38 | asyncResult: true, // <- true to make a shortcut to adding 'completed' and 'failed' children 39 | sync: false, // <- set the action to emit synchronously or asynchronously (sync by default unless there are child actions) 40 | preEmit: function(){...}, // shortcut for setting preEmit method (covered later) 41 | shouldEmit: function(){...} // shortcut for setting shouldEmit method (covered later) 42 | } 43 | ``` 44 | 45 | None of these are mandatory. You may create an action using `Reflux.createAction` without even providing a definition at all. And furthermore there are shortcuts for setting `actionName` as well. Some of these shortcuts were shown in the above "Creating Actions" section: 46 | 47 | 1. With either action creation function just setting a string as a definition is the equivalent of using `{actionName:'myString'}` in that place. 48 | 2. With `createActions` you can send an object instead of array and it will make one action for each property given using the property keys as the `actionName` for each definition provided. 49 | 50 | Examples of both of these would be: 51 | 52 | ```javascript 53 | // examples of #1 above 54 | var action = Reflux.createAction('myName'); 55 | var Actions = Reflux.createActions(['myName1', 'myName2']); 56 | // both are equivalent to (respectively): 57 | var action = Reflux.createAction({actionName:'myName'}); 58 | var Actions = Reflux.createActions([{actionName:'myName1'}, {actionName:'myName2'}]); 59 | 60 | // examples of #2 above 61 | var Actions = Reflux.createActions({'myName1':{sync:false}}); 62 | // is equivalent to: 63 | var Actions = Reflux.createActions([{actionName:'myName1', sync:false}]); 64 | ``` 65 | 66 | You will find throughout the RefluxJS documentation that the shorthand ways for creating actions with names are extremely common, as they tend to tie in well with other shorthand methods of doing things in Reflux. 67 | 68 | ### Async vs Sync Actions 69 | 70 | As you've read, actions can simply be invoked via `myAction()`. But internally, if the action's `sync` is set to true then the action does `myAction.trigger()` and if not it does `myAction.triggerAsync()` (you may also call these manually). The important difference is that synchronous action calls must emit immediately. Asynchronous actions emit on the next tick of the JS event loop, and may have things such as `children` child actions in their definitions which they can call. 71 | 72 | Actions are sync by default unless specifically defined otherwise, or unless the action has child actions within it. 73 | 74 | ### Asynchronous Loading via Child Actions 75 | 76 | You may perform actual asynchronous actions such as file loading via child actions. An action can listen for itself to be called, and then perform an asynchronous task, calling its child action when that task is complete. In simplest form it might look something like this: 77 | 78 | ```javascript 79 | var action = Reflux.createAction({children:['delayComplete']}); 80 | 81 | action.listen(function(){ 82 | setTimeout(this.delayComplete, 1000); 83 | }); 84 | ``` 85 | 86 | Anywhere listening to that action could then use `this.listenTo(action.delayComplete, onActionDelayComplete)`. And what arguments you send when calling the `delayComplete` child are what get sent through to the callback of the listener. This way you can do things like load files and have the completed action send the contents of the file to whatever is listening. 87 | 88 | The most common way in which this is used is to use `createActions` and its shorthand object form while taking advantage of a Reflux store's ability to listen to many actions at once with its `this.listenables` and `this.listenToMany` (see Reflux store documentation). Where the store can be made to automatically have actions call the stores methods named after the action's `actionName` (or the camecased `onActionName`), the child actions can be tacked on to that to read `actionNmeChildAction` or `onActionNmeChildAction`. Here is a full example of that: 89 | 90 | ```javascript 91 | var Actions = Reflux.createActions({ 92 | 'load': {children: ['completed', 'failed']} 93 | }); 94 | 95 | Actions.load.listen( function() { 96 | someAsyncOperation() 97 | .then( this.completed ) 98 | .catch( this.failed ); 99 | }); 100 | 101 | class MyStore extends Reflux.Store 102 | { 103 | constructor() 104 | { 105 | this.listenables = Actions; 106 | // or 107 | this.listenToMany(Actions); 108 | } 109 | 110 | onLoadCompleted(data) 111 | { 112 | // use the data here 113 | } 114 | 115 | onLoadFailed(message) 116 | { 117 | // failed, with whatever message you sent 118 | } 119 | } 120 | ``` 121 | 122 | ### Listening 123 | 124 | The main way in which one listens to actions in Reflux is by using functionality attached to [Reflux Stores](../stores/) such as `this.listenables` and `this.listenTo` within the store. However, it is important to note that one can listen directly to an action or child action as well: 125 | 126 | ```javascript 127 | // listen directly to an action 128 | myActions.actionName.listen(myCallbackFunc); 129 | 130 | // listen to a child action 131 | myActions.load.completed.listen(myCallbackFunc); 132 | ``` 133 | 134 | ### Removing Listeners 135 | 136 | Any time listening happens in Reflux it returns an unsubscribe function. Simply call that function to unsubscribe: 137 | 138 | ```javascript 139 | var unsubscribe = myActions.actionName.listen(myCallbackFunc); 140 | 141 | // later... 142 | unsubscribe(); // no longer listening 143 | ``` 144 | 145 | ### Action Hooks 146 | 147 | There are a couple of hooks available for each action. 148 | 149 | * `preEmit` - Is called before the action emits an event. It receives the arguments from the action invocation. If it returns something other than undefined, that will be used as arguments for `shouldEmit` and subsequent emission. 150 | 151 | * `shouldEmit` - Is called after `preEmit` and before the action emits an event. By default it returns `true` which will let the action emit the event. You may override this if you need to check the arguments that the action receives and see if it needs to emit the event. 152 | 153 | Example usage: 154 | 155 | ```javascript 156 | Actions.statusUpdate.preEmit = function() { console.log(arguments); }; 157 | Actions.statusUpdate.shouldEmit = function(value) { 158 | return value > 0; 159 | }; 160 | 161 | Actions.statusUpdate(0); 162 | Actions.statusUpdate(1); 163 | // Should output: 1 164 | ``` 165 | 166 | You can also set the hooks by sending them in a definition object as you create the action: 167 | 168 | ```javascript 169 | var action = Reflux.createAction({ 170 | preEmit: function(){...}, 171 | shouldEmit: function(){...} 172 | }); 173 | ``` 174 | 175 | ### Reflux.ActionMethods 176 | 177 | If you would like to have a common set of methods available to all actions you can extend the `Reflux.ActionMethods` object, which is mixed into the actions when they are created. 178 | 179 | Example usage: 180 | 181 | ```javascript 182 | Reflux.ActionMethods.exampleMethod = function() { console.log(arguments); }; 183 | 184 | Actions.statusUpdate.exampleMethod('arg1'); 185 | // Should output: 'arg1' 186 | ``` 187 | 188 | ### Next: 189 | 190 | Next learn about [Reflux Stores](../stores/). 191 | -------------------------------------------------------------------------------- /docs/advanced/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Advanced usage 3 | 4 | ### Server Mode 5 | 6 | Reflux has a server mode which allows it to operate more efficiently for the vastly different environment of server rendering. This mode is automatically determined by Reflux by the lack of presence of the `window` object (which means it is not being run in the browser). Therefore there is no need to change anything in Reflux between server and browser for server mode to work properly. However, it is accessible for manual use via setting the `Reflux.serverMode` property to your desired boolean value. 7 | 8 | ### Switching EventEmitter 9 | 10 | Don't like to use the EventEmitter provided? You can switch to another one, such as NodeJS's own like this: 11 | 12 | ```javascript 13 | // Do this before creating actions or stores 14 | 15 | Reflux.setEventEmitter(require('events').EventEmitter); 16 | ``` 17 | 18 | ### Switching nextTick 19 | 20 | Whenever action functors are called, they return immediately through the use of `setTimeout` (`nextTick` function) internally. 21 | 22 | You may switch out for your favorite `setTimeout`, `nextTick`, `setImmediate`, et al implementation: 23 | 24 | ```javascript 25 | 26 | // node.js env 27 | Reflux.nextTick(process.nextTick); 28 | ``` 29 | 30 | For better alternative to `setTimeout`, you may opt to use the [`setImmediate` polyfill](https://github.com/YuzuJS/setImmediate), [`setImmediate2`](https://github.com/Katochimoto/setImmediate) or [`macrotask`](https://github.com/calvinmetcalf/macrotask). 31 | 32 | 33 | ### Joining parallel listeners with composed listenables 34 | 35 | The Reflux API contains `join` methods that makes it easy to aggregate publishers that emit events in parallel. This corresponds to the `waitFor` method in Flux. 36 | 37 | #### Argument tracking 38 | 39 | A join is triggered once all participating publishers have emitted at least once. The callback will be called with the data from the various emissions, in the same order as the publishers were listed when the join was created. 40 | 41 | There are four join methods, each representing a different strategy to track the emission data: 42 | 43 | * `joinLeading`: Only the first emission from each publisher is saved. Subsequent emissions by the same publisher before all others are finished are ignored. 44 | * `joinTrailing`: If a publisher triggers twice, the second emission overwrites the first. 45 | * `joinConcat`: An array of emission arguments are stored for each publisher. 46 | * `joinStrict`: An error is thrown if a publisher emits twice before the join is completed. 47 | 48 | The method signatures all look like this: 49 | 50 | ```javascript 51 | joinXyz(...publisher,callback) 52 | ``` 53 | 54 | Once a join is triggered it will reset, and thus it can trigger again when all publishers have emitted anew. 55 | 56 | #### Using the listener instance methods 57 | 58 | All objects using the listener API (stores, React components using `ListenerMixin`, or other components using the `ListenerMethods`) gain access to the four join instance methods, named after the argument strategy. Here's an example saving the last emission from each publisher: 59 | 60 | ```javascript 61 | var gainHeroBadgeStore = Reflux.createStore({ 62 | init: function() { 63 | this.joinTrailing(actions.disarmBomb, actions.saveHostage, actions.recoverData, this.triggerAsync); 64 | } 65 | }); 66 | 67 | actions.disarmBomb("warehouse"); 68 | actions.recoverData("seedyletter"); 69 | actions.disarmBomb("docks"); 70 | actions.saveHostage("offices",3); 71 | // `gainHeroBadgeStore` will now asynchronously trigger `[["docks"],["offices",3],["seedyletter"]]`. 72 | ``` 73 | 74 | #### Using the static methods 75 | 76 | Since it is rather common to have a store where the only purpose is to listen to a join and trigger when the join is completed, the join methods have static counterparts on the `Reflux` object which return stores listening to the requested join. Using them, the store in the example above could instead be created like this: 77 | 78 | ```javascript 79 | var gainHeroBadgeStore = Reflux.joinTrailing(actions.disarmBomb, actions.saveHostage, actions.recoverData); 80 | ``` 81 | 82 | ### Sending initial state with the listenTo function 83 | 84 | The `listenTo` function provided by the `Store` and the `ListenerMixin` has a third parameter that accepts a callback. This callback will be invoked when the listener is registered with whatever the `getInitialState` is returning. 85 | 86 | ```javascript 87 | var exampleStore = Reflux.createStore({ 88 | init: function() {}, 89 | getInitialState: function() { 90 | return "the initial data"; 91 | } 92 | }); 93 | 94 | // Anything that will listen to the example store 95 | this.listenTo(exampleStore, onChangeCallback, initialCallback) 96 | 97 | // initialCallback will be invoked immediately with "the initial data" as first argument 98 | ``` 99 | 100 | Remember the `listenToMany` method? In case you use that with other stores, it supports `getInitialState`. That data is sent to the normal listening callback, or a `this.onDefault` method if that exists. 101 | 102 | [Back to docs](../) 103 | -------------------------------------------------------------------------------- /docs/components/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Reflux.Component Documentation 3 | 4 | ## Overview 5 | 6 | Of course, a main point of [actions](../actions/README.md) and [stores](../stores/README.md) is to eventually tie the data being stored/manipulated in them into a React component. 7 | 8 | For that Reflux gives you `Reflux.Component` and `Reflux.PureComponent`. `Reflux.Component` is itself an extension of `React.Component` that was created to facilitate hooking stores into the component. You would use a `Reflux.Component` the exact same as a `React.Component`, except that you'd use its specific properties and methods to hook in the state of your stores. `Reflux.PureComponent` is used the exact same, except it inherits the same behavior as `React.PureComponent` as for shallow checking state to see if updates are needed. 9 | 10 | ### Mixing State with `this.store` and `this.stores` 11 | 12 | The main declarative way to utilize one or more stores within a `Reflux.Component` is `this.store` (assign one store) or `this.stores` (assign an Array of stores). 13 | 14 | ```javascript 15 | class Counter extends Reflux.Component // <-- note Reflux.Component, not React.Component 16 | { 17 | constructor(props) 18 | { 19 | super(props); 20 | this.store = CountStore; // <-- a store w/state with a `count` property 21 | } 22 | 23 | render() 24 | { 25 | // since CountStore has a state.count, now this component shares it 26 | return
Count: {this.state.count}
27 | } 28 | } 29 | ``` 30 | 31 | The `count` property from the store is mixed into the component's state. It acts just like a normal part of the state within the component. As `setState` is used to change the value within the store the state within the component is updated and the component is re-rendered, `shouldComponentUpdate` works as normal, etc. 32 | 33 | Note that you assign the class itself. Reflux will automatically either A) make a singleton instance of it, or B) use a preexisting singleton if one already has been made. It is possible to assign an instance created from a `Reflux.Store` class, however it is practically always preferrable to simply assign the class itself so that Reflux can manage the singleton properly. 34 | 35 | Normal state properties are also preserved. You may use state from the component itself and state from one or more stores side-by-side: 36 | 37 | ```javascript 38 | class Counter extends Reflux.Component 39 | { 40 | constructor(props) 41 | { 42 | super(props); 43 | this.stores = [CountStore, MultiplierStore]; 44 | this.state = {offset: 5}; 45 | } 46 | 47 | render() 48 | { 49 | // the state props come from the CounterStore, MultiplierStore, and normal state 50 | var calculatedValue = this.state.count * this.state.multiplier + this.state.offset; 51 | return
Count: {calculatedValue}
52 | } 53 | } 54 | ``` 55 | 56 | ### Limiting Mixing with `this.storeKeys` 57 | 58 | By default, the entire store is mixed into your component. However, it is very useful to be able to easily bring in only part of the store. This prevents A) unintended name conflicts, such as a store state property added after-the-fact overwriting a piece of normal in-component state accidentally, and B) unneeded rendering, since some components may only need certain parts of the store and therefore do not need to re-render every time other properties update. 59 | 60 | This is what `this.storeKeys` is for. It allows you to assign an Array of strings, and only property key names from that Array will be taken from any stores mixed in via `this.store` or `this.stores`. 61 | 62 | ```javascript 63 | class Counter extends Reflux.Component 64 | { 65 | constructor(props) 66 | { 67 | super(props); 68 | this.store = CountStore; 69 | this.storeKeys = ['count']; // <-- only `count` will be mixed in from the store 70 | } 71 | 72 | render() 73 | { 74 | return
Count: {this.state.count}
75 | } 76 | } 77 | ``` 78 | 79 | ### Manually Mapping States with `this.mapStoreToState` 80 | 81 | In addition to the more declarative ways to merge store state into a component that are described above (i.e. `this.store`, `this.stores`, `this.storeKeys`) there is another more imperative style for merging state. That is the `Reflux.mapStoreToState` method that is part of Reflux stores. 82 | 83 | An important thing to note is that this is completely separate from the previously mentioned declarative side of things (such as `this.store`). You should not have the same store attached to a component both via `this.store` and using `this.mapStoreToState`. This method is also completely unaffected by this.storeKeys. These differing methods can both be used within a single component, but just shouldn't both be used for the same store within the same component. 84 | 85 | `this.mapStoreToState` takes 2 arguments: the store you want mapped to the component state, and a mapping function supplied by you. The mapping function will be called any time the store's `this.setState` is used to change the state of the store. The mapping function takes an argument which will be the state change object from the store for that particular change. It needs to return an object which will then be mapped to the component state (similar to if that very returned object were used in the component's `this.setState`). If an object with no properties is returned then the component will not re-render. The mapping function is also called with its `this` keyword representing the component, so comparing store values to current component state values via `this.state` is possible as well. 86 | 87 | ```javascript 88 | class MyComponent extends Reflux.Component 89 | { 90 | constructor(props) 91 | { 92 | super(props); 93 | 94 | this.mapStoreToState(MyStoreClass, function(fromStore){ 95 | var obj = {}; 96 | if (fromStore.color) 97 | obj.color = fromStore.color; 98 | if (fromStore.data && fromStore.data.classToUse) 99 | obj.class = fromStore.data.classToUse; 100 | return obj; 101 | }); 102 | } 103 | 104 | render() 105 | { 106 | return

The color is: {this.state.color}

; 107 | } 108 | } 109 | ``` 110 | 111 | Note that the `fromStore` in the example above may not always be the full state from the store. It only receives what is actually changing at that time. This way you are able to only map the parts of the store that have actually changed on each call. 112 | 113 | ### Extending a 3rd Party Class with `Reflux.Component.extend` 114 | 115 | Sometimes 3rd party libraries will have their own class that extends `React.Component` that they require you to use. Reflux handles this by exposing the `Reflux.Component.extend` method. If you have such a 3rd party class you can pass that class to this method and it will return a version of `Reflux.Component` that extends it instead of extending `React.Component` directly. Example: 116 | 117 | ```javascript 118 | import {ThirdPartyComponent} from 'third-party'; 119 | 120 | var RefluxThirdPartyComponent = Reflux.Component.extend(ThirdPartyComponent); 121 | 122 | class MyComponent extends RefluxThirdPartyComponent 123 | { 124 | // ... 125 | } 126 | ``` 127 | 128 | ### componentWillMount and componentWillUnmount 129 | 130 | The `Reflux.Component` class utilizes both the `componentWillMount` and `componentWillUnmount` methods in the React lifecycle. This means that any extension you create that uses these methods will override those methods within the `Reflux.Component` class you're extending. If you run into this problem the solution is simple: just run the appropriate `super`. For example, in a `componentWillMount`: 131 | 132 | ```javascript 133 | // ... 134 | 135 | componentWillMount() 136 | { 137 | // ... your stuff ... 138 | 139 | super.componentWillMount(); 140 | } 141 | 142 | //... 143 | ``` 144 | 145 | ### More: 146 | 147 | Learn about [Reflux Stores](../stores/) and [Actions](../actions/), or go back to the overview of the [docs](../). 148 | -------------------------------------------------------------------------------- /docs/other/examples.md: -------------------------------------------------------------------------------- 1 | 2 | ## Examples 3 | 4 | #### Simple Fully Functioning Example 5 | 6 | ```javascript 7 | var Actions = Reflux.createActions([ 8 | 'increment', 9 | 'decrement', 10 | 'changeBy' 11 | ]); 12 | 13 | class CounterStore extends Reflux.Store 14 | { 15 | constructor() 16 | { 17 | super(); 18 | this.state = {count: 0}; 19 | this.listenables = Actions; 20 | } 21 | 22 | onIncrement() 23 | { 24 | this.setState({count: this.state.count+1}); 25 | } 26 | 27 | onDecrement() 28 | { 29 | this.setState({count: this.state.count-1}); 30 | } 31 | 32 | onChangeBy(amount) 33 | { 34 | this.setState({count: this.state.count+amount}); 35 | } 36 | } 37 | 38 | class Counter extends Reflux.Component 39 | { 40 | constructor(props) 41 | { 42 | super(props); 43 | this.store = CounterStore; 44 | } 45 | 46 | render() 47 | { 48 | return
{this.state.count}
; 49 | } 50 | } 51 | 52 | ReactDOM.render 53 | ( 54 | , 55 | document.querySelector('#react-root') 56 | ); 57 | 58 | setInterval(Actions.increment, 1000); 59 | ``` 60 | 61 | [Back to docs](../) 62 | -------------------------------------------------------------------------------- /docs/other/reflux-vs-flux.md: -------------------------------------------------------------------------------- 1 | # Comparing RefluxJS with Facebook Flux 2 | 3 | The goal of the refluxjs project is to get this architecture easily up and running in your web application, both client-side or server-side. There are some differences between how this project works and how Facebook's proposed Flux architecture works: 4 | 5 | You can read more in this [blog post about React Flux vs Reflux](http://spoike.ghost.io/deconstructing-reactjss-flux/). 6 | 7 | ### Similarities with Flux 8 | 9 | Some concepts are still in Reflux in comparison with Flux: 10 | 11 | * There are actions 12 | * There are data stores 13 | * The data flow is unidirectional 14 | 15 | ### Differences with Flux 16 | 17 | Reflux has refactored Flux to be a bit more dynamic and be more Functional Reactive Programming (FRP) friendly: 18 | 19 | * The singleton dispatcher is removed in favor for letting every action act as dispatcher instead. 20 | * Because actions are listenable, the stores may listen to them. Stores don't need to have big switch statements that do static type checking (of action types) with strings 21 | * Stores may listen to other stores, i.e. it is possible to create stores that can *aggregate data further*, similar to a map/reduce. 22 | * `waitFor` is replaced in favor to handle *serial* and *parallel* data flows: 23 | * **Aggregate data stores** (mentioned above) may listen to other stores in *serial* 24 | * **Joins** for joining listeners in *parallel* 25 | * *Action creators* are not needed because RefluxJS actions are functions that will pass on the payload they receive to anyone listening to them 26 | 27 | [Back to docs](../) -------------------------------------------------------------------------------- /docs/stores/README.md: -------------------------------------------------------------------------------- 1 | 2 | # Reflux Store Documentation 3 | 4 | ## Overview 5 | 6 | Stores hold data and receive action calls to update and change that data. They then can be hooked into components so that those components' states reflect that data and update along with it. 7 | 8 | One important concept to understand about stores in Reflux is that you (usually) just define them, not create them. The concept is similar to components in React: you define the class and use them in your JSX, but React itself handles instantiating them. Similarly for Reflux stores: you define a store class and assign that class to components. Then, while mounting, those components will either instantiate a singleton of the class (if it hasn't been done yet) or use the existing singleton (if it has been made yet). That singleton is assigned to the static property `singleton` on the class itself (so, for example, a class `MyStore` would end up with `MyStore.singleton` being the reference to the instance once it was created). 9 | 10 | ### Defining Stores 11 | 12 | To define a store in Reflux you need to make a class that extends `Reflux.Store`. That store will store its state on a `this.state` property, and mutate its state via `this.setState()` in a way that is extremely similar to React classes themselves. So there's basically no learning curve on that front. 13 | 14 | You can then listen for actions and can update your store's state on them. For that the most basic way to listen is with `this.listenTo(action, callback)`: 15 | 16 | ```javascript 17 | var increment = Reflux.createAction(); 18 | 19 | class MyStore extends Reflux.Store 20 | { 21 | constructor() 22 | { 23 | super(); 24 | this.state = {count: 0}; 25 | this.listenTo(increment, this.incrementItUp); 26 | } 27 | 28 | incrementItUp() 29 | { 30 | var newCount = this.state.count + 1; 31 | this.setState({count: newCount}); 32 | } 33 | } 34 | ``` 35 | 36 | ### Listening Shortcuts 37 | 38 | In many cases there are quite a few actions at once, and writing out `this.listenTo(...)` for each of them would be wasteful and less easily edited/maintained. Therefore there are two shortcuts: a method `this.listenToMany()` and a property `this.listenables`. These are both designed to take an object where each property value is an action and the property name is the function name that that is going to be the callback that's used when that action is called. 39 | 40 | By design, that is exactly the type of object returned when using the `Reflux.createActions()` shortcut for making multiple actions at once. So that method becomes extremely useful when used in concert with these listening shortcuts: 41 | 42 | ```javascript 43 | var Actions = Reflux.createActions(['increment', 'decrement', 'changeBy']); 44 | 45 | class MyStore extends Reflux.Store 46 | { 47 | constructor() 48 | { 49 | super(); 50 | this.state = {count: 0}; 51 | this.listenToMany(Actions); 52 | //this.listenables = Actions; // <- would work equally well 53 | } 54 | 55 | increment() 56 | { 57 | var newCount = this.state.count + 1; 58 | this.setState({count: newCount}); 59 | } 60 | 61 | decrement() 62 | { 63 | var newCount = this.state.count - 1; 64 | this.setState({count: newCount}); 65 | } 66 | 67 | changeBy(amount) 68 | { 69 | var newCount = this.state.count + amount; 70 | this.setState({count: newCount}); 71 | } 72 | } 73 | ``` 74 | 75 | This next example also shows two other features. First: `this.listenables` also can be used with an Array of such action object. Second: you may also used the camelcased `onActionName` to define functions in this manner: 76 | 77 | ```javascript 78 | var RelativeChanges = Reflux.createActions(['increment', 'decrement', 'changeBy']); 79 | var AbsoluteChanges = Reflux.createActions(['zero', 'setTo']); 80 | 81 | class MyStore extends Reflux.Store 82 | { 83 | constructor() 84 | { 85 | super(); 86 | this.state = {count: 0}; 87 | this.listenables = [RelativeChanges, AbsoluteChanges]; 88 | } 89 | 90 | onIncrement() 91 | { 92 | var newCount = this.state.count + 1; 93 | this.setState({count: newCount}); 94 | } 95 | 96 | onDecrement() 97 | { 98 | var newCount = this.state.count - 1; 99 | this.setState({count: newCount}); 100 | } 101 | 102 | onChangeBy(amount) 103 | { 104 | var newCount = this.state.count + amount; 105 | this.setState({count: newCount}); 106 | } 107 | 108 | onZero() 109 | { 110 | this.setState({count: 0}); 111 | } 112 | 113 | onSetTo(value) 114 | { 115 | this.setState({count: value}); 116 | } 117 | } 118 | ``` 119 | 120 | ### Global State 121 | 122 | A great feature about Reflux stores is that Reflux is capable of keeping a global state object of all of them that can be read and manipulated. This is perfect for setting up a certain state for testing, or for storing the state of a running program for later, outputting full program state at the time of an error, state-based time travel to go back to a previously stored state, etc. And even better: it's optional on a store-by-store basis. To opt in to storing the contents of a given store globally all you need to do is give the store's class a static `id` property, like so: 123 | 124 | ```javascript 125 | class MyStore extends Reflux.Store 126 | { 127 | constructor() 128 | { 129 | super(); 130 | this.state = {count: 0}; 131 | } 132 | } 133 | 134 | MyStore.id = 'mystore'; 135 | ``` 136 | 137 | Now that object's data will be stored on an object `Reflux.GlobalState` under the property `mystore` (the id value), and updated as the store updates. So in this case, once the store was actually created, `Reflux.GlobalState.mystore.count` would be `0`. 138 | 139 | However, `Reflux.GlobalState` itself is just the reference to the simple object of data. Accessing it is great for grabbing quick reads of current state, but we need more than that for global state to be truly useful. For example, as a simple object you can't just mutate data on it and expect it to actually change the store in question's data. And if you grab a non-primitive from it (like `Reflux.GlobalState.mystore`) its properties will continue to mutate (so you can't just grab state from it to go back to later). 140 | 141 | Because of that we have 2 methods designed for use with global state: `Reflux.getGlobalState()` and `Reflux.setGlobalState()`. 142 | 143 | `Reflux.getGlobalState()` will return a deep clone of the current global state. Since it is a clone it will not continue to mutate as the global state continues to mutate. This means you can grab references to the global state now so that you can use them later. In our above example it would return an object `{mystore:{count:0}}`. 144 | 145 | `Reflux.setGlobalState(obj)` will set the global state to the argument provided. Not only that, but it will update the stores affected to reflect those changes. And not only that, but the object you provide does not need to be a full representation of all data the global state may store. The method will mix in just the data you provided for just the store id's you provide data for. This way you can reset parts of your global state without needing to know anything about the rest of it. 146 | 147 | Another important feature is that when stores are created they check the global state for data relevant to them. If they have an id and the global state already has some state data they will integrate that into their default state from creation. In this way you can set your programs to a specific state right from startup if you wish. 148 | 149 | ### Reflux.initStore() 150 | 151 | In normal usage Reflux stores are created as singletons by Reflux once needed. However, there are valid cases where one would need to create a store before then. However, it's a little more complex than just calling `new MyStoreClass()`, because of the need for them to operate as singletons (and also because, on stores with id's, there needs to be some other setup upon creation of that singleton in order to integrate them into the global state). For this reason Reflux exposes `Reflux.initStore`, used as follows: 152 | 153 | `var storeSingleton = Reflux.initStore(MyStoreClass);` 154 | 155 | This function will: create the singleton of the store and assign it properly if needed (or, just as importantly, **not** create a new instance if a singleton already exists), do whatever stuff needs to happen behind the scenes if it has an id and should be part of global state, and last but not least return the instance of the store (whether old or new) to you. 156 | 157 | ### Next: 158 | 159 | Next learn about [hooking stores to components](../components/). 160 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | module.exports = function(config) { 2 | config.set({ 3 | logLevel: 'LOG_DEBUG', 4 | 5 | reporters: ['spec'], 6 | 7 | singleRun : true, 8 | autoWatch : false, 9 | 10 | frameworks: [ 11 | 'mocha', 12 | 'browserify' 13 | ], 14 | 15 | files: [ 16 | 'test/shims/phantomjs-shims.js', 17 | 'test/*.spec.js' 18 | ], 19 | 20 | preprocessors: { 21 | 'test/shims/phantomjs-shims.js': ['browserify'], 22 | 'test/*.spec.js': ['browserify'] 23 | }, 24 | 25 | browserify: { 26 | debug: true 27 | } 28 | }); 29 | }; 30 | -------------------------------------------------------------------------------- /old-README.md: -------------------------------------------------------------------------------- 1 | # RefluxJS 2 | 3 | A simple library for unidirectional dataflow architecture inspired by ReactJS [Flux](http://facebook.github.io/react/blog/2014/05/06/flux.html). 4 | 5 | [![NPM Version][npm-image]][npm-url] 6 | [![NPM Downloads][downloads-image]][npm-url] 7 | [![Bower Version][bower-image]][bower-url] 8 | [![Dependencies][dependencies-image]][npm-url] 9 | [![Build Status][travis-image]][travis-url] 10 | [![Gratipay][gratipay-image]][gratipay-url] 11 | 12 | [![Sauce Test Status](https://saucelabs.com/browser-matrix/refluxjs.svg)](https://saucelabs.com/u/refluxjs) 13 | 14 | You can read an overview of Flux [here](https://facebook.github.io/flux/docs/overview.html), however the gist of it is to introduce a more functional programming style architecture by eschewing MVC like pattern and adopting a single data flow pattern. 15 | 16 | ``` 17 | ╔═════════╗ ╔════════╗ ╔═════════════════╗ 18 | ║ Actions ║──────>║ Stores ║──────>║ View Components ║ 19 | ╚═════════╝ ╚════════╝ ╚═════════════════╝ 20 | ^ │ 21 | └──────────────────────────────────────┘ 22 | 23 | ``` 24 | 25 | The pattern is composed of actions and data stores, where actions initiate new data to pass through data stores before coming back to the view components again. If a view component has an event that needs to make a change in the application's data stores, they need to do so by signaling to the stores through the actions available. 26 | 27 | Feel free to open an issue on our [**discussion forum**](https://github.com/reflux/discuss) for **questions and general discussion**. Here is a complete list of communication channels: 28 | 29 | 1. The [discussion forum](https://github.com/reflux/discuss) 30 | 2. [StackOverflow with the `refluxjs` tag](http://stackoverflow.com/questions/tagged/refluxjs) 31 | 3. `#flux` channel on Reactiflux Discord group. [Sign up here](https://discord.gg/0ZcbPKXt5bYedEbN) for an account. 32 | 4. [![Gitter](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/spoike/refluxjs?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) 33 | 5. [![Thinkful][thinkful-image]][thinkful-url] 34 | 35 | Please use the [issue tracker](https://github.com/spoike/refluxjs/issues) only for bugs and feature requests. 36 | 37 | If you don't want to use the React-specific API, or want to develop Reflux for your view engine framework of choice, have a look at [`reflux-core`](https://github.com/reflux/reflux-core). 38 | 39 | ## Content 40 | 41 | - [Comparing RefluxJS with Facebook Flux](#comparing-refluxjs-with-facebook-flux) 42 | - [Examples](#examples) 43 | - [Extensions and Plugins](#extensions-and-plugins) 44 | - [Installation](#installation) 45 | - [Usage](#usage) 46 | - [Actions](#creating-actions) 47 | - [Stores](#creating-data-stores) 48 | - [Component](#react-component-example) 49 | - [Advanced Usage](#advanced-usage) 50 | - [React ES6 Usage](#react-es6-usage) 51 | - [Colophon](#colophon) 52 | 53 | ## Comparing RefluxJS with Facebook Flux 54 | 55 | The goal of the refluxjs project is to get this architecture easily up and running in your web application, both client-side or server-side. There are some differences between how this project works and how Facebook's proposed Flux architecture works: 56 | 57 | You can read more in this [blog post about React Flux vs Reflux](http://spoike.ghost.io/deconstructing-reactjss-flux/). 58 | 59 | ### Similarities with Flux 60 | 61 | Some concepts are still in Reflux in comparison with Flux: 62 | 63 | * There are actions 64 | * There are data stores 65 | * The data flow is unidirectional 66 | 67 | ### Differences with Flux 68 | 69 | Reflux has refactored Flux to be a bit more dynamic and be more Functional Reactive Programming (FRP) friendly: 70 | 71 | * The singleton dispatcher is removed in favor for letting every action act as dispatcher instead. 72 | * Because actions are listenable, the stores may listen to them. Stores don't need to have big switch statements that do static type checking (of action types) with strings 73 | * Stores may listen to other stores, i.e. it is possible to create stores that can *aggregate data further*, similar to a map/reduce. 74 | * `waitFor` is replaced in favor to handle *serial* and *parallel* data flows: 75 | * **Aggregate data stores** (mentioned above) may listen to other stores in *serial* 76 | * **Joins** for joining listeners in *parallel* 77 | * *Action creators* are not needed because RefluxJS actions are functions that will pass on the payload they receive to anyone listening to them 78 | 79 | [Back to top](#content) 80 | 81 | ## Examples 82 | 83 | You can find some example projects at these locations: 84 | 85 | * [Todo Example Project](https://github.com/spoike/refluxjs-todo) 86 | * [Hacker News Clone](https://github.com/echenley/react-news) by echenley 87 | * [Another Todo Project with a Python backend](https://github.com/limelights/todo-reflux) by limelights 88 | * [Sample app with authentication, permissions, sidebar and editable collection](https://github.com/VladimirPal/react-flux-backbone) 89 | * [TodoMVC demonstrating Reflux + Angular](https://github.com/javamonn/Angular-TodoMVC-Redux) 90 | * [Sample blog by @akornatskyy](https://github.com/akornatskyy/sample-blog-react) 91 | 92 | [Back to top](#content) 93 | 94 | ## Extensions and Plugins 95 | 96 | * [Cartiv](https://github.com/yonatanmn/cartiv) - new reactive interface based on Reflux 97 | * [reflux-promise](https://github.com/reflux/reflux-promise) - Extends reflux with Promises 98 | * [reflux-triggerable-mixin](https://github.com/jesstelford/reflux-triggerable-mixin) - Stores mixin adding `triggerable` syntax similar to `listenable` 99 | * [reflux-state-mixin](https://github.com/yonatanmn/reflux-state-mixin) - Stores mixin adding `state` syntax similar to React components. 100 | 101 | ## Installation 102 | 103 | You can currently install the package as a npm package or bower component. 104 | 105 | ### NPM 106 | 107 | The following command installs reflux as a npm package: 108 | 109 | npm install reflux 110 | 111 | ### Bower 112 | 113 | The following command installs reflux as a bower component that can be used in the browser: 114 | 115 | bower install reflux 116 | 117 | Then the files may be embedded in your html file via `bower_components/reflux/dist/reflux.js` or `bower_components/reflux/dist/reflux.min.js`. 118 | 119 | ### CDN 120 | 121 | Reflux is available at [jsdelivr](http://www.jsdelivr.com/#!refluxjs). 122 | jsdelivr [v5.0.1](https://cdn.jsdelivr.net/refluxjs/5.0.1/reflux.min.js) 123 | 124 | ### ES5 125 | 126 | Like React, Reflux depends on an es5-shim for older browsers. The es5-shim.js from [kriskowal's es5-shim](https://github.com/kriskowal/es5-shim) provides everything required. 127 | 128 | [Back to top](#content) 129 | 130 | ## Usage 131 | 132 | ### Creating actions 133 | 134 | Create an action by calling `Reflux.createAction` with an optional options object. 135 | 136 | ```javascript 137 | var statusUpdate = Reflux.createAction(options); 138 | ``` 139 | 140 | An action is a [function object](http://en.wikipedia.org/wiki/Function_object) that can be invoked like any function. 141 | 142 | ```javascript 143 | statusUpdate(data); // Invokes the action statusUpdate 144 | statusUpdate.triggerAsync(data); // same effect as above 145 | ``` 146 | 147 | If `options.sync` is true, the functor will instead call `action.trigger` which is a synchronous operation. You can change `action.sync` during the lifetime of the action, and the following calls will honour that change. 148 | 149 | There is also a convenience function for creating multiple actions. 150 | 151 | ```javascript 152 | var Actions = Reflux.createActions([ 153 | "statusUpdate", 154 | "statusEdited", 155 | "statusAdded" 156 | ]); 157 | 158 | // Actions object now contains the actions 159 | // with the names given in the array above 160 | // that may be invoked as usual 161 | 162 | Actions.statusUpdate(); 163 | ``` 164 | 165 | #### Asynchronous actions 166 | 167 | For actions that represent asynchronous operations (e.g. API calls), a few separate dataflows result from the operation. In the most typical case, we consider completion and failure of the operation. To create related actions for these dataflows, which you can then access as attributes, use `options.children`. 168 | 169 | ```javascript 170 | // this creates 'load', 'load.completed' and 'load.failed' 171 | var Actions = Reflux.createActions({ 172 | "load": {children: ["completed","failed"]} 173 | }); 174 | 175 | // when 'load' is triggered, call async operation and trigger related actions 176 | Actions.load.listen( function() { 177 | // By default, the listener is bound to the action 178 | // so we can access child actions using 'this' 179 | someAsyncOperation() 180 | .then( this.completed ) 181 | .catch( this.failed ); 182 | }); 183 | ``` 184 | 185 | There is a shorthand to define the `completed` and `failed` actions in the typical case: `options.asyncResult`. The following are equivalent: 186 | 187 | ```javascript 188 | createAction({ 189 | children: ["progressed","completed","failed"] 190 | }); 191 | 192 | createAction({ 193 | asyncResult: true, 194 | children: ["progressed"] 195 | }); 196 | ``` 197 | 198 | #### Action hooks 199 | 200 | There are a couple of hooks available for each action. 201 | 202 | * `preEmit` - Is called before the action emits an event. It receives the arguments from the action invocation. If it returns something other than undefined, that will be used as arguments for `shouldEmit` and subsequent emission. 203 | 204 | * `shouldEmit` - Is called after `preEmit` and before the action emits an event. By default it returns `true` which will let the action emit the event. You may override this if you need to check the arguments that the action receives and see if it needs to emit the event. 205 | 206 | Example usage: 207 | 208 | ```javascript 209 | Actions.statusUpdate.preEmit = function() { console.log(arguments); }; 210 | Actions.statusUpdate.shouldEmit = function(value) { 211 | return value > 0; 212 | }; 213 | 214 | Actions.statusUpdate(0); 215 | Actions.statusUpdate(1); 216 | // Should output: 1 217 | ``` 218 | 219 | You can also set the hooks by sending them in a definition object as you create the action: 220 | 221 | ```javascript 222 | var action = Reflux.createAction({ 223 | preEmit: function(){...}, 224 | shouldEmit: function(){...} 225 | }); 226 | ``` 227 | 228 | #### Reflux.ActionMethods 229 | 230 | If you would like to have a common set of methods available to all actions you can extend the `Reflux.ActionMethods` object, which is mixed into the actions when they are created. 231 | 232 | Example usage: 233 | 234 | ```javascript 235 | Reflux.ActionMethods.exampleMethod = function() { console.log(arguments); }; 236 | 237 | Actions.statusUpdate.exampleMethod('arg1'); 238 | // Should output: 'arg1' 239 | ``` 240 | 241 | [Back to top](#content) 242 | 243 | ### Creating data stores 244 | 245 | Create a data store much like ReactJS's own `React.createClass` by passing a definition object to `Reflux.createStore`. You may set up all action listeners in the `init` function and register them by calling the store's own `listenTo` function. 246 | 247 | ```javascript 248 | // Creates a DataStore 249 | var statusStore = Reflux.createStore({ 250 | 251 | // Initial setup 252 | init: function() { 253 | 254 | // Register statusUpdate action 255 | this.listenTo(statusUpdate, this.output); 256 | }, 257 | 258 | // Callback 259 | output: function(flag) { 260 | var status = flag ? 'ONLINE' : 'OFFLINE'; 261 | 262 | // Pass on to listeners 263 | this.trigger(status); 264 | } 265 | 266 | }); 267 | ``` 268 | 269 | In the above example, whenever the action is called, the store's `output` callback will be called with whatever parameters were sent in the action. E.g. if the action is called as `statusUpdate(true)` then the flag argument in `output` function is `true`. 270 | 271 | A data store is a publisher much like the actions, so they too have the `preEmit` and `shouldEmit` hooks. 272 | 273 | #### Reflux.StoreMethods 274 | 275 | If you would like to have a common set of methods available to all stores you can extend the `Reflux.StoreMethods` object, which is mixed into the stores when they are created. 276 | 277 | Example usage: 278 | 279 | ```javascript 280 | Reflux.StoreMethods.exampleMethod = function() { console.log(arguments); }; 281 | 282 | statusStore.exampleMethod('arg1'); 283 | // Should output: 'arg1' 284 | ``` 285 | 286 | #### Mixins in stores 287 | 288 | Just as you can add mixins to React components, so it is possible to add your mixins to Store. 289 | 290 | ```javascript 291 | var MyMixin = { foo: function() { console.log('bar!'); } } 292 | var Store = Reflux.createStore({ 293 | mixins: [MyMixin] 294 | }); 295 | Store.foo(); // outputs "bar!" to console 296 | ``` 297 | 298 | Methods from mixins are available as well as the methods declared in the Store. So it's possible to access store's `this` from mixin, or methods of mixin from methods of store: 299 | 300 | ```javascript 301 | var MyMixin = { mixinMethod: function() { console.log(this.foo); } } 302 | var Store = Reflux.createStore({ 303 | mixins: [MyMixin], 304 | foo: 'bar!', 305 | storeMethod: function() { 306 | this.mixinMethod(); // outputs "bar!" to console 307 | } 308 | }); 309 | ``` 310 | 311 | A nice feature of mixins is that if a store is using multiple mixins and several mixins define the same lifecycle method (e.g. `init`, `preEmit`, `shouldEmit`), all of the lifecycle methods are guaranteed to be called. 312 | 313 | #### Listening to many actions at once 314 | 315 | Since it is a very common pattern to listen to all actions from a `createActions` call in a store `init` call, the store has a `listenToMany` function that takes an object of listenables. Instead of doing this: 316 | 317 | ```javascript 318 | var actions = Reflux.createActions(["fireBall","magicMissile"]); 319 | 320 | var Store = Reflux.createStore({ 321 | init: function() { 322 | this.listenTo(actions.fireBall,this.onFireBall); 323 | this.listenTo(actions.magicMissile,this.onMagicMissile); 324 | }, 325 | onFireBall: function(){ 326 | // whoooosh! 327 | }, 328 | onMagicMissile: function(){ 329 | // bzzzzapp! 330 | } 331 | }); 332 | ``` 333 | 334 | ...you can do this: 335 | 336 | ```javascript 337 | var actions = Reflux.createActions(["fireBall","magicMissile"]); 338 | 339 | var Store = Reflux.createStore({ 340 | init: function() { 341 | this.listenToMany(actions); 342 | }, 343 | onFireBall: function(){ 344 | // whoooosh! 345 | }, 346 | onMagicMissile: function(){ 347 | // bzzzzapp! 348 | } 349 | }); 350 | ``` 351 | 352 | This will add listeners to all actions `actionName` who have a corresponding `onActionName` (or `actionName` if you prefer) method in the store. Thus if the `actions` object should also have included an `iceShard` spell, that would simply be ignored. 353 | 354 | #### The listenables shorthand 355 | 356 | To make things more convenient still, if you give an object of actions to the `listenables` property of the store definition, that will be automatically passed to `listenToMany`. So the above example can be simplified even further: 357 | 358 | ```javascript 359 | var actions = Reflux.createActions(["fireBall","magicMissile"]); 360 | 361 | var Store = Reflux.createStore({ 362 | listenables: actions, 363 | onFireBall: function(){ 364 | // whoooosh! 365 | }, 366 | onMagicMissile: function(){ 367 | // bzzzzapp! 368 | } 369 | }); 370 | ``` 371 | 372 | The `listenables` property can also be an array of such objects, in which case all of them will be sent to `listenToMany`. This allows you to do convenient things like this: 373 | 374 | ```javascript 375 | var Store = Reflux.createStore({ 376 | listenables: [require('./darkspells'),require('./lightspells'),{healthChange:require('./healthstore')}], 377 | // rest redacted 378 | }); 379 | ``` 380 | 381 | #### Listenables and asynchronous actions 382 | 383 | If `options.children` is set, as in the example below, you can use `onActionSubaction` to add a listener to the child action. For example: 384 | 385 | ```javascript 386 | var Actions = Reflux.createActions({ 387 | "load": {children: ["completed", "failed"]} 388 | }); 389 | 390 | function handleLoad(Action, Subaction){ 391 | console.log("The on" + Action + Subaction + " handler was called"); 392 | }; 393 | 394 | var Store = Reflux.createStore({ 395 | listenables: Actions, 396 | onLoad: function() { 397 | handleLoad("Load"); 398 | }, 399 | onLoadCompleted: function() { 400 | handleLoad("Load", "Completed"); 401 | }, 402 | onLoadFailed: function() { 403 | handleLoad("Load", "Failed"); 404 | } 405 | }); 406 | ``` 407 | 408 | 409 | ### Listening to changes in data store 410 | 411 | In your component, register to listen to changes in your data store like this: 412 | 413 | ```javascript 414 | // Fairly simple view component that outputs to console 415 | function ConsoleComponent() { 416 | 417 | // Registers a console logging callback to the statusStore updates 418 | statusStore.listen(function(status) { 419 | console.log('status: ', status); 420 | }); 421 | }; 422 | 423 | var consoleComponent = new ConsoleComponent(); 424 | ``` 425 | 426 | Invoke actions as if they were functions: 427 | 428 | ```javascript 429 | statusUpdate(true); 430 | statusUpdate(false); 431 | ``` 432 | 433 | With the setup above this will output the following in the console: 434 | 435 | ``` 436 | status: ONLINE 437 | status: OFFLINE 438 | ``` 439 | 440 | [Back to top](#content) 441 | 442 | ### React component example 443 | 444 | Register your component to listen for changes in your data stores, preferably in the `componentDidMount` [lifecycle method](http://facebook.github.io/react/docs/component-specs.html) and unregister in the `componentWillUnmount`, like this: 445 | 446 | ```javascript 447 | var Status = React.createClass({ 448 | getInitialState: function() { }, 449 | onStatusChange: function(status) { 450 | this.setState({ 451 | currentStatus: status 452 | }); 453 | }, 454 | componentDidMount: function() { 455 | this.unsubscribe = statusStore.listen(this.onStatusChange); 456 | }, 457 | componentWillUnmount: function() { 458 | this.unsubscribe(); 459 | }, 460 | render: function() { 461 | // render specifics 462 | } 463 | }); 464 | ``` 465 | 466 | It's also important to note that Reflux now supports [React ES6 style usage](#react-es6-usage) as well. 467 | 468 | #### Convenience mixin for React 469 | 470 | You always need to unsubscribe components from observed actions and stores upon 471 | unmounting. To simplify this process you can use [mixins in React](http://facebook.github.io/react/docs/reusable-components.html#mixins). There is a convenience mixin available at `Reflux.ListenerMixin`. Using that, the above example can be written like thus: 472 | 473 | ```javascript 474 | var Status = React.createClass({ 475 | mixins: [Reflux.ListenerMixin], 476 | onStatusChange: function(status) { 477 | this.setState({ 478 | currentStatus: status 479 | }); 480 | }, 481 | componentDidMount: function() { 482 | this.listenTo(statusStore, this.onStatusChange); 483 | }, 484 | render: function() { 485 | // render specifics 486 | } 487 | }); 488 | ``` 489 | 490 | The mixin provides the `listenTo` method for the React component, that works much like the one found in the Reflux's stores, and handles the listeners during mount and unmount for you. You also get the same `listenToMany` method as the store has. 491 | 492 | 493 | #### Using Reflux.listenTo 494 | 495 | If you're not reliant on any special logic for the `this.listenTo` calls inside `componentDidMount`, you can instead use a call to `Reflux.listenTo` as a mixin. That will automatically set up the `componentDidMount` and the rest for you, as well as add the `ListenerMixin` functionality. With this our example above can be reduced even further: 496 | 497 | ```javascript 498 | var Status = React.createClass({ 499 | mixins: [Reflux.listenTo(statusStore,"onStatusChange")], 500 | onStatusChange: function(status) { 501 | this.setState({ 502 | currentStatus: status 503 | }); 504 | }, 505 | render: function() { 506 | // render using `this.state.currentStatus` 507 | } 508 | }); 509 | ``` 510 | 511 | You can have multiple calls to `Reflux.listenTo` in the same `mixins` array. 512 | 513 | There is also `Reflux.listenToMany` which works in exactly the same way, exposing `listener.listenToMany`. 514 | 515 | #### Using Reflux.connect 516 | 517 | If all you want to do is update the state of your component to whatever the data store transmits, you can use `Reflux.connect(listener,stateKey)` as a mixin. The state is updated via `this.setState({:data})`. Here's the example above changed to use this syntax: 518 | 519 | ```javascript 520 | var Status = React.createClass({ 521 | mixins: [Reflux.connect(statusStore,"currentStatus")], 522 | render: function() { 523 | // render using `this.state.currentStatus` 524 | } 525 | }); 526 | ``` 527 | 528 | The `Reflux.connect()` mixin will check the store for a `getInitialState` method. If found it will set the components `getInitialState` 529 | 530 | ```javascript 531 | var statusStore = Reflux.createStore({ 532 | getInitialState: function() { 533 | return "open"; 534 | } 535 | }); 536 | 537 | var Status = React.createClass({ 538 | mixins: [Reflux.connect(statusStore,"currentStatus")], 539 | render: function() { 540 | // render using `this.state.currentStatus` 541 | // this.state.currentStatus === "open" 542 | } 543 | }); 544 | ``` 545 | 546 | #### Using Reflux.connectFilter 547 | 548 | `Reflux.connectFilter` is used in a similar manner to `Reflux.connect`. Use the 549 | `connectFilter` mixin when you want only a subset of the items in a store. A 550 | blog written using Reflux would probably have a store with all posts in 551 | it. For an individual post page, you could use `Reflux.connectFilter` to 552 | filter the posts to the post that's being viewed. 553 | 554 | ```javascript 555 | var PostView = React.createClass({ 556 | mixins: [Reflux.connectFilter(postStore, "post", function(posts) { 557 | return posts.filter(function(post) { 558 | return post.id === this.props.id; 559 | }.bind(this))[0]; 560 | })], 561 | render: function() { 562 | // render using `this.state.post` 563 | } 564 | }); 565 | ``` 566 | 567 | ### Listening to changes in other data stores (aggregate data stores) 568 | 569 | A store may listen to another store's change, making it possible to safely chain stores for aggregated data without affecting other parts of the application. A store may listen to other stores using the same `listenTo` function as with actions: 570 | 571 | ```javascript 572 | // Creates a DataStore that listens to statusStore 573 | var statusHistoryStore = Reflux.createStore({ 574 | init: function() { 575 | 576 | // Register statusStore's changes 577 | this.listenTo(statusStore, this.output); 578 | 579 | this.history = []; 580 | }, 581 | 582 | // Callback 583 | output: function(statusString) { 584 | this.history.push({ 585 | date: new Date(), 586 | status: statusString 587 | }); 588 | // Pass the data on to listeners 589 | this.trigger(this.history); 590 | } 591 | 592 | }); 593 | ``` 594 | 595 | [Back to top](#content) 596 | 597 | ## Advanced usage 598 | 599 | ### Switching EventEmitter 600 | 601 | Don't like to use the EventEmitter provided? You can switch to another one, such as NodeJS's own like this: 602 | 603 | ```javascript 604 | // Do this before creating actions or stores 605 | 606 | Reflux.setEventEmitter(require('events').EventEmitter); 607 | ``` 608 | 609 | ### Switching nextTick 610 | 611 | Whenever action functors are called, they return immediately through the use of `setTimeout` (`nextTick` function) internally. 612 | 613 | You may switch out for your favorite `setTimeout`, `nextTick`, `setImmediate`, et al implementation: 614 | 615 | ```javascript 616 | 617 | // node.js env 618 | Reflux.nextTick(process.nextTick); 619 | ``` 620 | 621 | For better alternative to `setTimeout`, you may opt to use the [`setImmediate` polyfill](https://github.com/YuzuJS/setImmediate), [`setImmediate2`](https://github.com/Katochimoto/setImmediate) or [`macrotask`](https://github.com/calvinmetcalf/macrotask). 622 | 623 | 624 | ### Joining parallel listeners with composed listenables 625 | 626 | The Reflux API contains `join` methods that makes it easy to aggregate publishers that emit events in parallel. This corresponds to the `waitFor` method in Flux. 627 | 628 | #### Argument tracking 629 | 630 | A join is triggered once all participating publishers have emitted at least once. The callback will be called with the data from the various emissions, in the same order as the publishers were listed when the join was created. 631 | 632 | There are four join methods, each representing a different strategy to track the emission data: 633 | 634 | * `joinLeading`: Only the first emission from each publisher is saved. Subsequent emissions by the same publisher before all others are finished are ignored. 635 | * `joinTrailing`: If a publisher triggers twice, the second emission overwrites the first. 636 | * `joinConcat`: An array of emission arguments are stored for each publisher. 637 | * `joinStrict`: An error is thrown if a publisher emits twice before the join is completed. 638 | 639 | The method signatures all look like this: 640 | 641 | ```javascript 642 | joinXyz(...publisher,callback) 643 | ``` 644 | 645 | Once a join is triggered it will reset, and thus it can trigger again when all publishers have emitted anew. 646 | 647 | #### Using the listener instance methods 648 | 649 | All objects using the listener API (stores, React components using `ListenerMixin`, or other components using the `ListenerMethods`) gain access to the four join instance methods, named after the argument strategy. Here's an example saving the last emission from each publisher: 650 | 651 | ```javascript 652 | var gainHeroBadgeStore = Reflux.createStore({ 653 | init: function() { 654 | this.joinTrailing(actions.disarmBomb, actions.saveHostage, actions.recoverData, this.triggerAsync); 655 | } 656 | }); 657 | 658 | actions.disarmBomb("warehouse"); 659 | actions.recoverData("seedyletter"); 660 | actions.disarmBomb("docks"); 661 | actions.saveHostage("offices",3); 662 | // `gainHeroBadgeStore` will now asynchronously trigger `[["docks"],["offices",3],["seedyletter"]]`. 663 | ``` 664 | 665 | #### Using the static methods 666 | 667 | Since it is rather common to have a store where the only purpose is to listen to a join and trigger when the join is completed, the join methods have static counterparts on the `Reflux` object which return stores listening to the requested join. Using them, the store in the example above could instead be created like this: 668 | 669 | ```javascript 670 | var gainHeroBadgeStore = Reflux.joinTrailing(actions.disarmBomb, actions.saveHostage, actions.recoverData); 671 | ``` 672 | 673 | ### Sending initial state with the listenTo function 674 | 675 | The `listenTo` function provided by the `Store` and the `ListenerMixin` has a third parameter that accepts a callback. This callback will be invoked when the listener is registered with whatever the `getInitialState` is returning. 676 | 677 | ```javascript 678 | var exampleStore = Reflux.createStore({ 679 | init: function() {}, 680 | getInitialState: function() { 681 | return "the initial data"; 682 | } 683 | }); 684 | 685 | // Anything that will listen to the example store 686 | this.listenTo(exampleStore, onChangeCallback, initialCallback) 687 | 688 | // initialCallback will be invoked immediately with "the initial data" as first argument 689 | ``` 690 | 691 | Remember the `listenToMany` method? In case you use that with other stores, it supports `getInitialState`. That data is sent to the normal listening callback, or a `this.onDefault` method if that exists. 692 | 693 | [Back to top](#content) 694 | 695 | ## React ES6 Usage 696 | 697 | ### React ES6 component example 698 | 699 | Reflux exposes `Reflux.Component` for class extension for easy creation of ES6 style React components that automatically has the state of one or more Reflux stores mixed into the React component state. In order to accomplish this you simply need use Reflux stores that start with a `state` property with an object holding the default state of the store's data (i.e. set `this.state = {my:"defaults"}` in the store's `init`) , then you need to set `this.store` (to 1 store) or `this.stores` (to an Array of stores) from within the constructor of the component. An example would look like this: 700 | 701 | ```javascript 702 | class MyComponent extends Reflux.Component // <- Reflux.Component instead of React.Component 703 | { 704 | constructor(props) { 705 | super(props); 706 | this.state = {foo:'bar'}; // <- stays usable, so normal state usage can still happen 707 | this.store = myStore; // <- the only thing needed to tie the store into this component 708 | } 709 | 710 | render() { 711 | // `storeProp` is mixed in from the store, and reflects in the component state 712 | return

From Store: {this.state.storeProp}, Foo: {this.state.foo}

; 713 | } 714 | } 715 | ``` 716 | 717 | The default states of the stores will be mixed in from the start, and any time the store does a `trigger` the triggered data will be mixed in to the component and it will re-render. 718 | 719 | A fully working example may look something like this: 720 | 721 | ```javascript 722 | var Actions = Reflux.createActions(["increment"]); 723 | 724 | var counterStore = Reflux.createStore( 725 | { 726 | listenables: Actions, 727 | 728 | init: function() { 729 | this.state = {count:0}; 730 | }, 731 | 732 | onIncrement: function(txt) { 733 | this.state.count++; 734 | this.trigger(this.state); 735 | } 736 | }); 737 | 738 | class Counter extends Reflux.Component 739 | { 740 | constructor(props) { 741 | super(props); 742 | this.state = {}; 743 | this.store = counterStore; 744 | } 745 | 746 | render() { 747 | return

Count: {this.state.count}

; 748 | } 749 | } 750 | 751 | 752 | ReactDOM.render( 753 | , 754 | document.getElementById('container') 755 | ); 756 | 757 | setInterval(Actions.increment, 1000); 758 | ``` 759 | 760 | NOTE: If you are also using `Reflux.Store` ES6 stores and updating them properly via their `setState` method then you may also choose to only mix in certain properties from the store(s) attached to a component, instead of all of them. To do this you may define `this.storeKeys` in the component's constructor and set it to an array of key names (strings) for properties you want mixed in from the attached stores. The component will then only mix in state object properties of those key names for any stores attached to it. If the store state is changed and none of the changed state involves the keys in `this.storeKeys` then the component will not change state at all nor re-render. 761 | 762 | ```javascript 763 | //... 764 | constructor(props) { 765 | super(props); 766 | this.state = {}; 767 | this.store = MyStore; 768 | this.storeKeys = ['color', 'height']; 769 | // ^ will only include the color and height parts of MyStore's state 770 | } 771 | //... 772 | ``` 773 | 774 | ### Using ES6 Reflux Stores via Reflux.Store 775 | 776 | Stores do not directly integrate within React like `Reflux.Component` needs to, so using a more idiomatic way to declare them is not necessary. However, it can be very useful when using the `Reflux.Component` style components. Therefore whenever `Reflux.Component` is exposed Reflux also exposes `Reflux.Store` which can be extended to make a class that wraps and acts as a reflux store but with an approach that is easier to implement into `Reflux.Component` classes. 777 | 778 | To create one looks something like this: 779 | 780 | ```javascript 781 | class MyStore extends Reflux.Store 782 | { 783 | constructor() { 784 | super(); 785 | this.state = {foo:'bar'}; // <-- the store's default state 786 | } 787 | } 788 | ``` 789 | 790 | These act much like a normal store. You can use `this.listenTo`, `this.listenToMany`, etc. from within the constructor, and you can define things like a `this.listenables` property and it will automatically call `action` and `onAction` named methods on the class. It also exposes a `setState` method that you can use to modify your `state` property and automatically `trigger` the change: 791 | 792 | ```javascript 793 | var Actions = Reflux.createActions(["increment"]); 794 | 795 | class CounterStore extends Reflux.Store 796 | { 797 | constructor() { 798 | super(); 799 | this.listenables = Actions; 800 | this.state = {count:0}; 801 | } 802 | 803 | onIncrement() { 804 | var cnt = this.state.count; 805 | this.setState({count:cnt+1}); 806 | } 807 | } 808 | ``` 809 | 810 | `this.listenables` also accepts an array of actions in the event you want your store to listen to actions from other places as well: 811 | 812 | ```javascript 813 | var Actions1 = Reflux.createActions(["increment"]); 814 | var Actions2 = Reflux.createActions(["decrement"]); 815 | 816 | class CounterStore extends Reflux.Store 817 | { 818 | constructor() { 819 | super(); 820 | this.listenables = [Actions1, Actions2]; 821 | this.state = {count:0}; 822 | } 823 | 824 | onIncrement() { 825 | var cnt = this.state.count; 826 | this.setState({count:cnt+1}); 827 | } 828 | 829 | onDecrement() { 830 | var cnt = this.state.count; 831 | this.setState({count:cnt-1}); 832 | } 833 | } 834 | ``` 835 | 836 | One thing you may notice is that the original style `Reflux.createStore` creates an actual instance (as opposed to a class) which is what is assigned to `this.store` in the `Reflux.Component`. Extending `Reflux.Store` means you just have a class, not an instance of anything. Of course you can instantiate and use that store; however, if you just assign the class itself to `this.store` or `this.stores` in the `Reflux.Component` then it will automatically create a singleton instance of the store class (or use a previously created singleton instance of it if another component has already done so in its own construction). So, for example, to utilize the `Reflux.Store` store in the last example within a `Reflux.Component` class would look like this: 837 | 838 | ```javascript 839 | class Counter extends Reflux.Component 840 | { 841 | constructor(props) { 842 | super(props); 843 | this.state = {}; 844 | this.store = CounterStore; // <- just assign the class itself 845 | } 846 | 847 | render() { 848 | return

Count: {this.state.count}

; 849 | } 850 | } 851 | ``` 852 | 853 | **Note!** `Reflux.Store` still works with instances of stores (i.e. the class must get intantiated). Assigning the class itself to `this.store` just allows Reflux to handle the instantiation and do some internal things that allow features like global state tracking. it does *not* mean that the class itself is the store. Internally Reflux creates and utilizes a singleton instance of the class. After mounting you may access that singleton instance of the class via `MyStoreClass.singleton`. 854 | 855 | #### Using Reflux.Store without a component 856 | 857 | With to ability to do so much via global states (covered in the next section), and the fact that that functionality is tied to `Reflux.Store`, being able to properly utilize `Reflux.Store` on its own (without binding to a React component) becomes useful. However, just using `new MyStoreClass()` isn't enough, as it has to tie itself into the Reflux global state as a singleton. Therefore Reflux exposes an API for getting a properly globalized singleton instance of a `Reflux.Store` extended class without having to tie it to a React component. You do this via the following: 858 | 859 | ```javascript 860 | var mySingleton = Reflux.initializeGlobalStore(MyClassName); 861 | ``` 862 | 863 | When done this way the singleton instance of your `Reflux.Store` class can, externally, be used much like a non-ES6 store created via `Reflux.createStore` except with the advantages that it: 1) is written in the `Reflux.Store` ES6 syntax and 2) it ties in with the global state being tracked by Reflux. 864 | 865 | Note: your store _must_ be set up with an `id` to be used this way. 866 | 867 | Note: even after instantiating with `Reflux.initializeGlobalStore` you can still later assign the class name itself to `this.store` or `this.stores` in a `Reflux.Component`. The component will recognize that a singleton for the class has already been created and use that singleton. 868 | 869 | ##### A deeper understanding: 870 | 871 | To avoid confusion I want to better explain what `Reflux.initializeGlobalStore` is for on a deeper level. This is also a good section to read for people that just want a better understanding of working with `Reflux.Store` in general. 872 | 873 | When you define a store in ES6 `Reflux.Store` syntax you are creating a class, not an instance of a class. For Reflux to use that store internally an instance of the class must be created. But it's also important that only **one** instance of that store class get created (i.e. a singleton) so that each component using it is using the same store instance. That is why you're told to assign the class itself to `this.store` or `this.stores` in a `Reflux.Component`. Assigning an instance works...but if you assign the class itself then it can use its own internal logic to say *"Is a singleton already made for this class? If yes, use that singleton. If not, then make it first, then use it."* therefore automatically making sure only one singleton instance is used everywhere. On top of that (if the class has an id) it also makes sure that the global state is aware of that store so that it can track it. 874 | 875 | This works great as long as you're going to be using that store in a component. But what if you don't want to use it in any components? 876 | 877 | If you don't care about the global state knowing about it and tracking it then it's easy. Since no components are using it then there's no chance of multiple store instances accidentally being created, so just use `var store = new MyStoreClass();` like any other class you'd be instantiating. If you wanted to be thorough and make your instance in the same singleton fashion as is done internally (just in case some later component decides to use that class without your knowledge) then you could go `var store = new MyStoreClass(); MyStoreClass.singleton = store;` and then any future component usage would know about, and use, that singleton instance. 878 | 879 | But that still leaves a scenario out in the cold: needing to intantiate a store without it being attached to any components to handle it for you, but *also* wanting that store to be properly tracked by the global state. That is where `Reflux.initializeGlobalStore` can be used to create your singleton instance and handle what needs to be handled internally to track the global state. 880 | 881 | Another possible scenario would be if you need to access the singleton instance of the store *before* the `componentWillMount` part of any component's lifecycle (which is where the component would set up the singleton). You can use `Reflux.initializeGlobalStore` to create a singleton that you can access sooner. You can *still* assign the class itself to `this.store` or `this.stores` in any components though! The component will know that a singleton has already been created and automatically use it, there is no need for you to manually track the singleton for the components just because you used `Reflux.initializeGlobalStore` in order to get access to that singleton sooner! 882 | 883 | #### Mapping Stores to Components with `mapStoreToState` 884 | 885 | Reflux's ES6 stores and components work together fairly well in a very declarative syntax by simply assigning stores to components via the component's `this.store` and `this.stores` properties, and that functionality rounds itself out by adding some filtering ability with `this.storeKeys`. With enough thought given to architecture this can get you almost everything you need in a declarative syntax that is easy for you to write and others to read. Therefore it's highly suggested that you put the time into planning your architecture to be able to use those for connecting stores and components. 886 | 887 | However, there still exists a need to have deeper control and to be able to map stores to component states with your own custom logic in some cases. For that each `Reflux.Component` will have a `this.mapStoreToState` method. This is **completely separate** from the previously mentioned declarative side of things (such as `this.store`). That means you should not have the same store attached to a component both via `this.store` *and* using `this.mapStoreToState`, and also that this method is completely unaffected by `this.storeKeys`. These differing methods can both be used within a single component, they just shouldn't be both used for the *same store* within the same component. 888 | 889 | This method takes 2 arguments: the `Reflux.Store` you want mapped to the component state (either the class itself or the singleton instance) and a mapping function supplied by you. The mapping function will be called any time the store instance's `setState` is used to change the state of the store. The mapping function takes an argument which will be the state change object from the store for that particular change. It needs to return an object which will then be mapped to the component state (similar to if that very returned object were used in the component's `setState`). If an object with no properties is returned then the component will *not* re-render. The mapping function is also called with its `this` keyword representing the component, so comparing store values to current component state values via `this.state` is possible as well. 890 | 891 | ```javascript 892 | class Counter extends Reflux.Component 893 | { 894 | constructor(props) { 895 | super(props); 896 | this.mapStoreToState(MyStoreClass, function(fromStore){ 897 | var obj = {}; 898 | if (fromStore.color) 899 | obj.color = fromStore.color; 900 | if (fromStore.data && fromStore.data.classToUse) 901 | obj.class = fromStore.data.classToUse; 902 | return obj; 903 | }); 904 | } 905 | 906 | render() { 907 | return

The color is: {this.state.color}

; 908 | } 909 | } 910 | ``` 911 | 912 | In the above example `MyStoreClass` could have lots of state properties, but we use a bit of logic to 1) only trigger a re-render if the store's `state.color` or `state.data.classToUse` were among the parts of the state involved in the store's `setState` call (because, remember, if the returned object has no properties no re-render happens), and 2) to map the stores `state.color` straight to the component's `state.color`, but the store's `state.data.classToUse` to the component's `state.class`. 913 | 914 | Note that the example function above is merely that: an example. Whatever sort of logic you want to apply to get from the change object given by the store to how you want that to change the state of your component is fair game, except that you should not mutate the incoming data itself. 915 | 916 | ### Utilizing Reflux.GlobalState 917 | 918 | Another neat feature that the ES6 implementation of Reflux has is the ability to track a global state of all stores in use, as well as initialize all stores in use to a predefined global state. It happens internally too, so you don't have to do hardly anything to make it happen. This would be useful for many things, including tracking the state of an application and going back to that same state the next time the app is used. 919 | 920 | To make it happen you just have to use ES6 style reflux classes and stores like explained in the last couple sections and define a static `id` property in your `Reflux.Store` definition. That id will then be used as a property name within the `Reflux.GlobalState` object for the property holding that store's current state. Then you just need to make sure to use `setState` to modify the state of the `Reflux.Store` instead of mutating the state directly. After that the `Reflux.GlobalState` object will reflect a collection of all your stores at all times once the components using those stores are mounted. An example using the example above: 921 | 922 | ```javascript 923 | class CounterStore extends Reflux.Store 924 | { 925 | constructor() { 926 | super(); 927 | this.listenables = Actions; 928 | this.state = {count:0}; 929 | } 930 | 931 | onIncrement() { 932 | var cnt = this.state.count; 933 | this.setState({count:cnt+1}); 934 | } 935 | 936 | static get id() { 937 | return 'counterstore'; 938 | } 939 | } 940 | 941 | // ... make component and render as normal ... 942 | 943 | console.log(Reflux.GlobalState); // <- would be: {'counterstore':{'count':0}} 944 | ``` 945 | 946 | Notice that you can only read the GlobalState **after** the components using the stores have been mounted. Up until then is the time where you can manually set the `Reflux.GlobalState` in order to initialize the entire app in a state of your choosing (or a previous state you recorded earlier). For example we could do this: 947 | 948 | ```javascript 949 | class CounterStore extends Reflux.Store 950 | { 951 | constructor() { 952 | super(); 953 | this.listenables = Actions; 954 | this.state = {count:0}; 955 | } 956 | 957 | onIncrement() { 958 | var cnt = this.state.count; 959 | this.setState({count:cnt+1}); 960 | } 961 | 962 | static get id() { 963 | return 'counterstore'; 964 | } 965 | } 966 | 967 | Reflux.GlobalState = {'counterstore':{'count':50}}; 968 | 969 | // ... make component and render as normal ... 970 | 971 | // at this point it would render with a count of 50! 972 | ``` 973 | 974 | One of the most useful ways you could do this is to store a `Reflux.GlobalState` state as a JSON string in order to implement it again the next time the app starts up and have the user begin right where they left off. 975 | 976 | #### Reflux.setGlobalState and Reflux.getGlobalState 977 | 978 | Directly accessing `Reflux.GlobalState` is a fine way to do set the starting state of an app and to do automated testing, but it is also helpful to be able to manipulate the global state while the app is running as well. To do this Reflux exposes a `Reflux.getGlobalState()` function and a `Reflux.setGlobalState()` function. The former allows you to get a deep copy of the current global state (so that the copy will not mutate as the global state itself continues to mutate) and the latter allows you to set part or all of the global state at any time in the program. Between these two functions things like state time-travel, undo/redo, and move-by-move tracking become relatively easy. 979 | 980 | ### Making sure Reflux.Component is available 981 | 982 | `Reflux.Component` extends `React.Component`. Therefore Reflux needs to be able to access React in order to expose it. In the browser as long as React is loaded before Reflux then Reflux will automatically find it. Likewise in node-like environments where `require('react')` will function Reflux will try to access React that way. So in almost all situations Reflux will find React on its own. 983 | 984 | However, Reflux also exposes the method `Reflux.defineReact` that you can use to manually give Reflux a reference to the React object in case you need to: 985 | 986 | ```javascript 987 | // only needed if, for some reason, Reflux can't get reference to React: 988 | var React = /* however you access React */; 989 | Reflux.defineReact(React); 990 | // now Reflux.Component is accessible! 991 | ``` 992 | 993 | ### Extending a 3rd Party Class 994 | 995 | Sometimes 3rd party libraries will have their own class that extends `React.Component` that they require you to use. Reflux handles this by exposing the `Reflux.Component.extend` method. If you have such a 3rd party class you can pass that class to this method and it will return a version of `Reflux.Component` that extends it instead of extending `React.Component` directly. Example: 996 | 997 | ```javascript 998 | import {ThirdPartyComponent} from 'third-party'; 999 | 1000 | var RefluxThirdPartyComponent = Reflux.Component.extend(ThirdPartyComponent); 1001 | 1002 | class MyComponent extends RefluxThirdPartyComponent 1003 | { 1004 | // ... 1005 | } 1006 | ``` 1007 | 1008 | [Back to top](#content) 1009 | 1010 | ## Colophon 1011 | 1012 | [List of contributors](https://github.com/spoike/reflux/graphs/contributors) is available on Github. 1013 | 1014 | This project is licensed under [BSD 3-Clause License](http://opensource.org/licenses/BSD-3-Clause). Copyright (c) 2014, Mikael Brassman. 1015 | 1016 | For more information about the license for this particular project [read the LICENSE.md file](LICENSE.md). 1017 | 1018 | This project uses [eventemitter3](https://github.com/3rd-Eden/EventEmitter3), which [is currently MIT licensed](https://github.com/3rd-Eden/EventEmitter3/blob/master/LICENSE). 1019 | 1020 | [npm-image]: http://img.shields.io/npm/v/reflux.svg 1021 | [downloads-image]: http://img.shields.io/npm/dm/reflux.svg 1022 | [dependencies-image]: http://img.shields.io/david/reflux/refluxjs.svg 1023 | [npm-url]: https://www.npmjs.org/package/reflux 1024 | [bower-image]: http://img.shields.io/bower/v/reflux.svg 1025 | [bower-url]: http://bower.io/search/?q=reflux 1026 | [travis-image]: http://img.shields.io/travis/reflux/refluxjs/master.svg 1027 | [travis-url]: https://travis-ci.org/reflux/refluxjs 1028 | [gratipay-image]: http://img.shields.io/gratipay/spoike.svg 1029 | [gratipay-url]: https://gratipay.com/spoike/ 1030 | [thinkful-image]: https://tf-assets-staging.s3.amazonaws.com/badges/thinkful_repo_badge.svg 1031 | [thinkful-url]: http://start.thinkful.com/react/?utm_source=github&utm_medium=badge&utm_campaign=reflux 1032 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reflux", 3 | "version": "6.4.1", 4 | "description": "A simple library for uni-directional dataflow application architecture inspired by ReactJS Flux", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "grunt test", 8 | "test:sauce": "grunt karma:sauce", 9 | "benchmark": "node test/benchmarks", 10 | "build": "grunt build" 11 | }, 12 | "author": "Mikael Brassman", 13 | "license": "BSD-3-Clause", 14 | "repository": { 15 | "type": "git", 16 | "url": "http://github.com/reflux/refluxjs.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/reflux/refluxjs/issues" 20 | }, 21 | "keywords": [ 22 | "reflux", 23 | "react", 24 | "flux", 25 | "architecture", 26 | "dataflow", 27 | "action", 28 | "event", 29 | "data" 30 | ], 31 | "dependencies": { 32 | "eventemitter3": "^1.1.1", 33 | "reflux-core": "^1.0.0" 34 | }, 35 | "devDependencies": { 36 | "benchmark": "^1.0.0", 37 | "browserify": "~10.2.3", 38 | "chai": "latest", 39 | "chai-as-promised": "latest", 40 | "es6-promise": "^2.3.0", 41 | "grunt": "^0.4.5", 42 | "grunt-browserify": "3.8.0", 43 | "grunt-cli": "^0.1.13", 44 | "grunt-contrib-jshint": "^0.11.2", 45 | "grunt-contrib-uglify": "^0.5.0", 46 | "grunt-contrib-watch": "^0.6.1", 47 | "grunt-karma": "latest", 48 | "grunt-mocha-test": "~0.12.7", 49 | "karma": "latest", 50 | "karma-browserify": "latest", 51 | "karma-commonjs": "latest", 52 | "karma-mocha": "latest", 53 | "karma-phantomjs-launcher": "latest", 54 | "karma-sauce-launcher": "latest", 55 | "karma-spec-reporter": "latest", 56 | "matchdep": "^0.3.0", 57 | "mocha": "latest", 58 | "q": "^1.0.1", 59 | "react-dom": "^15.0.2", 60 | "sinon": "^1.10.3" 61 | }, 62 | "peerDependencies": { 63 | "react": "^15.0.2" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ListenerMixin.js: -------------------------------------------------------------------------------- 1 | var _ = require('reflux-core/lib/utils'), 2 | ListenerMethods = require('reflux-core/lib/ListenerMethods'); 3 | 4 | /** 5 | * A module meant to be consumed as a mixin by a React component. Supplies the methods from 6 | * `ListenerMethods` mixin and takes care of teardown of subscriptions. 7 | * Note that if you're using the `connect` mixin you don't need this mixin, as connect will 8 | * import everything this mixin contains! 9 | */ 10 | module.exports = _.extend({ 11 | 12 | /** 13 | * Cleans up all listener previously registered. 14 | */ 15 | componentWillUnmount: ListenerMethods.stopListeningToAll 16 | 17 | }, ListenerMethods); 18 | -------------------------------------------------------------------------------- /src/addES6.js: -------------------------------------------------------------------------------- 1 | 2 | /* globals React: false */ 3 | 4 | var Reflux = require('reflux-core'); 5 | Reflux.defineReact = require('./defineReact'); 6 | 7 | // useful utility for ES6 work, mimics the ability to extend 8 | Reflux.utils.inherits = function(subClass, superClass) { 9 | if (typeof superClass !== "function" && superClass !== null) { 10 | throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); 11 | } 12 | subClass.prototype = Object.create(superClass && superClass.prototype, { 13 | constructor: { 14 | value: subClass, 15 | enumerable: false, 16 | writable: true, 17 | configurable: true 18 | } 19 | }); 20 | if (superClass) { 21 | if (Object.setPrototypeOf) { 22 | Object.setPrototypeOf(subClass, superClass); 23 | } else { 24 | /* jshint proto: true */ 25 | subClass.__proto__ = superClass; 26 | } 27 | } 28 | }; 29 | 30 | // first try to see if there's a global React var and use it 31 | if (typeof React !== 'undefined' && React) { 32 | Reflux.defineReact(React); 33 | // otherwise we're gonna resort to 'try' stuff in case of other environments 34 | } else { 35 | try { 36 | var R = require("react"); // we ignore this in browserify manually (see grunt file), so it's more of a doublecheck for in node 37 | Reflux.defineReact(R); 38 | } catch (e) {} 39 | } 40 | -------------------------------------------------------------------------------- /src/connect.js: -------------------------------------------------------------------------------- 1 | var ListenerMethods = require('reflux-core/lib/ListenerMethods'), 2 | ListenerMixin = require('./ListenerMixin'), 3 | _ = require('reflux-core/lib/utils'); 4 | 5 | module.exports = function(listenable, key) { 6 | 7 | _.throwIf(typeof(key) === 'undefined', 'Reflux.connect() requires a key.'); 8 | 9 | return { 10 | getInitialState: function() { 11 | if (!_.isFunction(listenable.getInitialState)) { 12 | return {}; 13 | } 14 | 15 | return _.object([key],[listenable.getInitialState()]); 16 | }, 17 | componentDidMount: function() { 18 | var me = this; 19 | 20 | _.extend(me, ListenerMethods); 21 | 22 | this.listenTo(listenable, function(v) { 23 | me.setState(_.object([key],[v])); 24 | }); 25 | }, 26 | componentWillUnmount: ListenerMixin.componentWillUnmount 27 | }; 28 | }; 29 | -------------------------------------------------------------------------------- /src/connectFilter.js: -------------------------------------------------------------------------------- 1 | var ListenerMethods = require('reflux-core/lib/ListenerMethods'), 2 | ListenerMixin = require('./ListenerMixin'), 3 | _ = require('reflux-core/lib/utils'); 4 | 5 | module.exports = function(listenable, key, filterFunc) { 6 | 7 | _.throwIf(_.isFunction(key), 'Reflux.connectFilter() requires a key.'); 8 | 9 | return { 10 | getInitialState: function() { 11 | if (!_.isFunction(listenable.getInitialState)) { 12 | return {}; 13 | } 14 | 15 | // Filter initial payload from store. 16 | var result = filterFunc.call(this, listenable.getInitialState()); 17 | if (typeof(result) !== 'undefined') { 18 | return _.object([key], [result]); 19 | } else { 20 | return {}; 21 | } 22 | }, 23 | componentDidMount: function() { 24 | var me = this; 25 | 26 | _.extend(this, ListenerMethods); 27 | 28 | this.listenTo(listenable, function(value) { 29 | var result = filterFunc.call(me, value); 30 | me.setState(_.object([key], [result])); 31 | }); 32 | }, 33 | componentWillUnmount: ListenerMixin.componentWillUnmount 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/defineReact.js: -------------------------------------------------------------------------------- 1 | /* globals React: false */ 2 | 3 | var Reflux = require('reflux-core'); 4 | 5 | /** 6 | * Reflux.defineReact function where you can manually supply 7 | * the React object in order to create in case Reflux needs to load before 8 | * React or there is a modular environment where there won't be a global 9 | * React variable. 10 | * @note The third param is for internal usage only. 11 | */ 12 | var _react, _defined = false; 13 | function defineReact(react, noLongerUsed, extend) 14 | { 15 | var proto, _extend; 16 | 17 | // if no Reflux object is yet available then return and just wait until defineReact is called manually with it 18 | try { 19 | _react = react || _react || React; 20 | _extend = extend || _react.Component; 21 | } catch (e) { 22 | return; 23 | } 24 | 25 | // if Reflux and React aren't present then ignore, wait until they are properly present 26 | // also ignore if it's been called before UNLESS there's manual extending happening 27 | if (!_react || !_extend || (_defined && !extend)) { 28 | return; 29 | } 30 | 31 | // ----------- BEGIN Reflux.Component ------------ 32 | /** 33 | * Reflux.Component: 34 | * An implementation for idiomatic React.js classes that mix with 35 | * Reflux stores. To utilize extend Reflux.Component instead of 36 | * React.Component. Then you may hook any Reflux store that has a 37 | * `this.state` property containing its state values to the component 38 | * via `this.store` or an Array of Reflux stores via `this.stores` in 39 | * the component's constructor (similar to how you assign initial state 40 | * in the constructor in ES6 style React). The default values of the 41 | * stores will automatically reflect in the component's state, and any 42 | * further `trigger` calls from that store will update properties passed 43 | * in the trigger into the component automatically. 44 | */ 45 | var RefluxComponent = function(props, context, updater) { 46 | _extend.call(this, props, context, updater); 47 | }; 48 | 49 | // equivalent of `extends React.Component` or other class if provided via `extend` param 50 | Reflux.utils.inherits(RefluxComponent, _extend); 51 | 52 | proto = RefluxComponent.prototype; 53 | 54 | /** 55 | * this.storeKeys 56 | * When this is a falsey value (null by default) the component mixes in 57 | * all properties from the stores attached to it and updates on changes 58 | * from all of them. When set to an array of string keys it will only 59 | * utilized state property names of those keys in any store attached. This 60 | * lets you choose which parts of stores update the component on a component- 61 | * by-component basis. If using this it is best set in the constructor. 62 | */ 63 | proto.storeKeys = null; 64 | 65 | // on the mounting of the component that is where the store/stores are attached and initialized if needed 66 | proto.componentWillMount = function () { 67 | // if there is a this.store then simply push it onto the this.stores array or make one if needed 68 | if (this.store) { 69 | if (Array.isArray(this.stores)) { 70 | this.stores.unshift(this.store); 71 | } else { 72 | this.stores = [this.store]; 73 | } 74 | } 75 | 76 | if (this.stores) { 77 | this.__storeunsubscribes__ = this.__storeunsubscribes__ || []; 78 | var sS = this.setState.bind(this); 79 | // this handles the triggering of a store, checking what's updated if proto.storeKeys is utilized 80 | var onStoreTrigger = function(obj){ 81 | var updateObj = filterByStoreKeys(this.storeKeys, obj); 82 | if (updateObj) { 83 | sS(updateObj); 84 | } 85 | }.bind(this); 86 | // for each store in this.stores... 87 | for (var i = 0, ii = this.stores.length; i < ii; i++) { 88 | var str = this.stores[i]; 89 | // if's a function then we know it's a class getting passed, not an instance 90 | if (typeof str === 'function') { 91 | var storeId = str.id; 92 | // if there is NOT a .singleton property on the store then this store has not been initialized yet, so do so 93 | if (!str.singleton) { 94 | str.singleton = new str(); 95 | if (storeId) { 96 | Reflux.stores[storeId] = str.singleton; 97 | } 98 | } 99 | // before we weren't sure if we were working with an instance or class, so now we know an instance is created set it 100 | // to the variables we were using so that we can just continue on knowing it's the instance we're working with 101 | this.stores[i] = str = str.singleton; 102 | // the instance should have an .id property as well if the class does, so set that here 103 | str.id = storeId; 104 | // if there is an id and there is a global state property for this store then merge 105 | // the properties from that global state into the default state of the store AND then 106 | // set the global state to that new state (since it may have previously been partial) 107 | if (storeId && Reflux.GlobalState[storeId]) { 108 | for (var key in Reflux.GlobalState[storeId]) { 109 | str.state[key] = Reflux.GlobalState[storeId][key]; 110 | } 111 | Reflux.GlobalState[storeId] = str.state; 112 | // otherwise (if it has an id) set the global state to the default state of the store 113 | } else if (storeId) { 114 | Reflux.GlobalState[storeId] = str.state; 115 | } 116 | // if no id, then no messing with global state 117 | } 118 | // listen/subscribe for the ".trigger()" in the store, and track the unsubscribes so that we can unsubscribe on unmount 119 | if (!Reflux.serverMode) { 120 | this.__storeunsubscribes__.push(str.listen(onStoreTrigger)); 121 | } 122 | // run set state so that it mixes in the props from the store with the component 123 | var updateObj = filterByStoreKeys(this.storeKeys, str.state); 124 | if (updateObj) { 125 | this.setState(updateObj); 126 | } 127 | } 128 | } 129 | 130 | // mapStoreToState needs to know if is ready to map or must wait 131 | this.__readytomap__ = true; 132 | // if there are mappings that were delayed, do them now 133 | var dmaps = this.__delayedmaps__; 134 | if (dmaps) { 135 | for (var j=0,jj=dmaps.length; jHello World

' ); 52 | }); 53 | 54 | it('should accept React property values', function() 55 | { 56 | var MyComponent = (function (_super) { 57 | __extends(Component, _super); 58 | function Component(props) { 59 | _super.call(this, props); 60 | this.state = {}; 61 | this.store = Reflux.createStore({state:{a:'b'}}); 62 | } 63 | Component.prototype.render = function () { 64 | return React.createElement("p", null, this.props.foo); 65 | }; 66 | return Component; 67 | }(Reflux.Component)); 68 | 69 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, {foo:'bar'}) ); 70 | 71 | assert.equal( result, '

bar

' ); 72 | }); 73 | 74 | it('should accept React ancestors and children', function() 75 | { 76 | var MyChild = React.createClass({ 77 | render: function() { 78 | return React.createElement("span", null, 'Hello'); 79 | } 80 | }); 81 | 82 | var MyComponent = (function (_super) { 83 | __extends(Component, _super); 84 | function Component(props) { 85 | _super.call(this, props); 86 | this.state = {}; 87 | this.store = Reflux.createStore({state:{a:'b'}}); 88 | } 89 | Component.prototype.render = function () { 90 | return React.createElement("p", null, React.createElement(MyChild, null)); 91 | }; 92 | return Component; 93 | }(Reflux.Component)); 94 | 95 | var MyParent = React.createClass({ 96 | render: function() { 97 | return React.createElement("div", null, React.createElement(MyComponent, null)); 98 | } 99 | }); 100 | 101 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyParent, null) ); 102 | 103 | assert.equal( result, '

Hello

' ); 104 | }); 105 | 106 | it('should accept other Reflux.Component ancestors and children', function() 107 | { 108 | var MyChild = (function (_super) { 109 | __extends(Child, _super); 110 | function Child(props) { 111 | _super.call(this, props); 112 | this.state = {}; 113 | this.store = Reflux.createStore({state:{a:'b'}}); 114 | } 115 | Child.prototype.render = function () { 116 | return React.createElement("span", null, 'World'); 117 | }; 118 | return Child; 119 | }(Reflux.Component)); 120 | 121 | var MyComponent = (function (_super) { 122 | __extends(Component, _super); 123 | function Component(props) { 124 | _super.call(this, props); 125 | this.state = {}; 126 | this.store = Reflux.createStore({state:{a:'b'}}); 127 | } 128 | Component.prototype.render = function () { 129 | return React.createElement("p", null, React.createElement(MyChild, null)); 130 | }; 131 | return Component; 132 | }(Reflux.Component)); 133 | 134 | var MyParent = (function (_super) { 135 | __extends(Parent, _super); 136 | function Parent(props) { 137 | _super.call(this, props); 138 | this.state = {}; 139 | this.store = Reflux.createStore({state:{a:'b'}}); 140 | } 141 | Parent.prototype.render = function () { 142 | return React.createElement("div", null, React.createElement(MyComponent, null)); 143 | }; 144 | return Parent; 145 | }(Reflux.Component)); 146 | 147 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyParent, null) ); 148 | 149 | assert.equal( result, '

World

' ); 150 | }); 151 | 152 | it('should accept values from a store\'s state property', function() 153 | { 154 | var MyComponent = (function (_super) { 155 | __extends(Component, _super); 156 | function Component(props) { 157 | _super.call(this, props); 158 | this.state = {}; 159 | this.store = Reflux.createStore({state:{foo:'baz'}}); 160 | } 161 | Component.prototype.render = function () { 162 | return React.createElement("p", null, this.state.foo); 163 | }; 164 | return Component; 165 | }(Reflux.Component)); 166 | 167 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 168 | 169 | assert.equal( result, '

baz

' ); 170 | }); 171 | 172 | it('should accept values from multiple stores', function() 173 | { 174 | var MyComponent = (function (_super) { 175 | __extends(Component, _super); 176 | function Component(props) { 177 | _super.call(this, props); 178 | this.state = {}; 179 | this.stores = [Reflux.createStore({state:{foo:'bar'}}), Reflux.createStore({state:{bar:'foo'}})]; 180 | } 181 | Component.prototype.render = function () { 182 | return React.createElement("p", null, this.state.bar, ':', this.state.foo); 183 | }; 184 | return Component; 185 | }(Reflux.Component)); 186 | 187 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 188 | 189 | assert.equal( result, '

foo:bar

' ); 190 | }); 191 | 192 | it('should retain normal React state values', function() 193 | { 194 | var MyComponent = (function (_super) { 195 | __extends(Component, _super); 196 | function Component(props) { 197 | _super.call(this, props); 198 | this.state = {bar:'foo'}; 199 | this.store = Reflux.createStore({state:{foo:'bar'}}); 200 | } 201 | Component.prototype.render = function () { 202 | return React.createElement("p", null, this.state.bar, ':', this.state.foo); 203 | }; 204 | return Component; 205 | }(Reflux.Component)); 206 | 207 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 208 | 209 | assert.equal( result, '

foo:bar

' ); 210 | }); 211 | 212 | it('should extend third party components with Reflux.Component.extend()', function() 213 | { 214 | var OtherComponent = (function (_super) { 215 | __extends(Component, _super); 216 | function Component(props) { 217 | _super.call(this, props); 218 | this.foo = 'other class bar and '; 219 | } 220 | return Component; 221 | }(React.Component)); 222 | 223 | var MyComponent = (function (_super) { 224 | __extends(Component, _super); 225 | function Component(props) { 226 | _super.call(this, props); 227 | this.store = Reflux.createStore({state:{foo:'baz'}}); 228 | } 229 | Component.prototype.render = function () { 230 | return React.createElement("p", null, this.foo+this.state.foo); 231 | }; 232 | return Component; 233 | }(Reflux.Component.extend(OtherComponent))); 234 | 235 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 236 | 237 | assert.equal( result, '

other class bar and baz

' ); 238 | }); 239 | 240 | }); 241 | -------------------------------------------------------------------------------- /test/Reflux.GlobalState.spec.js: -------------------------------------------------------------------------------- 1 | 2 | var chai = require('chai'), 3 | assert = chai.assert; 4 | 5 | chai.use(require('chai-as-promised')); 6 | 7 | var React = require('react'); 8 | var ReactDOMServer = require('react-dom/server'); 9 | var Reflux; 10 | 11 | 12 | describe('Working with Reflux.GlobalState', function() 13 | { 14 | it('should allow defining of React with Reflux.defineReact without error and empty', function() 15 | { 16 | Reflux = require('../src'); 17 | Reflux.defineReact(React); 18 | 19 | return true; 20 | }); 21 | 22 | it('should construct with a default empty state object', function() 23 | { 24 | assert.equal( typeof Reflux.GlobalState, 'object' ); 25 | }); 26 | 27 | it('should remain empty when a store is created without a static id', function() 28 | { 29 | var MyStore = (function (_super) { 30 | Reflux.utils.inherits(Store, _super); 31 | function Store() { 32 | _super.call(this); 33 | this.state = {foo:'bar'}; 34 | } 35 | return Store; 36 | }(Reflux.Store)); 37 | 38 | var MyComponent = (function (_super) { 39 | Reflux.utils.inherits(Component, _super); 40 | function Component(props) { 41 | _super.call(this, props); 42 | this.state = {}; 43 | this.store = MyStore; 44 | } 45 | Component.prototype.render = function () { 46 | return React.createElement("p", null, this.state.foo); 47 | }; 48 | return Component; 49 | }(Reflux.Component)); 50 | 51 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 52 | 53 | assert.equal( result, '

bar

' ); 54 | assert.equal( Reflux.GlobalState.MyStore, undefined ); 55 | }); 56 | 57 | it('should gain state when a store is created with a static id', function() 58 | { 59 | Reflux.GlobalState = {}; 60 | Reflux.stores = {}; 61 | 62 | var MyStore = (function (_super) { 63 | Reflux.utils.inherits(Store, _super); 64 | function Store() { 65 | _super.call(this); 66 | this.state = {foo:'bar'}; 67 | } 68 | return Store; 69 | }(Reflux.Store)); 70 | MyStore.id = "MyStore"; 71 | 72 | var MyComponent = (function (_super) { 73 | Reflux.utils.inherits(Component, _super); 74 | function Component(props) { 75 | _super.call(this, props); 76 | this.state = {}; 77 | this.store = MyStore; 78 | } 79 | Component.prototype.render = function () { 80 | return React.createElement("p", null, this.state.foo); 81 | }; 82 | return Component; 83 | }(Reflux.Component)); 84 | 85 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 86 | 87 | assert.equal( result, '

bar

' ); 88 | assert.equal( Reflux.GlobalState.MyStore.foo, 'bar' ); 89 | }); 90 | 91 | it('should gain state when a store is created with a static id - initStore', function() 92 | { 93 | Reflux.GlobalState = {}; 94 | Reflux.stores = {}; 95 | 96 | var MyStore = (function (_super) { 97 | Reflux.utils.inherits(Store, _super); 98 | function Store() { 99 | _super.call(this); 100 | this.state = {foo:'bar'}; 101 | } 102 | return Store; 103 | }(Reflux.Store)); 104 | MyStore.id = "MyStore"; 105 | 106 | Reflux.initStore(MyStore); 107 | 108 | assert.equal( Reflux.GlobalState.MyStore.foo, 'bar' ); 109 | }); 110 | 111 | it('should update upon setState in store', function() 112 | { 113 | Reflux.GlobalState = {}; 114 | Reflux.stores = {}; 115 | 116 | var MyStore = (function (_super) { 117 | Reflux.utils.inherits(Store, _super); 118 | function Store() { 119 | _super.call(this); 120 | this.state = {foo:'bar'}; 121 | } 122 | return Store; 123 | }(Reflux.Store)); 124 | MyStore.id = "MyStore"; 125 | 126 | var MyComponent = (function (_super) { 127 | Reflux.utils.inherits(Component, _super); 128 | function Component(props) { 129 | _super.call(this, props); 130 | this.state = {}; 131 | this.store = MyStore; 132 | } 133 | Component.prototype.render = function () { 134 | return React.createElement("p", null, this.state.foo); 135 | }; 136 | return Component; 137 | }(Reflux.Component)); 138 | 139 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 140 | assert.equal( result, '

bar

' ); 141 | 142 | try { 143 | MyStore.singleton.setState({foo:'BAR'}); 144 | } catch (e) {} 145 | 146 | assert.equal( Reflux.GlobalState.MyStore.foo, 'BAR' ); 147 | }); 148 | 149 | it('should update upon setState in store - initStore', function() 150 | { 151 | Reflux.GlobalState = {}; 152 | Reflux.stores = {}; 153 | 154 | var MyStore = (function (_super) { 155 | Reflux.utils.inherits(Store, _super); 156 | function Store() { 157 | _super.call(this); 158 | this.state = {foo:'bar'}; 159 | } 160 | return Store; 161 | }(Reflux.Store)); 162 | MyStore.id = "MyStore"; 163 | 164 | var str = Reflux.initStore(MyStore); 165 | 166 | try { 167 | str.setState({foo:'BAR'}); 168 | } catch (e) {} 169 | 170 | assert.equal( Reflux.GlobalState.MyStore.foo, 'BAR' ); 171 | }); 172 | 173 | it('when set before component mounting should define initial state in store', function() 174 | { 175 | Reflux.GlobalState = {}; 176 | Reflux.stores = {}; 177 | 178 | var MyStore = (function (_super) { 179 | Reflux.utils.inherits(Store, _super); 180 | function Store() { 181 | _super.call(this); 182 | this.state = {foo:'bar'}; 183 | } 184 | return Store; 185 | }(Reflux.Store)); 186 | MyStore.id = "MyStore"; 187 | 188 | var MyComponent = (function (_super) { 189 | Reflux.utils.inherits(Component, _super); 190 | function Component(props) { 191 | _super.call(this, props); 192 | this.state = {}; 193 | this.store = MyStore; 194 | } 195 | Component.prototype.render = function () { 196 | return React.createElement("p", null, this.state.foo); 197 | }; 198 | return Component; 199 | }(Reflux.Component)); 200 | 201 | Reflux.GlobalState = {'MyStore':{'foo':'not bar'}}; 202 | 203 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 204 | 205 | assert.equal( result, '

not bar

' ); 206 | assert.equal( Reflux.GlobalState.MyStore.foo, 'not bar' ); 207 | }); 208 | 209 | it('when set before initStore should define initial state in store', function() 210 | { 211 | Reflux.GlobalState = {}; 212 | Reflux.stores = {}; 213 | 214 | var MyStore = (function (_super) { 215 | Reflux.utils.inherits(Store, _super); 216 | function Store() { 217 | _super.call(this); 218 | this.state = {foo:'bar'}; 219 | } 220 | return Store; 221 | }(Reflux.Store)); 222 | MyStore.id = "MyStore"; 223 | 224 | Reflux.GlobalState = {'MyStore':{'foo':'not bar'}}; 225 | 226 | var str = Reflux.initStore(MyStore); 227 | 228 | assert.equal( Reflux.GlobalState.MyStore.foo, 'not bar' ); 229 | assert.equal( str.state.foo, 'not bar' ); 230 | }); 231 | 232 | it('when set via setGlobalState before component mounting should define initial state in store', function() 233 | { 234 | Reflux.GlobalState = {}; 235 | Reflux.stores = {}; 236 | 237 | var MyStore = (function (_super) { 238 | Reflux.utils.inherits(Store, _super); 239 | function Store() { 240 | _super.call(this); 241 | this.state = {foo:'bar'}; 242 | } 243 | return Store; 244 | }(Reflux.Store)); 245 | MyStore.id = "MyStore"; 246 | 247 | var MyComponent = (function (_super) { 248 | Reflux.utils.inherits(Component, _super); 249 | function Component(props) { 250 | _super.call(this, props); 251 | this.state = {}; 252 | this.store = MyStore; 253 | } 254 | Component.prototype.render = function () { 255 | return React.createElement("p", null, this.state.foo); 256 | }; 257 | return Component; 258 | }(Reflux.Component)); 259 | 260 | Reflux.setGlobalState( {'MyStore':{'foo':'not bar'}} ); 261 | 262 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 263 | 264 | assert.equal( result, '

not bar

' ); 265 | assert.equal( Reflux.GlobalState.MyStore.foo, 'not bar' ); 266 | }); 267 | 268 | it('values from getGlobalState should not mutate upon further GlobalState mutations', function() 269 | { 270 | Reflux.GlobalState = {}; 271 | Reflux.stores = {}; 272 | 273 | var MyStore = (function (_super) { 274 | Reflux.utils.inherits(Store, _super); 275 | function Store() { 276 | _super.call(this); 277 | this.state = {foo:'bar'}; 278 | } 279 | return Store; 280 | }(Reflux.Store)); 281 | MyStore.id = "MyStore"; 282 | 283 | var MyComponent = (function (_super) { 284 | Reflux.utils.inherits(Component, _super); 285 | function Component(props) { 286 | _super.call(this, props); 287 | this.state = {}; 288 | this.store = MyStore; 289 | } 290 | Component.prototype.render = function () { 291 | return React.createElement("p", null, this.state.foo); 292 | }; 293 | return Component; 294 | }(Reflux.Component)); 295 | 296 | Reflux.setGlobalState( {'MyStore':{'foo':'not bar'}} ); 297 | 298 | var gotVal = Reflux.getGlobalState(); 299 | 300 | Reflux.setGlobalState( {'MyStore':{'foo':'foobar'}} ); 301 | 302 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 303 | 304 | assert.equal( result, '

foobar

' ); 305 | assert.equal( gotVal.MyStore.foo, 'not bar' ); 306 | assert.equal( Reflux.GlobalState.MyStore.foo, 'foobar' ); 307 | }); 308 | 309 | it('setGlobalState should update all global states when used with a full object', function() 310 | { 311 | Reflux.GlobalState = {}; 312 | Reflux.stores = {}; 313 | 314 | var MyStore = (function (_super) { 315 | Reflux.utils.inherits(Store, _super); 316 | function Store() { 317 | _super.call(this); 318 | this.state = {foo:'bar'}; 319 | } 320 | return Store; 321 | }(Reflux.Store)); 322 | MyStore.id = "MyStore"; 323 | 324 | var MyComponent = (function (_super) { 325 | Reflux.utils.inherits(Component, _super); 326 | function Component(props) { 327 | _super.call(this, props); 328 | this.state = {}; 329 | this.store = MyStore; 330 | } 331 | Component.prototype.render = function () { 332 | return React.createElement("p", null, this.state.bar + this.state.foo); 333 | }; 334 | return Component; 335 | }(Reflux.Component)); 336 | 337 | Reflux.GlobalState = {'MyStore':{'foo':'not bar', 'bar':'not foo'}}; 338 | Reflux.setGlobalState( {'MyStore':{'foo':'bar', 'bar':'foo'}} ); 339 | 340 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 341 | 342 | assert.equal( result, '

foobar

' ); 343 | assert.equal( Reflux.GlobalState.MyStore.foo, 'bar' ); 344 | assert.equal( Reflux.GlobalState.MyStore.bar, 'foo' ); 345 | }); 346 | 347 | it('setGlobalState should update partial global states when used with a partial object', function() 348 | { 349 | Reflux.GlobalState = {}; 350 | Reflux.stores = {}; 351 | 352 | var MyStore = (function (_super) { 353 | Reflux.utils.inherits(Store, _super); 354 | function Store() { 355 | _super.call(this); 356 | this.state = {foo:'bar'}; 357 | } 358 | return Store; 359 | }(Reflux.Store)); 360 | MyStore.id = "MyStore"; 361 | var singleton = new MyStore(); 362 | MyStore.singleton = singleton; 363 | Reflux.stores.MyStore = singleton; 364 | 365 | var MyComponent = (function (_super) { 366 | Reflux.utils.inherits(Component, _super); 367 | function Component(props) { 368 | _super.call(this, props); 369 | this.state = {}; 370 | this.store = MyStore; 371 | } 372 | Component.prototype.render = function () { 373 | return React.createElement("p", null, this.state.bar + this.state.foo); 374 | }; 375 | return Component; 376 | }(Reflux.Component)); 377 | 378 | Reflux.GlobalState = {}; 379 | Reflux.setGlobalState( {'MyStore':{'foo':'not bar', 'bar':'not foo'}} ); 380 | Reflux.setGlobalState( {'MyStore':{'foo':'bar'}} ); 381 | 382 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 383 | 384 | assert.equal( Reflux.GlobalState.MyStore.foo, 'bar' ); 385 | assert.equal( Reflux.GlobalState.MyStore.bar, 'not foo' ); 386 | assert.equal( result, '

not foobar

' ); 387 | }); 388 | }); 389 | -------------------------------------------------------------------------------- /test/Reflux.Store.spec.js: -------------------------------------------------------------------------------- 1 | 2 | var chai = require('chai'), 3 | assert = chai.assert, 4 | Reflux = require('../src'); 5 | 6 | chai.use(require('chai-as-promised')); 7 | 8 | var React = require('react'); 9 | var ReactDOMServer = require('react-dom/server'); 10 | 11 | Reflux.defineReact(React); 12 | 13 | 14 | describe('Creating ES6 style stores', function() 15 | { 16 | it('should allow defining of React with Reflux.defineReact without error', function() 17 | { 18 | Reflux.defineReact(React); 19 | 20 | return true; 21 | }); 22 | 23 | it('should construct with a default empty state object', function() 24 | { 25 | var MyStore = (function (_super) { 26 | Reflux.utils.inherits(Store, _super); 27 | function Store() { 28 | _super.call(this); 29 | } 30 | return Store; 31 | }(Reflux.Store)); 32 | 33 | var store = new MyStore(); 34 | 35 | assert.equal( typeof store.state, 'object' ); 36 | }); 37 | 38 | it('should add listenables and accept actions', function() 39 | { 40 | var Actions = Reflux.createActions(['up', 'down', 'finish']); 41 | 42 | var MyStore = (function (_super) { 43 | Reflux.utils.inherits(Store, _super); 44 | function Store() { 45 | _super.call(this); 46 | this.listenTo(Actions.up, function(){ this.state.count++; }); 47 | this.listenTo(Actions.down, function(){ this.state.count--; }); 48 | this.state = {count:0}; 49 | } 50 | return Store; 51 | }(Reflux.Store)); 52 | 53 | var store = new MyStore(); 54 | 55 | Actions.up(); 56 | Actions.up(); 57 | Actions.up(); 58 | Actions.down(); 59 | Actions.finish(); 60 | 61 | return assert.equal(store.state.count, 2); 62 | }); 63 | 64 | it('should accept listenables with \'action\' and \'onAction\' callbacks', function() 65 | { 66 | var Actions = Reflux.createActions(['up', 'down', 'finish']); 67 | 68 | var MyStore = (function (_super) { 69 | Reflux.utils.inherits(Store, _super); 70 | function Store() { 71 | _super.call(this); 72 | this.listenables = Actions; 73 | this.state = {count:0}; 74 | } 75 | Store.prototype.up = function(){ this.state.count++; }; 76 | Store.prototype.onDown = function(){ this.state.count--; }; 77 | return Store; 78 | }(Reflux.Store)); 79 | 80 | var store = new MyStore(); 81 | 82 | Actions.up(); 83 | Actions.up(); 84 | Actions.up(); 85 | Actions.down(); 86 | Actions.finish(); 87 | 88 | return assert.equal(store.state.count, 2); 89 | }); 90 | 91 | it('should be able to modify state with setState', function() 92 | { 93 | var Actions = Reflux.createActions(['up', 'down', 'finish']); 94 | 95 | var MyStore = (function (_super) { 96 | Reflux.utils.inherits(Store, _super); 97 | function Store() { 98 | _super.call(this); 99 | this.listenables = Actions; 100 | this.state = {count:0}; 101 | } 102 | Store.prototype.up = function(){ this.setState({count:this.state.count+1}); }; 103 | Store.prototype.onDown = function(){ this.setState({count:this.state.count-1}); }; 104 | return Store; 105 | }(Reflux.Store)); 106 | 107 | var store = new MyStore(); 108 | 109 | Actions.up(); 110 | Actions.up(); 111 | Actions.up(); 112 | Actions.down(); 113 | Actions.finish(); 114 | 115 | return assert.equal(store.state.count, 2); 116 | }); 117 | 118 | it('should accept actions with arguments', function() 119 | { 120 | var Actions = Reflux.createActions(['up', 'down', 'finish']); 121 | 122 | var MyStore = (function (_super) { 123 | Reflux.utils.inherits(Store, _super); 124 | function Store() { 125 | _super.call(this); 126 | this.listenables = Actions; 127 | this.state = {count:0}; 128 | } 129 | Store.prototype.up = function(){ this.state.count++; }; 130 | Store.prototype.onDown = function(by){ this.state.count -= by; }; 131 | return Store; 132 | }(Reflux.Store)); 133 | 134 | var store = new MyStore(); 135 | 136 | Actions.up(); 137 | Actions.up(); 138 | Actions.up(); 139 | Actions.down(2); 140 | Actions.finish(); 141 | 142 | return assert.equal(store.state.count, 1); 143 | }); 144 | 145 | it('should mix state in with a Reflux.Component instance', function() 146 | { 147 | var MyStore = (function (_super) { 148 | Reflux.utils.inherits(Store, _super); 149 | function Store() { 150 | _super.call(this); 151 | this.state = {foo:'bar'}; 152 | } 153 | return Store; 154 | }(Reflux.Store)); 155 | 156 | var MyComponent = (function (_super) { 157 | Reflux.utils.inherits(Component, _super); 158 | function Component(props) { 159 | _super.call(this, props); 160 | this.state = {}; 161 | this.store = MyStore; 162 | } 163 | Component.prototype.render = function () { 164 | return React.createElement("p", null, this.state.foo); 165 | }; 166 | return Component; 167 | }(Reflux.Component)); 168 | 169 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 170 | 171 | assert.equal( result, '

bar

' ); 172 | }); 173 | 174 | it('should use singleton instance when applied to multiple components', function() 175 | { 176 | var MyStore = (function (_super) { 177 | Reflux.utils.inherits(Store, _super); 178 | function Store() { 179 | _super.call(this); 180 | this.state = {foo:'bar'}; 181 | MyStore.instanceCount++; 182 | } 183 | return Store; 184 | }(Reflux.Store)); 185 | 186 | MyStore.instanceCount = 0; 187 | 188 | var MyComponent1 = (function (_super) { 189 | Reflux.utils.inherits(Component1, _super); 190 | function Component1(props) { 191 | _super.call(this, props); 192 | this.state = {}; 193 | this.store = MyStore; 194 | } 195 | Component1.prototype.render = function () { 196 | return React.createElement("p", null, this.state.foo); 197 | }; 198 | return Component1; 199 | }(Reflux.Component)); 200 | 201 | var MyComponent2 = (function (_super) { 202 | Reflux.utils.inherits(Component2, _super); 203 | function Component2(props) { 204 | _super.call(this, props); 205 | this.state = {}; 206 | this.store = MyStore; 207 | } 208 | Component2.prototype.render = function () { 209 | return React.createElement("p", null, this.state.foo); 210 | }; 211 | return Component2; 212 | }(Reflux.Component)); 213 | 214 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement('div', null, React.createElement(MyComponent1, null), React.createElement(MyComponent2, null)) ); 215 | 216 | assert.equal( result, '

bar

bar

' ); 217 | assert.equal( MyStore.instanceCount, 1 ); 218 | }); 219 | 220 | it('should mix state in with a Reflux.Component instance via mapStoreToState', function() 221 | { 222 | var MyStore = (function (_super) { 223 | Reflux.utils.inherits(Store, _super); 224 | function Store() { 225 | _super.call(this); 226 | this.state = {foo:'bar'}; 227 | } 228 | return Store; 229 | }(Reflux.Store)); 230 | 231 | var MyComponent = (function (_super) { 232 | Reflux.utils.inherits(Component, _super); 233 | function Component(props) { 234 | _super.call(this, props); 235 | this.state = {}; 236 | this.mapStoreToState(MyStore, function(fromStore){ 237 | return {foobar:fromStore.foo}; 238 | }); 239 | } 240 | Component.prototype.render = function () { 241 | return React.createElement("p", null, this.state.foobar); 242 | }; 243 | return Component; 244 | }(Reflux.Component)); 245 | 246 | var result = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent, null) ); 247 | 248 | assert.equal( result, '

bar

' ); 249 | }); 250 | 251 | it('should mix state separately with two Reflux.Component instances via mapStoreToState', function() 252 | { 253 | var MyStore = (function (_super) { 254 | Reflux.utils.inherits(Store, _super); 255 | function Store() { 256 | _super.call(this); 257 | this.state = {foo:'bar', bar:'foo'}; 258 | } 259 | return Store; 260 | }(Reflux.Store)); 261 | 262 | var MyComponent1 = (function (_super) { 263 | Reflux.utils.inherits(Component, _super); 264 | function Component(props) { 265 | _super.call(this, props); 266 | this.state = {foo:'?'}; 267 | this.mapStoreToState(MyStore, function(fromStore){ 268 | return {bar:fromStore.bar}; 269 | }); 270 | } 271 | Component.prototype.render = function () { 272 | return React.createElement("p", null, this.state.bar+this.state.foo); 273 | }; 274 | return Component; 275 | }(Reflux.Component)); 276 | 277 | var MyComponent2 = (function (_super) { 278 | Reflux.utils.inherits(Component, _super); 279 | function Component(props) { 280 | _super.call(this, props); 281 | this.state = {bar:'?'}; 282 | this.mapStoreToState(MyStore, function(fromStore){ 283 | return {foo:fromStore.foo}; 284 | }); 285 | } 286 | Component.prototype.render = function () { 287 | return React.createElement("p", null, this.state.foo+this.state.bar); 288 | }; 289 | return Component; 290 | }(Reflux.Component)); 291 | 292 | var result1 = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent1, null) ); 293 | var result2 = ReactDOMServer.renderToStaticMarkup( React.createElement(MyComponent2, null) ); 294 | 295 | assert.equal( result1, '

foo?

' ); 296 | assert.equal( result2, '

bar?

' ); 297 | }); 298 | 299 | // this shortcut feature fails in IE9 and IE10, so removing the test for now, until the day that we can drop those 2 browsers 300 | /*it('should allow shortcut access to MyStore.singleton.state via MyStore.state', function() 301 | { 302 | var MyStore = (function (_super) { 303 | Reflux.utils.inherits(Store, _super); 304 | function Store() { 305 | _super.call(this); 306 | this.state = {foo:'bar', bar:'foo'}; 307 | } 308 | return Store; 309 | }(Reflux.Store)); 310 | 311 | Reflux.initStore(MyStore); 312 | 313 | assert.equal( MyStore.state.foo, MyStore.singleton.state.foo ); 314 | assert.equal( MyStore.state.bar, MyStore.singleton.state.bar ); 315 | });*/ 316 | }); 317 | -------------------------------------------------------------------------------- /test/benchmarks/index.js: -------------------------------------------------------------------------------- 1 | var Benchmark = require('benchmark'), 2 | suite = new Benchmark.Suite(), 3 | fs = require('fs'), 4 | path = require('path'); 5 | 6 | // Find all benchmark tests and add them to the suite 7 | fs.readdirSync(__dirname).forEach(function(file) { 8 | if (file === 'index.js') { 9 | return; 10 | } 11 | var filename = path.basename(file, '.js'), 12 | module = require(path.join(__dirname, file)); 13 | 14 | if (module.fn) { 15 | console.log('Added:', file, module.name ? '- ' + module.name : ''); 16 | suite.add(module.name || filename, module.fn); 17 | } 18 | }); 19 | 20 | suite.on('cycle', function(event) { 21 | console.log(String(event.target)); 22 | }) 23 | .run({ 'async': true }); 24 | -------------------------------------------------------------------------------- /test/benchmarks/listeningToActions.js: -------------------------------------------------------------------------------- 1 | var Reflux = require('../../src'), 2 | action = Reflux.createAction(), 3 | i, 4 | noop = function() {}, 5 | NUMBER_OF_LISTENERS = 100; 6 | 7 | // noops are listening to an action emit 8 | for (i = 0; i < NUMBER_OF_LISTENERS; i++) { 9 | action.listen(noop); 10 | } 11 | 12 | exports.name = "Listening to actions with " + NUMBER_OF_LISTENERS + " listeners"; 13 | 14 | exports.fn = function() { 15 | action(Math.random()); 16 | }; 17 | -------------------------------------------------------------------------------- /test/benchmarks/listeningToJoins.js: -------------------------------------------------------------------------------- 1 | var Reflux = require('../../src'), 2 | actions = [], 3 | joinedActions, 4 | i, 5 | noop = function() {}, 6 | NUMBER_OF_LISTENERS = 100, 7 | NUMBER_OF_JOINED_ACTIONS = 10; 8 | 9 | for (i = 0; i < NUMBER_OF_JOINED_ACTIONS; i++) { 10 | actions = Reflux.createAction(); 11 | } 12 | 13 | joinedActions = Reflux.all(actions); 14 | 15 | for (i = 0; i < NUMBER_OF_LISTENERS; i++) { 16 | joinedActions.listen(noop); 17 | } 18 | 19 | exports.name = "Listening to " + NUMBER_OF_JOINED_ACTIONS + " joined actions with " + NUMBER_OF_LISTENERS + " listeners"; 20 | 21 | exports.fn = function() { 22 | for (i = 0; i < actions.length; i++) { 23 | actions[i](Math.random()); 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /test/benchmarks/listeningToStores.js: -------------------------------------------------------------------------------- 1 | var Reflux = require('../../src'), 2 | action = Reflux.createAction(), 3 | store, 4 | i, 5 | noop = function() {}, 6 | NUMBER_OF_LISTENERS = 100; 7 | 8 | store = Reflux.createStore({ 9 | init: function() { 10 | this.listenTo(action, this.trigger); 11 | } 12 | }); 13 | 14 | // noops are listening to the store to emit 15 | for (i = 0; i < NUMBER_OF_LISTENERS; i++) { 16 | store.listen(noop); 17 | } 18 | 19 | exports.name = "Listening to stores with " + NUMBER_OF_LISTENERS + " listeners"; 20 | 21 | exports.fn = function() { 22 | action(Math.random()); 23 | }; 24 | -------------------------------------------------------------------------------- /test/listenTo.spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert, 2 | sinon = require('sinon'), 3 | listenTo = require('../src/listenTo'), 4 | Reflux = require('../src'), 5 | _ = Reflux.utils; 6 | 7 | describe('the listenTo shorthand',function(){ 8 | 9 | describe("when calling the factory",function(){ 10 | var unsubscriber = sinon.spy(), 11 | initialstate = "DATA", 12 | listenable = { 13 | listen: sinon.stub().returns(unsubscriber), 14 | getInitialState: sinon.stub().returns(initialstate) 15 | }, 16 | initial = sinon.spy(), 17 | callback = "CALLBACK", 18 | result = _.extend({method:callback},listenTo(listenable,"method",initial)); 19 | 20 | it("should return object with componentDidMount and componentWillUnmount methods",function(){ 21 | assert.isFunction(result.componentDidMount); 22 | assert.isFunction(result.componentWillUnmount); 23 | }); 24 | 25 | describe("when calling the added componentDidMount",function(){ 26 | result.componentDidMount(); 27 | 28 | it("should add all methods from ListenerMethods",function(){ 29 | for(var m in Reflux.ListenerMethods){ 30 | assert.equal(result[m],Reflux.ListenerMethods[m]); 31 | } 32 | }); 33 | 34 | it("should add a subscriptions array",function(){ 35 | assert.isArray(result.subscriptions); 36 | }); 37 | 38 | it("should call listen on the listenable correctly (via listenTo)",function(){ 39 | assert.equal(listenable.listen.callCount,1); 40 | assert.deepEqual(listenable.listen.firstCall.args,[callback,result]); 41 | }); 42 | 43 | it("should send listenable initial state to initial cb (via listenTo)",function(){ 44 | assert.equal(listenable.getInitialState.callCount,1); 45 | assert.equal(initial.callCount,1); 46 | assert.equal(initial.firstCall.args[0],initialstate); 47 | }); 48 | }); 49 | 50 | describe("the componentWillUnmount method",function(){ 51 | it("should be the same as ListenerMethods stopListeningToAll",function(){ 52 | assert.equal(assert.equal(result.componentWillUnmount,Reflux.ListenerMethods.stopListeningToAll)); 53 | }); 54 | }); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /test/listenToMany.spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert, 2 | sinon = require('sinon'), 3 | listenToMany = require('../src/listenToMany'), 4 | Reflux = require('../src'), 5 | _ = Reflux.utils; 6 | 7 | describe('the listenToMany shorthand',function(){ 8 | 9 | describe("when calling the factory",function(){ 10 | var unsubscriber = sinon.spy(), 11 | listenable1 = {listen: sinon.stub().returns(unsubscriber)}, 12 | listenable2 = {listen: sinon.stub().returns(unsubscriber)}, 13 | listenables = { 14 | firstAction: listenable1, 15 | secondAction: listenable2 16 | }, 17 | context = { 18 | onFirstAction: sinon.spy(), 19 | onSecondAction: sinon.spy() 20 | }, 21 | result = _.extend(context,listenToMany(listenables)); 22 | 23 | it("should return object with componentDidMount and componentWillUnmount methods",function(){ 24 | assert.isFunction(result.componentDidMount); 25 | assert.isFunction(result.componentWillUnmount); 26 | }); 27 | 28 | describe("when calling the added componentDidMount",function(){ 29 | result.componentDidMount(); 30 | 31 | it("should add all methods from ListenerMethods",function(){ 32 | for(var m in Reflux.ListenerMethods){ 33 | assert.equal(result[m],Reflux.ListenerMethods[m]); 34 | } 35 | }); 36 | 37 | it("should add to a subscriptions array (via listenToMany)",function(){ 38 | var subs = result.subscriptions; 39 | assert.isArray(subs); 40 | assert.equal(subs[0].listenable,listenable1); 41 | assert.equal(subs[1].listenable,listenable2); 42 | }); 43 | 44 | it("should call listen on the listenables correctly (via listenToMany)",function(){ 45 | assert.equal(listenable1.listen.callCount,1); 46 | assert.deepEqual(listenable1.listen.firstCall.args,[context.onFirstAction,result]); 47 | assert.equal(listenable2.listen.callCount,1); 48 | assert.deepEqual(listenable2.listen.firstCall.args,[context.onSecondAction,result]); 49 | }); 50 | }); 51 | 52 | describe("the componentWillUnmount method",function(){ 53 | 54 | it("should be the same as ListenerMethods stopListeningToAll",function(){ 55 | assert.equal(assert.equal(result.componentWillUnmount,Reflux.ListenerMethods.stopListeningToAll)); 56 | }); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /test/sauceLaunchers.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | sl_chrome: { 3 | base: 'SauceLabs', 4 | browserName: 'chrome', 5 | platform: 'Windows 7', 6 | version: '35' 7 | }, 8 | sl_firefox: { 9 | base: 'SauceLabs', 10 | browserName: 'firefox', 11 | version: '30' 12 | }, 13 | /* 14 | sl_ios_safari: { 15 | base: 'SauceLabs', 16 | browserName: 'iphone', 17 | platform: 'OS X 10.9', 18 | version: '7.1' 19 | },*/ 20 | sl_ie_9: { 21 | base: 'SauceLabs', 22 | browserName: 'internet explorer', 23 | platform: 'Windows 7', 24 | version: '9' 25 | }, 26 | sl_ie_10: { 27 | base: 'SauceLabs', 28 | browserName: 'internet explorer', 29 | platform: 'Windows 7', 30 | version: '10' 31 | }, 32 | sl_ie_11: { 33 | base: 'SauceLabs', 34 | browserName: 'internet explorer', 35 | platform: 'Windows 8.1', 36 | version: '11' 37 | }, 38 | sl_opera_12: { 39 | base: 'SauceLabs', 40 | browserName: 'opera', 41 | version: '12' 42 | }, 43 | sl_opera_11: { 44 | base: 'SauceLabs', 45 | browserName: 'opera', 46 | version: '11' 47 | } 48 | }; 49 | -------------------------------------------------------------------------------- /test/shims/phantomjs-shims.js: -------------------------------------------------------------------------------- 1 | // From https://github.com/facebook/react/blob/master/src/test/phantomjs-shims.js 2 | (function() { 3 | 4 | var Ap = Array.prototype; 5 | var slice = Ap.slice; 6 | var Fp = Function.prototype; 7 | require('es6-promise').polyfill(); 8 | 9 | if (!Fp.bind) { 10 | // PhantomJS doesn't support Function.prototype.bind natively, so 11 | // polyfill it whenever this module is required. 12 | Fp.bind = function(context) { 13 | var func = this; 14 | var args = slice.call(arguments, 1); 15 | 16 | function bound() { 17 | var invokedAsConstructor = func.prototype && (this instanceof func); 18 | return func.apply( 19 | // Ignore the context parameter when invoking the bound function 20 | // as a constructor. Note that this includes not only constructor 21 | // invocations using the new keyword but also calls to base class 22 | // constructors such as BaseClass.call(this, ...) or super(...). 23 | !invokedAsConstructor && context || this, 24 | args.concat(slice.call(arguments)) 25 | ); 26 | } 27 | 28 | // The bound function must share the .prototype of the unbound 29 | // function so that any object created by one constructor will count 30 | // as an instance of both constructors. 31 | bound.prototype = func.prototype; 32 | 33 | return bound; 34 | }; 35 | } 36 | 37 | })(); 38 | -------------------------------------------------------------------------------- /test/usingConnectFilterMixin.spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert, 2 | sinon = require('sinon'), 3 | connectFilter = require('../src/connectFilter'), 4 | Reflux = require('../src'), 5 | _ = Reflux.utils; 6 | 7 | var dummyFilter = function(value) { return value.slice(0,2); }; 8 | 9 | describe('using the connectFilter(...) mixin',function(){ 10 | 11 | it("should be exposed in Reflux",function(){ 12 | assert.equal(connectFilter, Reflux.connectFilter); 13 | }); 14 | 15 | describe("when calling with action",function() { 16 | var listenable = { 17 | listen: sinon.spy() 18 | }, 19 | context = {setState: sinon.spy()}; 20 | _.extend(context,connectFilter(listenable, "KEY", dummyFilter)); 21 | 22 | it("should pass empty object to state",function(){ 23 | assert.deepEqual({},context.getInitialState()); 24 | }); 25 | }); 26 | 27 | describe("when filter func returns", function() { 28 | var listenable, context, expectedResult; 29 | function withFilterReturning(val) { 30 | var key = "KEY"; 31 | listenable = { 32 | listen: sinon.spy(), 33 | getInitialState: sinon.stub().returns("DOES NOT MATTER") 34 | }; 35 | context = {setState: sinon.spy()}; 36 | expectedResult = {}; 37 | expectedResult[key] = val; 38 | _.extend(context,connectFilter(listenable, key, function() {return val;})); 39 | } 40 | describe("0", function() { 41 | beforeEach(function () { 42 | withFilterReturning(0); 43 | }); 44 | it("should return that value", function() { 45 | assert.deepEqual(expectedResult, context.getInitialState()); 46 | }); 47 | }); 48 | describe("empty string", function() { 49 | beforeEach(function () { 50 | withFilterReturning(""); 51 | }); 52 | it("should return that value", function() { 53 | assert.deepEqual(expectedResult, context.getInitialState()); 54 | }); 55 | }); 56 | describe("false", function() { 57 | beforeEach(function () { 58 | withFilterReturning(false); 59 | }); 60 | it("should return that value", function() { 61 | assert.deepEqual(expectedResult, context.getInitialState()); 62 | }); 63 | }); 64 | describe("undefined", function() { 65 | beforeEach(function () { 66 | withFilterReturning(); 67 | }); 68 | it("should return empty object", function() { 69 | assert.deepEqual({}, context.getInitialState()); 70 | }); 71 | }); 72 | }); 73 | 74 | describe("when calling without key",function(){ 75 | 76 | it("should throw.",function(){ 77 | 78 | var listenable = { 79 | listen: function() {}, 80 | getInitialState: function() {} 81 | }; 82 | 83 | assert.throws(function () { 84 | connectFilter(listenable, dummyFilter); 85 | }, 'Reflux.connectFilter() requires a key.'); 86 | }); 87 | 88 | }); 89 | 90 | describe("when calling with key",function(){ 91 | var initialstate = "DEFAULTDATA", 92 | triggerdata = "TRIGGERDATA", 93 | key = "KEY", 94 | listenable = { 95 | listen: sinon.spy(), 96 | getInitialState: sinon.stub().returns(initialstate) 97 | }, 98 | context = {setState: sinon.spy()}, 99 | result = _.extend(context,connectFilter(listenable,key,dummyFilter)); 100 | 101 | it("should pass initial state to state correctly",function(){ 102 | assert.deepEqual({KEY:initialstate.slice(0,2)},context.getInitialState()); 103 | }); 104 | 105 | result.componentDidMount(); 106 | 107 | it("should call listen on the listenable correctly",function(){ 108 | assert.equal(1,listenable.listen.callCount); 109 | assert.isFunction(listenable.listen.firstCall.args[0]); 110 | assert.equal(context,listenable.listen.firstCall.args[1]); 111 | }); 112 | 113 | it("should send listenable callback which calls setState correctly",function(){ 114 | listenable.listen.firstCall.args[0](triggerdata); 115 | assert.deepEqual([_.object([key],[triggerdata.slice(0,2)])],context.setState.firstCall.args); 116 | }); 117 | }); 118 | describe("when calling with falsy key",function(){ 119 | var triggerdata = "TRIGGERDATA", 120 | key = 0, 121 | listenable = {listen: sinon.spy()}, 122 | context = {setState: sinon.spy()}, 123 | result = _.extend(context,connectFilter(listenable,key,dummyFilter)); 124 | result.componentDidMount(); 125 | it("should send listenable callback which calls setState correctly",function(){ 126 | listenable.listen.firstCall.args[0](triggerdata); 127 | assert.deepEqual([_.object([key],[triggerdata.slice(0,2)])],context.setState.firstCall.args); 128 | }); 129 | }); 130 | }); 131 | -------------------------------------------------------------------------------- /test/usingConnectMixin.spec.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert, 2 | sinon = require('sinon'), 3 | connect = require('../src/connect'), 4 | Reflux = require('../src'), 5 | _ = Reflux.utils; 6 | 7 | describe('using the connect(...) mixin',function(){ 8 | 9 | it("should be exposed in Reflux",function(){ 10 | assert.equal(connect, Reflux.connect); 11 | }); 12 | 13 | describe("when calling with action",function() { 14 | var listenable = { 15 | listen: sinon.spy() 16 | }, 17 | context = {setState: sinon.spy()}; 18 | _.extend(context,connect(listenable, "KEY")); 19 | 20 | it("should pass empty object to state",function(){ 21 | assert.deepEqual({},context.getInitialState()); 22 | }); 23 | }); 24 | 25 | describe("when calling without key",function(){ 26 | 27 | it("should throw.",function(){ 28 | 29 | var listenable = { 30 | listen: function() {}, 31 | getInitialState: function() {} 32 | }; 33 | 34 | assert.throws(function () { 35 | connect(listenable); 36 | }, 'Reflux.connect() requires a key.'); 37 | }); 38 | 39 | }); 40 | 41 | describe("when calling with key",function(){ 42 | var initialstate = "DEFAULTDATA", 43 | triggerdata = "TRIGGERDATA", 44 | key = "KEY", 45 | listenable = { 46 | listen: sinon.spy(), 47 | getInitialState: sinon.stub().returns(initialstate) 48 | }, 49 | context = {setState: sinon.spy()}, 50 | result = _.extend(context,connect(listenable,key)); 51 | 52 | it("should pass initial state to state correctly",function(){ 53 | assert.deepEqual({KEY:initialstate},context.getInitialState()); 54 | }); 55 | 56 | result.componentDidMount(); 57 | 58 | it("should call listen on the listenable correctly",function(){ 59 | assert.equal(1,listenable.listen.callCount); 60 | assert.isFunction(listenable.listen.firstCall.args[0]); 61 | assert.equal(context,listenable.listen.firstCall.args[1]); 62 | }); 63 | 64 | it("should send listenable callback which calls setState correctly",function(){ 65 | listenable.listen.firstCall.args[0](triggerdata); 66 | assert.deepEqual([_.object([key],[triggerdata])],context.setState.firstCall.args); 67 | }); 68 | }); 69 | describe("when calling with falsy key",function(){ 70 | var triggerdata = "TRIGGERDATA", 71 | key = 0, 72 | listenable = {listen: sinon.spy()}, 73 | context = {setState: sinon.spy()}, 74 | result = _.extend(context,connect(listenable,key)); 75 | result.componentDidMount(); 76 | it("should send listenable callback which calls setState correctly",function(){ 77 | listenable.listen.firstCall.args[0](triggerdata); 78 | assert.deepEqual([_.object([key],[triggerdata])],context.setState.firstCall.args); 79 | }); 80 | }); 81 | }); 82 | -------------------------------------------------------------------------------- /test/usingListenerMethodsMixin.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'), 2 | assert = chai.assert, 3 | Reflux = require('../src'), 4 | sinon = require('sinon'); 5 | 6 | describe("using the ListenerMethods",function(){ 7 | var ListenerMethods = Reflux.ListenerMethods; 8 | 9 | describe("the listenToMany function",function(){ 10 | var listenables = { foo: "FOO", bar: "BAR", baz: "BAZ", missing: "MISSING", parent: { 11 | children: ['foo', 'bar', 'baz', "notThere"], foo: "FOOChild", bar: "BARChild", baz: "BAZChild" 12 | }}, 13 | context = { 14 | onFoo:"onFoo", 15 | bar:"bar", 16 | onBaz:"onBaz", 17 | onBazDefault:"onBazDefault", 18 | onParent: "onParent", 19 | onParentFoo: "onParentFoo", 20 | parentBar: "parentBar", 21 | onParentBaz: "onParentBaz", 22 | onParentBazDefault: "onParentBazDefault", 23 | listenTo:sinon.spy() 24 | }; 25 | Reflux.ListenerMixin.listenToMany.call(context,listenables); 26 | 27 | it("should call listenTo for all listenables with corresponding callbacks",function(){ 28 | assert.equal(context.listenTo.callCount,7); 29 | assert.deepEqual(context.listenTo.firstCall.args,[listenables.foo,"onFoo","onFoo"]); 30 | assert.deepEqual(context.listenTo.secondCall.args,[listenables.bar,"bar","bar"]); 31 | assert.deepEqual(context.listenTo.thirdCall.args,[listenables.baz,"onBaz","onBazDefault"]); 32 | assert.deepEqual(context.listenTo.getCall(3).args,[listenables.parent,"onParent","onParent"]); 33 | assert.deepEqual(context.listenTo.getCall(4).args,[listenables.parent.foo,"onParentFoo","onParentFoo"]); 34 | assert.deepEqual(context.listenTo.getCall(5).args,[listenables.parent.bar,"parentBar","parentBar"]); 35 | assert.deepEqual(context.listenTo.getCall(6).args,[listenables.parent.baz,"onParentBaz","onParentBazDefault"]); 36 | }); 37 | }); 38 | 39 | describe("the listenTo function",function(){ 40 | var listenTo = ListenerMethods.listenTo; 41 | 42 | it("will throw error if validation of listenable returns text",function(){ 43 | var errormsg = "ERROR! ERROR!", 44 | context = {validateListening: sinon.stub().returns(errormsg)}, 45 | listenable = "LISTENABLE"; 46 | assert.throws(function() { 47 | listenTo.call(context,listenable); 48 | }, errormsg); 49 | assert.equal(context.validateListening.callCount,1); 50 | assert.equal(context.validateListening.firstCall.args[0],listenable); 51 | }); 52 | 53 | describe("when setting a subscription",function(){ 54 | var unsub = sinon.spy(), 55 | listenable = {listen:sinon.stub().returns(unsub)}, 56 | callback = "CALLBACK", 57 | defaultcallback = "DEFCALL", 58 | context = { 59 | validateListening: function(){}, 60 | fetchInitialState: sinon.stub() 61 | }, 62 | subobj = listenTo.call(context,listenable,callback,defaultcallback); 63 | 64 | it("adds the returned subscription object to a new array if none was there",function(){ 65 | assert.equal(subobj,context.subscriptions[0]); 66 | }); 67 | 68 | it("calls the listen method on the listener",function(){ 69 | assert.deepEqual(listenable.listen.firstCall.args,[callback,context]); 70 | }); 71 | 72 | it("tries to get initial state correctly",function(){ 73 | assert.deepEqual(context.fetchInitialState.firstCall.args,[listenable,defaultcallback]); 74 | }); 75 | 76 | describe("the returned subscription object",function(){ 77 | it("contains the listenable and a stop function",function(){ 78 | assert.isFunction(subobj.stop); 79 | assert.equal(subobj.listenable,listenable); 80 | }); 81 | }); 82 | 83 | describe("when setting a second subscription",function(){ 84 | var secondlistenable = {listen:sinon.stub()}, 85 | secondsubobj = listenTo.call(context,secondlistenable,callback); 86 | it("adds the subobj to the existing array",function(){ 87 | assert.equal(context.subscriptions[1],secondsubobj); 88 | }); 89 | }); 90 | 91 | }); 92 | }); 93 | 94 | describe('the fetchInitialState method',function(){ 95 | 96 | describe('when called with method name and publisher with getInitialState method',function(){ 97 | var initialstate = "DEFAULTDATA", 98 | listenable = { 99 | getInitialState: sinon.stub().returns(initialstate) 100 | }, 101 | context = { 102 | defcb: sinon.spy() 103 | }; 104 | ListenerMethods.fetchInitialState.call(context,listenable,"defcb"); 105 | 106 | it("calls getInitialState on the publisher",function(){ 107 | assert.equal(listenable.getInitialState.callCount,1); 108 | }); 109 | 110 | it("passes the returned data to the named method",function(){ 111 | assert.deepEqual(context.defcb.firstCall.args,[initialstate]); 112 | }); 113 | }); 114 | }); 115 | 116 | describe('the hasListener method',function(){ 117 | var action1 = Reflux.createAction(), 118 | action2 = Reflux.createAction(), 119 | action3 = Reflux.createAction(), 120 | action4 = Reflux.createAction(), 121 | store = Reflux.createStore(); 122 | store.listenTo(action1,function(){}); 123 | store.joinLeading(action1,action2,action3,function(){}); 124 | it('should return true if context is listening',function(){ 125 | assert.equal(true,store.hasListener(action1)); 126 | }); 127 | it('should return false if context isn\'t listening',function(){ 128 | assert.equal(false,store.hasListener(action4)); 129 | }); 130 | it('should return true if context is listening to listenable as part of a join',function(){ 131 | assert.equal(true,store.hasListener(action2)); 132 | assert.equal(true,store.hasListener(action3)); 133 | }); 134 | }); 135 | 136 | }); 137 | -------------------------------------------------------------------------------- /test/usingListenerMixin.spec.js: -------------------------------------------------------------------------------- 1 | var chai = require('chai'), 2 | assert = chai.assert, 3 | Reflux = require('../src'), 4 | Q = require('q'); 5 | 6 | chai.use(require('chai-as-promised')); 7 | 8 | describe('Managing subscriptions via ListenerMixin', function() { 9 | var component, 10 | action, 11 | promise, 12 | store; 13 | 14 | beforeEach(function() { 15 | // simulate ReactJS component instantiation and mounting 16 | component = Object.create(Reflux.ListenerMixin); 17 | delete component.subscriptions; 18 | 19 | action = Reflux.createAction(); 20 | 21 | promise = Q.Promise(function(resolve) { 22 | component.listenTo(action, function() { 23 | resolve(Array.prototype.slice.call(arguments, 0)); 24 | }); 25 | }); 26 | }); 27 | 28 | it('should get argument given on action', function() { 29 | action('my argument'); 30 | 31 | return assert.eventually.equal(promise, 'my argument'); 32 | }); 33 | 34 | it('should get any arbitrary arguments given on action', function() { 35 | action(1337, 'ninja'); 36 | 37 | return assert.eventually.deepEqual(promise, [1337, 'ninja']); 38 | }); 39 | 40 | describe('using a store and listening to it', function() { 41 | beforeEach(function () { 42 | store = Reflux.createStore({ 43 | init: function() { 44 | this.listenTo(action, this.trigger); 45 | } 46 | }); 47 | 48 | component.listenTo(store, function() {}); 49 | }); 50 | 51 | it('should be possible to listen to the store using two different components', function() { 52 | var component2 = Object.create(Reflux.ListenerMixin); 53 | component2.listenTo(store, function() {}); 54 | }); 55 | }); 56 | 57 | describe('get initial state', function () { 58 | beforeEach(function() { 59 | component.componentWillUnmount(); 60 | }); 61 | 62 | function mountComponent() { 63 | delete component.subscriptions; 64 | promise = Q.Promise(function(resolve) { 65 | var setData = function () { 66 | resolve(Array.prototype.slice.call(arguments, 0)); 67 | }; 68 | component.listenTo(store, setData, setData); 69 | }); 70 | } 71 | 72 | it('should get initial state from getInitialState()', function () { 73 | store = Reflux.createStore({ 74 | getInitialState: function () { 75 | return 'initial state'; 76 | } 77 | }); 78 | mountComponent(); 79 | return assert.eventually.equal(promise, 'initial state'); 80 | }); 81 | 82 | it('should get initial state from getInitialState() returned promise', function () { 83 | store = Reflux.createStore({ 84 | getInitialState: function () { 85 | return Q.Promise(function (resolve) { 86 | setTimeout(function () { 87 | resolve('initial state'); 88 | }, 20); 89 | }); 90 | } 91 | }); 92 | mountComponent(); 93 | return assert.eventually.equal(promise, 'initial state'); 94 | }); 95 | }); 96 | 97 | it("should include ListenerMethods",function(){ 98 | var s = Reflux.createStore({}); 99 | for(var m in Reflux.ListenerMethods){ 100 | assert.equal(s[m],Reflux.ListenerMethods[m]); 101 | } 102 | }); 103 | 104 | it("should use ListenerMethods.stopListeningToAll as componentWillUnmount",function(){ 105 | assert.equal(Reflux.ListenerMixin.componentWillUnmount,Reflux.ListenerMethods.stopListeningToAll); 106 | }); 107 | 108 | it("should not mix in its own methods into ListenerMethods",function(){ 109 | assert.isUndefined(Reflux.ListenerMethods.componentWillUnmount); 110 | }); 111 | 112 | }); 113 | --------------------------------------------------------------------------------