├── .editorconfig ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── codegen ├── index.js └── templates │ ├── action.js │ ├── actionHandler.js │ ├── actions.js │ ├── constants.js │ ├── index.js │ ├── module.js │ └── store.js ├── dist └── react-flux.js ├── examples └── example1 │ ├── .gitignore │ ├── flux │ ├── actions │ │ └── user.js │ ├── constants │ │ └── user.js │ └── stores │ │ ├── father.js │ │ ├── mother.js │ │ └── user.js │ ├── gulpfile.js │ ├── index.html │ ├── index.js │ ├── index.jsx │ └── package.json ├── index.js ├── lib ├── actions.js ├── configs.js ├── constants.js ├── dispatcher.js ├── index.js ├── mixinFor.js ├── store.js └── storeActionHandler.js ├── package.json ├── test ├── actions.js ├── configs.js ├── constants.js ├── dispatcher.js ├── mixinFor.js ├── store.js └── storeActionHandler.js └── webpack.config.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | # top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | insert_final_newline = true 9 | indent_style = tab 10 | indent_size = 2 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .idea -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.11" 4 | - "0.10" 5 | script: "npm run test-cov" 6 | # Send coverage data to Coveralls 7 | after_script: "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Khaled Jouda 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/kjda/ReactFlux.svg?branch=master)](https://travis-ci.org/kjda/ReactFlux) 2 | [![Coverage Status](https://coveralls.io/repos/kjda/ReactFlux/badge.svg?branch=master)](https://coveralls.io/r/kjda/ReactFlux?branch=master) 3 | 4 | 5 | **This library is not maintained anymore!** 6 | 7 | React-Flux 8 | ========== 9 | 10 | Read about [Flux-Data-Flow](http://facebook.github.io/flux/docs/overview.html) 11 | 12 | Install 13 | ======= 14 | Npm: 15 | ``` 16 | $ npm install react-flux 17 | ``` 18 | Bower: 19 | ``` 20 | $ bower install react-flux 21 | ``` 22 | 23 | Use it 24 | ====== 25 | ``` 26 | var ReactFlux = require("react-flux"); 27 | ``` 28 | 29 | Browser: 30 | ``` 31 | 32 | ``` 33 | 34 | 35 | ReactFlux 36 | ========= 37 | ReactFlux expose the following methods: 38 | 1. createConstants( Array constants, String prefix ) 39 | 2. createActions( Object actions ) 40 | 3. createStore( Object mixin, Array listeners ) 41 | 4. dispatch( String constant, Object payload ) 42 | 5. mixinFor( Store store, ..... ) 43 | 44 | Constants 45 | ========= 46 | ``` 47 | var userConstants = ReactFlux.createConstants([ 48 | 'LOGIN', 49 | 'LOGOUT' 50 | ], 'USER'); 51 | ``` 52 | The second parameter which is a prefix to be added to all messages is optional 53 | 54 | 55 | this will result in the following: 56 | 57 | ``` 58 | { 59 | LOGIN: 'USER_LOGIN', 60 | LOGIN_SUCCESS: 'USER_LOGIN_SUCCESS', 61 | LOGIN_FAIL: 'USER_LOGIN_FAIL', 62 | LOGIN_AFTER: 'USER_LOGIN_AFTER', 63 | LOGOUT: 'USER_LOGOUT', 64 | ... 65 | } 66 | ``` 67 | 68 | It is also possible to configure constant generation, one may configure separator, success- and fail suffixes 69 | ``` 70 | ReactFlux.configs.constants.setSeparator(':'); 71 | ReactFlux.configs.constants.setSuccessSuffix('OK'); 72 | ReactFlux.configs.constants.setFailSuffix('ERROR'); 73 | ReactFlux.configs.constants.setAfterSuffix('DONE'); 74 | ``` 75 | 76 | now the previous example will result in: 77 | ``` 78 | { 79 | LOGIN: 'USER:LOGIN', 80 | LOGIN_OK: 'USER:LOGIN:OK', 81 | LOGIN_ERROR: 'USER:LOGIN:ERROR', 82 | LOGIN_DONE: 'USER:LOGIN:DONE', 83 | LOGOUT: 'USER:LOGOUT', 84 | ... 85 | } 86 | ``` 87 | to go back to default configurations use: 88 | ``` 89 | ReactFlux.configs.constants.resetToDefaults(); 90 | ``` 91 | 92 | Actions 93 | ======= 94 | ``` 95 | var userActions = ReactFlux.createActions({ 96 | 97 | login: [userConstants.LOGIN, function loginAction(username, password){ 98 | return { 99 | id: 1, 100 | username: 'Max Mustermann' 101 | } 102 | }] 103 | 104 | }); 105 | ``` 106 | calling userActions.login("mustermann", "1234567") will do the following: 107 | 1. **USER_LOGIN** gets dispatched directly before the passed callback gets executed. which means: any store handlers registered for USER_LOGIN will get invoked. 108 | 2. the passed callback gets executed, in this case **loginAction**. 109 | 3. Depending on the result of the action callback, it will either dispatch **USER_LOGIN_SUCCESS** or **USER_LOGIN_FAIL**. 110 | 4. **USER_LOGIN_AFTER** get's dispatched after step 3 111 | 112 | **USER_LOGIN_SUCCESS** gets dispatched in two cases: 113 | 1. The callback returns a value, like in the example above 114 | 2. the callback returns a promise which gets resolved 115 | 116 | **USER_LOGIN_FAIL** gets dispatched in two cases: 117 | 1. The action callback throws an exception or returns an Error 118 | 2. It returns a promise which gets rejected 119 | 120 | 121 | Stores 122 | ==== 123 | ``` 124 | var userStore = ReactFlux.createStore({ 125 | 126 | mixins: [ SomeStoreMixin, AnotherStoreMixin ], 127 | 128 | getInitialState: function(){ 129 | return { 130 | isAuth: false, 131 | data: null, 132 | isLoggingIn: false, 133 | error: null 134 | }; 135 | }, 136 | 137 | storeDidMount: function(){ 138 | //Get called when store is ready 139 | }, 140 | 141 | isAuth: function(){ 142 | return this.get('isAuth') 143 | } 144 | 145 | }, [ 146 | 147 | /** 148 | * called directly before executing login action 149 | */ 150 | [userConstants.LOGIN, function onLogin(){ 151 | this.setState({ 152 | isLoggingIn: true, 153 | error: null 154 | }); 155 | }], 156 | 157 | /** 158 | * called if login action was successful 159 | */ 160 | [userConstants.LOGIN_SUCCESS, function onLoginSuccess(payload){ 161 | this.setState({ 162 | isAuth: true, 163 | data: payload 164 | }); 165 | }], 166 | 167 | /** 168 | * called if login action failed 169 | */ 170 | [userConstants.LOGIN_FAIL, function onloginFail(error){ 171 | this.setState({ 172 | error: error.message 173 | }); 174 | }] 175 | 176 | /** 177 | * called after login action succeeds or fails 178 | */ 179 | [userConstants.LOGIN_AFTER, function onloginAfter(error){ 180 | this.setState({ 181 | isLoggingIn: false 182 | }); 183 | }], 184 | 185 | ]); 186 | ``` 187 | 188 | 189 | ReactFlux.createStore takes two parameters: 190 | 191 | 192 | 1. A mixin object for the store: 193 | 194 | 195 | > Thanks to [Leland Richardson][1] store definition accepts mixins which get mixed into the store. A store mixin may be recursive and it may hook into store lifecycle events i.e getInitialState and storeDidMount. Please have a look at the tests for more insights. 196 | 197 | 2. an array of action listeners 198 | 199 | 200 | ###all *_SUCCESS callbacks get payload as parameter, which is the value returned from an actioin, or the payload passed to it's promise resolve function 201 | 202 | ###all *_FAIL callbacks get an Error object, or whatever you pass to a promise reject function 203 | 204 | ###Store API 205 | #####store.setState(obj) 206 | sets the state of the store 207 | #####store.replaceState(obj) 208 | replaces the state of the store 209 | #####store.getState() 210 | gets the store state. getState does not return a copy or a clone of state, rather it returns the object itself. 211 | #####store.getStateClone() 212 | gets a clone of the store state 213 | #####store.get(key) 214 | gets the value corresponding to the key from store's state. *get* does not return a clone of the value if it's an object, rather it returns the object itself. 215 | #####store.getClone(key) 216 | gets a clone of the value corresponding to the key from store's state 217 | #####store.onChange(callback) 218 | registers a callback which gets invoked whenever store's state changes 219 | #####store.offChange(callback) 220 | deregisters callback from store changes 221 | #####store.mixinFor() 222 | Each store exposes a "mixinFor" method which returns a ReactMixin, so that you don't need to manually couple your your components with different stores. If you use this mixin you must implement a getStateFromStores method on the component which gets called in componentWillMount and whenever the store's state gets updated 223 | ``` 224 | var fooComponent = React.createClass({ 225 | mixins: [ 226 | fooStore.mixinFor() 227 | ], 228 | getStateFromStores: function(){ 229 | return { 230 | fooState: fooStore.getState() 231 | }; 232 | }, 233 | 234 | }); 235 | ``` 236 | #####store.addActionHandler(constant, actionHandlerMixin) 237 | register an action handler for the given constant.. please refer to ActionHandlers sections for more details. 238 | 239 | #####store.getActionState(constant) 240 | returns a copy of the state of the action handler related to the given constant 241 | #####store.setActionState(const, newState) 242 | sets the state of the action handler related to the given constant 243 | 244 | 245 | ### Store Action Handlers 246 | stores also provide a different way for setting handlers, namely through StoreActionHandler. 247 | StoreActionHandler is an object defining handlers for all action possible constants. It provides a **seperate sub-state** specific to every single action handler. This could be useful when maintaining different UI states in a store that is used by different UI views. This way we don't need to pollute a Store's state with many variables correlating to the state of UI Views, we can just dump those variables into sub-states, while keeping store's state dedicated to real data. 248 | 249 | ``` 250 | UserStore.addActionHandler(constants.SAVE_NEW_USERNAME, { 251 | 252 | //returns initial state specific only to this handler 253 | getInitialState: function(){ 254 | isSaving: false, 255 | error: null, 256 | success: false 257 | }, 258 | 259 | //this gets called before the action associated with SAVE_NEW_USERNAME is executed 260 | before: function(){ 261 | //this inside handler callbacks refers to the action handler itself and not to the store 262 | this.setState({ 263 | isSaving: true, 264 | error: null 265 | }); 266 | }, 267 | 268 | //this gets called after the action associated with SAVE_NEW_USERNAME succeeds or fails 269 | after: function(){ 270 | this.setState({ 271 | isSaving: false 272 | }); 273 | }, 274 | 275 | //this gets called if the action associated with SAVE_NEW_USERNAME succeeds 276 | success: function(payload){ 277 | this.setState({ 278 | success: true 279 | }); 280 | 281 | //here we set the state of parent store(UserStore) using this.parent.setState 282 | this.parent.setState({ 283 | username: payload.username 284 | }); 285 | }, 286 | 287 | //this gets called if the action associated with SAVE_NEW_USERNAME fails 288 | fail: function(error){ 289 | this.setState({ 290 | error: error 291 | }); 292 | } 293 | 294 | }); 295 | ``` 296 | 297 | **getInitialState**, **before**, **after**, **success** and fail are optional. 298 | 299 | 300 | We can access handler specific state using: 301 | ``` 302 | UserStore.getActionState(constants.SAVE_NEW_USERNAME); // returns the state object 303 | ``` 304 | or we can get a specific property from handler state 305 | ``` 306 | UserStore.getActionState(constants.SAVE_NEW_USERNAME, 'isSaving'); 307 | ``` 308 | to reset a handler state use: 309 | ``` 310 | UserStore.resetActionState(constants.SAVE_NEW_USERNAME); 311 | ``` 312 | to set a handler state use: 313 | ``` 314 | UserStore.setActionState(constants.SAVE_NEW_USERNAME, { 315 | //..... 316 | }); 317 | ``` 318 | 319 | setting handler state will cause the store to emit a change event 320 | 321 | Inside an ActionHandler the parent store is accessible through this.parent. So, to update the state of the parent store from within the actionHandler, use **this.parent.setState** 322 | 323 | Code Generation 324 | =============== 325 | ReactFlux ships with a code generation utility which can make life a lot easier. 326 | To use this functionality create a special js file in your working directory: 327 | 328 | flux.js 329 | ``` 330 | #!/usr/bin/env node 331 | require('react-flux/codegen'); 332 | ``` 333 | 334 | make it executable 335 | ``` 336 | chmod +x flux.js 337 | ``` 338 | 339 | then look at the help 340 | ``` 341 | ./flux --help 342 | ``` 343 | 344 | code generator will put your files into "flux" directory by default. if you want to change it, create another file "reactflux.json" in the same directory as "flux.js" and specify where you want to have your flux folder 345 | ``` 346 | { 347 | "directory": "my-special-flux-directory" 348 | } 349 | ``` 350 | 351 | 352 | 353 | Example React component 354 | ======================= 355 | ``` 356 | var LoginComponent = React.createClass({ 357 | 358 | mixins: [ 359 | ReactFlux.mixinFor(userStore) //or userStore.mixin() 360 | ], 361 | 362 | getStateFromStores: function(){ 363 | return { 364 | user: userStore.state 365 | }; 366 | }, 367 | 368 | render: function(){ 369 | if( !this.state.user.get('isAuth') ){ 370 | return this.renderLogin(); 371 | } 372 | return this.renderLogout(); 373 | }, 374 | 375 | renderLogin: function(){ 376 | if( this.state.user.get('isLoggingIn') ){ 377 | return (
...
); 378 | } 379 | return( 380 |
381 | Hello Stranger! 382 | login 383 |
384 | ); 385 | }, 386 | 387 | renderLogout: function(){ 388 | return( 389 |
390 | Hello {this.state.user.get('data').username}! 391 | Logout 392 |
393 | ); 394 | }, 395 | 396 | login: function(){ 397 | userActions.login("mustermann", "1234567"); 398 | return false; 399 | }, 400 | 401 | logout: function(){ 402 | userActions.logout(); 403 | return false; 404 | } 405 | 406 | }); 407 | ``` 408 | [1]: https://github.com/lelandrichardson 409 | -------------------------------------------------------------------------------- /codegen/index.js: -------------------------------------------------------------------------------- 1 | var Template = require('underscore').template; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var colors = require('colors'); 5 | var commander = require('commander'); 6 | 7 | var FLUX_DIR, CONSTANTS_DIR, ACTIONS_DIR, STORES_DIR, INDEX_PATH; 8 | 9 | if( fs.existsSync('./reactflux.json') ){ 10 | var configs = fs.readFileSync(path.resolve('./reactflux.json')).toString(); 11 | configs = JSON.parse(configs); 12 | FLUX_DIR = configs.directory; 13 | } 14 | else{ 15 | FLUX_DIR = path.resolve('./flux'); 16 | } 17 | 18 | CONSTANTS_DIR = FLUX_DIR + '/constants'; 19 | ACTIONS_DIR = FLUX_DIR + '/actions'; 20 | STORES_DIR = FLUX_DIR + '/stores'; 21 | INDEX_PATH = FLUX_DIR + '/index.js'; 22 | var DID_INIT_FLUX = fs.existsSync(INDEX_PATH); 23 | 24 | function initFluxDirectory(){ 25 | var directories = [ 26 | FLUX_DIR, 27 | CONSTANTS_DIR, 28 | ACTIONS_DIR, 29 | STORES_DIR 30 | ]; 31 | for(var i = 0; i < directories.length; i++){ 32 | if( !fs.existsSync(directories[i]) ){ 33 | log("Creating directory: " + directories[i]); 34 | fs.mkdirSync(directories[i]); 35 | } 36 | } 37 | if( !fs.existsSync(INDEX_PATH) ){ 38 | var contents = getDefaultIndexFile(); 39 | fs.writeFileSync(INDEX_PATH, contents); 40 | } 41 | } 42 | 43 | 44 | function logError(error){ 45 | console.error(error.red); 46 | } 47 | 48 | function log(msg){ 49 | console.log(msg.blue); 50 | } 51 | 52 | function getTemplate(templateName){ 53 | var templatePath = path.resolve(__dirname + '/templates/' + templateName + '.js'); 54 | var content = fs.readFileSync(templatePath).toString(); 55 | return Template(content); 56 | } 57 | 58 | function getFluxFilePaths(name){ 59 | return { 60 | constants: path.resolve(CONSTANTS_DIR + '/' + name + '.js'), 61 | actions: path.resolve(ACTIONS_DIR + '/' + name + '.js'), 62 | store: path.resolve(STORES_DIR + '/' + name + '.js'), 63 | module: path.resolve(FLUX_DIR + '/' + name + '.js') 64 | }; 65 | } 66 | 67 | function getDefaultConstantsFile(name){ 68 | var template = getTemplate('constants'); 69 | return template({ 70 | PREFIX: ", '" + name.toUpperCase() + "'" 71 | }); 72 | } 73 | 74 | function getDefaultActionsFile(name){ 75 | var template = getTemplate('actions'); 76 | return template({ 77 | name: name 78 | }); 79 | } 80 | 81 | function getDefaultStoreFile(name){ 82 | var template = getTemplate('store'); 83 | return template({ 84 | name: name 85 | }); 86 | } 87 | 88 | function getDefaultModuleFile(name){ 89 | var template = getTemplate('module'); 90 | return template({ 91 | name: name 92 | }); 93 | } 94 | 95 | function getDefaultIndexFile(){ 96 | var template = getTemplate('index'); 97 | return template(); 98 | } 99 | 100 | 101 | function createFlux(name){ 102 | var fluxPaths = getFluxFilePaths(name); 103 | if (fs.existsSync(fluxPaths.module)) { 104 | logError("FILE EXISTS: " + fluxPaths.module); 105 | return; 106 | } 107 | if (fs.existsSync(fluxPaths.constants)) { 108 | logError("FILE EXISTS: " + fluxPaths.constants); 109 | return; 110 | } 111 | if (fs.existsSync(fluxPaths.actions)) { 112 | logError("FILE EXISTS: " + fluxPaths.actions); 113 | return; 114 | } 115 | if (fs.existsSync(fluxPaths.store)) { 116 | logError("FILE EXISTS: " + fluxPaths.store); 117 | return; 118 | } 119 | log("Creating file: " + fluxPaths.module); 120 | fs.writeFileSync(fluxPaths.module, getDefaultModuleFile(name)); 121 | log("Creating file: " + fluxPaths.constants); 122 | fs.writeFileSync(fluxPaths.constants, getDefaultConstantsFile(name)); 123 | log("Creating file: " + fluxPaths.actions); 124 | fs.writeFileSync(fluxPaths.actions, getDefaultActionsFile(name)); 125 | log("Creating file: " + fluxPaths.store); 126 | fs.writeFileSync(fluxPaths.store, getDefaultStoreFile(name)); 127 | addFluxToIndex(name); 128 | } 129 | 130 | function addFluxToIndex(name){ 131 | console.log("ADDING TO INDEX"); 132 | var contents = fs.readFileSync(INDEX_PATH).toString(); 133 | var lines = contents.split("\n"); 134 | var newLines = []; 135 | for(var i=0, len = lines.length; i < len; i++){ 136 | newLines.push(lines[i]); 137 | if( lines[i].match('module\\.exports') ){ 138 | newLines.push( "\t" + name + ": require('./" + name + "')," ); 139 | } 140 | } 141 | fs.writeFileSync(INDEX_PATH, newLines.join("\n")); 142 | } 143 | 144 | function createConstant(filePath, constantName) { 145 | constantName = constantName.toUpperCase(); 146 | if (!fs.existsSync(filePath)) { 147 | logError("FILE DOES NOT EXIST: " + filePath); 148 | return false; 149 | } 150 | var fileContents = fs.readFileSync(filePath).toString(); 151 | if (fileContents.match("'" + constantName + "'")) { 152 | var msg = "ERROR: Constant '" + constantName + "' seems to exists in " + filePath + ""; 153 | logError(msg); 154 | return false; 155 | } 156 | var lines = fileContents.split('\n'); 157 | var newLines = []; 158 | for (var i = 0, len = lines.length; i < len; i++) { 159 | var line = lines[i].trim(); 160 | newLines.push(lines[i]); 161 | if (lines[i].match('createConstants')) { 162 | var nextLine = lines[i + 1].trim(); 163 | if (nextLine.match('\'')) { 164 | newLines.push("\t'" + constantName + "',"); 165 | } else { 166 | newLines.push("\t'" + constantName + "'"); 167 | } 168 | } 169 | } 170 | fs.writeFileSync(filePath, newLines.join("\n")); 171 | return true; 172 | } 173 | 174 | function createAction(filePath, constantName) { 175 | constantName = constantName.toUpperCase(); 176 | if (!fs.existsSync(filePath)) { 177 | logError("FILE DOES NOT EXIST: " + filePath); 178 | return false; 179 | } 180 | var ps = constantName.toLowerCase().split('_'); 181 | var actionName = ps[0]; 182 | for(var i=1; i < ps.length; i++){ 183 | actionName += ps[i].charAt(0).toUpperCase() + ps[i].substr(1); 184 | } 185 | var fileContents = fs.readFileSync(filePath).toString(); 186 | if (fileContents.match(actionName + ":")) { 187 | logError("ERROR: Action for '" + constantName + "' seems to exists in " + filePath); 188 | return false; 189 | } 190 | 191 | var lines = fileContents.split('\n'); 192 | var newLines = []; 193 | for (var i = 0, len = lines.length; i < len; i++) { 194 | newLines.push(lines[i]); 195 | if (lines[i].match(".createActions")) { 196 | var template = getTemplate('action'); 197 | var contents = template({ 198 | actionName: actionName, 199 | constant: constantName 200 | }); 201 | newLines.push(contents); 202 | } 203 | } 204 | fs.writeFileSync(filePath, newLines.join("\n")); 205 | return true; 206 | } 207 | 208 | function createActionHandler(filePath, constantName) { 209 | constantName = constantName.toUpperCase(); 210 | if (!fs.existsSync(filePath)) { 211 | logError("FILE DOES NOT EXIST: " + filePath); 212 | return false; 213 | } 214 | var fileContents = fs.readFileSync(filePath).toString(); 215 | var expr = "addActionHandler\\(constants\\." + constantName; 216 | if ( fileContents.match(expr) ) { 217 | logError("ERROR: ActionHandler for '" + constantName + "' seems to exists in " + filePath); 218 | return false; 219 | } 220 | var lines = fileContents.split('\n'); 221 | var newLines = []; 222 | for (var i = 0, len = lines.length; i < len; i++) { 223 | if (lines[i].match('module\\.exports')) { 224 | var template = getTemplate('actionHandler'); 225 | var handlerContents = template({ 226 | constant: constantName 227 | }); 228 | newLines.push(handlerContents); 229 | } 230 | newLines.push(lines[i]); 231 | } 232 | fs.writeFileSync(filePath, newLines.join("\n")); 233 | return true; 234 | } 235 | 236 | 237 | commander.version('0.0.1'); 238 | 239 | commander 240 | .command('init') 241 | .description('initiates flux') 242 | .action(function(){ 243 | initFluxDirectory(); 244 | }); 245 | 246 | commander 247 | .command('flux ') 248 | .description('Creates a flux group(constants, actions and store)') 249 | .action(function (fluxName) { 250 | if( !DID_INIT_FLUX ){ 251 | logError('You did not init flux. run init command'); 252 | return; 253 | } 254 | createFlux(fluxName); 255 | }); 256 | 257 | commander 258 | .command('constant ') 259 | .option('-a', 'create a corresponding action') 260 | .option('-s', 'create a corresponding storeActionHandler') 261 | .description('Creates a constant') 262 | .action(function (fluxName, constant, opts) { 263 | if( !DID_INIT_FLUX ){ 264 | logError('You did not init flux. run init command'); 265 | return; 266 | } 267 | var paths = getFluxFilePaths(fluxName); 268 | if( createConstant(paths.constants, constant) ){ 269 | log('Created a new constant [' + constant + '] in [' + fluxName + ']'); 270 | } 271 | if( !!opts.A && createAction(paths.actions, constant) ){ 272 | log('Created an action for constant [' + constant + ']'); 273 | } 274 | if( !!opts.S && createActionHandler(paths.store, constant) ){ 275 | log('Created an actionHandler for constant [' + constant + ']'); 276 | } 277 | }); 278 | 279 | commander.on('--help', function(){ 280 | console.log(' Options for constant creation:'); 281 | console.log(' -a: creates a corresponding action'); 282 | console.log(' -h: creates a corresponding storeActionHandler'); 283 | console.log(''); 284 | console.log(" Examples:"); 285 | var progName = module.parent.filename.split('/').pop(); 286 | console.log(" Init flux directory:"); 287 | console.log(" ./" + progName + ' init\n'); 288 | console.log(" Create user flux group:"); 289 | console.log(" ./" + progName + ' flux user\n'); 290 | console.log(" Create a constant[LOAD] for user group:"); 291 | console.log(" ./" + progName + ' constant user LOAD\n'); 292 | console.log(" Create a constant[LOAD] for user group along with a corresponding action:"); 293 | console.log(" ./" + progName + ' constant -a user LOAD\n'); 294 | console.log(" Create a constant[LOAD] for user group along with a corresponding store handler:"); 295 | console.log(" ./" + progName + ' constant -s user LOAD\n'); 296 | console.log(" Create a constant[LOAD] for user group along with corresponding action and store handler:"); 297 | console.log(" ./" + progName + ' constant -as user LOAD\n'); 298 | }); 299 | 300 | commander.parse(process.argv); 301 | -------------------------------------------------------------------------------- /codegen/templates/action.js: -------------------------------------------------------------------------------- 1 | 2 | <%= actionName %>: [constants.<%= constant%>, function(){ 3 | return {}; 4 | }], 5 | -------------------------------------------------------------------------------- /codegen/templates/actionHandler.js: -------------------------------------------------------------------------------- 1 | 2 | Store.addActionHandler(constants.<%= constant %>, { 3 | 4 | getInitialState: function(){ 5 | return { 6 | 7 | }; 8 | }, 9 | 10 | before: function(){ 11 | this.setState( 12 | this.getInitialState() 13 | ); 14 | }, 15 | 16 | success: function(resp){ 17 | 18 | }, 19 | 20 | fail: function(resp){ 21 | 22 | }, 23 | 24 | after: function(){ 25 | 26 | } 27 | 28 | }); -------------------------------------------------------------------------------- /codegen/templates/actions.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('react-flux'); 2 | var constants = require('../constants/<%= name %>'); 3 | 4 | var Actions = ReactFlux.createActions({ 5 | 6 | }); 7 | 8 | module.exports = Actions; -------------------------------------------------------------------------------- /codegen/templates/constants.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('react-flux'); 2 | 3 | var Constants = ReactFlux.createConstants([ 4 | ]<%= PREFIX %>); 5 | 6 | module.exports = Constants; -------------------------------------------------------------------------------- /codegen/templates/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 3 | }; -------------------------------------------------------------------------------- /codegen/templates/module.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | constants: require('./constants/<%= name %>.js'), 3 | actions: require('./actions/<%= name %>.js'), 4 | store: require('./stores/<%= name %>.js'), 5 | }; -------------------------------------------------------------------------------- /codegen/templates/store.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('react-flux'); 2 | var constants = require('../constants/<%= name %>'); 3 | 4 | var Store = ReactFlux.createStore({ 5 | 6 | displayName: '<%= name %>Store', 7 | 8 | getInitialState: function(){ 9 | return { 10 | }; 11 | } 12 | 13 | }); 14 | 15 | 16 | module.exports = Store; -------------------------------------------------------------------------------- /dist/react-flux.js: -------------------------------------------------------------------------------- 1 | !function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n(require("promise")):"function"==typeof define&&define.amd?define(["promise"],n):"object"==typeof exports?exports.ReactFlux=n(require("promise")):t.ReactFlux=n(t.promise)}(this,function(t){return function(t){function n(r){if(e[r])return e[r].exports;var o=e[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,n),o.loaded=!0,o.exports}var e={};return n.m=t,n.c=e,n.p="",n(0)}([function(t,n,e){var r=e(28),o=e(31),i=e(45),s=e(1),a=e(56),c=e(30),u=new a;t.exports={configs:c,createActions:function(t){return new o(u,t)},createStore:function(t,n){return new i(u,t,n)},createConstants:function(t,n){return new r(t,n)},dispatch:function(t,n){u.dispatch(t,n)},dispatcher:u,mixinFor:s}},function(t,n,e){var r=e(2);t.exports=function(){var t=Array.prototype.slice.call(arguments);if(!t.length)throw new Error("Flux.mixinFor expects a store or a list of stores");return r(t,function(t){var n="undefined"==typeof t||"function"!=typeof t.onChange||"function"!=typeof t.offChange;if(n)throw new Error("Flux.mixinFor expects a store or an array of stores")}),{componentWillMount:function(){"undefined"==typeof this._react_flux_onChange&&(this._react_flux_onChange=function(){this.isMounted()&&this.setState(this.getStateFromStores())}.bind(this)),this.setState(this.getStateFromStores())},componentDidMount:function(){for(var n=0;n-1&&t%1==0&&o>=t}var o=9007199254740991;t.exports=r},function(t,n,e){function r(t){for(var n=c(t),e=n.length,r=e&&t.length,u=!!r&&a(r)&&(i(t)||o(t)),p=-1,l=[];++p-1&&t%1==0&&n>t}var o=/^\d+$/,i=9007199254740991;t.exports=r},function(t,n,e){function r(t){if(null==t)return[];c(t)||(t=Object(t));var n=t.length;n=n&&a(n)&&(i(t)||o(t))&&n||0;for(var e=t.constructor,r=-1,u="function"==typeof e&&e.prototype===t,p=Array(n),l=n>0;++r0&&(n+=s.separator);var e={};return r(t,function(t){if(!i(t))throw new Error("createConstants expects all constants to be strings");e[t]=n+t,e[t+"_"+s.successSuffix]=n+t+s.separator+s.successSuffix,e[t+"_"+s.failSuffix]=n+t+s.separator+s.failSuffix,e[t+"_"+s.afterSuffix]=n+t+s.separator+s.afterSuffix}),e}},function(t,n,e){function r(t){return"string"==typeof t||o(t)&&a.call(t)==i}var o=e(14),i="[object String]",s=Object.prototype,a=s.toString;t.exports=r},function(t,n,e){var r=e(29),o="_",i="SUCCESS",s="FAIL",a="AFTER",c={constants:{separator:o,successSuffix:i,failSuffix:s,afterSuffix:a}};t.exports={constants:{setSeparator:function(t){if(!r(t)||!t.length)throw new Error("Constants.separator must be a non empty string");c.constants.separator=t},setSuccessSuffix:function(t){if(!r(t)||!t.length)throw new Error("Constants.successSuffix must be a non empty string");c.constants.successSuffix=t},setFailSuffix:function(t){if(!r(t)||!t.length)throw new Error("Constants.failSuffix must be a non empty string");c.constants.failSuffix=t},setAfterSuffix:function(t){if(!r(t)||!t.length)throw new Error("Constants.afterSuffix must be a non empty string");c.constants.afterSuffix=t},resetToDefaults:function(){c.constants.separator=o,c.constants.successSuffix=i,c.constants.failSuffix=s,c.constants.afterSuffix=a},get:function(){return c.constants}}}},function(t,n,e){var r=e(32),o=e(2),i=e(21),s=s||e(44),a=e(30).constants.get(),c=function(t,n){this._dispatcher=t,this._registerActions(n)};c.prototype=r(c.prototype,{_registerActions:function(t){o(t,function(t,n){if(!i(t))throw new Error("ReactFlux.Actions: Action must be an array {login: [CONSTANT, callback]}");var e=t[0],r=t[1];if("undefined"==typeof r)r=function(){};else if("function"!=typeof r)throw new Error("ReactFlux.Actions: you did not provide a valid callback for action: "+n);this[n]=this._createAction(n,e,r)}.bind(this))},_createAction:function(t,n,e){return function(){this._dispatch(n,null,arguments);var t=null;try{if(t=e.apply(this,arguments),t&&"object"==typeof t&&"[object Error]"==Object.prototype.toString.call(t))throw t}catch(r){t=new s(function(t,n){n(r)})}s.resolve(t).then(function(t){this._dispatch(n,"successSuffix",t),this._dispatch(n,"afterSuffix",t)}.bind(this),function(t){this._dispatch(n,"failSuffix",t),this._dispatch(n,"afterSuffix",t)}.bind(this))["catch"](function(t){console.error(t.toString(),t.stack)})}.bind(this)},_dispatch:function(t,n,e){n&&(t+=a.separator+a[n]),this._dispatcher.dispatch(t,e)}}),t.exports=c},function(t,n,e){var r=e(33),o=e(41),i=o(r);t.exports=i},function(t,n,e){function r(t,n,e,l,h){if(!c(t))return t;var d=a(n)&&(s(n)||f(n)),v=d?void 0:p(n);return o(v||n,function(o,s){if(v&&(s=o,o=n[s]),u(o))l||(l=[]),h||(h=[]),i(t,n,s,r,e,l,h);else{var a=t[s],c=e?e(a,o,s,t,n):void 0,f=void 0===c;f&&(c=o),void 0===c&&(!d||s in t)||!f&&(c===c?c===a:a!==a)||(t[s]=c)}}),t}var o=e(3),i=e(34),s=e(21),a=e(15),c=e(9),u=e(14),f=e(38),p=e(10);t.exports=r},function(t,n,e){function r(t,n,e,r,p,l,h){for(var d=l.length,v=n[e];d--;)if(l[d]==v)return void(t[e]=h[d]);var g=t[e],y=p?p(g,v,e,t,n):void 0,b=void 0===y;b&&(y=v,a(v)&&(s(v)||u(v))?y=s(g)?g:a(g)?o(g):[]:c(v)||i(v)?y=i(g)?f(g):c(g)?g:{}:b=!1),l.push(v),h.push(y),b?t[e]=r(y,v,p,l,h):(y===y?y!==g:g===g)&&(t[e]=y)}var o=e(35),i=e(20),s=e(21),a=e(15),c=e(36),u=e(38),f=e(39);t.exports=r},function(t,n,e){function r(t,n){var e=-1,r=t.length;for(n||(n=Array(r));++e2?e[s-2]:void 0,c=s>2?e[2]:void 0,u=s>1?e[s-1]:void 0;for("function"==typeof a?(a=o(a,u,5),s-=2):(a="function"==typeof u?u:void 0,s-=a?1:0),c&&i(e[0],e[1],c)&&(a=3>s?void 0:a,s=1);++re;e++)if(this._events[t][e]===n){this._events[t].splice(e,1);break}},_emit:function(t){if("undefined"!=typeof this._events[t]){var n=Array.prototype.slice.call(arguments,1);i(this._events[t],function(t){"function"==typeof t&&t.apply(null,n)})}}},t.exports=p},function(t,n,e){function r(t,n,e){return"function"==typeof n?o(t,!0,i(n,e,3)):o(t,!0)}var o=e(47),i=e(26);t.exports=r},function(t,n,e){function r(t,n,e,d,v,g,y){var x;if(e&&(x=v?e(t,d,v):e(t)),void 0!==x)return x;if(!l(t))return t;var _=p(t);if(_){if(x=c(t),!n)return o(t,x)}else{var j=B.call(t),w=j==b;if(j!=S&&j!=h&&(!w||v))return R[j]?u(t,j,n):v?t:{};if(x=f(w?{}:t),!n)return s(x,t)}g||(g=[]),y||(y=[]);for(var m=g.length;m--;)if(g[m]==t)return y[m];return g.push(t),y.push(x),(_?i:a)(t,function(o,i){x[i]=r(o,n,e,i,t,g,y)}),x}var o=e(35),i=e(3),s=e(48),a=e(5),c=e(49),u=e(50),f=e(52),p=e(21),l=e(9),h="[object Arguments]",d="[object Array]",v="[object Boolean]",g="[object Date]",y="[object Error]",b="[object Function]",x="[object Map]",_="[object Number]",S="[object Object]",j="[object RegExp]",w="[object Set]",m="[object String]",A="[object WeakMap]",E="[object ArrayBuffer]",C="[object Float32Array]",I="[object Float64Array]",F="[object Int8Array]",O="[object Int16Array]",D="[object Int32Array]",H="[object Uint8Array]",k="[object Uint8ClampedArray]",M="[object Uint16Array]",U="[object Uint32Array]",R={};R[h]=R[d]=R[E]=R[v]=R[g]=R[C]=R[I]=R[F]=R[O]=R[D]=R[_]=R[S]=R[j]=R[m]=R[H]=R[k]=R[M]=R[U]=!0,R[y]=R[b]=R[x]=R[w]=R[A]=!1;var P=Object.prototype,B=P.toString;t.exports=r},function(t,n,e){function r(t,n){return null==n?t:o(n,i(n),t)}var o=e(40),i=e(10);t.exports=r},function(t,n,e){function r(t){var n=t.length,e=new t.constructor(n);return n&&"string"==typeof t[0]&&i.call(t,"index")&&(e.index=t.index,e.input=t.input),e}var o=Object.prototype,i=o.hasOwnProperty;t.exports=r},function(t,n,e){function r(t,n,e){var r=t.constructor;switch(n){case f:return o(t);case i:case s:return new r(+t);case p:case l:case h:case d:case v:case g:case y:case b:case x:var S=t.buffer;return new r(e?o(S):S,t.byteOffset,t.length);case a:case u:return new r(t);case c:var j=new r(t.source,_.exec(t));j.lastIndex=t.lastIndex}return j}var o=e(51),i="[object Boolean]",s="[object Date]",a="[object Number]",c="[object RegExp]",u="[object String]",f="[object ArrayBuffer]",p="[object Float32Array]",l="[object Float64Array]",h="[object Int8Array]",d="[object Int16Array]",v="[object Int32Array]",g="[object Uint8Array]",y="[object Uint8ClampedArray]",b="[object Uint16Array]",x="[object Uint32Array]",_=/\w*$/;t.exports=r},function(t,n,e){(function(n){function e(t){var n=new r(t.byteLength),e=new o(n);return e.set(new o(t)),n}var r=n.ArrayBuffer,o=n.Uint8Array;t.exports=e}).call(n,function(){return this}())},function(t,n,e){function r(t){var n=t.constructor;return"function"==typeof n&&n instanceof n||(n=Object),new n}t.exports=r},function(t,n,e){var r=e(54),o=e(48),i=e(41),s=i(function(t,n,e){return e?r(t,n,e):o(t,n)});t.exports=s},function(t,n,e){function r(t,n,e){for(var r=-1,i=o(n),s=i.length;++rc;c++){var u=s[c];if(null!==this[u]){if("function"!=typeof this[u])throw new Error('StoreActionHandler expects "'+u+'" to be a function');var f=this.constant;"before"!==u&&(f+=i.separator+i[u+"Suffix"]),r.push([f,this[u].bind(this)])}}t._setConstantHandlers(r)}var o=e(29),i=e(30).constants.get(),s=["before","after","success","fail"];r.prototype={setState:function(t){this.parent.setActionState(this.constant,t)},getState:function(){return this.parent.getActionState(this.constant)}},t.exports=r},function(t,n,e){function r(){this._registry={}}var o=o||e(44),i=e(32),s=e(21),a=e(29),c=e(2);r.prototype=i(r.prototype,{register:function(t,n,e){if(!a(t)||!t.length)throw new Error("Dispatcher.register: constant must be a string");if(e=e||null,"function"!=typeof n)throw new Error("Dispatcher.register expects second parameter to be a callback");if(null!==e&&!s(e))throw new Error("Dispatcher.register expects third parameter to be null or an array");var r=this._getRegistry(t);return r.callbacks.push(n),r.waitFor.push(e),r.callbacks.length-1},dispatch:function(t,n){var e=this._getRegistry(t);e.dispatchQueue.push({constant:t,payload:n}),this._dispatch(e)},_dispatch:function(t){if(!t.isDispatching&&t.dispatchQueue.length){t.isDispatching=!0;var n=t.dispatchQueue.shift();this._createDispatchPromises(t),c(t.callbacks,function(e,r){var i=function(t,n,e){return function(){o.resolve(t.callbacks[n](e)).then(function(){t.resolves[n](e)},function(){t.rejects[n](new Error("Dispatch callback error"))})}}(t,r,n.payload),s=t.waitFor[r];if(s){var a=this._getPromisesByIndexes(t,s);o.all(a).then(i,i)}else i()}.bind(this)),o.all(t.promises).then(function(){this._onDispatchEnd(t)}.bind(this),function(){this._onDispatchEnd(t)})}},_getRegistry:function(t){return"undefined"==typeof this._registry[t]&&(this._registry[t]={callbacks:[],waitFor:[],promises:[],resolves:[],rejects:[],dispatchQueue:[],isDispatching:!1}),this._registry[t]},_getPromisesByIndexes:function(t,n){return n.map(function(n){return t.promises[n]})},_createDispatchPromises:function(t){t.promises=[],t.resolves=[],t.rejects=[],c(t.callbacks,function(n,e){t.promises[e]=new o(function(n,r){t.resolves[e]=n,t.rejects[e]=r})})},_onDispatchEnd:function(t){t.promises=[],t.resolves=[],t.rejects=[],t.isDispatching=!1,this._dispatch(t)}}),t.exports=r}])}); -------------------------------------------------------------------------------- /examples/example1/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build -------------------------------------------------------------------------------- /examples/example1/flux/actions/user.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('../../../../index'); 2 | var userConstants = require('../constants/user'); 3 | 4 | module.exports = ReactFlux.createActions({ 5 | 6 | /** 7 | * Action may return a value(SUCCESS PAYLOAD), an error, or a promise 8 | */ 9 | login: [userConstants.LOGIN, function(username, password){ 10 | console.log("UserActions.login", username, password); 11 | var promise = new Promise(function(resolve, reject){ 12 | setTimeout(function(){ 13 | if( username.length ){ 14 | resolve({ 15 | id: 1, 16 | username: username 17 | }); 18 | } 19 | else{ 20 | reject(new Error('Invalid login')); 21 | } 22 | }, 500); 23 | }); 24 | return promise; 25 | }], 26 | 27 | 28 | /** 29 | * An actoin without a callback will always be successful 30 | */ 31 | logout: [userConstants.LOGOUT] 32 | 33 | 34 | }); 35 | -------------------------------------------------------------------------------- /examples/example1/flux/constants/user.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('../../../../index'); 2 | 3 | module.exports = ReactFlux.createConstants([ 4 | 'LOGIN', 5 | 'LOGOUT' 6 | ], 'USER'); -------------------------------------------------------------------------------- /examples/example1/flux/stores/father.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('../../../../index'); 2 | var userConstants = require('../constants/user'); 3 | 4 | var MotherStore = require('./mother'); 5 | 6 | var Store = ReactFlux.createStore(null, [ 7 | 8 | [userConstants.LOGIN_SUCCESS, [MotherStore], function notifyFatherOfLogin(payload){ 9 | 10 | console.log("I'm Father, I need 1 sec to answer!"); 11 | 12 | var promise = new Promise(function(resolve, reject){ 13 | setTimeout(function(){ 14 | console.log("I am Father, I have been notifed of login! Username: ", payload.username); 15 | resolve(); 16 | }, 1000); 17 | }); 18 | 19 | return promise; 20 | 21 | }] 22 | 23 | ]); 24 | 25 | 26 | module.exports = Store; -------------------------------------------------------------------------------- /examples/example1/flux/stores/mother.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('../../../../index'); 2 | var userConstants = require('../constants/user'); 3 | 4 | 5 | var Store = ReactFlux.createStore({ 6 | 7 | }, [ 8 | 9 | [userConstants.LOGIN_SUCCESS, function NotifyMotherOfLogin(payload){ 10 | 11 | console.log("I'm Mother, I need 0.5 sec to answer!"); 12 | 13 | var promise = new Promise(function(resolve, reject){ 14 | setTimeout(function(){ 15 | console.log("I'm Mother, I have been notified of login! Username:", payload.username); 16 | resolve(); 17 | }, 500); 18 | }); 19 | 20 | return promise; 21 | 22 | }] 23 | 24 | ]); 25 | 26 | module.exports = Store; -------------------------------------------------------------------------------- /examples/example1/flux/stores/user.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('../../../../index'); 2 | var userConstants = require('../constants/user'); 3 | 4 | var MotherStore = require('./mother'); 5 | var FatherStore = require('./father'); 6 | 7 | var Store = ReactFlux.createStore({ 8 | 9 | getInitialState: function(){ 10 | console.log("UserStore.getInitialState"); 11 | return { 12 | data: null, 13 | isAuth: false, 14 | error: null 15 | } 16 | }, 17 | 18 | storeDidMount: function(){ 19 | console.log("UserStore.storeDidMount"); 20 | }, 21 | 22 | getUsername: function(){ 23 | return this.get('isAuth') ? this.get('data').username : null; 24 | } 25 | 26 | }, [ 27 | 28 | /** 29 | * Dispatcher calls this directly when it receives a USER_LOGIN message, 30 | * just before it tries to execute the corresponding action 31 | */ 32 | [userConstants.LOGIN, function onLogin(payload){ 33 | console.log("UserStore.onLogin", JSON.stringify(payload)); 34 | this.setState({ 35 | isLoggingIn: true 36 | }); 37 | }], 38 | 39 | /** 40 | * This gets called if USER_LOGIN action was successfull 41 | * This store waits for MotherStore and FatherStore to process this message 42 | */ 43 | [userConstants.LOGIN_SUCCESS, [MotherStore, FatherStore], function handleLoginSuccess(payload){ 44 | console.log("UserStore.handleLogin", JSON.stringify(payload)); 45 | this.setState({ 46 | isLoggingIn: false, 47 | error: null, 48 | data: payload, 49 | isAuth: true 50 | }); 51 | }], 52 | 53 | /** 54 | * This gets called if USER_LOGIN action was unsuccessful 55 | */ 56 | [userConstants.LOGIN_FAIL, function handleLoginFailure(error){ 57 | console.log("UserStore.handleLoginFailure", error.message); 58 | this.setState({ 59 | isLoggingIn: false, 60 | error: error.message 61 | }); 62 | }] 63 | 64 | ]); 65 | 66 | Store.addActionHandler(userConstants.LOGOUT, { 67 | success: function(){ 68 | console.log("UserStore.handleLogout"); 69 | this.parent.setState({ 70 | isAuth: false, 71 | data: null 72 | }); 73 | } 74 | }); 75 | 76 | module.exports = Store; 77 | -------------------------------------------------------------------------------- /examples/example1/gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var browserify = require('gulp-browserify'); 3 | var react = require('gulp-react'); 4 | var plumber = require('gulp-plumber'); 5 | var concat = require('gulp-concat'); 6 | 7 | gulp.task('default', function(cb){ 8 | return gulp.src([ 9 | './index.jsx' 10 | ]) 11 | .pipe(plumber()) 12 | .pipe(react({ addPragma: false })) 13 | .pipe(browserify({ 14 | insertGlobals: true, 15 | debug: false 16 | })) 17 | .pipe(concat('index.js')) 18 | .pipe(gulp.dest('./')); 19 | }); 20 | -------------------------------------------------------------------------------- /examples/example1/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 18 | 19 | 20 |
21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /examples/example1/index.jsx: -------------------------------------------------------------------------------- 1 | var React = require('react'); 2 | var userStore = require('./flux/stores/user'); 3 | var userActions = require('./flux/actions/user'); 4 | 5 | var Flux = require('../../'); 6 | var App = React.createClass({ 7 | 8 | mixins: [ 9 | userStore.mixinFor() 10 | ], 11 | 12 | getStateFromStores: function(){ 13 | console.log("App.getStateFromStores"); 14 | return { 15 | user: userStore.getState() 16 | }; 17 | }, 18 | 19 | login: function(){ 20 | var username = this.refs.username.getDOMNode().value; 21 | userActions.login(username, '1234567'); 22 | return false; 23 | }, 24 | 25 | logout: function(){ 26 | userActions.logout(); 27 | return false; 28 | }, 29 | 30 | render: function(){ 31 | if( !this.state.user.isAuth ){ 32 | return this.renderLogin(); 33 | } 34 | return this.renderHome(); 35 | }, 36 | 37 | renderHome: function(){ 38 | return ( 39 |
40 |

Hello {this.state.user.data.username}!

41 | Logout 42 |
43 | ); 44 | }, 45 | 46 | renderLogin: function(){ 47 | if( this.state.user.isLoggingIn ){ 48 | return(
Logging in...
); 49 | } 50 | return( 51 |
52 |

LOGIN

53 | Username: Leave empty to cause an error 54 |
55 | 56 | {this.renderLoginError()} 57 |
58 | ); 59 | }, 60 | 61 | renderLoginError: function(){ 62 | if( !this.state.user.error ){ 63 | return; 64 | } 65 | return (
{this.state.user.error}
) 66 | } 67 | 68 | }); 69 | 70 | window.onload = function(){ 71 | React.renderComponent(, document.getElementById('__wrap')); 72 | }; 73 | -------------------------------------------------------------------------------- /examples/example1/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ReactFluxExample", 3 | "version": "0.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [ 10 | "React", 11 | "Flux" 12 | ], 13 | "author": "Khaled Jouda", 14 | "license": "MIT", 15 | "dependencies": { 16 | "react": "~0.11.0", 17 | "gulp": "~3.8.6", 18 | "gulp-browserify": "~0.5.0", 19 | "gulp-plumber": "~0.6.4", 20 | "gulp-react": "~0.5.0", 21 | "gulp-concat": "~2.3.3" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/index'); -------------------------------------------------------------------------------- /lib/actions.js: -------------------------------------------------------------------------------- 1 | var _merge = require('lodash/object/merge'); 2 | var _forEach = require('lodash/collection/forEach'); 3 | var _isArray = require('lodash/lang/isArray'); 4 | var Promise = Promise || require('promise'); 5 | 6 | var constantsConfigs = require('./configs').constants.get(); 7 | 8 | var Actions = function (dispather, actions) { 9 | 10 | this._dispatcher = dispather; 11 | this._registerActions(actions); 12 | 13 | }; 14 | 15 | Actions.prototype = _merge(Actions.prototype, { 16 | 17 | /** 18 | *@param {object} actions 19 | */ 20 | _registerActions: function (actions) { 21 | _forEach(actions, function (options, actionName) { 22 | if (!_isArray(options)) { 23 | throw new Error('ReactFlux.Actions: Action must be an array {login: [CONSTANT, callback]}'); 24 | } 25 | var constant = options[0]; 26 | var callback = options[1]; 27 | if (typeof callback === "undefined") { 28 | callback = function () { 29 | }; 30 | } 31 | else if (typeof callback != 'function') { 32 | throw new Error('ReactFlux.Actions: you did not provide a valid callback for action: ' + actionName); 33 | } 34 | this[actionName] = this._createAction(actionName, constant, callback); 35 | }.bind(this)); 36 | }, 37 | 38 | /** 39 | *@param {string} name 40 | *@param {string} constant 41 | *@param {string} callback 42 | */ 43 | _createAction: function (name, constant, callback) { 44 | return function () { 45 | this._dispatch(constant, null, arguments); 46 | var resp = null; 47 | try { 48 | resp = callback.apply(this, arguments); 49 | if (!!resp && typeof resp == 'object' && Object.prototype.toString.call(resp) == '[object Error]') { 50 | throw resp; 51 | } 52 | } catch (e) { 53 | resp = new Promise(function (_, reject) { 54 | reject(e); 55 | }); 56 | } 57 | Promise.resolve(resp).then(function (payload) { 58 | this._dispatch(constant, 'successSuffix', payload); 59 | this._dispatch(constant, 'afterSuffix', payload); 60 | }.bind(this), function (payload) { 61 | this._dispatch(constant, 'failSuffix', payload); 62 | this._dispatch(constant, 'afterSuffix', payload); 63 | }.bind(this)) 64 | .catch(function (e) { 65 | console.error(e.toString(), e.stack); 66 | }); 67 | }.bind(this); 68 | }, 69 | 70 | /** 71 | *@param {string} constant 72 | *@param {string} suffixName 73 | *@param {mixed} payload 74 | */ 75 | _dispatch: function (constant, suffixName, payload) { 76 | if (!!suffixName) { 77 | constant += constantsConfigs.separator + constantsConfigs[suffixName]; 78 | } 79 | this._dispatcher.dispatch(constant, payload); 80 | } 81 | 82 | }); 83 | 84 | module.exports = Actions; 85 | -------------------------------------------------------------------------------- /lib/configs.js: -------------------------------------------------------------------------------- 1 | var _isString = require('lodash/lang/isString'); 2 | 3 | var CONSTANTS_DEFAULT_SEPARATOR = '_'; 4 | var CONSTANTS_DEFAULT_SUCCESS_SUFFIX = 'SUCCESS'; 5 | var CONSTANTS_DEFAULT_FAIL_SUFFIX = 'FAIL'; 6 | var CONSTANTS_DEFAULT_AFTER_SUFFIX = 'AFTER'; 7 | 8 | var CONFIGS = { 9 | constants: { 10 | separator: CONSTANTS_DEFAULT_SEPARATOR, 11 | successSuffix: CONSTANTS_DEFAULT_SUCCESS_SUFFIX, 12 | failSuffix: CONSTANTS_DEFAULT_FAIL_SUFFIX, 13 | afterSuffix: CONSTANTS_DEFAULT_AFTER_SUFFIX 14 | 15 | } 16 | }; 17 | 18 | module.exports = { 19 | 20 | /** 21 | * constants 22 | */ 23 | constants: { 24 | 25 | /** 26 | * @param {string} separator 27 | */ 28 | setSeparator: function (separator) { 29 | if (!_isString(separator) || !separator.length) { 30 | throw new Error('Constants.separator must be a non empty string'); 31 | } 32 | CONFIGS.constants.separator = separator; 33 | }, 34 | 35 | /** 36 | * @param {string} suffix 37 | */ 38 | setSuccessSuffix: function (suffix) { 39 | if (!_isString(suffix) || !suffix.length) { 40 | throw new Error('Constants.successSuffix must be a non empty string'); 41 | } 42 | CONFIGS.constants.successSuffix = suffix; 43 | }, 44 | 45 | /** 46 | * @param {string} suffix 47 | */ 48 | setFailSuffix: function (suffix) { 49 | if (!_isString(suffix) || !suffix.length) { 50 | throw new Error('Constants.failSuffix must be a non empty string'); 51 | } 52 | CONFIGS.constants.failSuffix = suffix; 53 | }, 54 | 55 | /** 56 | * @param {string} suffix 57 | */ 58 | setAfterSuffix: function (suffix) { 59 | if (!_isString(suffix) || !suffix.length) { 60 | throw new Error('Constants.afterSuffix must be a non empty string'); 61 | } 62 | CONFIGS.constants.afterSuffix = suffix; 63 | }, 64 | 65 | /** 66 | * 67 | */ 68 | resetToDefaults: function () { 69 | CONFIGS.constants.separator = CONSTANTS_DEFAULT_SEPARATOR; 70 | CONFIGS.constants.successSuffix = CONSTANTS_DEFAULT_SUCCESS_SUFFIX; 71 | CONFIGS.constants.failSuffix = CONSTANTS_DEFAULT_FAIL_SUFFIX; 72 | CONFIGS.constants.afterSuffix = CONSTANTS_DEFAULT_AFTER_SUFFIX; 73 | }, 74 | 75 | get: function () { 76 | return CONFIGS.constants; 77 | } 78 | 79 | } 80 | 81 | }; 82 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | var _forEach = require('lodash/collection/forEach'); 2 | var _isArray = require('lodash/lang/isArray'); 3 | var _isString = require('lodash/lang/isString'); 4 | 5 | var cfgs = require('./configs').constants.get(); 6 | 7 | module.exports = function (constants, prefix) { 8 | 9 | if (!_isArray(constants)) { 10 | throw new Error('createConstants expects first parameter to be an array of strings'); 11 | } 12 | 13 | prefix = prefix || ''; 14 | 15 | if (!_isString(prefix)) { 16 | throw new Error('createConstants expects second parameter string'); 17 | } 18 | 19 | if (prefix.length > 0) { 20 | prefix += cfgs.separator; 21 | } 22 | 23 | var ret = {}; 24 | _forEach(constants, function (constant) { 25 | 26 | if (!_isString(constant)) { 27 | throw new Error('createConstants expects all constants to be strings'); 28 | } 29 | 30 | ret[constant] = prefix + constant; 31 | ret[constant + '_' + cfgs.successSuffix] = prefix + constant + cfgs.separator + cfgs.successSuffix; 32 | ret[constant + '_' + cfgs.failSuffix] = prefix + constant + cfgs.separator + cfgs.failSuffix; 33 | ret[constant + '_' + cfgs.afterSuffix] = prefix + constant + cfgs.separator + cfgs.afterSuffix; 34 | }); 35 | return ret; 36 | 37 | }; 38 | -------------------------------------------------------------------------------- /lib/dispatcher.js: -------------------------------------------------------------------------------- 1 | var Promise = Promise || require('promise'); 2 | var _merge = require('lodash/object/merge'); 3 | var _isArray = require('lodash/lang/isArray'); 4 | var _isString = require('lodash/lang/isString'); 5 | var _forEach = require('lodash/collection/forEach'); 6 | 7 | /** 8 | * Dispatcher 9 | */ 10 | function Dispatcher() { 11 | /** 12 | * Registry of callbacks, waitFor, promises, etc 13 | * each constant has it's own array of callbacks, waitFor, promises, etc 14 | */ 15 | this._registry = {}; 16 | } 17 | 18 | Dispatcher.prototype = _merge(Dispatcher.prototype, { 19 | 20 | /** 21 | * Registers a callback 22 | * @param {string} constant 23 | * @param {function} callback 24 | * @param {array|null} waitFor Array indexes to callbacks 25 | */ 26 | register: function (constant, callback, waitForIndexes) { 27 | if (!_isString(constant) || !constant.length) { 28 | throw new Error('Dispatcher.register: constant must be a string'); 29 | } 30 | waitForIndexes = waitForIndexes || null; 31 | 32 | if (typeof callback != 'function') { 33 | throw new Error('Dispatcher.register expects second parameter to be a callback'); 34 | } 35 | 36 | if (waitForIndexes !== null && !_isArray(waitForIndexes)) { 37 | throw new Error('Dispatcher.register expects third parameter to be null or an array'); 38 | } 39 | 40 | var registry = this._getRegistry(constant); 41 | registry.callbacks.push(callback); 42 | registry.waitFor.push(waitForIndexes); 43 | return registry.callbacks.length - 1; 44 | }, 45 | 46 | 47 | /** 48 | * Dispatch 49 | * @param {string} constant 50 | * @param {object} payload 51 | */ 52 | dispatch: function (constant, payload) { 53 | var registry = this._getRegistry(constant); 54 | registry.dispatchQueue.push({ 55 | constant: constant, 56 | payload: payload 57 | }); 58 | this._dispatch(registry); 59 | }, 60 | 61 | _dispatch: function (registry) { 62 | 63 | if (registry.isDispatching || !registry.dispatchQueue.length) { 64 | return; 65 | } 66 | 67 | registry.isDispatching = true; 68 | var job = registry.dispatchQueue.shift(); 69 | 70 | this._createDispatchPromises(registry); 71 | 72 | _forEach(registry.callbacks, function (callback, idx) { 73 | 74 | var resolver = (function (registry, idx, payload) { 75 | return function () { 76 | Promise.resolve(registry.callbacks[idx](payload)).then(function () { 77 | registry.resolves[idx](payload); 78 | }, function () { 79 | registry.rejects[idx](new Error('Dispatch callback error')); 80 | }); 81 | }; 82 | })(registry, idx, job.payload); 83 | 84 | var waitFor = registry.waitFor[idx]; 85 | if (!waitFor) { 86 | resolver(); 87 | } 88 | else { 89 | var promisesToWaitFor = this._getPromisesByIndexes(registry, waitFor); 90 | Promise.all(promisesToWaitFor).then( 91 | resolver, 92 | resolver //Should we really resolve the callback here? 93 | // Some of the WaitForStores callbacks rejected the request 94 | ); 95 | } 96 | }.bind(this));//_forEach(registry.callbacks, 97 | 98 | Promise.all(registry.promises).then(function () { 99 | this._onDispatchEnd(registry); 100 | }.bind(this), function () { 101 | this._onDispatchEnd(registry); 102 | }); 103 | 104 | },//dispatch 105 | 106 | 107 | /** 108 | * Gets a registry for a constant 109 | * @param {string} constant 110 | */ 111 | _getRegistry: function (constant) { 112 | if (typeof this._registry[constant] == "undefined") { 113 | this._registry[constant] = { 114 | callbacks: [], 115 | waitFor: [], 116 | promises: [], 117 | resolves: [], 118 | rejects: [], 119 | dispatchQueue: [], 120 | isDispatching: false 121 | }; 122 | } 123 | return this._registry[constant]; 124 | }, 125 | 126 | /** 127 | * @param {object} registry 128 | * @param {array} callbacks 129 | */ 130 | _getPromisesByIndexes: function (registry, indexes) { 131 | return indexes.map(function (idx) { 132 | return registry.promises[idx]; 133 | }); 134 | }, 135 | 136 | /** 137 | * Create promises for all callbacks in this registry 138 | * @param {object} registry 139 | */ 140 | _createDispatchPromises: function (registry) { 141 | registry.promises = []; 142 | registry.resolves = []; 143 | registry.rejects = []; 144 | _forEach(registry.callbacks, function (callback, i) { 145 | registry.promises[i] = new Promise(function (resolve, reject) { 146 | registry.resolves[i] = resolve; 147 | registry.rejects[i] = reject; 148 | }); 149 | }); 150 | }, 151 | 152 | 153 | /** 154 | * Clean registry 155 | * @param {object} registry 156 | */ 157 | _onDispatchEnd: function (registry) { 158 | registry.promises = []; 159 | registry.resolves = []; 160 | registry.rejects = []; 161 | registry.isDispatching = false; 162 | this._dispatch(registry); 163 | } 164 | 165 | }); 166 | 167 | module.exports = Dispatcher; 168 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | var Constants = require('./constants'); 2 | var Actions = require('./actions'); 3 | var Store = require('./store'); 4 | var MixinFor = require('./mixinFor'); 5 | var Dispatcher = require('./dispatcher'); 6 | var Configs = require('./configs'); 7 | 8 | var dispatcher = new Dispatcher(); 9 | 10 | module.exports = { 11 | 12 | configs: Configs, 13 | /** 14 | * createActions 15 | * @param {object} actions 16 | */ 17 | createActions: function (actions) { 18 | return new Actions(dispatcher, actions); 19 | }, 20 | 21 | /** 22 | * createStore 23 | * @param {object} storeMixin 24 | * @param {array} handlers 25 | */ 26 | createStore: function (storeMixin, handlers) { 27 | return new Store(dispatcher, storeMixin, handlers); 28 | }, 29 | 30 | /** 31 | * createConstants 32 | * @param {array} constants 33 | * 34 | */ 35 | createConstants: function (constants, prefix) { 36 | return new Constants(constants, prefix); 37 | }, 38 | 39 | /** 40 | * dispatch a message 41 | * @param {string} constant 42 | * @param {object} payload 43 | */ 44 | dispatch: function (constant, payload) { 45 | dispatcher.dispatch(constant, payload); 46 | }, 47 | 48 | /** 49 | * The global dispatcher 50 | */ 51 | dispatcher: dispatcher, 52 | 53 | /** 54 | * Mixin 55 | */ 56 | mixinFor: MixinFor 57 | 58 | }; 59 | -------------------------------------------------------------------------------- /lib/mixinFor.js: -------------------------------------------------------------------------------- 1 | var _forEach = require('lodash/collection/forEach'); 2 | 3 | 4 | module.exports = function () { 5 | var stores = Array.prototype.slice.call(arguments); 6 | if (!stores.length) { 7 | throw new Error('Flux.mixinFor expects a store or a list of stores'); 8 | } 9 | _forEach(stores, function (store) { 10 | var isNotOk = ( 11 | typeof store === 'undefined' || typeof store.onChange !== 'function' || typeof store.offChange !== 'function' 12 | ); 13 | if (isNotOk) { 14 | throw new Error('Flux.mixinFor expects a store or an array of stores'); 15 | } 16 | }); 17 | 18 | 19 | return { 20 | 21 | componentWillMount: function () { 22 | if (typeof this._react_flux_onChange === "undefined") { 23 | this._react_flux_onChange = function () { 24 | if (this.isMounted()) { 25 | this.setState(this.getStateFromStores()); 26 | } 27 | }.bind(this); 28 | } 29 | this.setState(this.getStateFromStores()); 30 | }, 31 | 32 | componentDidMount: function () { 33 | for (var i = 0; i < stores.length; i++) { 34 | stores[i].onChange(this._react_flux_onChange); 35 | } 36 | }, 37 | 38 | componentWillUnmount: function () { 39 | for (var i = 0; i < stores.length; i++) { 40 | stores[i].offChange(this._react_flux_onChange); 41 | } 42 | } 43 | }; 44 | 45 | }; 46 | -------------------------------------------------------------------------------- /lib/store.js: -------------------------------------------------------------------------------- 1 | var _assign = require('lodash/object/assign'); 2 | var _forEach = require('lodash/collection/forEach'); 3 | var _isArray = require('lodash/lang/isArray'); 4 | var _cloneDeep = require('lodash/lang/cloneDeep'); 5 | var MixinFor = require('./mixinFor'); 6 | 7 | var CHANGE_EVENT = 'change'; 8 | 9 | var StoreActionHandler = require('./storeActionHandler'); 10 | 11 | 12 | function FluxState(initState) { 13 | var _state = initState; 14 | 15 | this.assign = function (newState) { 16 | _state = _assign(_state, newState); 17 | }, 18 | 19 | this.get = function (key) { 20 | return _state[key]; 21 | }, 22 | 23 | this.getClone = function (key) { 24 | var v = _state[key]; 25 | if (v instanceof Object && v !== null) { 26 | return _cloneDeep(v); 27 | } 28 | return v; 29 | }, 30 | 31 | this.replaceState = function (newState) { 32 | _state = newState; 33 | }, 34 | 35 | this.getState = function () { 36 | return _state; 37 | } 38 | 39 | this.getStateClone = function () { 40 | return _cloneDeep(_state); 41 | } 42 | 43 | } 44 | 45 | /** 46 | * Flux Store 47 | * @param {object} dispatcher 48 | * @param {object} storeMixin 49 | * @param {array} handlers 50 | */ 51 | var Store = function (dispatcher, storeMixin, handlers) { 52 | this.state = new FluxState({ 53 | _action_states: {} 54 | }); 55 | this._events = {};//used by store event emitter 56 | this._actionHandlers = {}; 57 | this._constantHandlers = {}; 58 | this._dispatcherIndexes = {}; 59 | this._dispatcher = dispatcher; 60 | this._getInitialStateCalls = []; 61 | this._storeDidMountCalls = []; 62 | 63 | this._storeMixin(storeMixin); 64 | this._setInitialState(); 65 | if (!!handlers) { 66 | this._setConstantHandlers(handlers); 67 | } 68 | _forEach(this._storeDidMountCalls, function (fn) { 69 | fn(); 70 | }); 71 | }; 72 | 73 | 74 | Store.prototype = { 75 | 76 | /** 77 | * @param {object} newState 78 | */ 79 | setState: function (newState) { 80 | this.state.assign(newState); 81 | this._emit(CHANGE_EVENT); 82 | }, 83 | 84 | /** 85 | * @param {object} newState 86 | */ 87 | replaceState: function (newState) { 88 | this.state.replaceState(newState); 89 | this._emit(CHANGE_EVENT); 90 | }, 91 | 92 | /** 93 | * @param {string} propertyName 94 | * @return {mixed} 95 | */ 96 | get: function (propertyName) { 97 | return this.state.get(propertyName); 98 | }, 99 | 100 | getClone: function (propertyName) { 101 | return this.state.getClone(propertyName); 102 | }, 103 | 104 | getState: function () { 105 | return this.state.getState(); 106 | }, 107 | 108 | getStateClone: function () { 109 | return this.state.getStateClone(); 110 | }, 111 | 112 | 113 | /** 114 | * @param {string} constant 115 | * @param {object} newState 116 | */ 117 | setActionState: function (constant, newState) { 118 | var actionStates = this.state.get('_action_states'); 119 | actionStates[constant] = _assign(actionStates[constant] || {}, newState); 120 | this.setState({ 121 | '_action_states': actionStates 122 | }); 123 | }, 124 | 125 | /** 126 | * @param {string} constant 127 | */ 128 | resetActionState: function (constant) { 129 | if (typeof this._actionHandlers[constant] === 'undefined') { 130 | throw new Error('Store.resetActionState constant handler for [' + constant + '] is not defined'); 131 | } 132 | var actionStates = this.state.get('_action_states'); 133 | actionStates[constant] = this._actionHandlers[constant].getInitialState(); 134 | this.setState({ 135 | '_action_states': actionStates 136 | }); 137 | }, 138 | 139 | /** 140 | * @param {string} constant - constant to get handler state for 141 | * @param {string} [key] - a specfic key to get 142 | */ 143 | getActionState: function (constant, key) { 144 | if (typeof this._actionHandlers[constant] === 'undefined') { 145 | throw new Error('Store.getActionState constant handler for [' + constant + '] is not defined'); 146 | } 147 | 148 | var actionState = this.state.get('_action_states'); 149 | 150 | if (typeof key === "undefined") { 151 | return actionState[constant]; 152 | } 153 | return actionState[constant][key]; 154 | }, 155 | 156 | /** 157 | * 158 | */ 159 | isStore: function () { 160 | return true; 161 | }, 162 | 163 | /** 164 | * @param {function} callback 165 | */ 166 | onChange: function (callback) { 167 | this._on(CHANGE_EVENT, callback); 168 | }, 169 | 170 | /** 171 | * @param {function} callback 172 | */ 173 | offChange: function (callback) { 174 | this._off(CHANGE_EVENT, callback); 175 | }, 176 | 177 | 178 | /** 179 | * set extra properties & methods for this Store 180 | * @param {object} storeMixin 181 | */ 182 | _storeMixin: function (storeMixin) { 183 | if (storeMixin && storeMixin.mixins && _isArray(storeMixin.mixins)) { 184 | _forEach(storeMixin.mixins, this._storeMixin.bind(this)); 185 | } 186 | _forEach(storeMixin, function (prop, propName) { 187 | if (propName === 'mixins') { 188 | return; 189 | } 190 | 191 | if (typeof prop === 'function') { 192 | prop = prop.bind(this); 193 | } 194 | 195 | if (propName === 'getInitialState') { 196 | this._getInitialStateCalls.push(prop); 197 | } else if (propName === 'storeDidMount') { 198 | this._storeDidMountCalls.push(prop); 199 | } else { 200 | this[propName] = prop; 201 | } 202 | }.bind(this)); 203 | }, 204 | 205 | /** 206 | * @param {string} constant 207 | * @param {object} configs 208 | * @return {FluxStore} self 209 | */ 210 | addActionHandler: function (constant, configs) { 211 | this._actionHandlers[constant] = new StoreActionHandler(this, constant, configs); 212 | return this; 213 | }, 214 | 215 | /** 216 | * Set constant handlers for this Store 217 | * @param {array} handlers 218 | */ 219 | _setConstantHandlers: function (handlers) { 220 | if (!_isArray(handlers)) { 221 | throw new Error('store expects handler definitions to be an array'); 222 | } 223 | _forEach(handlers, function (options) { 224 | if (!_isArray(options)) { 225 | throw new Error('store expects handler definition to be an array'); 226 | } 227 | var constant, handler, waitFor; 228 | 229 | constant = options[0]; 230 | if (options.length === 2) { 231 | waitFor = null; 232 | handler = options[1]; 233 | } 234 | else { 235 | waitFor = options[1]; 236 | handler = options[2]; 237 | } 238 | if (typeof constant !== 'string') { 239 | throw new Error('store expects all handler definitions to contain a constant as the first parameter'); 240 | } 241 | if (typeof handler !== 'function') { 242 | throw new Error('store expects all handler definitions to contain a callback'); 243 | } 244 | if (!!waitFor && !_isArray(waitFor)) { 245 | throw new Error('store expects waitFor to be an array of stores'); 246 | } 247 | var waitForIndexes = null; 248 | if (waitFor) { 249 | waitForIndexes = waitFor.map(function (store) { 250 | if (!(store instanceof Store)) { 251 | throw new Error('store expects waitFor to be an array of stores'); 252 | } 253 | return store._getHandlerIndex(constant); 254 | }); 255 | } 256 | 257 | this._constantHandlers[constant] = handler.bind(this); 258 | var dispatcherIndex = this._dispatcher.register(constant, this._constantHandlers[constant], waitForIndexes); 259 | this._dispatcherIndexes[constant] = dispatcherIndex; 260 | }.bind(this)); 261 | }, 262 | 263 | /** 264 | * Get dispatcher idx of this constant callback for this store 265 | * @param {string} constant 266 | * @return {number} Index of constant callback 267 | */ 268 | _getHandlerIndex: function (constant) { 269 | if (typeof this._dispatcherIndexes[constant] === "undefined") { 270 | throw new Error('Can not get store handler for constant: ' + constant); 271 | } 272 | return this._dispatcherIndexes[constant]; 273 | }, 274 | 275 | /** 276 | * Sets intial state of the Store 277 | */ 278 | _setInitialState: function () { 279 | this.setState(this.getInitialState()); 280 | }, 281 | 282 | /** 283 | * Gets initial state of the store 284 | * 285 | * @return {mixed} Store initial state 286 | */ 287 | getInitialState: function () { 288 | var state = {}; 289 | 290 | _forEach(this._getInitialStateCalls, function (fn) { 291 | _assign(state, fn()); 292 | }); 293 | 294 | return state; 295 | }, 296 | 297 | 298 | /** 299 | * @return {Object} A mixin for React Components 300 | */ 301 | mixinFor: function () { 302 | return MixinFor(this); 303 | }, 304 | 305 | /** 306 | * 307 | * @param {String} evt 308 | * @param {Function} handler 309 | * @return {Function} handler 310 | */ 311 | _on: function (evt, handler) { 312 | if (typeof this._events[evt] === 'undefined') { 313 | this._events[evt] = []; 314 | } 315 | this._events[evt].push(handler); 316 | return handler; 317 | }, 318 | 319 | /** 320 | * 321 | * @param {String} evt 322 | * @param {Function} handler 323 | */ 324 | _off: function (evt, handler) { 325 | if (typeof this._events[evt] !== 'undefined') { 326 | for (var i = 0, len = this._events[evt].length; i < len; i++) { 327 | if (this._events[evt][i] === handler) { 328 | this._events[evt].splice(i, 1); 329 | break; 330 | } 331 | } 332 | } 333 | }, 334 | 335 | /** 336 | * 337 | * @param {String} evt 338 | */ 339 | _emit: function (evt) { 340 | if (typeof this._events[evt] === 'undefined') { 341 | return; 342 | } 343 | var args = Array.prototype.slice.call(arguments, 1); 344 | _forEach(this._events[evt], function (listener) { 345 | if ('function' === typeof listener) { 346 | listener.apply(null, args); 347 | } 348 | }); 349 | }, 350 | 351 | }; 352 | 353 | module.exports = Store; 354 | -------------------------------------------------------------------------------- /lib/storeActionHandler.js: -------------------------------------------------------------------------------- 1 | var _isString = require('lodash/lang/isString'); 2 | 3 | var constantsConfigs = require('./configs').constants.get(); 4 | 5 | var HANDLER_NAMES = ['before', 'after', 'success', 'fail']; 6 | 7 | function StoreActionHandler(store, constant, configs) { 8 | if (!store || typeof store.isStore !== 'function' || !store.isStore()) { 9 | throw new Error('StoreActionHandler expects first parameter to be a store'); 10 | } 11 | if (!_isString(constant) || !constant.length) { 12 | throw new Error('StoreActionHandler expects second parameter to be a constant(string)'); 13 | } 14 | if (typeof configs.getInitialState == 'undefined') { 15 | configs.getInitialState = function () { 16 | return {}; 17 | }; 18 | } 19 | if (typeof configs.getInitialState != 'function') { 20 | throw new Error('StoreActionHandler expects getInitialState to be a function'); 21 | } 22 | 23 | configs = configs || {}; 24 | this.parent = store; 25 | this.constant = constant; 26 | this.getInitialState = configs.getInitialState; 27 | this.before = configs.before || null; 28 | this.after = configs.after || null; 29 | this.success = configs.success || null; 30 | this.fail = configs.fail || null; 31 | 32 | this.parent.setActionState(this.constant, this.getInitialState()); 33 | 34 | //register handlers for this constant 35 | var handlers = []; 36 | var len = HANDLER_NAMES.length; 37 | for (var i = 0; i < len; i++) { 38 | var handlerName = HANDLER_NAMES[i]; 39 | if (this[handlerName] === null) { 40 | continue; 41 | } 42 | if (typeof this[handlerName] !== 'function') { 43 | throw new Error('StoreActionHandler expects "' + handlerName + '" to be a function'); 44 | } 45 | var constantName = this.constant; 46 | if (handlerName !== 'before') { 47 | constantName += constantsConfigs.separator + constantsConfigs[handlerName + 'Suffix']; 48 | } 49 | handlers.push([constantName, this[handlerName].bind(this)]); 50 | } 51 | store._setConstantHandlers(handlers); 52 | } 53 | 54 | StoreActionHandler.prototype = { 55 | /** 56 | * @param {object} newState 57 | */ 58 | setState: function (newState) { 59 | this.parent.setActionState(this.constant, newState); 60 | }, 61 | /** 62 | * @return {object} state 63 | */ 64 | getState: function () { 65 | return this.parent.getActionState(this.constant); 66 | } 67 | }; 68 | 69 | module.exports = StoreActionHandler; 70 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-flux", 3 | "version": "1.0.1", 4 | "description": "A library implementing React Flux data flow", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "./node_modules/mocha/bin/mocha -G", 8 | "test-cov": "./node_modules/istanbul/lib/cli.js cover ./node_modules/mocha/bin/_mocha -- -R spec" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/kjda/ReactFlux.git" 13 | }, 14 | "keywords": [ 15 | "React", 16 | "Flux", 17 | "ReactJS" 18 | ], 19 | "author": "Khaled Jouda", 20 | "license": "MIT", 21 | "bugs": { 22 | "url": "https://github.com/kjda/ReactFlux/issues" 23 | }, 24 | "homepage": "https://github.com/kjda/ReactFlux", 25 | "dependencies": { 26 | "lodash": "^3.10.1", 27 | "promise": "^7.0.1" 28 | }, 29 | "devDependencies": { 30 | "chai": "^2.2.0", 31 | "colors": "^1.0.3", 32 | "commander": "^2.7.1", 33 | "coveralls": "^2.11.2", 34 | "istanbul": "^0.3.13", 35 | "mocha": "^2.2.4", 36 | "sinon": "^1.14.1", 37 | "underscore": "^1.8.3", 38 | "webpack": "^1.9.10" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /test/actions.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('../'); 2 | var assert = require('chai').assert; 3 | var sinon = require("sinon"); 4 | 5 | describe("actions", function(){ 6 | 7 | var constants = ReactFlux.createConstants(['ONE', 'TWO']); 8 | var actions, action1Spy; 9 | 10 | beforeEach(function(){ 11 | action1Spy = sinon.spy(function(par1){ 12 | return { 13 | par1: par1 14 | }; 15 | }); 16 | 17 | actions = ReactFlux.createActions({ 18 | 19 | action1: [constants.ONE, action1Spy], 20 | 21 | action2: [constants.TWO, function(){ 22 | 23 | }] 24 | 25 | }); 26 | 27 | }); 28 | 29 | it("should create actions", function(){ 30 | assert.typeOf(actions, 'object'); 31 | assert.typeOf(actions.action1, 'function'); 32 | assert.typeOf(actions.action2, 'function'); 33 | }); 34 | 35 | it("action callback should be callable", function(){ 36 | actions.action1('arg1', 'arg2'); 37 | assert.isTrue(action1Spy.calledWith('arg1', 'arg2')); 38 | }); 39 | 40 | it("accepts an array as the only possible action definition", function(){ 41 | function throwsError(){ 42 | ReactFlux.createActions({ 43 | action1: function(){} 44 | }); 45 | } 46 | assert.throw(throwsError, /Action must be an array/g); 47 | }); 48 | 49 | it("action must be a function", function(){ 50 | function throwsError(){ 51 | ReactFlux.createActions({ 52 | action1: ['ACTION1', 'someOtherValue'] 53 | }); 54 | } 55 | assert.throw(throwsError, /you did not provide a valid callback for action/g); 56 | }); 57 | 58 | it("creates a default empty callbacl", function(){ 59 | var actions = ReactFlux.createActions({ 60 | action1: ['ACTION1'] 61 | }); 62 | assert.isFunction(actions.action1); 63 | }); 64 | 65 | }); 66 | -------------------------------------------------------------------------------- /test/configs.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('../'); 2 | var assert = require('chai').assert; 3 | 4 | 5 | describe("configs", function(){ 6 | 7 | it("constant generation should be configurable", function(){ 8 | ReactFlux.configs.constants.setSeparator(':'); 9 | ReactFlux.configs.constants.setSuccessSuffix('OK'); 10 | ReactFlux.configs.constants.setFailSuffix('ERROR'); 11 | ReactFlux.configs.constants.setAfterSuffix('DONE'); 12 | 13 | var fooConstants = ReactFlux.createConstants(['ONE'], 'FOO'); 14 | assert.equal(fooConstants.ONE_OK, 'FOO:ONE:OK'); 15 | assert.equal(fooConstants.ONE_ERROR, 'FOO:ONE:ERROR'); 16 | assert.equal(fooConstants.ONE_DONE, 'FOO:ONE:DONE'); 17 | 18 | ReactFlux.configs.constants.resetToDefaults(); 19 | 20 | var barConstants = ReactFlux.createConstants(['TWO'], 'BAR'); 21 | assert.equal(barConstants.TWO_SUCCESS, 'BAR_TWO_SUCCESS'); 22 | assert.equal(barConstants.TWO_FAIL, 'BAR_TWO_FAIL'); 23 | assert.equal(barConstants.TWO_AFTER, 'BAR_TWO_AFTER'); 24 | }); 25 | 26 | 27 | 28 | it("complains if Constants separator is not a string", function(){ 29 | assert.throw(function(){ 30 | ReactFlux.configs.constants.setSeparator({}); 31 | }, 'Constants.separator must be a non empty string'); 32 | 33 | assert.throw(function(){ 34 | ReactFlux.configs.constants.setSeparator(''); 35 | }, 'Constants.separator must be a non empty string'); 36 | 37 | }); 38 | 39 | 40 | it("complains if successSuffix is not a string", function(){ 41 | assert.throw(function(){ 42 | ReactFlux.configs.constants.setSuccessSuffix({}); 43 | }, 'Constants.successSuffix must be a non empty string'); 44 | 45 | assert.throw(function(){ 46 | ReactFlux.configs.constants.setSuccessSuffix(''); 47 | }, 'Constants.successSuffix must be a non empty string'); 48 | 49 | }); 50 | 51 | it("complains if failSuffix is not a string", function(){ 52 | assert.throw(function(){ 53 | ReactFlux.configs.constants.setFailSuffix({}); 54 | }, 'Constants.failSuffix must be a non empty string'); 55 | 56 | assert.throw(function(){ 57 | ReactFlux.configs.constants.setFailSuffix(''); 58 | }, 'Constants.failSuffix must be a non empty string'); 59 | 60 | }); 61 | 62 | it("complains if afterSuffix is not a string", function(){ 63 | assert.throw(function(){ 64 | ReactFlux.configs.constants.setAfterSuffix({}); 65 | }, 'Constants.afterSuffix must be a non empty string'); 66 | 67 | assert.throw(function(){ 68 | ReactFlux.configs.constants.setAfterSuffix(''); 69 | }, 'Constants.afterSuffix must be a non empty string'); 70 | 71 | }); 72 | 73 | }); 74 | -------------------------------------------------------------------------------- /test/constants.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('../'); 2 | var assert = require('chai').assert; 3 | 4 | 5 | 6 | describe("constants", function(){ 7 | 8 | var constantsWithPrefix = ReactFlux.createConstants(['ONE', 'TWO'], 'PRE'); 9 | 10 | var constants = ReactFlux.createConstants(['ONE', 'TWO']); 11 | 12 | it("should create constants without prefix", function(){ 13 | assert.equal(constants.ONE, 'ONE'); 14 | assert.equal(constants.TWO, 'TWO'); 15 | }); 16 | 17 | it("should create constants with prefix", function(){ 18 | assert.equal(constantsWithPrefix.ONE, 'PRE_ONE'); 19 | assert.equal(constantsWithPrefix.TWO, 'PRE_TWO'); 20 | }); 21 | 22 | it("should create success constants", function(){ 23 | assert.equal(constants.ONE_SUCCESS, 'ONE_SUCCESS'); 24 | assert.equal(constants.TWO_SUCCESS, 'TWO_SUCCESS'); 25 | }); 26 | 27 | it("should create failure constants", function(){ 28 | assert.equal(constants.ONE_FAIL, 'ONE_FAIL'); 29 | assert.equal(constants.TWO_FAIL, 'TWO_FAIL'); 30 | }); 31 | 32 | it("should accepts an array of constants only", function(){ 33 | assert.throw(function(){ 34 | ReactFlux.createConstants({}); 35 | }, /createConstants expects first parameter to be an array of strings/); 36 | }); 37 | 38 | it("all constants must be strings", function(){ 39 | assert.throw(function(){ 40 | ReactFlux.createConstants(['ONE', null]); 41 | }, /createConstants expects all constants to be strings/); 42 | }); 43 | 44 | it("prefix should be a string", function(){ 45 | assert.throw(function(){ 46 | ReactFlux.createConstants([], 12); 47 | }, /createConstants expects second parameter string/); 48 | }); 49 | 50 | 51 | 52 | }); 53 | -------------------------------------------------------------------------------- /test/dispatcher.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('../'); 2 | var assert = require('chai').assert; 3 | var sinon = require("sinon"); 4 | 5 | var constants = ReactFlux.createConstants(['ONE', 'TWO', 'THREE'], 'DIS'); 6 | 7 | describe("dispatcher", function(){ 8 | 9 | var action1Spy, action2Spy; 10 | var store1Action1BeforeSpy, store1Action1AfterSpy, store1Action1SuccessSpy; 11 | var actionHandlerBeforeSpy, actionHandlerAfterSpy, actionHandlerSuccessSpy, actionHandlerFailSpy; 12 | var store1Action2FailSpy; 13 | var store2Action1SuccessSpy; 14 | 15 | var actions; 16 | var store1, store2; 17 | 18 | beforeEach(function(){ 19 | 20 | action1Spy = sinon.spy(function(id, username){ 21 | return { 22 | id: id, 23 | username: username 24 | }; 25 | }); 26 | 27 | action2Spy = sinon.spy(function(id, username){ 28 | return new Error('error2'); 29 | }); 30 | 31 | actions = ReactFlux.createActions({ 32 | action1: [constants.ONE, action1Spy], 33 | action2: [constants.TWO, action2Spy], 34 | action3: [constants.THREE, function(){ return {};}] 35 | }); 36 | 37 | store1Action1BeforeSpy = sinon.spy(function(){}); 38 | store1Action1AfterSpy = sinon.spy(function(){}); 39 | store1Action1SuccessSpy = sinon.spy(function(){}); 40 | store1Action2FailSpy = sinon.spy(function(){}); 41 | 42 | store2Action1SuccessSpy = sinon.spy(function(){}); 43 | 44 | actionHandlerBeforeSpy = sinon.spy(function(){}); 45 | actionHandlerAfterSpy = sinon.spy(function(){}); 46 | actionHandlerSuccessSpy = sinon.spy(function(){}); 47 | actionHandlerFailSpy = sinon.spy(function(){}); 48 | 49 | 50 | store1 = ReactFlux.createStore({ 51 | getName: function(){ 52 | return "store1"; 53 | } 54 | }, [ 55 | [constants.ONE, store1Action1BeforeSpy], 56 | [constants.ONE_AFTER, store1Action1AfterSpy], 57 | [constants.ONE_SUCCESS, store1Action1SuccessSpy], 58 | [constants.TWO_FAIL, store1Action2FailSpy] 59 | ]); 60 | 61 | store1.addActionHandler(constants.THREE, { 62 | before: actionHandlerBeforeSpy, 63 | after: actionHandlerAfterSpy, 64 | success: actionHandlerSuccessSpy, 65 | fail: actionHandlerFailSpy 66 | }); 67 | 68 | store2 = ReactFlux.createStore({ 69 | getName: function(){ 70 | return "store2"; 71 | } 72 | }, [ 73 | [constants.ONE, sinon.spy(function(){})], 74 | [constants.TWO_FAIL, sinon.spy(function(){})], 75 | [constants.ONE_SUCCESS, [store1], store2Action1SuccessSpy] 76 | ]); 77 | }); 78 | 79 | it("should throw an error if constant is empty or undefined", function(){ 80 | function createStore(constant){ 81 | ReactFlux.createStore({}, [ 82 | [constant, function(){}] 83 | ]); 84 | } 85 | assert.throws(function(){ 86 | createStore(); 87 | }, /constant/); 88 | assert.throws(function(){ 89 | createStore(''); 90 | }, /constant/); 91 | }); 92 | 93 | it("should execute action", function(){ 94 | actions.action1(1, "mustermann"); 95 | assert.isTrue(action1Spy.called); 96 | actions.action2(); 97 | assert.isTrue(action2Spy.called); 98 | }); 99 | 100 | it("should dispatch action to store", function(){ 101 | actions.action1(1, "mustermann"); 102 | assert.isTrue(store1Action1BeforeSpy.called); 103 | }); 104 | 105 | it("should dispatch AFTER to store", function(done){ 106 | actions.action1(1, "mustermann"); 107 | setTimeout(function(){ 108 | assert.isTrue(store1Action1AfterSpy.called); 109 | done(); 110 | }, 0); 111 | }); 112 | 113 | it("should dispatch SUCCESS to store", function(done){ 114 | actions.action1(1, "mustermann"); 115 | setTimeout(function(){ 116 | assert.isTrue(store1Action1SuccessSpy.called); 117 | done(); 118 | }, 0); 119 | }); 120 | 121 | it("should dispatch SUCCESS with correct payload", function(done){ 122 | actions.action1(1, "mustermann"); 123 | setTimeout(function(){ 124 | assert.isTrue(store1Action1SuccessSpy.calledWith({id: 1, username: 'mustermann'})); 125 | done(); 126 | }, 0); 127 | }); 128 | 129 | it("should dispatch FAIL to store", function(done){ 130 | actions.action2(); 131 | setTimeout(function(){ 132 | assert.isTrue(store1Action2FailSpy.called); 133 | done(); 134 | }, 0); 135 | }); 136 | 137 | it("should be able to wait for other stores", function(done){ 138 | actions.action1(); 139 | setTimeout(function(){ 140 | assert.isTrue(store2Action1SuccessSpy.calledAfter(store1Action1SuccessSpy)); 141 | done(); 142 | }, 0); 143 | }); 144 | 145 | it("should be able to dispatch specific messages without going through actions", function(done){ 146 | ReactFlux.dispatch(constants.ONE_SUCCESS); 147 | setTimeout(function(){ 148 | assert.isTrue( store1Action1SuccessSpy.called ); 149 | assert.isTrue( store2Action1SuccessSpy.called ); 150 | done(); 151 | }, 0); 152 | }); 153 | 154 | it("should dispatch BEFORE to actionHandler", function(done){ 155 | actions.action3(1, "mustermann"); 156 | assert.isTrue(actionHandlerBeforeSpy.called); 157 | done(); 158 | }); 159 | 160 | it("should dispatch AFTER to actionHandler", function(done){ 161 | actions.action3(1, "mustermann"); 162 | setTimeout(function(){ 163 | assert.isTrue(actionHandlerAfterSpy.called); 164 | done(); 165 | }, 0); 166 | }); 167 | 168 | it("should dispatch SUCCESS to actionHandler", function(done){ 169 | actions.action3(1, "mustermann"); 170 | setTimeout(function(){ 171 | assert.isTrue(actionHandlerSuccessSpy.called); 172 | done(); 173 | }, 0); 174 | }); 175 | 176 | it("should NOT dispatch FAIL to actionHandler when SUCCESS", function(done){ 177 | actions.action3(1, "mustermann"); 178 | setTimeout(function(){ 179 | assert.isFalse(actionHandlerFailSpy.called); 180 | done(); 181 | }, 0); 182 | }); 183 | 184 | it("register should throw an error if callback is not a function", function(){ 185 | assert.throw(function(){ 186 | ReactFlux.dispatcher.register("SOME_CONST", null); 187 | }, /Dispatcher.register expects second parameter to be a callback/); 188 | }); 189 | 190 | it("register should throw an error if waitForIndexes is not an array", function(){ 191 | assert.throw(function(){ 192 | ReactFlux.dispatcher.register("SOME_CONST", function(){}, {}); 193 | }, /Dispatcher.register expects third parameter to be null or an array/); 194 | }); 195 | 196 | }); 197 | -------------------------------------------------------------------------------- /test/mixinFor.js: -------------------------------------------------------------------------------- 1 | var assert = require('chai').assert; 2 | var sinon = require("sinon"); 3 | var Flux = require('../'); 4 | var sinon = require("sinon"); 5 | 6 | describe("MixinFor", function(){ 7 | 8 | it("Flux should provide mixinFor interface", function(){ 9 | assert.isFunction(Flux.mixinFor); 10 | 11 | var mixinFor = Flux.mixinFor(Flux.createStore()); 12 | assert.isFunction(mixinFor.componentWillMount, 'mixinFor.componentWillMount is misiing'); 13 | assert.isFunction(mixinFor.componentDidMount, 'mixinFor.componentDidMount is misiing'); 14 | assert.isFunction(mixinFor.componentWillUnmount, 'mixinFor.componentWillUnmount is missing'); 15 | }); 16 | 17 | it("accepts a store or an array of stores only", function(){ 18 | assert.throws(function(){ 19 | Flux.mixinFor(); 20 | }, Error, /stores/); 21 | assert.throws(function(){ 22 | Flux.mixinFor(''); 23 | }, Error, /stores/); 24 | assert.throws(function(){ 25 | Flux.mixinFor({}); 26 | }, Error, /stores/); 27 | }); 28 | 29 | it("should call component.setState", function(done){ 30 | var mixinFor = Flux.mixinFor( Flux.createStore() ); 31 | mixinFor.state = {}; 32 | mixinFor.setState = function(state){ 33 | mixinFor.state = state; 34 | }; 35 | mixinFor.getStateFromStores = function(){ 36 | return { 37 | foo: 'bar' 38 | }; 39 | }; 40 | mixinFor.isMounted = function(){ 41 | return true; 42 | }; 43 | mixinFor.componentWillMount(); 44 | setTimeout(function(){ 45 | assert.equal(mixinFor.state.foo, 'bar'); 46 | done(); 47 | }, 0); 48 | }); 49 | 50 | it("should call getStateFromStores onChange", function(done){ 51 | var store = Flux.createStore(); 52 | var mixinFor = Flux.mixinFor( store ); 53 | mixinFor.state = {}; 54 | mixinFor.setState = function(state){ 55 | mixinFor.state = state; 56 | }; 57 | mixinFor.getStateFromStores = sinon.spy(function(){ 58 | return { 59 | foo: 'bar' 60 | }; 61 | }); 62 | mixinFor.isMounted = function(){ 63 | return true; 64 | }; 65 | mixinFor.componentWillMount(); 66 | mixinFor.componentDidMount(); 67 | store.setState({foo: 'bar'}); 68 | setTimeout(function(){ 69 | assert.isTrue( mixinFor.getStateFromStores.calledTwice ); 70 | done(); 71 | }, 0); 72 | }); 73 | 74 | it("should not call getStateFromStores onChange if component is not mounted", function(done){ 75 | var store = Flux.createStore(); 76 | var mixinFor = Flux.mixinFor( store ); 77 | mixinFor.state = {}; 78 | mixinFor.setState = function(state){ 79 | mixinFor.state = state; 80 | }; 81 | mixinFor.getStateFromStores = sinon.spy(function(){ 82 | return { 83 | foo: 'bar' 84 | }; 85 | }); 86 | mixinFor.isMounted = function(){ 87 | return false; 88 | }; 89 | mixinFor.componentWillMount(); 90 | mixinFor.componentDidMount(); 91 | store.setState({foo: 'bar'}); 92 | setTimeout(function(){ 93 | assert.isTrue( mixinFor.getStateFromStores.calledOnce ); 94 | done(); 95 | }, 0); 96 | }); 97 | 98 | it("it should subscribe and unsubscribe for store changes", function(){ 99 | 100 | var store = Flux.createStore(); 101 | store.onChange = sinon.spy(function(){ 102 | }); 103 | store.offChange = sinon.spy(function(){ 104 | }); 105 | 106 | var mixinFor = Flux.mixinFor( store ); 107 | 108 | mixinFor.componentDidMount(); 109 | assert.isTrue(store.onChange.called, 'mixinFor did not subscribe for store changes'); 110 | assert.isFalse(store.offChange.called); 111 | 112 | mixinFor.componentWillUnmount(); 113 | assert.isTrue(store.offChange.called, 'mixinFor did not unsubscribe from store changes'); 114 | 115 | }); 116 | 117 | }); 118 | -------------------------------------------------------------------------------- /test/store.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('../'); 2 | var assert = require('chai').assert; 3 | var sinon = require("sinon"); 4 | 5 | var constants = ReactFlux.createConstants(['ONE','TWO'], 'STORE'); 6 | 7 | 8 | var storeDidMountSpy = sinon.spy(); 9 | 10 | var FooMixin = { 11 | bar: sinon.spy(), 12 | getInitialState: sinon.spy(function () { 13 | return { 14 | abc: "abc", 15 | xyz: "xyz" 16 | }; 17 | }), 18 | storeDidMount: sinon.spy() 19 | }; 20 | 21 | var BarMixin = { 22 | foo: sinon.spy(), 23 | getInitialState: sinon.spy(function () { 24 | return { 25 | def: "def" 26 | }; 27 | }), 28 | storeDidMount: sinon.spy() 29 | }; 30 | 31 | var EnhancedFooMixin = { 32 | mixins: [FooMixin], 33 | enhancedBar: sinon.spy(), 34 | getInitialState: sinon.spy(function () { 35 | return { 36 | abc: "abc-enhanced" 37 | }; 38 | }), 39 | storeDidMount: sinon.spy() 40 | }; 41 | 42 | var getInitialStateSpy = sinon.spy(function(){ 43 | return { 44 | id: 1, 45 | username: 'mustermann' 46 | }; 47 | }); 48 | 49 | var store = ReactFlux.createStore({ 50 | 51 | mixins: [ 52 | EnhancedFooMixin, 53 | BarMixin 54 | ], 55 | 56 | getInitialState: getInitialStateSpy, 57 | 58 | storeDidMount: storeDidMountSpy, 59 | 60 | getId: function(){ 61 | return this.state.get('id'); 62 | } 63 | 64 | }, [ 65 | [constants.ONE, function(){}] 66 | ]); 67 | 68 | store.addActionHandler(constants.TWO, { 69 | getInitialState: function(){ 70 | return { 71 | name: 'TWO_HANDLER' 72 | }; 73 | }, 74 | before: function(){ 75 | }, 76 | after: function(){ 77 | }, 78 | success: function(){ 79 | }, 80 | fail: function(){ 81 | } 82 | }); 83 | 84 | describe("store", function(){ 85 | 86 | it("should create store with mixins", function(){ 87 | assert.typeOf(store, 'object'); 88 | assert.typeOf(store.getId, 'function'); 89 | }); 90 | 91 | it("should use the mixins property", function(){ 92 | assert.typeOf(store.foo, 'function'); 93 | assert.typeOf(store.enhancedBar, 'function'); 94 | }); 95 | 96 | it("should recursively use the mixins property", function(){ 97 | assert.typeOf(store.bar, 'function'); 98 | }); 99 | 100 | it("should call each mixin's storeDidMount function", function(){ 101 | assert.isTrue(FooMixin.storeDidMount.calledOnce); 102 | assert.isTrue(BarMixin.storeDidMount.calledOnce); 103 | assert.isTrue(EnhancedFooMixin.storeDidMount.calledOnce); 104 | assert.isTrue(storeDidMountSpy.calledOnce); 105 | }); 106 | 107 | it("should call and merge each mixin's getInitialState function", function(){ 108 | assert.equal(store.state.get('abc'),'abc-enhanced'); 109 | assert.equal(store.state.get('xyz'),'xyz'); 110 | assert.equal(store.state.get('def'),'def'); 111 | }); 112 | 113 | it("should call getInitialState", function(){ 114 | assert.isTrue( getInitialStateSpy.called ); 115 | }); 116 | 117 | it("should call storeDidMount", function(){ 118 | assert.isTrue( storeDidMountSpy.called ); 119 | }); 120 | 121 | it("should call storeDidMount after getInitialState", function(){ 122 | assert.isTrue( storeDidMountSpy.calledAfter( getInitialStateSpy ) ); 123 | }); 124 | 125 | it("should have a state", function(){ 126 | assert.typeOf(store.state, "object"); 127 | }); 128 | 129 | it("store.state.getState() should work", function(){ 130 | assert.typeOf(store.getState, "function"); 131 | assert.typeOf(store.getState(), "object"); 132 | }); 133 | 134 | it("store.state.getStateClone() should return a copy of state", function(){ 135 | var id = store.get('id'); 136 | var stateCopy = store.getStateClone(); 137 | stateCopy.id = 'foo'; 138 | assert.equal(store.get('id'), id); 139 | }); 140 | 141 | it("store.state.get() should work", function(){ 142 | assert.equal(store.state.get('id'), 1); 143 | assert.equal(store.state.get('username'), "mustermann"); 144 | }); 145 | 146 | it("store.get() should work", function(){ 147 | assert.equal(store.get('id'), 1); 148 | assert.equal(store.get('username'), "mustermann"); 149 | }); 150 | 151 | it("store.state.getClone() should return a copy of state", function(){ 152 | var stateClone = store.getStateClone(); 153 | stateClone.deepFoo = { 154 | x: 1 155 | }; 156 | store.setState(stateClone); 157 | var deepFoo = store.getClone('deepFoo'); 158 | deepFoo.x = 'bar'; 159 | assert.equal(store.get('deepFoo').x, 1); 160 | }); 161 | 162 | it("should be able to call mixin methods", function(){ 163 | assert.equal(store.getId(), 1); 164 | }); 165 | 166 | it("should have a working setState", function(){ 167 | store.setState({ 168 | id: 3 169 | }); 170 | assert.equal(store.state.get('id'), 3); 171 | assert.equal(store.state.get('username'), 'mustermann'); 172 | }); 173 | 174 | 175 | it("should have onChange/offChange", function(){ 176 | assert.typeOf(store.onChange, "function"); 177 | assert.typeOf(store.offChange, "function"); 178 | }); 179 | 180 | it("should call onChange when state changes", function(){ 181 | var spy = sinon.spy(); 182 | store.onChange( spy ); 183 | store.setState({id: 2}); 184 | assert.isTrue( spy.called ); 185 | }); 186 | 187 | it("offChange should remove listener", function(){ 188 | var spy = sinon.spy(); 189 | store.onChange( spy ); 190 | store.offChange( spy ); 191 | store.setState({id: 2}); 192 | assert.isFalse( spy.called ); 193 | }); 194 | 195 | it("getActionState should work", function(){ 196 | 197 | assert.typeOf(store.getActionState, 'function', 'store.getActionState should be a function'); 198 | 199 | var state = store.getActionState(constants.TWO); 200 | 201 | assert.typeOf(state, 'object', 'store.getActionState should return an object'); 202 | assert.equal(state.name, 'TWO_HANDLER', 'store.getActionState should return state object correctly'); 203 | assert.equal(store.getActionState(constants.TWO, 'name'), 'TWO_HANDLER'); 204 | 205 | assert.throw(function(){ 206 | store.getActionState('nonexistant'); 207 | }, 'Store.getActionState constant handler for [nonexistant] is not defined'); 208 | 209 | assert.equal(store.getActionState(constants.TWO, 'nonexistantKey'), undefined); 210 | }); 211 | 212 | it("should have a working setActionState method", function(){ 213 | assert.typeOf(store.setActionState, 'function', 'store.setActionState should be a function'); 214 | 215 | store.setActionState(constants.TWO, {'name': 'bar'}); 216 | assert.equal(store.getActionState(constants.TWO, 'name'), 'bar', 'setActionState should reset state'); 217 | }); 218 | 219 | it("should have a working resetActionState method", function(){ 220 | assert.typeOf(store.resetActionState, 'function', 'store.resetActionState should be a function'); 221 | 222 | assert.throw(function(){ 223 | store.resetActionState('nonexistant'); 224 | }, 'Store.resetActionState constant handler for [nonexistant] is not defined'); 225 | 226 | store.setActionState(constants.TWO, {'name': 'bar'}); 227 | store.resetActionState(constants.TWO); 228 | assert.equal(store.getActionState(constants.TWO, 'name'), 'TWO_HANDLER', 'resetActionState should reset state'); 229 | }); 230 | 231 | it("should be able to replaceState", function(){ 232 | store.setState({'foo': 'bar'}); 233 | store.replaceState({ 234 | 'foo2': 'bar2' 235 | }); 236 | assert.isUndefined(store.get('foo'), "foo should not exist"); 237 | assert.equal(store.get('foo2'), 'bar2', "foo2 should equal bar2"); 238 | }); 239 | 240 | it("_getHandlerIndex should throw an error when provided a non-existant constant", function(){ 241 | assert.throw(function(){ 242 | store._getHandlerIndex(); 243 | }, /Can not get store handler for constant/); 244 | }); 245 | 246 | it("should provide a mixinFor method for react components", function(){ 247 | var mixinFor = store.mixinFor(); 248 | assert.typeOf(mixinFor, 'Object', 'Store mixinFor should return a mixin'); 249 | assert.typeOf(mixinFor.componentWillMount, 'function', 'store.mixinFor should return a mixin with componentWillMount'); 250 | assert.typeOf(mixinFor.componentDidMount, 'function', 'store.mixinFor should return a mixin with componentDidMount'); 251 | assert.typeOf(mixinFor.componentWillUnmount, 'function', 'store.mixinFor should return a mixin with componentWillUnmount'); 252 | }); 253 | 254 | 255 | it("createStore should complain about wrong handler definitions", function(){ 256 | assert.throw(function(){ 257 | ReactFlux.createStore({ 258 | 259 | }, {}); 260 | }, /store expects handler definitions to be an array/); 261 | 262 | assert.throw(function(){ 263 | ReactFlux.createStore({ 264 | }, [ 265 | [null, function(){}] 266 | ]); 267 | }, /store expects all handler definitions to contain a constant as the first parameter/); 268 | 269 | assert.throw(function(){ 270 | ReactFlux.createStore({ 271 | }, [ 272 | ['FOO', null] 273 | ]); 274 | }, /store expects all handler definitions to contain a callback/); 275 | 276 | assert.throw(function(){ 277 | ReactFlux.createStore({ 278 | },[ 279 | 'buggy', function(){} 280 | ]); 281 | }, /store expects handler definition to be an array/); 282 | 283 | assert.throw(function(){ 284 | ReactFlux.createStore({ 285 | },[ 286 | ['FOO', store, function(){}] 287 | ]); 288 | }, /store expects waitFor to be an array of stores/); 289 | 290 | assert.throw(function(){ 291 | ReactFlux.createStore({ 292 | },[ 293 | ['FOO', [function(){}], function(){}] 294 | ]); 295 | }, /store expects waitFor to be an array of stores/); 296 | 297 | }); 298 | 299 | }); 300 | -------------------------------------------------------------------------------- /test/storeActionHandler.js: -------------------------------------------------------------------------------- 1 | var ReactFlux = require('../'); 2 | var assert = require('chai').assert; 3 | 4 | var StoreActionHandler = require('../lib/storeActionHandler'); 5 | 6 | describe("storeActionHandler", function(){ 7 | 8 | it("should work on store only", function(){ 9 | assert.throw(function(){ 10 | new StoreActionHandler({}); 11 | }, 'StoreActionHandler expects first parameter to be a store'); 12 | }); 13 | 14 | it("expects second parameter to be a constant", function(){ 15 | assert.throw(function(){ 16 | var store = ReactFlux.createStore(); 17 | new StoreActionHandler(store, ''); 18 | }, 'StoreActionHandler expects second parameter to be a constant(string)'); 19 | }); 20 | 21 | it("should create a default getInitialState", function(){ 22 | var store = ReactFlux.createStore(); 23 | var actionHandler = new StoreActionHandler(store, 'FOO', {}); 24 | assert.isFunction(actionHandler.getInitialState); 25 | }); 26 | 27 | it("should have a setState", function(){ 28 | var store = ReactFlux.createStore(); 29 | var actionHandler = new StoreActionHandler(store, 'FOO', {}); 30 | assert.isFunction(actionHandler.setState); 31 | }); 32 | 33 | it("should have a getState", function(){ 34 | var store = ReactFlux.createStore(); 35 | var actionHandler = new StoreActionHandler(store, 'FOO', {}); 36 | assert.isFunction(actionHandler.getState); 37 | }); 38 | 39 | it("should have a parent", function(){ 40 | var store = ReactFlux.createStore(); 41 | var actionHandler = new StoreActionHandler(store, 'FOO', {}); 42 | assert.isObject(actionHandler.parent) 43 | assert.isTrue(actionHandler.parent.isStore()); 44 | }); 45 | 46 | //@todo: test that setting state work 47 | 48 | it("it should complain if getInitialState is not a function", function(){ 49 | assert.throw(function(){ 50 | var store = ReactFlux.createStore(); 51 | new StoreActionHandler(store, 'FOO', { 52 | getInitialState: {} 53 | }); 54 | }, 'StoreActionHandler expects getInitialState to be a function'); 55 | }); 56 | 57 | it("it should complain if before,success,error or after is not a function", function(){ 58 | assert.throw(function(){ 59 | var store = ReactFlux.createStore(); 60 | new StoreActionHandler(store, 'FOO', { 61 | before: {} 62 | }); 63 | }, 'StoreActionHandler expects "before" to be a function'); 64 | }); 65 | 66 | 67 | }); 68 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | 3 | var UglifyJsPlugin = require('webpack/lib/optimize/UglifyJsPlugin'); 4 | 5 | module.exports = { 6 | 7 | context: __dirname + '/lib/', 8 | 9 | watch: true, 10 | 11 | entry: './index.js', 12 | 13 | output: { 14 | path: './dist/', 15 | filename: 'react-flux.js', 16 | library: 'ReactFlux', 17 | libraryTarget: 'umd' 18 | }, 19 | 20 | resolve: { 21 | extensions: ['', '.js'], 22 | }, 23 | 24 | module: {}, 25 | 26 | externals: { 27 | promise: true, 28 | react: { 29 | root: 'React', 30 | commonjs: 'react', 31 | commonjs2: 'react', 32 | amd: 'react' 33 | } 34 | }, 35 | 36 | plugins: [ 37 | new UglifyJsPlugin() 38 | ] 39 | }; 40 | --------------------------------------------------------------------------------