├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bower.json ├── build ├── fluxxor.js ├── fluxxor.js.map ├── fluxxor.min.js └── fluxxor.min.js.map ├── examples ├── async │ ├── README.md │ ├── app │ │ ├── app.jsx │ │ ├── bundle.js │ │ ├── bundle.js.map │ │ └── index.html │ ├── start.sh │ └── webpack.config.js ├── carousel │ ├── README.md │ ├── app │ │ ├── actions.js │ │ ├── app.jsx │ │ ├── bundle.js │ │ ├── bundle.js.map │ │ ├── components │ │ │ ├── application.jsx │ │ │ ├── carousel.jsx │ │ │ └── image_form.jsx │ │ ├── constants.js │ │ ├── images │ │ │ ├── bttf1.png │ │ │ ├── bttf2.png │ │ │ ├── bttf3.png │ │ │ ├── bttf4.png │ │ │ ├── bttf5.png │ │ │ └── bttf6.png │ │ ├── index.html │ │ ├── stores │ │ │ ├── carousel_store.js │ │ │ └── image_store.js │ │ └── style.less │ ├── fluxxor-carousel-screenshot.png │ ├── start.sh │ └── webpack.config.js ├── react-router │ ├── README.md │ ├── app │ │ ├── actions.jsx │ │ ├── app.jsx │ │ ├── bundle.js │ │ ├── bundle.js.map │ │ ├── components │ │ │ ├── empty_view.jsx │ │ │ ├── recipe.jsx │ │ │ ├── recipe_adder.jsx │ │ │ ├── recipe_editor.jsx │ │ │ └── recipe_list.jsx │ │ ├── forms │ │ │ └── recipe_form.jsx │ │ ├── index.html │ │ ├── routes.jsx │ │ ├── schemas │ │ │ ├── ingredient.jsx │ │ │ └── recipe.jsx │ │ ├── stores │ │ │ ├── recipe_store.jsx │ │ │ └── route_store.jsx │ │ └── style.less │ ├── start.sh │ └── webpack.config.js └── todo-basic │ ├── README.md │ ├── app │ ├── app.jsx │ ├── bundle.js │ ├── bundle.js.map │ └── index.html │ ├── start.sh │ └── webpack.config.js ├── fluxxor.svg ├── index.js ├── lib ├── create_store.js ├── dispatcher.js ├── flux.js ├── flux_child_mixin.js ├── flux_mixin.js ├── store.js ├── store_watch_mixin.js └── util │ └── inherits.js ├── package.json ├── script ├── build-examples ├── build-fluxxor └── write-version-file ├── site ├── config.json ├── contents │ ├── CNAME │ ├── changelog.md │ ├── documentation │ │ ├── actions.md │ │ ├── flux-mixin.md │ │ ├── flux.md │ │ ├── index.md │ │ ├── store-watch-mixin.md │ │ └── stores.md │ ├── examples │ │ ├── carousel │ │ │ ├── carousel-bundle.js │ │ │ ├── images │ │ │ │ ├── bttf1.png │ │ │ │ ├── bttf2.png │ │ │ │ ├── bttf3.png │ │ │ │ ├── bttf4.png │ │ │ │ ├── bttf5.png │ │ │ │ └── bttf6.png │ │ │ └── index.md │ │ ├── index.md │ │ ├── react-router-bundle.js │ │ └── react-router.md │ ├── faq.md │ ├── fluxbox.svg │ ├── fluxxor.png │ ├── fluxxor.svg │ ├── getting-started │ │ ├── index.md │ │ ├── installation.md │ │ └── quick-start.md │ ├── guides │ │ ├── async-bundle.js │ │ ├── async-data.md │ │ ├── index.md │ │ ├── installation.md │ │ ├── quick-start.md │ │ ├── react.md │ │ ├── routing.md │ │ └── todo-bundle.js │ ├── images │ │ ├── flux-complex.png │ │ ├── flux-diagram.gliffy │ │ ├── flux-diagram.png │ │ ├── flux-simple.png │ │ ├── mvc-complex.png │ │ ├── mvc-diagram.gliffy │ │ ├── mvc-diagram.png │ │ └── mvc-simple.png │ ├── index.md │ ├── style │ │ └── main.less │ └── what-is-flux.md └── templates │ ├── index.ejs │ └── page.ejs ├── test └── unit │ ├── test_dispatch_interceptor.js │ ├── test_dispatcher.js │ ├── test_flux.js │ ├── test_flux_mixin.js │ ├── test_store.js │ └── test_store_watch_mixin.js ├── version.js └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | site/build/ 3 | npm-debug.log 4 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | build/ 2 | examples/ 3 | test/ 4 | site/ 5 | script/ 6 | fluxxor.svg 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | node_js: 4 | - 0.10 5 | - 0.12 6 | - iojs-v1.8.4 7 | - iojs-v2.5.0 8 | notifications: 9 | webhooks: 10 | urls: 11 | - https://webhooks.gitter.im/e/c7db868897cb976ade66 12 | on_success: change 13 | on_failure: always 14 | on_start: false 15 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Version 1.7.3 2 | ============= 3 | 4 | * Fix `bind` warning when using `StoreWatchMixin` with `createClass` (#140) 5 | 6 | Version 1.7.2 7 | ============= 8 | 9 | * Update `StoreWatchMixin` to support ES6 classes (#135) 10 | 11 | Version 1.7.1 12 | ============= 13 | 14 | * Relax restrictions on Lodash version (#128) 15 | 16 | Version 1.7.0 17 | ============= 18 | 19 | * Add `Flux#getAllStores()` to retrieve all currently registered stores (#127) 20 | 21 | Version 1.6.0 22 | ============= 23 | 24 | * Add `Flux#setDispatchInterceptor` to wrap or replace dispatch functionality (#100, #92) 25 | 26 | Version 1.5.4 27 | ============= 28 | 29 | * Fix incompatibility with Lodash 3.9.0 30 | 31 | Version 1.5.3 32 | ============= 33 | 34 | * Use built-in inherits instead of npm package (#116) 35 | 36 | Version 1.5.2 37 | ============= 38 | 39 | * Upgrade to Lo-Dash 3.x 40 | * Fix minor typo in mixin warnings 41 | 42 | Version 1.5.1 43 | ============= 44 | 45 | * Watch stores in `componentDidMount` instead of `componentWillMount` to make it harder to leak memory on the server 46 | 47 | Version 1.5.0 48 | ============= 49 | 50 | **Additions/Non-Breaking Changes** 51 | 52 | * You can add stores and actions to existing Flux instances via `addStore`, `addStores`, `addAction`, and `addActions` (#68, #71, #77) 53 | * `Flux` instances are now EventEmitters, and emit a `"dispatch"` event (with `type` and `payload` arguments) when an action calls `this.dispatch(type, payload)`, useful for cross-cutting concerns like logging 54 | * `Store#bindActions` now takes a hash (similar to the static `actions` hash) in addition to an argument list (#51, #78) 55 | * Fluxxor will throw more descriptive errors in many situations that are obviously incorrect (for example, when an action handler is not defined, or an action type in `bindActions` is falsy) 56 | 57 | **Deprecations** 58 | 59 | * `Fluxxor.FluxChildMixin` is now deprecated; instead, use `FluxMixin` anywhere you want access to `getFlux()` (#59) 60 | 61 | Version 1.4.2 62 | ============= 63 | 64 | * Throw an error when binding to a falsy action type (#50) 65 | * Reduce npm package size with `.npmignore` (#65) 66 | * Warn if a dispatch is not handled by any store (#66) 67 | * Reduce file size slightly by using [EventEmitter3](https://github.com/3rd-Eden/EventEmitter3) instead of Node.js `events` module 68 | 69 | Version 1.4.1 70 | ============= 71 | 72 | * Reduce file size by generating a smaller version file (#63) and using inherits package (#64) 73 | 74 | Version 1.4.0 75 | ============= 76 | 77 | * Action generating methods (methods in the `actions` parameter to the `Fluxxor.Flux` constructor) can access the `Flux` instance via `this.flux`. 78 | 79 | Version 1.3.2 80 | ============= 81 | 82 | * Ensure component is mounted before setting state in in StoreWatchMixin 83 | 84 | Version 1.3.1 85 | ============= 86 | 87 | * Fix Bower versioning 88 | 89 | Version 1.3.0 90 | ============= 91 | 92 | * Add support for namespaced actions 93 | 94 | Version 1.2.2 95 | ============= 96 | 97 | * Maintain references to stores on Flux object 98 | 99 | Version 1.2.1 100 | ============= 101 | 102 | * Throw when dispatching a value without a `type` property 103 | 104 | Version 1.2.0 105 | ============= 106 | 107 | * Allow synchronous back-to-back dispatches 108 | * Gracefully handle exceptions in action handlers 109 | * Add hasOwnProperty(key) checks throughout codebase 110 | 111 | Version 1.1.3 112 | ============= 113 | 114 | * Add [Bower](http://bower.io/) support 115 | 116 | Version 1.1.2 117 | ============= 118 | 119 | * Fix compilation when using webpack (#9) 120 | 121 | Version 1.1.1 122 | ============= 123 | 124 | * Reduce bundle size by 40% 125 | 126 | Version 1.1.0 127 | ============= 128 | 129 | * Add `FluxChildMixin` to access flux on child components 130 | * Use `getFlux()` to access flux with either `FluxMixin` or `FluxChildMixin` 131 | 132 | Version 1.0.2 133 | ============= 134 | 135 | * Documentation updates 136 | 137 | Version 1.0.1 138 | ============= 139 | 140 | * Throw when mixing `StoreWatchMixin` in without calling it as a function 141 | 142 | Version 1.0.0 143 | ============= 144 | 145 | First stable release 146 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014-2015 Michelle Tilley 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Fluxxor 3 |
4 | 5 | Fluxxor is a set of tools to aid in developing 6 | [React](http://facebook.github.io/react/) applications with the [Flux 7 | architecture](http://facebook.github.io/react/docs/flux-overview.html). 8 | 9 | [![Travis CI](https://api.travis-ci.org/BinaryMuse/fluxxor.svg?branch=master)](https://travis-ci.org/BinaryMuse/fluxxor) 10 | 11 | [![NPM](https://nodei.co/npm/fluxxor.png?downloads=true)](https://nodei.co/npm/fluxxor/) 12 | 13 | Installation 14 | ------------ 15 | 16 | Fluxxor is available on npm and works with module bundlers like 17 | [Browserify](http://browserify.org/) and [Webpack](http://webpack.github.io/). 18 | 19 | npm install [--save] fluxxor 20 | 21 | Standalone browser builds can be downloaded from 22 | [the GitHub releases page](https://github.com/BinaryMuse/fluxxor/releases) or installed via 23 | Bower: 24 | 25 | bower install fluxxor 26 | 27 | More detailed installation instructions can be found on 28 | [the Fluxxor website](http://fluxxor.com/guides/installation.html). 29 | 30 | ### Third Party Releases 31 | 32 | The following releases are maintained by third parties, and support inquiries should be directed to their maintainers. 33 | 34 | #### WebJar 35 | 36 | For JVM languages, there are [WebJar](http://www.webjars.org) packages available on Maven Central and jsDelivr as the following: 37 | 38 | SBT/Play Framework 2: 39 | 40 | ```scala 41 | "org.webjars" % "fluxxor" % fluxxorVersion 42 | ``` 43 | 44 | Maven: 45 | 46 | ```xml 47 | 48 | org.webjars 49 | fluxxor 50 | ${fluxxor.version} 51 | 52 | ``` 53 | 54 | For detailed instructions, refer to the [WebJars documentation](http://www.webjars.org/documentation). For update requests, open a pull request on the [Fluxxor WebJar repository on Github](https://github.com/webjars/fluxxor). 55 | 56 | Browser Compatibility 57 | --------------------- 58 | 59 | Fluxxor is compatible with any [ES5-compliant browser](http://kangax.github.io/compat-table/es5/) (IE 9+, FF 4+, Safari 5.1.4+, Chrome 19+, Opera 12.10+). You can use [es5-shim](https://github.com/es-shims/es5-shim) for other browsers. 60 | 61 | 62 | Documentation 63 | ------------- 64 | 65 | See [the Fluxxor website](http://fluxxor.com) for in-depth 66 | [documentation](http://fluxxor.com/documentation/), 67 | [installation instructions](http://fluxxor.com/guides/installation.html), 68 | [examples](http://fluxxor.com/examples/), and 69 | [a getting started guide](http://fluxxor.com/guides/quick-start.html). 70 | 71 | Support and Chat 72 | ---------------- 73 | 74 | Get help with and chat about Fluxxor [on Gitter](https://gitter.im/BinaryMuse/fluxxor). 75 | 76 | [![Gitter chat](https://badges.gitter.im/BinaryMuse/fluxxor.png)](https://gitter.im/BinaryMuse/fluxxor) 77 | 78 | License 79 | ------- 80 | 81 | Fluxxor is licensed under the [MIT license](LICENSE). 82 | 83 | > The MIT License (MIT) 84 | > 85 | > Copyright (c) 2014 Michelle Tilley 86 | > 87 | > Permission is hereby granted, free of charge, to any person obtaining a copy 88 | > of this software and associated documentation files (the "Software"), to deal 89 | > in the Software without restriction, including without limitation the rights 90 | > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 91 | > copies of the Software, and to permit persons to whom the Software is 92 | > furnished to do so, subject to the following conditions: 93 | > 94 | > The above copyright notice and this permission notice shall be included in 95 | > all copies or substantial portions of the Software. 96 | > 97 | > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 98 | > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 99 | > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 100 | > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 101 | > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 102 | > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 103 | > THE SOFTWARE. 104 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluxxor", 3 | "version": "1.7.3", 4 | "main": "build/fluxxor.min.js", 5 | "description": "Flux architecture tools for React", 6 | "homepage": "", 7 | "authors": [ 8 | "Michelle Tilley " 9 | ], 10 | "license": "MIT", 11 | "ignore": [ 12 | ".git", 13 | "examples", 14 | "lib", 15 | "node_modules", 16 | "script", 17 | "site", 18 | "test", 19 | ".gitignore", 20 | ".travis.yml", 21 | "fluxxor.svg", 22 | "index.js", 23 | "package.json", 24 | "version.js", 25 | "webpack.config.js" 26 | ], 27 | "keywords": [ 28 | "flux", 29 | "react" 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /examples/async/README.md: -------------------------------------------------------------------------------- 1 | Asynchronous Data Example 2 | ========================= 3 | 4 | This is a very simple Fluxxor application demonstrating how to deal with asynchronous data. It fakes an asynchronous API that returns suggested catch phrases for your new startup and allows you to submit new suggested catch phrases (50% of the time it simulates an error instead). The Fluxxor website describes the technique in more detail in [the asynchronous data guide](http://fluxxor.com/guides/async-data.html). 5 | 6 | To run, simply open `app/index.html` in your web browser—`index.html`, `bundle.js`, and an internet connection (for loading libraries via CDN) are all that's required to run the application (`bundle.js` is build with [Webpack](http://webpack.github.io/)). If you want to modify the source and see your updates, run `./start.sh` and point your browser to `http://localhost:8089/index.html` (you will need to install the `devDependencies` from the root of the Fluxxor project first). 7 | -------------------------------------------------------------------------------- /examples/async/app/app.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"), 2 | Fluxxor = require("../../../"); 3 | 4 | window.React = React; 5 | 6 | var BuzzwordClient = { 7 | load: function(success, failure) { 8 | setTimeout(function() { 9 | success(_.range(10).map(Faker.Company.catchPhrase)); 10 | }, 1000); 11 | }, 12 | 13 | submit: function(word, success, failure) { 14 | setTimeout(function() { 15 | if (Math.random() > 0.5) { 16 | success(word); 17 | } else { 18 | failure("Failed to " + Faker.Company.bs()); 19 | } 20 | }, Math.random() * 1000 + 500); 21 | } 22 | }; 23 | 24 | var constants = { 25 | LOAD_BUZZ: "LOAD_BUZZ", 26 | LOAD_BUZZ_SUCCESS: "LOAD_BUZZ_SUCCESS", 27 | LOAD_BUZZ_FAIL: "LOAD_BUZZ_FAIL", 28 | 29 | ADD_BUZZ: "ADD_BUZZ", 30 | ADD_BUZZ_SUCCESS: "ADD_BUZZ_SUCCESS", 31 | ADD_BUZZ_FAIL: "ADD_BUZZ_FAIL" 32 | }; 33 | 34 | var actions = { 35 | loadBuzz: function() { 36 | this.dispatch(constants.LOAD_BUZZ); 37 | 38 | BuzzwordClient.load(function(words) { 39 | this.dispatch(constants.LOAD_BUZZ_SUCCESS, {words: words}); 40 | }.bind(this), function(error) { 41 | this.dispatch(constants.LOAD_BUZZ_FAIL, {error: error}); 42 | }.bind(this)); 43 | }, 44 | 45 | addBuzz: function(word) { 46 | var id = _.uniqueId(); 47 | this.dispatch(constants.ADD_BUZZ, {id: id, word: word}); 48 | 49 | BuzzwordClient.submit(word, function() { 50 | this.dispatch(constants.ADD_BUZZ_SUCCESS, {id: id}); 51 | }.bind(this), function(error) { 52 | this.dispatch(constants.ADD_BUZZ_FAIL, {id: id, error: error}); 53 | }.bind(this)); 54 | } 55 | }; 56 | 57 | var BuzzwordStore = Fluxxor.createStore({ 58 | initialize: function() { 59 | this.loading = false; 60 | this.error = null; 61 | this.words = {}; 62 | 63 | this.bindActions( 64 | constants.LOAD_BUZZ, this.onLoadBuzz, 65 | constants.LOAD_BUZZ_SUCCESS, this.onLoadBuzzSuccess, 66 | constants.LOAD_BUZZ_FAIL, this.onLoadBuzzFail, 67 | 68 | constants.ADD_BUZZ, this.onAddBuzz, 69 | constants.ADD_BUZZ_SUCCESS, this.onAddBuzzSuccess, 70 | constants.ADD_BUZZ_FAIL, this.onAddBuzzFail 71 | ); 72 | }, 73 | 74 | onLoadBuzz: function() { 75 | this.loading = true; 76 | this.emit("change"); 77 | }, 78 | 79 | onLoadBuzzSuccess: function(payload) { 80 | this.loading = false; 81 | this.error = null; 82 | 83 | this.words = payload.words.reduce(function(acc, word) { 84 | var clientId = _.uniqueId(); 85 | acc[clientId] = {id: clientId, word: word, status: "OK"}; 86 | return acc; 87 | }, {}); 88 | this.emit("change"); 89 | }, 90 | 91 | onLoadBuzzFail: function(payload) { 92 | this.loading = false; 93 | this.error = payload.error; 94 | this.emit("change"); 95 | }, 96 | 97 | onAddBuzz: function(payload) { 98 | var word = {id: payload.id, word: payload.word, status: "ADDING"}; 99 | this.words[payload.id] = word; 100 | this.emit("change"); 101 | }, 102 | 103 | onAddBuzzSuccess: function(payload) { 104 | this.words[payload.id].status = "OK"; 105 | this.emit("change"); 106 | }, 107 | 108 | onAddBuzzFail: function(payload) { 109 | this.words[payload.id].status = "ERROR"; 110 | this.words[payload.id].error = payload.error; 111 | this.emit("change"); 112 | } 113 | }); 114 | 115 | var stores = { 116 | BuzzwordStore: new BuzzwordStore() 117 | }; 118 | 119 | var flux = new Fluxxor.Flux(stores, actions); 120 | 121 | window.flux = flux; 122 | 123 | flux.on("dispatch", function(type, payload) { 124 | if (console && console.log) { 125 | console.log("[Dispatch]", type, payload); 126 | } 127 | }); 128 | 129 | var FluxMixin = Fluxxor.FluxMixin(React), 130 | StoreWatchMixin = Fluxxor.StoreWatchMixin; 131 | 132 | var Application = React.createClass({ 133 | mixins: [FluxMixin, StoreWatchMixin("BuzzwordStore")], 134 | 135 | getInitialState: function() { 136 | return { 137 | suggestBuzzword: "" 138 | }; 139 | }, 140 | 141 | getStateFromFlux: function() { 142 | var store = this.getFlux().store("BuzzwordStore"); 143 | return { 144 | loading: store.loading, 145 | error: store.error, 146 | words: _.values(store.words) 147 | }; 148 | }, 149 | 150 | render: function() { 151 | return ( 152 |
153 |

All the Buzzwords

154 | {this.state.error ? "Error loading data" : null} 155 | 161 |

Suggest a New Buzzword

162 |
163 | 165 | 166 |
167 |
168 | ); 169 | }, 170 | 171 | componentDidMount: function() { 172 | this.getFlux().actions.loadBuzz(); 173 | }, 174 | 175 | handleSuggestedWordChange: function(e) { 176 | this.setState({suggestBuzzword: e.target.value}); 177 | }, 178 | 179 | handleSubmitForm: function(e) { 180 | e.preventDefault(); 181 | if (this.state.suggestBuzzword.trim()) { 182 | this.getFlux().actions.addBuzz(this.state.suggestBuzzword); 183 | this.setState({suggestBuzzword: ""}); 184 | } 185 | } 186 | }); 187 | 188 | var Word = React.createClass({ 189 | render: function() { 190 | var statusText, statusStyle = {}; 191 | switch(this.props.word.status) { 192 | case "OK": 193 | statusText = ""; 194 | break; 195 | case "ADDING": 196 | statusText = "adding..."; 197 | statusStyle = { color: "#ccc" }; 198 | break; 199 | case "ERROR": 200 | statusText = "error: " + this.props.word.error; 201 | statusStyle = { color: "red" }; 202 | break; 203 | } 204 | 205 | return ( 206 |
  • 207 | {this.props.word.word} {statusText} 208 |
  • 209 | ); 210 | } 211 | }); 212 | 213 | React.render(, document.getElementById("app")); 214 | -------------------------------------------------------------------------------- /examples/async/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
    8 | 9 | -------------------------------------------------------------------------------- /examples/async/start.sh: -------------------------------------------------------------------------------- 1 | ../../node_modules/.bin/webpack-dev-server --port 8089 --no-info --content-base app 2 | -------------------------------------------------------------------------------- /examples/async/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | 3 | module.exports = { 4 | cache: true, 5 | entry: "./app/app.jsx", 6 | output: { 7 | path: __dirname + "/app", 8 | filename: "bundle.js" 9 | }, 10 | devtool: "source-map", 11 | module: { 12 | loaders: [ 13 | { test: /\.less$/, loader: "style!css!less" }, 14 | { test: /\.jsx$/, loader: "jsx-loader" }, 15 | { test: /\.json$/, loader: "json" } 16 | ] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /examples/carousel/README.md: -------------------------------------------------------------------------------- 1 | Carousel Example 2 | ================ 3 | 4 | This is a small example React application using Fluxxor. 5 | 6 | ![Screenshot](fluxxor-carousel-screenshot.png) 7 | 8 | To run, simply open `app/index.html` in your web browser—`index.html`, `bundle.js`, and the `images/` directory are the only files required to run the application (`bundle.js` is build with [Webpack](http://webpack.github.io/)). If you want to modify the source and see your updates, run `./start.sh` and point your browser to `http://localhost:8089/index.html` (you will need to install the `devDependencies` from the root of the Fluxxor project first). 9 | 10 | Alternatively, check out the running example [on the Fluxxor website](http://fluxxor.com/examples/carousel/). 11 | 12 | The entry point of the application is `app/app.jsx`. The `Fluxxor.Flux` instance is exported to `window.flux` so that you can manipulate the application from the standard JavaScript console (e.g. the methods on `window.flux.actions`). 13 | 14 | Stores 15 | ------ 16 | 17 | The system contains two stores: 18 | 19 | * `ImageStore` - Contains the list of images to display in the carousel 20 | * `CarouselStore` - Contains information about the carousel position (which image is being displayed, etc.) and ensures that the user cannot change images while a transition is in progress 21 | 22 | This separation is perhaps a bit contrived, but shows how you might use multiple stores in a Flux-based React application. 23 | 24 | Actions 25 | ------- 26 | 27 | There are four actions that manipulate the stores: 28 | 29 | * `prevImage` - Moves the carousel to the previous image (modifies `CarouselStore`) 30 | * `nextImage` - Moves the carousel to the next image (modifies `CarouselStore`) 31 | * `selectImage` - Moves the carousel to a specific image (modifies `CarouselStore`) 32 | * `addImage` - Adds an image to the carousel and moves to that image (modifies `ImageStore` and `CarouselStore`) 33 | 34 | `addImage` is particularly interesting because the action handler inside `CarouselStore` utilizes the `waitFor` mechanism to allow the `ImageStore` to accept or reject adding the image (based on its file extension and whether the image is already in the carousel). 35 | 36 | Components 37 | ---------- 38 | 39 | * `Application` - The top-level component that maintains state from the stores using `Fluxxor.StoreWatchMixin` 40 | * `Carousel` - The actual image carousel itself; all data is passed as props, and user interaction is propagated back to `Application` 41 | * `ImageForm` - A small form for adding images to the carousel; user interaction is propagated back to `Application` 42 | 43 | Note that `ImageForm` has state outside of the data from the stores—however, it's localized to the form, and isn't part of the larger "application state." When the form is submitted, this local state is sent to the parent component through a property, where it is then sent to the dispatcher via the `addImage` action. 44 | 45 | --- 46 | 47 | In this small application, only the top level `Application` component requires access to the Flux data. However, since `Application` mixes in `Fluxxor.FluxMixin`, any descendants of `Application` with the `Fluxxor.FluxChildMixin` would automatically have access to the `Fluxxor.Flux` instance via `this.getFlux()`. 48 | -------------------------------------------------------------------------------- /examples/carousel/app/actions.js: -------------------------------------------------------------------------------- 1 | var Constants = require("./constants"); 2 | 3 | module.exports = { 4 | nextImage: function() { 5 | this.dispatch(Constants.NEXT_IMAGE); 6 | }, 7 | 8 | prevImage: function() { 9 | this.dispatch(Constants.PREV_IMAGE); 10 | }, 11 | 12 | selectImage: function(i) { 13 | this.dispatch(Constants.SEL_IMAGE, {index: i}); 14 | }, 15 | 16 | addImage: function(url) { 17 | this.dispatch(Constants.ADD_IMAGE, {url: url}); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /examples/carousel/app/app.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"), 2 | Fluxxor = require("../../../"); 3 | 4 | var Application = require("./components/application.jsx"), 5 | ImageStore = require("./stores/image_store"), 6 | CarouselStore = require("./stores/carousel_store"), 7 | actions = require("./actions"); 8 | 9 | require("./style.less"); 10 | 11 | var images = ["images/bttf1.png", "images/bttf2.png", "images/bttf3.png", 12 | "images/bttf4.png", "images/bttf5.png", "images/bttf6.png"]; 13 | 14 | var stores = { 15 | CarouselStore: new CarouselStore({count: images.length}), 16 | ImageStore: new ImageStore({images: images}) 17 | }; 18 | 19 | var flux = new Fluxxor.Flux(stores, actions); 20 | window.flux = flux; 21 | 22 | flux.on("dispatch", function(type, payload) { 23 | if (console && console.log) { 24 | console.log("[Dispatch]", type, payload); 25 | } 26 | }); 27 | 28 | React.render(, document.getElementById("app")); 29 | -------------------------------------------------------------------------------- /examples/carousel/app/components/application.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"), 2 | Fluxxor = require("../../../../"), 3 | FluxMixin = Fluxxor.FluxMixin(React), 4 | StoreWatchMixin = Fluxxor.StoreWatchMixin; 5 | 6 | var Carousel = require("./carousel.jsx"), 7 | ImageForm = require("./image_form.jsx"); 8 | 9 | var Application = React.createClass({ 10 | mixins: [FluxMixin, StoreWatchMixin("ImageStore", "CarouselStore")], 11 | 12 | // Required by StoreWatchMixin 13 | getStateFromFlux: function() { 14 | var flux = this.getFlux(); 15 | return { 16 | images: flux.store("ImageStore").getState(), 17 | carousel: flux.store("CarouselStore").getState() 18 | }; 19 | }, 20 | 21 | render: function() { 22 | return ( 23 |
    24 | 29 | 30 |
    31 | ); 32 | }, 33 | 34 | onClickLeft: function() { 35 | this.getFlux().actions.prevImage(); 36 | }, 37 | 38 | onClickRight: function() { 39 | this.getFlux().actions.nextImage(); 40 | }, 41 | 42 | onSelectImage: function(i) { 43 | this.getFlux().actions.selectImage(i); 44 | }, 45 | 46 | onAddUrl: function(url) { 47 | this.getFlux().actions.addImage(url); 48 | } 49 | }); 50 | 51 | module.exports = Application; 52 | -------------------------------------------------------------------------------- /examples/carousel/app/components/carousel.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"); 2 | 3 | var Carousel = React.createClass({ 4 | propTypes: { 5 | images: React.PropTypes.array.isRequired, 6 | selected: React.PropTypes.number.isRequired, 7 | onClickLeft: React.PropTypes.func.isRequired, 8 | onClickRight: React.PropTypes.func.isRequired, 9 | onSelectImage: React.PropTypes.func.isRequired 10 | }, 11 | 12 | render: function() { 13 | var left = this.props.selected * 300 * -1, 14 | ulStyle = { 15 | width: this.props.images.length * 300, 16 | "-ms-transform": "translate(" + left + "px,0px)", 17 | "-webkit-transform": "translate(" + left + "px,0px)", 18 | transform: "translate(" + left + "px,0px)" 19 | }; 20 | 21 | return ( 22 |
    23 | 25 |
    26 |
      27 | {this.props.images.map(function(image, i) { 28 | return
    • ; 29 | })} 30 |
    31 |
      32 | {this.props.images.map(function(image, i) { 33 | var activeClass = i === this.props.selected ? "active" : ""; 34 | return
    • ; 37 | }.bind(this))} 38 |
    39 |
    40 | 42 |
    43 | ) 44 | }, 45 | 46 | onClickDot: function(index) { 47 | this.props.onSelectImage(index); 48 | } 49 | }); 50 | 51 | module.exports = Carousel; 52 | -------------------------------------------------------------------------------- /examples/carousel/app/components/image_form.jsx: -------------------------------------------------------------------------------- 1 | React = require("react"); 2 | 3 | var ImageForm = React.createClass({ 4 | propTypes: { 5 | onAddUrl: React.PropTypes.func.isRequired 6 | }, 7 | 8 | getInitialState: function() { 9 | return { url: '' }; 10 | }, 11 | 12 | render: function() { 13 | return ( 14 |
    15 | 17 | 18 |
    19 | ); 20 | }, 21 | 22 | handleChange: function(e) { 23 | this.setState({url: e.target.value}); 24 | }, 25 | 26 | onSubmit: function(e) { 27 | e.preventDefault(); 28 | this.props.onAddUrl(this.state.url); 29 | this.setState({url: ''}); 30 | } 31 | }); 32 | 33 | module.exports = ImageForm; 34 | -------------------------------------------------------------------------------- /examples/carousel/app/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | NEXT_IMAGE: "NEXT_IMAGE", 3 | PREV_IMAGE: "PREV_IMAGE", 4 | SEL_IMAGE: "SEL_IMAGE", 5 | ADD_IMAGE: "ADD_IMAGE" 6 | }; 7 | -------------------------------------------------------------------------------- /examples/carousel/app/images/bttf1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/examples/carousel/app/images/bttf1.png -------------------------------------------------------------------------------- /examples/carousel/app/images/bttf2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/examples/carousel/app/images/bttf2.png -------------------------------------------------------------------------------- /examples/carousel/app/images/bttf3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/examples/carousel/app/images/bttf3.png -------------------------------------------------------------------------------- /examples/carousel/app/images/bttf4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/examples/carousel/app/images/bttf4.png -------------------------------------------------------------------------------- /examples/carousel/app/images/bttf5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/examples/carousel/app/images/bttf5.png -------------------------------------------------------------------------------- /examples/carousel/app/images/bttf6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/examples/carousel/app/images/bttf6.png -------------------------------------------------------------------------------- /examples/carousel/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 6 | -------------------------------------------------------------------------------- /examples/carousel/app/stores/carousel_store.js: -------------------------------------------------------------------------------- 1 | var Fluxxor = require("../../../../"), 2 | Constants = require("../constants"); 3 | 4 | var CarouselStore = Fluxxor.createStore({ 5 | initialize: function(options) { 6 | this.current = options.current || 0; 7 | this.count = options.count || 0; 8 | 9 | this.bindActions( 10 | Constants.NEXT_IMAGE, this.handleNextImage, 11 | Constants.PREV_IMAGE, this.handlePrevImage, 12 | Constants.SEL_IMAGE, this.handleSelectImage, 13 | Constants.ADD_IMAGE, this.handleAddImage 14 | ); 15 | }, 16 | 17 | getState: function() { 18 | return { 19 | current: this.current, 20 | count: this.count 21 | }; 22 | }, 23 | 24 | handleNextImage: function() { 25 | if (this.animating) return; 26 | var next = this.current + 1; 27 | if (next >= this.count) next = 0; 28 | this.selectImage(next); 29 | this.emit("change"); 30 | }, 31 | 32 | handlePrevImage: function() { 33 | if (this.animating) return; 34 | var next = this.current - 1; 35 | if (next < 0) next = this.count - 1; 36 | this.selectImage(next); 37 | this.emit("change"); 38 | }, 39 | 40 | handleSelectImage: function(payload) { 41 | this.selectImage(payload.index); 42 | this.emit("change"); 43 | }, 44 | 45 | handleAddImage: function() { 46 | this.waitFor(["ImageStore"], function(imageStore) { 47 | var length = imageStore.getState().images.length; 48 | // ImageStore may block adding a new image 49 | if (this.count < length) { 50 | this.count = length; 51 | this.selectImage(this.count - 1); 52 | this.emit("change"); 53 | } 54 | }); 55 | }, 56 | 57 | selectImage: function(index) { 58 | if (this.animating) return; 59 | this.animating = true; 60 | this.current = index; 61 | setTimeout(function() { 62 | this.animating = false; 63 | }.bind(this), 300); 64 | } 65 | }); 66 | 67 | module.exports = CarouselStore; 68 | -------------------------------------------------------------------------------- /examples/carousel/app/stores/image_store.js: -------------------------------------------------------------------------------- 1 | var Fluxxor = require("../../../../"), 2 | Constants = require("../constants"); 3 | 4 | var ImageStore = Fluxxor.createStore({ 5 | initialize: function(options) { 6 | this.images = options.images || []; 7 | 8 | this.bindActions(Constants.ADD_IMAGE, this.handleAddImage); 9 | }, 10 | 11 | getState: function() { 12 | return { 13 | images: this.images 14 | }; 15 | }, 16 | 17 | handleAddImage: function(payload) { 18 | if (!payload.url) return; 19 | // Don't allow duplicates 20 | if (this.images.indexOf(payload.url) > -1) return; 21 | var parts = payload.url.split("."); 22 | // Only allow images 23 | if (["png", "jpg", "jpeg", "gif"].indexOf(parts[parts.length - 1]) > -1) { 24 | this.images.push(payload.url); 25 | this.emit("change"); 26 | } 27 | } 28 | }); 29 | 30 | module.exports = ImageStore; 31 | -------------------------------------------------------------------------------- /examples/carousel/app/style.less: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | } 4 | 5 | .application-container { 6 | width: 400px; 7 | margin: 0 auto; 8 | } 9 | 10 | .arrow, .circle { 11 | cursor: pointer; 12 | -webkit-touch-callout: none; 13 | -webkit-user-select: none; 14 | -khtml-user-select: none; 15 | -moz-user-select: none; 16 | -ms-user-select: none; 17 | user-select: none; 18 | } 19 | 20 | .arrow { 21 | position: relative; 22 | top: -100px; 23 | padding: 10px; 24 | } 25 | 26 | .carousel-stage { 27 | width: 300px; 28 | height: 200px; 29 | border: 1px solid black; 30 | overflow: hidden; 31 | display: inline-block; 32 | position: relative; 33 | 34 | ul.carousel-list { 35 | display: inline-block; 36 | list-style: none; 37 | padding: 0; 38 | margin: 0; 39 | position: relative; 40 | transition: 300ms ease-in-out; 41 | 42 | li { 43 | display: inline-block; 44 | width: 300px; 45 | height: 200px; 46 | float: left; 47 | 48 | img { 49 | object-fit: cover; 50 | object-position: center center; 51 | width: 300px; 52 | height: 200px; 53 | } 54 | } 55 | } 56 | 57 | ul.dots { 58 | width: 300px; 59 | list-style: none; 60 | padding: 0; 61 | margin: 0; 62 | position: absolute; 63 | bottom: 0; 64 | text-align: center; 65 | 66 | li { 67 | display: inline-block; 68 | width: 14px; 69 | height: 14px; 70 | background-color: white; 71 | margin: 2px; 72 | border-radius: 50%; 73 | line-height: 5px; 74 | border: 2px solid black; 75 | 76 | &.active { 77 | background-color: #ccc; 78 | } 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /examples/carousel/fluxxor-carousel-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/examples/carousel/fluxxor-carousel-screenshot.png -------------------------------------------------------------------------------- /examples/carousel/start.sh: -------------------------------------------------------------------------------- 1 | ../../node_modules/.bin/webpack-dev-server --port 8089 --no-info --content-base app 2 | -------------------------------------------------------------------------------- /examples/carousel/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | 3 | module.exports = { 4 | cache: true, 5 | entry: "./app/app.jsx", 6 | output: { 7 | path: __dirname + "/app", 8 | filename: "bundle.js" 9 | }, 10 | devtool: "source-map", 11 | module: { 12 | loaders: [ 13 | { test: /\.less$/, loader: "style!css!less" }, 14 | { test: /\.jsx$/, loader: "jsx-loader" }, 15 | { test: /\.json$/, loader: "json" } 16 | ] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /examples/react-router/README.md: -------------------------------------------------------------------------------- 1 | React Router Example 2 | ==================== 3 | 4 | This app shows how to use [React Router](https://github.com/rackt/react-router) with Fluxxor. The Fluxxor website describes the technique used in more detail in [the Routing guide](http://fluxxor.com/guides/routing.html). 5 | 6 | To run, simply open `app/index.html` in your web browser—`index.html` and `bundle.js` are the only files required to run the application (`bundle.js` is build with [Webpack](http://webpack.github.io/)). If you want to modify the source and see your updates, run `./start.sh` and point your browser to `http://localhost:8089/index.html` (you will need to install the `devDependencies` from the root of the Fluxxor project first). 7 | -------------------------------------------------------------------------------- /examples/react-router/app/actions.jsx: -------------------------------------------------------------------------------- 1 | var c = { 2 | RECIPE: { 3 | ADD: "RECIPE:ADD", 4 | EDIT: "RECIPE:EDIT", 5 | REMOVE: "RECIPE:REMOVE" 6 | }, 7 | 8 | ROUTE: { 9 | TRANSITION: "ROUTE:TRANSITION" 10 | } 11 | }; 12 | 13 | var methods = { 14 | recipes: { 15 | add: function(name, desc, ingredients, directions, preventTransition) { 16 | this.dispatch(c.RECIPE.ADD, { 17 | name: name, 18 | description: desc, 19 | ingredients: ingredients, 20 | directions: directions, 21 | preventTransition: preventTransition 22 | }); 23 | }, 24 | 25 | edit: function(id, name, desc, ingredients, directions) { 26 | this.dispatch(c.RECIPE.EDIT, { 27 | id: id, 28 | name: name, 29 | description: desc, 30 | ingredients: ingredients, 31 | directions: directions 32 | }); 33 | }, 34 | 35 | remove: function(id) { 36 | this.dispatch(c.RECIPE.REMOVE, id); 37 | } 38 | }, 39 | 40 | routes: { 41 | transition: function(path, params) { 42 | this.dispatch(c.ROUTE.TRANSITION, {path: path, params: params}); 43 | } 44 | } 45 | }; 46 | 47 | module.exports = { 48 | methods: methods, 49 | constants: c 50 | }; 51 | -------------------------------------------------------------------------------- /examples/react-router/app/app.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"), 2 | Router = require("react-router"), 3 | Fluxxor = require("../../../"); 4 | 5 | var actions = require("./actions.jsx"), 6 | routes = require("./routes.jsx"), 7 | RecipeStore = require("./stores/recipe_store.jsx"), 8 | RouteStore = require("./stores/route_store.jsx"); 9 | 10 | require("./style.less"); 11 | 12 | var router = Router.create({routes: routes}); 13 | 14 | var stores = { 15 | recipe: new RecipeStore(), 16 | route: new RouteStore({router: router}) 17 | }; 18 | 19 | var flux = new Fluxxor.Flux(stores, actions.methods); 20 | flux.on("dispatch", function(type, payload) { 21 | console.log("Dispatch:", type, payload); 22 | }); 23 | 24 | flux.actions.recipes.add( 25 | "Strawberry Smoothie", 26 | "A yummy fruit smoothie made with tropical fruits.", 27 | [ 28 | { quantity: "8", item: "strawberries, hulled" }, 29 | { quantity: "1/2 cup", item: "skim milk" }, 30 | { quantity: "1/2 cup", item: "plain yogurt" }, 31 | { quantity: "3 tbsp", item: "white sugar" }, 32 | { quantity: "2 tsp", item: "vanilla extract" }, 33 | { quantity: "6", item: "ice cubes, crushed" } 34 | ], 35 | "In a blender combine strawberries, milk, yogurt, sugar and vanilla. Toss in the ice. Blend until smooth and creamy. Pour into glasses and serve.", 36 | true 37 | ); 38 | 39 | router.run(function(Handler) { 40 | React.render( 41 | , 42 | document.getElementById("app") 43 | ); 44 | }); 45 | -------------------------------------------------------------------------------- /examples/react-router/app/components/empty_view.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"), 2 | Router = require("react-router"), 3 | RouteHandler = Router.RouteHandler; 4 | 5 | module.exports = React.createClass({ 6 | render: function() { 7 | return ; 8 | } 9 | }); 10 | -------------------------------------------------------------------------------- /examples/react-router/app/components/recipe.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"), 2 | Router = require("react-router"), 3 | Link = Router.Link, 4 | Fluxxor = require("../../../../"); 5 | 6 | var RecipeStore = require("../stores/recipe_store.jsx"); 7 | 8 | var Recipe = React.createClass({ 9 | mixins: [ 10 | Fluxxor.FluxMixin(React), 11 | Fluxxor.StoreWatchMixin("recipe") 12 | ], 13 | 14 | contextTypes: { 15 | router: React.PropTypes.func 16 | }, 17 | 18 | getStateFromFlux: function() { 19 | var params = this.context.router.getCurrentParams(); 20 | 21 | return { 22 | recipe: this.getFlux().store("recipe").getRecipe(params.id) 23 | }; 24 | }, 25 | 26 | componentWillReceiveProps: function(nextProps) { 27 | this.setState(this.getStateFromFlux()); 28 | }, 29 | 30 | render: function() { 31 | var recipe = this.state.recipe; 32 | 33 | if (recipe === RecipeStore.NOT_FOUND_TOKEN) { 34 | return this.renderNotFound(); 35 | } 36 | 37 | var description = (recipe.description || "").replace(/\n/g, "
    "), 38 | directions = (recipe.directions || "").replace(/\n/g, "
    "); 39 | 40 | return this.renderWithLayout( 41 |
    42 |

    {recipe.name}

    43 |

    44 | Ingredients: 45 |

      {recipe.ingredients.map(this.renderIngredient)}
    46 | Directions: 47 |

    48 | 49 |

    50 | Edit Recipe 51 | {" | "}Delete Recipe 52 |

    53 |
    54 | ); 55 | }, 56 | 57 | renderIngredient: function(ingredient, idx) { 58 | return ( 59 |
  • 60 | {ingredient.quantity} {ingredient.item} 61 |
  • 62 | ); 63 | }, 64 | 65 | renderNotFound: function() { 66 | return this.renderWithLayout( 67 |
    That recipe was not found.
    68 | ); 69 | }, 70 | 71 | renderWithLayout: function(content) { 72 | return ( 73 |
    74 | {content} 75 |
    76 | Home 77 | {" | "}Add New Recipe 78 |
    79 | ); 80 | }, 81 | 82 | deleteRecipe: function(e) { 83 | if (confirm("Really delete this recipe?")) { 84 | this.getFlux().actions.recipes.remove(this.state.recipe.id); 85 | } else { 86 | e.preventDefault(); 87 | } 88 | } 89 | }); 90 | 91 | module.exports = Recipe; 92 | -------------------------------------------------------------------------------- /examples/react-router/app/components/recipe_adder.jsx: -------------------------------------------------------------------------------- 1 | var t = require("tcomb-form"), 2 | React = require("react"), 3 | Router = require("react-router"), 4 | RouteHandler = Router.RouteHandler, 5 | Link = Router.Link, 6 | Fluxxor = require("../../../../"); 7 | 8 | var Recipe = require("../schemas/recipe.jsx"), 9 | RecipeForm = require("../forms/recipe_form.jsx"); 10 | 11 | var RecipeAdder = React.createClass({ 12 | mixins: [ 13 | Fluxxor.FluxMixin(React) 14 | ], 15 | 16 | contextTypes: { 17 | router: React.PropTypes.func 18 | }, 19 | 20 | render: function() { 21 | return this.renderWithLayout( 22 |
    23 |
    24 | 25 | 26 | 27 |
    28 | ); 29 | }, 30 | 31 | renderWithLayout: function(content) { 32 | return ( 33 |
    34 | {content} 35 |
    36 | Home 37 | {" | "}Add New Recipe 38 |
    39 | ); 40 | }, 41 | 42 | onSubmit: function(e) { 43 | e.preventDefault(); 44 | 45 | var newRecipe = this.refs.form.getValue(); 46 | if (newRecipe) { 47 | this.getFlux().actions.recipes.add( 48 | newRecipe.name, 49 | newRecipe.description, 50 | newRecipe.ingredients, 51 | newRecipe.directions 52 | ); 53 | } 54 | }, 55 | 56 | deleteRecipe: function(e) { 57 | if (confirm("Really delete this recipe?")) { 58 | this.getFlux().actions.recipes.remove(this.state.recipe.id); 59 | } else { 60 | e.preventDefault(); 61 | } 62 | } 63 | }); 64 | 65 | module.exports = RecipeAdder; 66 | -------------------------------------------------------------------------------- /examples/react-router/app/components/recipe_editor.jsx: -------------------------------------------------------------------------------- 1 | var t = require("tcomb-form"), 2 | React = require("react"), 3 | Router = require("react-router"), 4 | RouteHandler = Router.RouteHandler, 5 | Link = Router.Link, 6 | Fluxxor = require("../../../../"); 7 | 8 | var Recipe = require("../schemas/recipe.jsx"), 9 | RecipeForm = require("../forms/recipe_form.jsx"), 10 | RecipeStore = require("../stores/recipe_store.jsx"); 11 | 12 | var RecipeEditor = React.createClass({ 13 | mixins: [ 14 | Fluxxor.FluxMixin(React), 15 | Fluxxor.StoreWatchMixin("recipe") 16 | ], 17 | 18 | contextTypes: { 19 | router: React.PropTypes.func 20 | }, 21 | 22 | getStateFromFlux: function() { 23 | var params = this.context.router.getCurrentParams(); 24 | 25 | return { 26 | recipe: this.getFlux().store("recipe").getRecipe(params.id) 27 | }; 28 | }, 29 | 30 | componentWillReceiveProps: function(nextProps) { 31 | this.setState(this.getStateFromFlux()); 32 | }, 33 | 34 | render: function() { 35 | var recipe = this.state.recipe; 36 | 37 | if (recipe === RecipeStore.NOT_FOUND_TOKEN) { 38 | return this.renderNotFound(); 39 | } 40 | 41 | return this.renderWithLayout( 42 |
    43 |
    44 | 45 | 46 | 47 | 48 |

    49 | Delete Recipe 50 |

    51 |
    52 | ); 53 | }, 54 | 55 | renderNotFound: function() { 56 | return this.renderWithLayout( 57 |
    That recipe was not found.
    58 | ); 59 | }, 60 | 61 | renderWithLayout: function(content) { 62 | return ( 63 |
    64 | {content} 65 |
    66 | Home 67 | {" | "}Add New Recipe 68 |
    69 | ); 70 | }, 71 | 72 | onSubmit: function(e) { 73 | e.preventDefault(); 74 | 75 | var newRecipe = this.refs.form.getValue(); 76 | if (newRecipe) { 77 | this.getFlux().actions.recipes.edit( 78 | this.state.recipe.id, 79 | newRecipe.name, 80 | newRecipe.description, 81 | newRecipe.ingredients, 82 | newRecipe.directions 83 | ); 84 | 85 | this.context.router.transitionTo("recipe", {id: this.state.recipe.id}); 86 | } 87 | }, 88 | 89 | deleteRecipe: function(e) { 90 | if (confirm("Really delete this recipe?")) { 91 | this.getFlux().actions.recipes.remove(this.state.recipe.id); 92 | } else { 93 | e.preventDefault(); 94 | } 95 | } 96 | }); 97 | 98 | module.exports = RecipeEditor; 99 | -------------------------------------------------------------------------------- /examples/react-router/app/components/recipe_list.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"), 2 | Router = require("react-router"), 3 | RouteHandler = Router.RouteHandler, 4 | Link = Router.Link, 5 | Fluxxor = require("../../../../"); 6 | 7 | var RecipeList = React.createClass({ 8 | mixins: [Fluxxor.FluxMixin(React), Fluxxor.StoreWatchMixin("recipe")], 9 | 10 | getStateFromFlux: function() { 11 | return { 12 | recipes: this.getFlux().store("recipe").getRecipes() 13 | }; 14 | }, 15 | 16 | render: function() { 17 | return ( 18 |
    19 |

    Recipes

    20 |
      {this.state.recipes.map(this.renderRecipeLink)}
    21 |
    22 | Add New Recipe 23 |
    24 |
    25 | ); 26 | }, 27 | 28 | renderRecipeLink: function(recipe) { 29 | return ( 30 |
  • 31 | {recipe.name} 32 |
  • 33 | ); 34 | } 35 | }); 36 | 37 | module.exports = RecipeList; 38 | -------------------------------------------------------------------------------- /examples/react-router/app/forms/recipe_form.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"), 2 | t = require("tcomb-form"); 3 | 4 | var Recipe = require("../schemas/recipe.jsx"); 5 | 6 | var ingredients = function(locals) { 7 | var inputs = locals.inputs; 8 | 9 | return ( 10 |
    11 |
    12 | {inputs.quantity} 13 |
    14 |
    15 | {inputs.item} 16 |
    17 |
    18 | ); 19 | }; 20 | 21 | var struct = function(locals) { 22 | if (locals.config.type === "ingredients") return ingredients(locals); 23 | 24 | var inputs = locals.inputs; 25 | 26 | return ( 27 |
    28 |
    29 | {inputs.name} 30 |
    31 |
    32 | {inputs.description} 33 |
    34 |
    35 | 38 | {inputs.ingredients} 39 |
    40 |
    41 | {inputs.directions} 42 |
    43 |
    44 | ); 45 | 46 | return ( 47 |
    48 | { 49 | locals.order.map(function(field) { 50 | return ( 51 |
    52 | {locals.inputs[field]} 53 |
    54 | ); 55 | }) 56 | } 57 |
    58 | ); 59 | }; 60 | 61 | var list = function(locals) { 62 | return ( 63 |
    64 | { 65 | locals.items.map(function(item) { 66 | return ( 67 |
    68 | {item.input} 69 |
    70 | {item.buttons.map(function(btn) { 71 | return ( 72 | 73 | ); 74 | })} 75 |
    76 |
    77 | ); 78 | }) 79 | } 80 |
    81 | 82 |
    83 |
    84 | ); 85 | }; 86 | 87 | module.exports = { 88 | auto: 'none', 89 | templates: { 90 | struct: struct, 91 | list: list 92 | }, 93 | fields: { 94 | description: { 95 | type: "textarea" 96 | }, 97 | directions: { 98 | type: "textarea" 99 | }, 100 | ingredients: { 101 | config: { type: "ingredients" } 102 | } 103 | } 104 | }; 105 | 106 | -------------------------------------------------------------------------------- /examples/react-router/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 6 | -------------------------------------------------------------------------------- /examples/react-router/app/routes.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"), 2 | Router = require("react-router"), 3 | Route = Router.Route, 4 | DefaultRoute = Router.DefaultRoute; 5 | 6 | var EmptyView = require("./components/empty_view.jsx"), 7 | Recipe = require("./components/recipe.jsx"), 8 | RecipeEditor = require("./components/recipe_editor.jsx"), 9 | RecipeAdder = require("./components/recipe_adder.jsx"), 10 | RecipeList = require("./components/recipe_list.jsx"); 11 | 12 | var routes = ( 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | ); 24 | 25 | module.exports = routes; 26 | -------------------------------------------------------------------------------- /examples/react-router/app/schemas/ingredient.jsx: -------------------------------------------------------------------------------- 1 | var t = require("tcomb-form"); 2 | 3 | module.exports = t.struct({ 4 | quantity: t.Str, 5 | item: t.Str 6 | }); 7 | -------------------------------------------------------------------------------- /examples/react-router/app/schemas/recipe.jsx: -------------------------------------------------------------------------------- 1 | var t = require("tcomb-form"); 2 | 3 | var Ingredient = require("./ingredient.jsx"); 4 | 5 | module.exports = t.struct({ 6 | name: t.Str, 7 | description: t.maybe(t.Str), 8 | ingredients: t.list(Ingredient), 9 | directions: t.maybe(t.Str) 10 | }); 11 | -------------------------------------------------------------------------------- /examples/react-router/app/stores/recipe_store.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require("../../../../"); 2 | 3 | var actions = require("../actions.jsx"); 4 | 5 | var NOT_FOUND_TOKEN = {}; 6 | 7 | var RecipeStore = Fluxxor.createStore({ 8 | initialize: function() { 9 | this.recipeId = 0; 10 | this.recipes = {}; 11 | 12 | this.bindActions( 13 | actions.constants.RECIPE.ADD, this.handleAddRecipe, 14 | actions.constants.RECIPE.EDIT, this.handleEditRecipe, 15 | actions.constants.RECIPE.REMOVE, this.handleRemoveRecipe 16 | ); 17 | }, 18 | 19 | getRecipes: function() { 20 | return Object.keys(this.recipes).map(function(key) { 21 | return this.recipes[key]; 22 | }.bind(this)); 23 | }, 24 | 25 | getRecipe: function(id) { 26 | return this.recipes[id] || NOT_FOUND_TOKEN; 27 | }, 28 | 29 | handleAddRecipe: function(payload) { 30 | var recipe = { 31 | name: payload.name, 32 | description: payload.description, 33 | ingredients: payload.ingredients, 34 | directions: payload.directions 35 | }; 36 | recipe.id = ++this.recipeId; 37 | this.recipes[recipe.id] = recipe; 38 | 39 | // Normally an API call to save a new item would be asynchronous, 40 | // but we're faking a back-end store here, so we'll fake the 41 | // asynchrony too. 42 | setTimeout(function() { 43 | if (!payload.preventTransition) { 44 | // See https://github.com/BinaryMuse/fluxxor/pull/95#discussion_r23178351 45 | // for the reason for this dispatch from the store. 46 | this.flux.actions.routes.transition("recipe", {id: recipe.id}); 47 | } 48 | }.bind(this)); 49 | 50 | this.emit("change"); 51 | }, 52 | 53 | handleEditRecipe: function(payload) { 54 | this.recipes[payload.id] = { 55 | id: payload.id, 56 | name: payload.name, 57 | description: payload.description, 58 | ingredients: payload.ingredients, 59 | directions: payload.directions 60 | }; 61 | 62 | this.emit("change"); 63 | }, 64 | 65 | handleRemoveRecipe: function(id) { 66 | delete this.recipes[id]; 67 | this.emit("change"); 68 | } 69 | }); 70 | 71 | RecipeStore.NOT_FOUND_TOKEN = NOT_FOUND_TOKEN; 72 | 73 | module.exports = RecipeStore; 74 | -------------------------------------------------------------------------------- /examples/react-router/app/stores/route_store.jsx: -------------------------------------------------------------------------------- 1 | var Fluxxor = require("../../../../"); 2 | 3 | var actions = require("../actions.jsx"); 4 | 5 | var RouteStore = Fluxxor.createStore({ 6 | initialize: function(options) { 7 | this.router = options.router; 8 | 9 | this.bindActions( 10 | actions.constants.ROUTE.TRANSITION, this.handleRouteTransition 11 | ); 12 | }, 13 | 14 | handleRouteTransition: function(payload) { 15 | var path = payload.path, 16 | params = payload.params; 17 | 18 | this.router.transitionTo(path, params); 19 | } 20 | }); 21 | 22 | module.exports = RouteStore; 23 | -------------------------------------------------------------------------------- /examples/react-router/app/style.less: -------------------------------------------------------------------------------- 1 | fieldset { 2 | border: 0; 3 | padding: 0; 4 | margin: 0; 5 | } 6 | 7 | fieldset legend { 8 | display: none; 9 | } 10 | 11 | .form-group { 12 | padding-bottom: 10px; 13 | } 14 | 15 | input[name="name"] { 16 | font-size: 18pt; 17 | width: 400px; 18 | } 19 | 20 | textarea[name="description"], textarea[name="directions"] { 21 | height: 75px; 22 | width: 400px; 23 | font-size: 10pt; 24 | } 25 | 26 | .form-group.has-error input { 27 | border-color: red; 28 | } 29 | 30 | .form-row.ingredients { 31 | padding-top: 10px; 32 | padding-bottom: 20px; 33 | 34 | label { 35 | display: inline-block; 36 | padding-bottom: 5px; 37 | } 38 | 39 | input { 40 | font-size: 10pt; 41 | } 42 | 43 | .form-row.quantity, .form-row.item, .form-list-buttons, .form-struct { 44 | display: inline-block; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/react-router/start.sh: -------------------------------------------------------------------------------- 1 | ../../node_modules/.bin/webpack-dev-server --port 8089 --no-info --content-base app 2 | -------------------------------------------------------------------------------- /examples/react-router/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | 3 | module.exports = { 4 | cache: true, 5 | entry: "./app/app.jsx", 6 | output: { 7 | path: __dirname + "/app", 8 | filename: "bundle.js" 9 | }, 10 | devtool: "source-map", 11 | module: { 12 | loaders: [ 13 | { test: /\.less$/, loader: "style!css!less" }, 14 | { test: /\.jsx$/, loader: "jsx-loader" }, 15 | { test: /\.json$/, loader: "json" } 16 | ] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /examples/todo-basic/README.md: -------------------------------------------------------------------------------- 1 | Basic Todos Example 2 | =================== 3 | 4 | This is a very, very simple todo app. The Fluxxor website builds this app step by step in [the quick-start guide](http://fluxxor.com/guides/quick-start.html). 5 | 6 | To run, simply open `app/index.html` in your web browser—`index.html` and `bundle.js` are the only files required to run the application (`bundle.js` is build with [Webpack](http://webpack.github.io/)). If you want to modify the source and see your updates, run `./start.sh` and point your browser to `http://localhost:8089/index.html` (you will need to install the `devDependencies` from the root of the Fluxxor project first). 7 | -------------------------------------------------------------------------------- /examples/todo-basic/app/app.jsx: -------------------------------------------------------------------------------- 1 | var React = require("react"), 2 | Fluxxor = require("../../../"); 3 | 4 | window.React = React; 5 | 6 | var constants = { 7 | ADD_TODO: "ADD_TODO", 8 | TOGGLE_TODO: "TOGGLE_TODO", 9 | CLEAR_TODOS: "CLEAR_TODOS" 10 | }; 11 | 12 | var TodoStore = Fluxxor.createStore({ 13 | initialize: function() { 14 | this.todoId = 0; 15 | this.todos = {}; 16 | 17 | this.bindActions( 18 | constants.ADD_TODO, this.onAddTodo, 19 | constants.TOGGLE_TODO, this.onToggleTodo, 20 | constants.CLEAR_TODOS, this.onClearTodos 21 | ); 22 | }, 23 | 24 | onAddTodo: function(payload) { 25 | var id = this._nextTodoId(); 26 | var todo = { 27 | id: id, 28 | text: payload.text, 29 | complete: false 30 | }; 31 | this.todos[id] = todo; 32 | this.emit("change"); 33 | }, 34 | 35 | onToggleTodo: function(payload) { 36 | var id = payload.id; 37 | this.todos[id].complete = !this.todos[id].complete; 38 | this.emit("change"); 39 | }, 40 | 41 | onClearTodos: function() { 42 | var todos = this.todos; 43 | 44 | Object.keys(todos).forEach(function(key) { 45 | if(todos[key].complete) { 46 | delete todos[key]; 47 | } 48 | }); 49 | 50 | this.emit("change"); 51 | }, 52 | 53 | getState: function() { 54 | return { 55 | todos: this.todos 56 | }; 57 | }, 58 | 59 | _nextTodoId: function() { 60 | return ++this.todoId; 61 | } 62 | }); 63 | 64 | var actions = { 65 | addTodo: function(text) { 66 | this.dispatch(constants.ADD_TODO, {text: text}); 67 | }, 68 | 69 | toggleTodo: function(id) { 70 | this.dispatch(constants.TOGGLE_TODO, {id: id}); 71 | }, 72 | 73 | clearTodos: function() { 74 | this.dispatch(constants.CLEAR_TODOS); 75 | } 76 | }; 77 | 78 | var stores = { 79 | TodoStore: new TodoStore() 80 | }; 81 | 82 | var flux = new Fluxxor.Flux(stores, actions); 83 | 84 | window.flux = flux; 85 | 86 | flux.on("dispatch", function(type, payload) { 87 | if (console && console.log) { 88 | console.log("[Dispatch]", type, payload); 89 | } 90 | }); 91 | 92 | var FluxMixin = Fluxxor.FluxMixin(React), 93 | StoreWatchMixin = Fluxxor.StoreWatchMixin; 94 | 95 | var Application = React.createClass({ 96 | mixins: [FluxMixin, StoreWatchMixin("TodoStore")], 97 | 98 | getInitialState: function() { 99 | return { newTodoText: "" }; 100 | }, 101 | 102 | getStateFromFlux: function() { 103 | var flux = this.getFlux(); 104 | // Our entire state is made up of the TodoStore data. In a larger 105 | // application, you will likely return data from multiple stores, e.g.: 106 | // 107 | // return { 108 | // todoData: flux.store("TodoStore").getState(), 109 | // userData: flux.store("UserStore").getData(), 110 | // fooBarData: flux.store("FooBarStore").someMoreData() 111 | // }; 112 | return flux.store("TodoStore").getState(); 113 | }, 114 | 115 | render: function() { 116 | var todos = this.state.todos; 117 | return ( 118 |
    119 |
      120 | {Object.keys(todos).map(function(id) { 121 | return
    • ; 122 | })} 123 |
    124 |
    125 | 128 | 129 |
    130 | 131 |
    132 | ); 133 | }, 134 | 135 | handleTodoTextChange: function(e) { 136 | this.setState({newTodoText: e.target.value}); 137 | }, 138 | 139 | onSubmitForm: function(e) { 140 | e.preventDefault(); 141 | if (this.state.newTodoText.trim()) { 142 | this.getFlux().actions.addTodo(this.state.newTodoText); 143 | this.setState({newTodoText: ""}); 144 | } 145 | }, 146 | 147 | clearCompletedTodos: function(e) { 148 | this.getFlux().actions.clearTodos(); 149 | } 150 | }); 151 | 152 | var TodoItem = React.createClass({ 153 | mixins: [FluxMixin], 154 | 155 | propTypes: { 156 | todo: React.PropTypes.object.isRequired 157 | }, 158 | 159 | render: function() { 160 | var style = { 161 | textDecoration: this.props.todo.complete ? "line-through" : "" 162 | }; 163 | 164 | return {this.props.todo.text}; 165 | }, 166 | 167 | onClick: function() { 168 | this.getFlux().actions.toggleTodo(this.props.todo.id); 169 | } 170 | }); 171 | 172 | React.render(, document.getElementById("app")); 173 | -------------------------------------------------------------------------------- /examples/todo-basic/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
    5 | 6 | -------------------------------------------------------------------------------- /examples/todo-basic/start.sh: -------------------------------------------------------------------------------- 1 | ../../node_modules/.bin/webpack-dev-server --port 8089 --no-info --content-base app 2 | -------------------------------------------------------------------------------- /examples/todo-basic/webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | 3 | module.exports = { 4 | cache: true, 5 | entry: "./app/app.jsx", 6 | output: { 7 | path: __dirname + "/app", 8 | filename: "bundle.js" 9 | }, 10 | devtool: "source-map", 11 | module: { 12 | loaders: [ 13 | { test: /\.less$/, loader: "style!css!less" }, 14 | { test: /\.jsx$/, loader: "jsx-loader" }, 15 | { test: /\.json$/, loader: "json" } 16 | ] 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /fluxxor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 32 | 39 | 41 | 43 | 54 | 63 | 64 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var Dispatcher = require("./lib/dispatcher"), 2 | Flux = require("./lib/flux"), 3 | FluxMixin = require("./lib/flux_mixin"), 4 | FluxChildMixin = require("./lib/flux_child_mixin"), 5 | StoreWatchMixin = require("./lib/store_watch_mixin"), 6 | createStore = require("./lib/create_store"); 7 | 8 | var Fluxxor = { 9 | Dispatcher: Dispatcher, 10 | Flux: Flux, 11 | FluxMixin: FluxMixin, 12 | FluxChildMixin: FluxChildMixin, 13 | StoreWatchMixin: StoreWatchMixin, 14 | createStore: createStore, 15 | version: require("./version") 16 | }; 17 | 18 | module.exports = Fluxxor; 19 | -------------------------------------------------------------------------------- /lib/create_store.js: -------------------------------------------------------------------------------- 1 | var _each = require("lodash/collection/forEach"), 2 | _isFunction = require("lodash/lang/isFunction"), 3 | Store = require("./store"), 4 | inherits = require("./util/inherits"); 5 | 6 | var RESERVED_KEYS = ["flux", "waitFor"]; 7 | 8 | var createStore = function(spec) { 9 | _each(RESERVED_KEYS, function(key) { 10 | if (spec[key]) { 11 | throw new Error("Reserved key '" + key + "' found in store definition"); 12 | } 13 | }); 14 | 15 | var constructor = function(options) { 16 | options = options || {}; 17 | Store.call(this); 18 | 19 | for (var key in spec) { 20 | if (key === "actions") { 21 | this.bindActions(spec[key]); 22 | } else if (key === "initialize") { 23 | // do nothing 24 | } else if (_isFunction(spec[key])) { 25 | this[key] = spec[key].bind(this); 26 | } else { 27 | this[key] = spec[key]; 28 | } 29 | } 30 | 31 | if (spec.initialize) { 32 | spec.initialize.call(this, options); 33 | } 34 | }; 35 | 36 | inherits(constructor, Store); 37 | return constructor; 38 | }; 39 | 40 | module.exports = createStore; 41 | -------------------------------------------------------------------------------- /lib/dispatcher.js: -------------------------------------------------------------------------------- 1 | var _clone = require("lodash/lang/clone"), 2 | _mapValues = require("lodash/object/mapValues"), 3 | _forOwn = require("lodash/object/forOwn"), 4 | _intersection = require("lodash/array/intersection"), 5 | _keys = require("lodash/object/keys"), 6 | _map = require("lodash/collection/map"), 7 | _each = require("lodash/collection/forEach"), 8 | _size = require("lodash/collection/size"), 9 | _findKey = require("lodash/object/findKey"), 10 | _uniq = require("lodash/array/uniq"); 11 | 12 | var defaultDispatchInterceptor = function(action, dispatch) { 13 | dispatch(action); 14 | }; 15 | 16 | var Dispatcher = function(stores) { 17 | this.stores = {}; 18 | this.currentDispatch = null; 19 | this.currentActionType = null; 20 | this.waitingToDispatch = []; 21 | this.dispatchInterceptor = defaultDispatchInterceptor; 22 | this._boundDispatch = this._dispatch.bind(this); 23 | 24 | for (var key in stores) { 25 | if (stores.hasOwnProperty(key)) { 26 | this.addStore(key, stores[key]); 27 | } 28 | } 29 | }; 30 | 31 | Dispatcher.prototype.addStore = function(name, store) { 32 | store.dispatcher = this; 33 | this.stores[name] = store; 34 | }; 35 | 36 | Dispatcher.prototype.dispatch = function(action) { 37 | this.dispatchInterceptor(action, this._boundDispatch); 38 | }; 39 | 40 | Dispatcher.prototype._dispatch = function(action) { 41 | if (!action || !action.type) { 42 | throw new Error("Can only dispatch actions with a 'type' property"); 43 | } 44 | 45 | if (this.currentDispatch) { 46 | var complaint = "Cannot dispatch an action ('" + action.type + "') while another action ('" + 47 | this.currentActionType + "') is being dispatched"; 48 | throw new Error(complaint); 49 | } 50 | 51 | this.waitingToDispatch = _clone(this.stores); 52 | 53 | this.currentActionType = action.type; 54 | this.currentDispatch = _mapValues(this.stores, function() { 55 | return { resolved: false, waitingOn: [], waitCallback: null }; 56 | }); 57 | 58 | try { 59 | this.doDispatchLoop(action); 60 | } finally { 61 | this.currentActionType = null; 62 | this.currentDispatch = null; 63 | } 64 | }; 65 | 66 | Dispatcher.prototype.doDispatchLoop = function(action) { 67 | var dispatch, canBeDispatchedTo, wasHandled = false, 68 | removeFromDispatchQueue = [], dispatchedThisLoop = []; 69 | 70 | _forOwn(this.waitingToDispatch, function(value, key) { 71 | dispatch = this.currentDispatch[key]; 72 | canBeDispatchedTo = !dispatch.waitingOn.length || 73 | !_intersection(dispatch.waitingOn, _keys(this.waitingToDispatch)).length; 74 | if (canBeDispatchedTo) { 75 | if (dispatch.waitCallback) { 76 | var stores = _map(dispatch.waitingOn, function(key) { 77 | return this.stores[key]; 78 | }, this); 79 | var fn = dispatch.waitCallback; 80 | dispatch.waitCallback = null; 81 | dispatch.waitingOn = []; 82 | dispatch.resolved = true; 83 | fn.apply(null, stores); 84 | wasHandled = true; 85 | } else { 86 | dispatch.resolved = true; 87 | var handled = this.stores[key].__handleAction__(action); 88 | if (handled) { 89 | wasHandled = true; 90 | } 91 | } 92 | 93 | dispatchedThisLoop.push(key); 94 | 95 | if (this.currentDispatch[key].resolved) { 96 | removeFromDispatchQueue.push(key); 97 | } 98 | } 99 | }, this); 100 | 101 | if (_keys(this.waitingToDispatch).length && !dispatchedThisLoop.length) { 102 | var storesWithCircularWaits = _keys(this.waitingToDispatch).join(", "); 103 | throw new Error("Indirect circular wait detected among: " + storesWithCircularWaits); 104 | } 105 | 106 | _each(removeFromDispatchQueue, function(key) { 107 | delete this.waitingToDispatch[key]; 108 | }, this); 109 | 110 | if (_size(this.waitingToDispatch)) { 111 | this.doDispatchLoop(action); 112 | } 113 | 114 | if (!wasHandled && console && console.warn) { 115 | console.warn("An action of type " + action.type + " was dispatched, but no store handled it"); 116 | } 117 | 118 | }; 119 | 120 | Dispatcher.prototype.waitForStores = function(store, stores, fn) { 121 | if (!this.currentDispatch) { 122 | throw new Error("Cannot wait unless an action is being dispatched"); 123 | } 124 | 125 | var waitingStoreName = _findKey(this.stores, function(val) { 126 | return val === store; 127 | }); 128 | 129 | if (stores.indexOf(waitingStoreName) > -1) { 130 | throw new Error("A store cannot wait on itself"); 131 | } 132 | 133 | var dispatch = this.currentDispatch[waitingStoreName]; 134 | 135 | if (dispatch.waitingOn.length) { 136 | throw new Error(waitingStoreName + " already waiting on stores"); 137 | } 138 | 139 | _each(stores, function(storeName) { 140 | var storeDispatch = this.currentDispatch[storeName]; 141 | if (!this.stores[storeName]) { 142 | throw new Error("Cannot wait for non-existent store " + storeName); 143 | } 144 | if (storeDispatch.waitingOn.indexOf(waitingStoreName) > -1) { 145 | throw new Error("Circular wait detected between " + waitingStoreName + " and " + storeName); 146 | } 147 | }, this); 148 | 149 | dispatch.resolved = false; 150 | dispatch.waitingOn = _uniq(dispatch.waitingOn.concat(stores)); 151 | dispatch.waitCallback = fn; 152 | }; 153 | 154 | Dispatcher.prototype.setDispatchInterceptor = function(fn) { 155 | if (fn) { 156 | this.dispatchInterceptor = fn; 157 | } else { 158 | this.dispatchInterceptor = defaultDispatchInterceptor; 159 | } 160 | }; 161 | 162 | module.exports = Dispatcher; 163 | -------------------------------------------------------------------------------- /lib/flux.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require("eventemitter3"), 2 | inherits = require("./util/inherits"), 3 | objectPath = require("object-path"), 4 | _each = require("lodash/collection/forEach"), 5 | _reduce = require("lodash/collection/reduce"), 6 | _isFunction = require("lodash/lang/isFunction"), 7 | _isString = require("lodash/lang/isString"); 8 | 9 | var Dispatcher = require("./dispatcher"); 10 | 11 | var findLeaves = function(obj, path, callback) { 12 | path = path || []; 13 | 14 | for (var key in obj) { 15 | if (obj.hasOwnProperty(key)) { 16 | if (_isFunction(obj[key])) { 17 | callback(path.concat(key), obj[key]); 18 | } else { 19 | findLeaves(obj[key], path.concat(key), callback); 20 | } 21 | } 22 | } 23 | }; 24 | 25 | var Flux = function(stores, actions) { 26 | EventEmitter.call(this); 27 | this.dispatcher = new Dispatcher(stores); 28 | this.actions = {}; 29 | this.stores = {}; 30 | 31 | var dispatcher = this.dispatcher; 32 | var flux = this; 33 | this.dispatchBinder = { 34 | flux: flux, 35 | dispatch: function(type, payload) { 36 | try { 37 | flux.emit("dispatch", type, payload); 38 | } finally { 39 | dispatcher.dispatch({type: type, payload: payload}); 40 | } 41 | } 42 | }; 43 | 44 | this.addActions(actions); 45 | this.addStores(stores); 46 | }; 47 | 48 | inherits(Flux, EventEmitter); 49 | 50 | Flux.prototype.addActions = function(actions) { 51 | findLeaves(actions, [], this.addAction.bind(this)); 52 | }; 53 | 54 | // addAction has two signatures: 55 | // 1: string[, string, string, string...], actionFunction 56 | // 2: arrayOfStrings, actionFunction 57 | Flux.prototype.addAction = function() { 58 | if (arguments.length < 2) { 59 | throw new Error("addAction requires at least two arguments, a string (or array of strings) and a function"); 60 | } 61 | 62 | var args = Array.prototype.slice.call(arguments); 63 | 64 | if (!_isFunction(args[args.length - 1])) { 65 | throw new Error("The last argument to addAction must be a function"); 66 | } 67 | 68 | var func = args.pop().bind(this.dispatchBinder); 69 | 70 | if (!_isString(args[0])) { 71 | args = args[0]; 72 | } 73 | 74 | var leadingPaths = _reduce(args, function(acc, next) { 75 | if (acc) { 76 | var nextPath = acc[acc.length - 1].concat([next]); 77 | return acc.concat([nextPath]); 78 | } else { 79 | return [[next]]; 80 | } 81 | }, null); 82 | 83 | // Detect trying to replace a function at any point in the path 84 | _each(leadingPaths, function(path) { 85 | if (_isFunction(objectPath.get(this.actions, path))) { 86 | throw new Error("An action named " + args.join(".") + " already exists"); 87 | } 88 | }, this); 89 | 90 | // Detect trying to replace a namespace at the final point in the path 91 | if (objectPath.get(this.actions, args)) { 92 | throw new Error("A namespace named " + args.join(".") + " already exists"); 93 | } 94 | 95 | objectPath.set(this.actions, args, func, true); 96 | }; 97 | 98 | Flux.prototype.store = function(name) { 99 | return this.stores[name]; 100 | }; 101 | 102 | Flux.prototype.getAllStores = function() { 103 | return this.stores; 104 | }; 105 | 106 | Flux.prototype.addStore = function(name, store) { 107 | if (name in this.stores) { 108 | throw new Error("A store named '" + name + "' already exists"); 109 | } 110 | store.flux = this; 111 | this.stores[name] = store; 112 | this.dispatcher.addStore(name, store); 113 | }; 114 | 115 | Flux.prototype.addStores = function(stores) { 116 | for (var key in stores) { 117 | if (stores.hasOwnProperty(key)) { 118 | this.addStore(key, stores[key]); 119 | } 120 | } 121 | }; 122 | 123 | Flux.prototype.setDispatchInterceptor = function(fn) { 124 | this.dispatcher.setDispatchInterceptor(fn); 125 | }; 126 | 127 | module.exports = Flux; 128 | -------------------------------------------------------------------------------- /lib/flux_child_mixin.js: -------------------------------------------------------------------------------- 1 | var FluxChildMixin = function(React) { 2 | return { 3 | componentWillMount: function() { 4 | if (console && console.warn) { 5 | var namePart = this.constructor.displayName ? " in " + this.constructor.displayName : "", 6 | message = "Fluxxor.FluxChildMixin was found in use" + namePart + ", " + 7 | "but has been deprecated. Use Fluxxor.FluxMixin instead."; 8 | console.warn(message); 9 | } 10 | }, 11 | 12 | contextTypes: { 13 | flux: React.PropTypes.object 14 | }, 15 | 16 | getFlux: function() { 17 | return this.context.flux; 18 | } 19 | }; 20 | }; 21 | 22 | FluxChildMixin.componentWillMount = function() { 23 | throw new Error("Fluxxor.FluxChildMixin is a function that takes React as a " + 24 | "parameter and returns the mixin, e.g.: mixins[Fluxxor.FluxChildMixin(React)]"); 25 | }; 26 | 27 | module.exports = FluxChildMixin; 28 | -------------------------------------------------------------------------------- /lib/flux_mixin.js: -------------------------------------------------------------------------------- 1 | var FluxMixin = function(React) { 2 | return { 3 | componentWillMount: function() { 4 | if (!this.props.flux && (!this.context || !this.context.flux)) { 5 | var namePart = this.constructor.displayName ? " of " + this.constructor.displayName : ""; 6 | throw new Error("Could not find flux on this.props or this.context" + namePart); 7 | } 8 | }, 9 | 10 | childContextTypes: { 11 | flux: React.PropTypes.object 12 | }, 13 | 14 | contextTypes: { 15 | flux: React.PropTypes.object 16 | }, 17 | 18 | getChildContext: function() { 19 | return { 20 | flux: this.getFlux() 21 | }; 22 | }, 23 | 24 | getFlux: function() { 25 | return this.props.flux || (this.context && this.context.flux); 26 | } 27 | }; 28 | }; 29 | 30 | FluxMixin.componentWillMount = function() { 31 | throw new Error("Fluxxor.FluxMixin is a function that takes React as a " + 32 | "parameter and returns the mixin, e.g.: mixins: [Fluxxor.FluxMixin(React)]"); 33 | }; 34 | 35 | module.exports = FluxMixin; 36 | -------------------------------------------------------------------------------- /lib/store.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require("eventemitter3"), 2 | inherits = require("./util/inherits"), 3 | _isFunction = require("lodash/lang/isFunction"), 4 | _isObject = require("lodash/lang/isObject"); 5 | 6 | function Store(dispatcher) { 7 | this.dispatcher = dispatcher; 8 | this.__actions__ = {}; 9 | EventEmitter.call(this); 10 | } 11 | 12 | inherits(Store, EventEmitter); 13 | 14 | Store.prototype.__handleAction__ = function(action) { 15 | var handler; 16 | if (!!(handler = this.__actions__[action.type])) { 17 | if (_isFunction(handler)) { 18 | handler.call(this, action.payload, action.type); 19 | } else if (handler && _isFunction(this[handler])) { 20 | this[handler].call(this, action.payload, action.type); 21 | } else { 22 | throw new Error("The handler for action type " + action.type + " is not a function"); 23 | } 24 | return true; 25 | } else { 26 | return false; 27 | } 28 | }; 29 | 30 | Store.prototype.bindActions = function() { 31 | var actions = Array.prototype.slice.call(arguments); 32 | 33 | if (actions.length > 1 && actions.length % 2 !== 0) { 34 | throw new Error("bindActions must take an even number of arguments."); 35 | } 36 | 37 | var bindAction = function(type, handler) { 38 | if (!handler) { 39 | throw new Error("The handler for action type " + type + " is falsy"); 40 | } 41 | 42 | this.__actions__[type] = handler; 43 | }.bind(this); 44 | 45 | if (actions.length === 1 && _isObject(actions[0])) { 46 | actions = actions[0]; 47 | for (var key in actions) { 48 | if (actions.hasOwnProperty(key)) { 49 | bindAction(key, actions[key]); 50 | } 51 | } 52 | } else { 53 | for (var i = 0; i < actions.length; i += 2) { 54 | var type = actions[i], 55 | handler = actions[i+1]; 56 | 57 | if (!type) { 58 | throw new Error("Argument " + (i+1) + " to bindActions is a falsy value"); 59 | } 60 | 61 | bindAction(type, handler); 62 | } 63 | } 64 | }; 65 | 66 | Store.prototype.waitFor = function(stores, fn) { 67 | this.dispatcher.waitForStores(this, stores, fn.bind(this)); 68 | }; 69 | 70 | module.exports = Store; 71 | -------------------------------------------------------------------------------- /lib/store_watch_mixin.js: -------------------------------------------------------------------------------- 1 | var _each = require("lodash/collection/forEach"); 2 | 3 | var StoreWatchMixin = function() { 4 | var storeNames = Array.prototype.slice.call(arguments); 5 | return { 6 | componentDidMount: function() { 7 | var flux = this.props.flux || this.context.flux; 8 | this.mounted = true; 9 | 10 | // No autobinding in ES6 classes 11 | this._setStateFromFlux = function() { 12 | if(this.mounted) { 13 | this.setState(this.getStateFromFlux()); 14 | } 15 | }.bind(this); 16 | 17 | _each(storeNames, function(store) { 18 | flux.store(store).on("change", this._setStateFromFlux); 19 | }, this); 20 | }, 21 | 22 | componentWillUnmount: function() { 23 | var flux = this.props.flux || this.context.flux; 24 | this.mounted = false; 25 | _each(storeNames, function(store) { 26 | flux.store(store).removeListener("change", this._setStateFromFlux); 27 | }, this); 28 | }, 29 | 30 | getInitialState: function() { 31 | return this.getStateFromFlux(); 32 | } 33 | }; 34 | }; 35 | 36 | StoreWatchMixin.componentWillMount = function() { 37 | throw new Error("Fluxxor.StoreWatchMixin is a function that takes one or more " + 38 | "store names as parameters and returns the mixin, e.g.: " + 39 | "mixins: [Fluxxor.StoreWatchMixin(\"Store1\", \"Store2\")]"); 40 | }; 41 | 42 | module.exports = StoreWatchMixin; 43 | -------------------------------------------------------------------------------- /lib/util/inherits.js: -------------------------------------------------------------------------------- 1 | // From https://github.com/isaacs/inherits 2 | // inherits is licensed under the ISC license: 3 | // 4 | // 5 | // The ISC License 6 | // 7 | // Copyright (c) Isaac Z. Schlueter 8 | // 9 | // Permission to use, copy, modify, and/or distribute this software for any 10 | // purpose with or without fee is hereby granted, provided that the above 11 | // copyright notice and this permission notice appear in all copies. 12 | // 13 | // THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 14 | // REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 15 | // FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 16 | // INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 17 | // LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 18 | // OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 19 | // PERFORMANCE OF THIS SOFTWARE. 20 | 21 | if (typeof Object.create === 'function') { 22 | // implementation from standard node.js 'util' module 23 | module.exports = function inherits(ctor, superCtor) { 24 | ctor.super_ = superCtor; 25 | ctor.prototype = Object.create(superCtor.prototype, { 26 | constructor: { 27 | value: ctor, 28 | enumerable: false, 29 | writable: true, 30 | configurable: true 31 | } 32 | }); 33 | }; 34 | } else { 35 | // old school shim for old browsers 36 | module.exports = function inherits(ctor, superCtor) { 37 | ctor.super_ = superCtor; 38 | var TempCtor = function () {}; 39 | TempCtor.prototype = superCtor.prototype; 40 | ctor.prototype = new TempCtor(); 41 | ctor.prototype.constructor = ctor; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fluxxor", 3 | "version": "1.7.3", 4 | "description": "Flux architecture tools for React", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/BinaryMuse/fluxxor.git" 8 | }, 9 | "main": "index.js", 10 | "scripts": { 11 | "test": "npm run jshint && mocha --recursive", 12 | "jshint": "jsxhint lib/ test/", 13 | "build": "./script/build-fluxxor && ./script/build-examples", 14 | "preview-site": "wintersmith preview -C site", 15 | "build-site": "wintersmith build -C site" 16 | }, 17 | "keywords": [ 18 | "react", 19 | "flux" 20 | ], 21 | "author": "Michelle Tilley ", 22 | "license": "MIT", 23 | "devDependencies": { 24 | "chai": "^1.9.1", 25 | "css-loader": "^0.6.12", 26 | "envify": "^1.2.1", 27 | "jsdom": "~3.1.2", 28 | "json-loader": "^0.5.0", 29 | "jsx-loader": "^0.12.0", 30 | "jsxhint": "^0.5.0", 31 | "less": "^1.7.0", 32 | "less-loader": "^0.7.3", 33 | "mocha": "^2.2.1", 34 | "react": "^0.13.0", 35 | "react-router": "^0.13.0", 36 | "sinon": "^1.9.1", 37 | "sinon-chai": "^2.5.0", 38 | "style-loader": "^0.6.3", 39 | "tcomb-form": "^0.4.8", 40 | "webpack": "^1.1.11", 41 | "webpack-dev-server": "^1.2.7", 42 | "wintersmith": "^2.0.10", 43 | "wintersmith-ejs": "^0.1.4", 44 | "wintersmith-less": "^0.2.2" 45 | }, 46 | "dependencies": { 47 | "eventemitter3": "^0.1.5", 48 | "lodash": "^3.8.0", 49 | "object-path": "^0.6.0" 50 | }, 51 | "jshintConfig": { 52 | "camelcase": true, 53 | "curly": true, 54 | "eqeqeq": true, 55 | "forin": true, 56 | "latedef": true, 57 | "newcap": false, 58 | "undef": true, 59 | "unused": true, 60 | "trailing": true, 61 | "node": true, 62 | "browser": true, 63 | "predef": [ 64 | "it", 65 | "describe", 66 | "beforeEach", 67 | "afterEach" 68 | ] 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /script/build-examples: -------------------------------------------------------------------------------- 1 | cd examples/todo-basic && ../../node_modules/.bin/webpack -p && cd - 2 | cd examples/carousel && ../../node_modules/.bin/webpack -p && cd - 3 | cd examples/async && ../../node_modules/.bin/webpack -p && cd - 4 | cd examples/react-router && ../../node_modules/.bin/webpack -p && cd - 5 | 6 | cp examples/carousel/app/bundle.js site/contents/examples/carousel/carousel-bundle.js 7 | cp examples/todo-basic/app/bundle.js site/contents/guides/todo-bundle.js 8 | cp examples/async/app/bundle.js site/contents/guides/async-bundle.js 9 | cp examples/react-router/app/bundle.js site/contents/examples/react-router-bundle.js 10 | -------------------------------------------------------------------------------- /script/build-fluxxor: -------------------------------------------------------------------------------- 1 | ./script/write-version-file 2 | ./node_modules/.bin/webpack 3 | ./node_modules/.bin/webpack -p --output-file "fluxxor.min.js" --output-source-map-file "fluxxor.min.js.map" 4 | -------------------------------------------------------------------------------- /script/write-version-file: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | var fs = require("fs"), 4 | pkg = require("../package.json"), 5 | bower = require("../bower.json"); 6 | 7 | bower.version = pkg.version; 8 | fs.writeFileSync("version.js", "module.exports = " + JSON.stringify(pkg.version), "utf8"); 9 | fs.writeFileSync("bower.json", JSON.stringify(bower, null, " ") + "\n", "utf8"); 10 | -------------------------------------------------------------------------------- /site/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "locals": { 3 | "title": "Fluxxor" 4 | }, 5 | "plugins": ["wintersmith-ejs", "wintersmith-less"] 6 | } 7 | -------------------------------------------------------------------------------- /site/contents/CNAME: -------------------------------------------------------------------------------- 1 | fluxxor.com 2 | -------------------------------------------------------------------------------- /site/contents/changelog.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelog 3 | template: page.ejs 4 | --- 5 | 6 | Changelog 7 | ========= 8 | 9 | Version 1.7.3 10 | ------------- 11 | 12 | * Fix `bind` warning when using StoreWatchMixin with createClass (#140) 13 | 14 | Version 1.7.2 15 | ------------- 16 | 17 | * Update `StoreWatchMixin` to support ES6 classes (#135) 18 | 19 | Version 1.7.1 20 | ------------- 21 | 22 | * Relax restrictions on Lodash version (#128) 23 | 24 | Version 1.7.0 25 | ------------- 26 | 27 | * Add `Flux#getAllStores()` to retrieve all currently registered stores (#127) 28 | 29 | Version 1.6.0 30 | ------------- 31 | 32 | * Add `Flux#setDispatchInterceptor` to wrap or replace dispatch functionality (#100, #92) 33 | 34 | Version 1.5.4 35 | ------------- 36 | 37 | * Fix incompatibility with Lodash 3.9.0 38 | 39 | Version 1.5.3 40 | ------------- 41 | 42 | * Use built-in inherits instead of npm package (#116) 43 | 44 | Version 1.5.2 45 | ------------- 46 | 47 | * Upgrade to Lo-Dash 3.x 48 | * Fix minor typo in mixin warnings 49 | 50 | Version 1.5.1 51 | ------------- 52 | 53 | * Watch stores in `componentDidMount` instead of `componentWillMount` to make it harder to leak memory on the server 54 | 55 | Version 1.5.0 56 | ------------- 57 | 58 | **Additions/Non-Breaking Changes** 59 | 60 | * You can add stores and actions to existing Flux instances via `addStore`, `addStores`, `addAction`, and `addActions` (#68, #71, #77) 61 | * `Flux` instances are now EventEmitters, and emit a `"dispatch"` event (with `type` and `payload` arguments) when an action calls `this.dispatch(type, payload)`, useful for cross-cutting concerns like logging 62 | * `Store#bindActions` now takes a hash (similar to the static `actions` hash) in addition to an argument list (#51, #78) 63 | * Fluxxor will throw more descriptive errors in many situations that are obviously incorrect (for example, when an action handler is not defined, or an action type in `bindActions` is falsy) 64 | 65 | **Deprecations** 66 | 67 | * `Fluxxor.FluxChildMixin` is now deprecated; instead, use `FluxMixin` anywhere you want access to `getFlux()` (#59) 68 | 69 | Version 1.4.2 70 | ------------- 71 | 72 | * Throw an error when binding to a falsy action type (#50) 73 | * Reduce npm package size with `.npmignore` (#65) 74 | * Warn if a dispatch is not handled by any store (#66) 75 | * Reduce file size slightly by using [EventEmitter3](https://github.com/3rd-Eden/EventEmitter3) instead of Node.js `events` module 76 | 77 | Version 1.4.1 78 | ------------- 79 | 80 | * Reduce file size by generating a smaller version file (#63) and using inherits package (#64) 81 | 82 | Version 1.4.0 83 | ------------- 84 | 85 | * Action generating methods (methods in the `actions` parameter to the `Fluxxor.Flux` constructor) can access the `Flux` instance via `this.flux`. 86 | 87 | Version 1.3.2 88 | ------------- 89 | 90 | * Ensure component is mounted before setting state in in StoreWatchMixin 91 | 92 | Version 1.3.1 93 | ------------- 94 | 95 | * Fix Bower versioning 96 | 97 | Version 1.3.0 98 | ------------- 99 | 100 | * Add support for namespaced actions 101 | 102 | Version 1.2.2 103 | ------------- 104 | 105 | * Maintain references to stores on Flux object 106 | 107 | Version 1.2.1 108 | ------------- 109 | 110 | * Throw when dispatching a value without a `type` property 111 | 112 | Version 1.2.0 113 | ------------- 114 | 115 | * Allow synchronous back-to-back dispatches 116 | * Gracefully handle exceptions in action handlers 117 | * Add hasOwnProperty(key) checks throughout codebase 118 | 119 | Version 1.1.3 120 | ------------- 121 | 122 | * Add [Bower](http://bower.io/) support 123 | 124 | Version 1.1.2 125 | ------------- 126 | 127 | * Fix compilation when using webpack (#9) 128 | 129 | Version 1.1.1 130 | ------------- 131 | 132 | * Reduce bundle size by 40% 133 | 134 | Version 1.1.0 135 | ------------- 136 | 137 | * Add `FluxChildMixin` to access flux on child components 138 | * Use `getFlux()` to access flux with either `FluxMixin` or `FluxChildMixin` 139 | 140 | Version 1.0.2 141 | ------------- 142 | 143 | * Documentation updates 144 | 145 | Version 1.0.1 146 | ------------- 147 | 148 | * Throw when mixing `StoreWatchMixin` in without calling it as a function 149 | 150 | Version 1.0.0 151 | ------------- 152 | 153 | First stable release 154 | -------------------------------------------------------------------------------- /site/contents/documentation/actions.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Actions 3 | template: page.ejs 4 | --- 5 | 6 | Actions 7 | ======= 8 | 9 | Actions, which are simply combinations of a string `type` and an object `payload`, represent the intent to perform some manipulation of the data in a Flux application. They are dispatched via the aptly named dispatcher to every store the dispatcher knows about. The only way to update stores is to send them actions. 10 | 11 | To make programming with Flux more semantic, the [Flux](/documentation/flux.html) constructor takes an object with function values; these functions can call `this.dispatch(type, payload)` to dispatch an action to the dispatcher. Dispatching an action then looks like a normal method call. As an example, imagine you want to dispatch a method to add a URL at a certain index. Dispatching manually, you would write 12 | 13 | ```javascript 14 | flux.dispatcher.dispatch({type: "ADD_URL", payload: {url: "http://google.com", index: 3}}); 15 | ``` 16 | 17 | By using the `actions` hash, it would look more like this: 18 | 19 | ```javascript 20 | var actions = { 21 | addUrl: function(url, index) { 22 | this.dispatch("ADD_URL", {url: url, index: index}); 23 | } 24 | }; 25 | 26 | var flux = new Fluxxor.Flux(stores, actions); 27 | 28 | // somewhere later... 29 | flux.actions.addUrl("http://google.com", 3); 30 | ``` 31 | 32 | Actions can be added to a `Flux` instance programmatically using `Flux#addAction` or `Flux#addActions`; see the [Flux documentation](/documentation/flux.html) for more details. 33 | 34 | Accessing the Flux Instance 35 | --------------------------- 36 | 37 | The [Flux](/documentation/flux.html) instance the action generators are bound to is available via `this.flux` from inside the functions. 38 | 39 | ```javascript 40 | var actions = { 41 | addUrl: function(url) { 42 | var someData = this.flux.store("someStore").getData(); 43 | this.dispatch("ADD_URL", {url: url, additionalData: someData}); 44 | } 45 | }; 46 | 47 | var stores = { 48 | someStore: new SomeStore(...); 49 | }; 50 | 51 | var flux = new Fluxxor.Flux(stores, actions); 52 | ``` 53 | 54 | Namespaced Actions 55 | ------------------ 56 | 57 | Fluxxor will iterate over objects in your actions definition, allowing for namespaced actions: 58 | 59 | ```javascript 60 | var actions = { 61 | user: { 62 | login: function() { this.dispatch(...); }, 63 | logout: function() { this.dispatch(...); } 64 | }, 65 | post: { 66 | add: function() { this.dispatch(...); }, 67 | remove: function() { this.dispatch(...); } 68 | } 69 | }; 70 | 71 | var flux = new Fluxxor.Flux(stores, actions); 72 | 73 | // somewhere later... 74 | flux.actions.user.login(...); 75 | ``` 76 | 77 | Responding to Actions 78 | --------------------- 79 | 80 | [Stores](/documentation/stores.html) respond to specific action types via the `actions` object in their spec or via calls to `bindActions` during initialization. 81 | 82 | ```javascript 83 | var UrlStore = Fluxxor.createStore({ 84 | actions: { 85 | "ADD_URL": "handleAddUrl" 86 | }, 87 | 88 | // ... 89 | 90 | handleAddUrl: function(payload, type) { 91 | this.urls.splice(payload.index, 0, payload.url); 92 | this.emit("change"); 93 | } 94 | }); 95 | ``` 96 | 97 | See the documentation on [stores](/documentation/stores.html) for more information. 98 | -------------------------------------------------------------------------------- /site/contents/documentation/flux-mixin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FluxMixin 3 | template: page.ejs 4 | --- 5 | 6 | Fluxxor.FluxMixin 7 | ================= 8 | 9 | `Fluxxor.FluxMixin` is a simple React mixin that assists with making a [`Flux`](/documentation/flux.html) instance available to a component hierarchy. Pass an instance of `Flux` as a property named `flux` and mix `FluxMixin` in to a top level component, and mix in `FluxMixin` to any child components, and the `Flux` object will be available as `this.getFlux()` in any component using the mixin. 10 | 11 | Keep in mind that implicitly passing data through context can make it more difficult to reason about things like `shouldComponentUpdate`. Ideally, an instance of `Flux` on the context of a child component should only be used to dispatch actions, and *not* to read data from the stores—read data from the stores at the top-level component and pass the data through props as necessary. 12 | 13 | Note that `FluxMixin` is a function that takes `React` as an argument and returns the associated mixin. 14 | 15 | Example: 16 | 17 | ```javascript 18 | var React = require("react"), 19 | Fluxxor = require("fluxxor"), 20 | FluxMixin = Fluxxor.FluxMixin(React); // or window.React, etc. 21 | 22 | var ParentComponent = React.createClass({ 23 | mixins: [FluxMixin], 24 | 25 | render: function() { 26 | return ; 27 | } 28 | }); 29 | 30 | var ChildComponent = React.createClass({ 31 | render: function() { 32 | return ; 33 | } 34 | }); 35 | 36 | var GrandchildComponent = React.createClass({ 37 | mixins: [FluxMixin], 38 | 39 | render: function() { 40 | return ; 41 | }, 42 | 43 | onClick: function() { 44 | this.getFlux().actions.someAction(); 45 | } 46 | }); 47 | 48 | var flux = new Fluxxor.Flux(...); 49 | React.renderComponent(, ...); 50 | ``` 51 | -------------------------------------------------------------------------------- /site/contents/documentation/flux.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Fluxxor.Flux 3 | template: page.ejs 4 | --- 5 | 6 | Fluxxor.Flux 7 | ============ 8 | 9 | `Fluxxor.Flux` is the main container object for a Flux application. It provides access to the [stores](/documentation/stores.html) and the [actions](/documentation/actions.html), and is responsible for managing the dispatcher internally. 10 | 11 | ## `new Fluxxor.Flux(stores, actions)` 12 | 13 | Creates a new `Flux` instance. 14 | 15 | * `stores` - An object map of store names to store instances. Stores can be retrieved later by their name. See [Stores](/documentation/stores.html) for more information. 16 | * `actions` - An object map of action names to action functions. See [Actions](/documentation/actions.html) for more information. 17 | 18 | Example: 19 | 20 | ```javascript 21 | var stores = { 22 | MyStore: new MyStore({options: here}), 23 | OtherStore: new OtherStore({options: here}) 24 | }; 25 | 26 | var actions = { 27 | processThing: function(thing) { 28 | this.dispatch("PROCESS_THING", {thing: thing}); 29 | } 30 | }; 31 | 32 | var flux = new Fluxxor.Flux(stores, actions); 33 | ``` 34 | 35 | ## `Fluxxor.Flux#store(name)` 36 | 37 | Retrieves a store by its name. 38 | 39 | * `name` - The name of the store as passed to the `Flux` constructor. 40 | 41 | Example: 42 | 43 | ```javascript 44 | var stores = { 45 | MyStore: new MyStore(); 46 | }; 47 | 48 | var flux = new Fluxxor.Flux(stores, actions); 49 | 50 | var myStore = flux.store("MyStore"); 51 | ``` 52 | 53 | ## `Fluxxor.Flux#getAllStores()` 54 | 55 | Retrieves all stores. The return value is an object where the keys are the names of the stores and the values are the stores themselves. 56 | 57 | **Note:** This is a reference to the underlying stores implementation, and should not be modified. 58 | 59 | ## `Fluxxor.Flux#actions` 60 | 61 | Retrieves the map of actions. 62 | 63 | Example: 64 | 65 | ```javascript 66 | var actions = { 67 | processThing: function(thing) { 68 | this.dispatch("PROCESS_THING", {thing: thing}); 69 | } 70 | }; 71 | 72 | var flux = new Fluxxor.Flux(stores, actions); 73 | 74 | flux.actions.processThing(myThing); 75 | ``` 76 | 77 | ## `Fluxxor.Flux#addStore(name, store)` 78 | 79 | Adds a new store to the `Flux` instance. 80 | 81 | * `name` - The name used to identify the store. Stores can be retrieved later by their name. See [Stores](/documentation/stores.html) for more information. 82 | * `store` - The store instance to add. 83 | 84 | ```javascript 85 | flux.addStore("user", new UserStore()); 86 | ``` 87 | 88 | ## `Fluxxor.Flux#addStores(stores)` 89 | 90 | Adds stores to the `Flux` instance. 91 | 92 | * `stores` - A hash of stores to add, in the same format as the `Fluxxor.Flux` constructor. 93 | 94 | ```javascript 95 | var newStores = { 96 | user: new UserStore(), 97 | post: new PostStore() 98 | }; 99 | 100 | flux.addStores(newStores); 101 | ``` 102 | 103 | ## `Fluxxor.Flux#addAction(path..., function)` 104 | 105 | Adds an action to the `Flux` instance's action hash. This function takes a path (either as an array of strings or as individual strings) followed by a function. 106 | 107 | * `path...` - The path in the actions object to add the action. 108 | * `function` - The action function to add. 109 | 110 | `path` can be specified either as an array of strings where each element is one part of the path, or as a free list of strings. For example: 111 | 112 | ```javascript 113 | // The action hash we want to end up with: 114 | { 115 | user: { 116 | login: function() { ... } 117 | } 118 | } 119 | 120 | // Path as an array 121 | flux.addAction(["user", "login"], function() { ... }); 122 | 123 | // Path as free arguments 124 | flux.addAction("user", "login", function() { ... }); 125 | ``` 126 | 127 | Fluxxor will automatically create any intermediary objects as necessary, and will intelligently merge the new action into the existing hash, but does not allow overwriting any existing functions. 128 | 129 | ## `Fluxxor.Flux#addActions(actions)` 130 | 131 | Adds actions to the `Flux` instance's action hash. 132 | 133 | * `actions` - A hash of actions to add, in the same format as the `Fluxxor.Flux` constructor. 134 | 135 | ```javascript 136 | var newActions = { 137 | user: { 138 | login: function() { ... }, 139 | logout: function() { ... } 140 | } 141 | }; 142 | 143 | flux.addActions(newActions); 144 | ``` 145 | 146 | Fluxxor will intelligently merge the new actions with the existing actions, but does not allow overwriting any existing functions. 147 | 148 | ## `Fluxxor.Flux#setDispatchInterceptor(interceptor)` 149 | 150 | Sets `interceptor` as the `Flux` instance's dispatch interceptor. The dispatch interceptor allows you to surround or replace the action dispatch with custom functionality. 151 | 152 | * `interceptor` - A function with the signature `function(action, dispatch)` where `action` is the action being dispatched and `dispatch` is the original (non-intercepted) dispatch function. If a falsy value, resets the dispatch interceptor to the default (no-op) interceptor. 153 | 154 | Sometimes it's useful to inject custom logic into the normal dispatch flow. `setDispatchInterceptor` allows you to wrap or replace the original dispatch function with your own logic. The default dispatch interceptor is essentially a no-op: 155 | 156 | ```javascript 157 | flux.setDispatchInterceptor(function(action, dispatch) { 158 | dispatch(action); 159 | }); 160 | ``` 161 | 162 | In particular, it can be very useful to wrap action dispatches in React's batched updates (if you're using React). To do so, wrap the dispatch in `ReactDOM.unstable_batchedUpdates` (which was `React.addons.batchedUpdates` before React v0.14): 163 | 164 | ```javascript 165 | flux.setDispatchInterceptor(function(action, dispatch) { 166 | ReactDOM.unstable_batchedUpdates(function() { 167 | dispatch(action); 168 | }); 169 | }); 170 | ``` 171 | 172 | (See the [Using with React page](/guides/react.html) for more information on how `unstable_batchedUpdates` can help.) 173 | 174 | You can even bypass the original dispatch function entirely for testing or more exotic implementations: 175 | 176 | ```javascript 177 | flux.setDispatchInterceptor(function(action, dispatch) { 178 | // Ignore the `dispatch` argument and do our own thing with the action, for example: 179 | window.postMessage({ type: "myCustomThing", action: action }); 180 | }); 181 | ``` 182 | 183 | ## `EventEmitter` methods 184 | 185 | `Flux` instances are also instances of EventEmitters, and thus [inherit all the EventEmitter methods](http://nodejs.org/api/events.html#events_class_events_eventemitter). Most notably, `Flux` instances dispatch a `"dispatch"` event with `type` and `payload` arguments when `this.dispatch` is called from an action. This is useful for cross-cutting concerns (like logging), and should not be used for managing the flow of data in a Fluxxor application. 186 | 187 | Example: 188 | 189 | ```javascript 190 | flux.on("dispatch", function(type, payload) { 191 | console.log("Dispatched", type, payload); 192 | } 193 | ``` 194 | 195 | Note that the action will still be dispatched even if the `"dispatch"` event handler throws an exception. 196 | -------------------------------------------------------------------------------- /site/contents/documentation/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Documentation 3 | template: page.ejs 4 | --- 5 | 6 | Documentation 7 | ============= 8 | 9 | Fluxxor consists of three core pieces: 10 | 11 | * [Fluxxor.Flux](/documentation/flux.html) 12 | * [Stores](/documentation/stores.html) 13 | * [Actions](/documentation/actions.html) 14 | 15 | Additionally, Fluxxor ships with some mixins to make it easier to use in conjunction with React:

    16 | 17 | * [Fluxxor.FluxMixin](/documentation/flux-mixin.html) 18 | * [Fluxxor.StoreWatchMixin](/documentation/store-watch-mixin.html) 19 | 20 | For installation instructions, see the [installation](/guides/installation.html) page. Be sure to check out the [quick-start guide](/guides/quick-start.html) and the [examples](/examples/), too! 21 | -------------------------------------------------------------------------------- /site/contents/documentation/store-watch-mixin.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: StoreWatchMixin 3 | template: page.ejs 4 | --- 5 | 6 | Fluxxor.StoreWatchMixin 7 | ======================= 8 | 9 | `Fluxxor.StoreWatchMixin` is a simple React mixin that assists with watching for `"change"` events on one or more stores. Normally, you'd need to bind to store change events in `componentDidMount` and unbind them in `componentWillUnmount` to keep from leaking memory. Additionally, you'd need to set up handlers to pull data from the stores during change events and lifecycle hooks (such as `getInitialState`). 10 | 11 | `StoreWatchMixin` simply requires that you: 12 | 13 | 1. Define a method on your component called `getStateFromFlux` that returns an object representing the part of the component's state that comes from the Flux stores. 14 | 2. Have a `prop` or `context` property named `flux` that points to the `Flux` instance with the stores. This is automatic if you use [`FluxMixin`](/documentation/flux-mixin.html). 15 | 16 | The mixin will then automatically 17 | 18 | 1. Bind to `"change"` events for each store when the component mounts 19 | 2. Unbind from `"change"` events when the component unmounts 20 | 3. Automatically call `setState` with the return value of `getStateFromFlux` when a store emits a `"change"` event 21 | 4. Automatically set the component's initial state based on the return value of `getStateFromFlux` when the component mounts (note that this object is merged with any other `getInitialState` functions defined on the component or other mixins) 22 | 23 | Note that `StoreWatchMixin` binds events in `componentDidMount` and not `componentWillMount` in order to prevent memory leaks when rendering React components that use Fluxxor on the server using Node.js. This means that actions fired from `componentWillMount` that result in `"change"` events in your stores will not update the component; consider moving such actions to `componentDidMount` instead. 24 | 25 | Example: 26 | 27 | ```javascript 28 | var React = require("react"), 29 | Fluxxor = require("fluxxor"), 30 | FluxMixin = Fluxxor.FluxMixin(React), // or window.React, etc. 31 | StoreWatchMixin = Fluxxor.StoreWatchMixin; 32 | 33 | var MyStore = Fluxxor.createStore({ ... }), 34 | OtherStore = Fluxxor.createStore({ ... }); 35 | 36 | var stores = { 37 | MyStore: new MyStore(), 38 | OtherStore: new OtherStore() 39 | }; 40 | var actions = { ... }; 41 | 42 | var flux = new Fluxxor.Flux(stores, actions); 43 | 44 | var MyComponent = React.createClass({ 45 | mixins: [FluxMixin, StoreWatchMixin("MyStore", "OtherStore")], 46 | 47 | getStateFromFlux: function() { 48 | var flux = this.getFlux(); 49 | return { 50 | stateFromMyStore: flux.store("MyStore").getSomeData(), 51 | stateFromOtherStore: flux.store("OtherStore").getMoreData(), 52 | }; 53 | }, 54 | 55 | render: function() { 56 | // ... 57 | } 58 | }); 59 | ``` 60 | -------------------------------------------------------------------------------- /site/contents/documentation/stores.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Stores 3 | template: page.ejs 4 | --- 5 | 6 | Stores 7 | ====== 8 | 9 | In a Flux application, the stores are responsible for managing business logic and data. They're akin to models or collections in MVC systems, but stores may manage more than a single piece of data or a single collection, as they are responsible for a *domain* of the application. 10 | 11 | The *only* way to update stores is to send them an action by way of the dispatcher; stores should not have setter methods or properties that allow users to manipulate the store directly. Stores register their intent to respond to certain action types and actions with those types are routed to the appropriate handlers in the stores. Handlers are called with the action's payload and type as parameters. 12 | 13 | Fluxxor supports dependencies between stores when necessary. If a store depends on data from other stores, it can wait for those stores to finish handling the currently dispatched action with the `waitFor` method. 14 | 15 | Action dispatches are synchronous; stores can perform asynchronous updates, but should fire new actions at the end of the asynchronous operation if the rest of the system should be notified of a change in state as a result of the async operation. (Alternatively, perform asynchronous operations in the action methods themselves.) Once a store returns anything (including `undefined`) from an action handler, that store is considered to be done with that action's dispatch (unless it calls `waitFor`). 16 | 17 | Stores can be added to a `Flux` instance programmatically using `Flux#addStore` or `Flux#addStores`; see the [Flux documentation](/documentation/flux.html) for more details. 18 | 19 | ## `Fluxxor.createStore(spec)` 20 | 21 | Create a new store constructor. 22 | 23 | * `spec` - An object describing the stores created with the returned constructor. 24 | 25 | `spec` may contain any properties; functions will be automatically bound to the store instance and attached to it, and other properties will simply be attached to it. The following properties of `spec` behave specially: 26 | 27 | * `actions` - An object map of action types to method names. Actions dispatched to the store with the given type will automatically be handled by the method with the corresponding name. Dynamic action types can be bound with the `bindActions` method. (`bindActions` is the recommended approach, since you can refer to constants defined elsewhere, which helps catch errors earlier and better supports minification.) 28 | 29 | * `initialize(options)` - A function that will be called right after a store is instantiated. `options` is an optional object passed from the constructor. 30 | 31 | Example: 32 | 33 | ```javascript 34 | var MyStore = Fluxxor.createStore({ 35 | actions: { 36 | "ACTION_TYPE": "handleActionType" 37 | }, 38 | 39 | initialize: function(options) { 40 | this.value = options.value; 41 | 42 | // We could also use this in place of the `actions` hash, above: 43 | this.bindActions( 44 | "ACTION_TYPE", this.handleActionType 45 | ); 46 | }, 47 | 48 | handleActionType: function(payload, type) { 49 | // ... 50 | } 51 | }); 52 | 53 | var myStore = new MyStore({value: 123}); 54 | ``` 55 | 56 | ## `Store#flux` 57 | 58 | The [Flux](/documentation/flux.html) instance this store is contained within. 59 | 60 | ## `Store#bindActions(type, handler[, ...])` 61 | ## `Store#bindActions(actions)` 62 | 63 | Binds action types to methods on the store. 64 | 65 | `bindActions(type, handler[, ...])` takes any even number of arguments: 66 | 67 | * `type` - The action type to bind to. 68 | * `handler` - A function reference or method name (as a string) to call when actions of that type are dispatched to the store. 69 | 70 | `bindActions(actions)` takes an actions hash in the same format as `spec.actions` described in `createStore`, above. 71 | 72 | Using `bindActions` with constants defined elsewhere is less error prone and supports minification better than using the `actions` hash in the `createStore` spec or passing a hash to `bindActions`. 73 | 74 | Example: 75 | 76 | ```javascript 77 | var ACTION_TYPE = "ACTION_TYPE_1", 78 | OTHER_ACTION_TYPE = "ACTION_TYPE_2"; 79 | 80 | var MyStore = Fluxxor.createStore({ 81 | initialize: function() { 82 | this.bindActions( 83 | ACTION_TYPE, this.handleActionType, 84 | OTHER_ACTION_TYPE, "handleOtherActionType" 85 | ); 86 | 87 | // OR: 88 | 89 | this.bindActions({ 90 | "ACTION_TYPE_1": "handleActionType", 91 | "ACTION_TYPE_2": "handleOtherActionType" 92 | }); 93 | }, 94 | 95 | handleActionType: function(payload, type) { 96 | // ... 97 | }, 98 | 99 | handleOtherActionType: function(payload, type) { 100 | // ... 101 | } 102 | }); 103 | ``` 104 | 105 | ## `Store#waitFor(stores, callback)` 106 | 107 | Waits for other stores to finish dispatching the current action, executing `callback` afterwards. Since action handlers are synchronous, a store is "finished" handling an action when the handler function returns. 108 | 109 | * `stores` - An array of names of stores to wait for. 110 | * `callback(stores)` - A function to call after all the specified stores have handled the current dispatch. The function is called with the store instances (corresponding to the array of names). 111 | 112 | `waitFor` will throw if it detects circular dependencies among the stores being dispatched to. 113 | 114 | Example: 115 | 116 | ```javascript 117 | Fluxxor.createStore({ 118 | actions: { 119 | "ACTION_TYPE": "actionHandler" 120 | }, 121 | 122 | actionHandler: function(payload, type) { 123 | this.waitFor(["otherStore", "anotherStore"], function(other, another) { 124 | // ... 125 | }); 126 | } 127 | }); 128 | ``` 129 | 130 | Note that the callback is called synchronously. 131 | 132 | ## `EventEmitter` methods 133 | 134 | Stores are instances of EventEmitters, and thus [inherit all the EventEmitter methods](http://nodejs.org/api/events.html#events_class_events_eventemitter). Most notably, stores should `emit` an event to notify the views that their data has changed. 135 | 136 | The [`StoreWatchMixin`](/documentation/store-watch-mixin.html) assists with attaching event handlers to store change events in React applications. 137 | 138 | Example: 139 | 140 | ```javascript 141 | var MyStore = Fluxxor.createStore({ 142 | // ... 143 | 144 | actionHandler: function(payload, type) { 145 | // some update to the store's data here 146 | this.emit("change"); 147 | } 148 | }); 149 | 150 | var myStore = new MyStore(); 151 | 152 | myStore.on("change", function() { 153 | // the store has updated its data 154 | }); 155 | ``` 156 | -------------------------------------------------------------------------------- /site/contents/examples/carousel/images/bttf1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/examples/carousel/images/bttf1.png -------------------------------------------------------------------------------- /site/contents/examples/carousel/images/bttf2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/examples/carousel/images/bttf2.png -------------------------------------------------------------------------------- /site/contents/examples/carousel/images/bttf3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/examples/carousel/images/bttf3.png -------------------------------------------------------------------------------- /site/contents/examples/carousel/images/bttf4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/examples/carousel/images/bttf4.png -------------------------------------------------------------------------------- /site/contents/examples/carousel/images/bttf5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/examples/carousel/images/bttf5.png -------------------------------------------------------------------------------- /site/contents/examples/carousel/images/bttf6.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/examples/carousel/images/bttf6.png -------------------------------------------------------------------------------- /site/contents/examples/carousel/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Carousel 3 | template: page.ejs 4 | --- 5 | 6 | Image Carousel 7 | ============== 8 | 9 |
    10 | 11 | This image carousel is a very simple React application built using Fluxxor. The source for the example can be found [on GitHub](https://github.com/BinaryMuse/fluxxor/tree/master/examples/carousel). 12 | 13 | The entry point of the application is `app/app.jsx`. The `Fluxxor.Flux` instance is exported to `window.flux` so that you can manipulate the application from the standard JavaScript console (e.g. the methods on `window.flux.actions`). 14 | 15 | Stores 16 | ------ 17 | 18 | The system contains two stores: 19 | 20 | * `ImageStore` - Contains the list of images to display in the carousel 21 | * `CarouselStore` - Contains information about the carousel position (which image is being displayed, etc.) and ensures that the user cannot change images while a transition is in progress 22 | 23 | This separation is perhaps a bit contrived, but shows how you might use multiple stores in a Flux-based React application. 24 | 25 | Actions 26 | ------- 27 | 28 | There are four actions that manipulate the stores: 29 | 30 | * `prevImage` - Moves the carousel to the previous image (modifies `CarouselStore`) 31 | * `nextImage` - Moves the carousel to the next image (modifies `CarouselStore`) 32 | * `selectImage` - Moves the carousel to a specific image (modifies `CarouselStore`) 33 | * `addImage` - Adds an image to the carousel and moves to that image (modifies `ImageStore` and `CarouselStore`) 34 | 35 | `addImage` is particularly interesting because the action handler inside `CarouselStore` utilizes the `waitFor` mechanism to allow the `ImageStore` to accept or reject adding the image (based on its file extension and whether the image is already in the carousel). 36 | 37 | Components 38 | ---------- 39 | 40 | * `Application` - The top-level component that maintains state from the stores using `Fluxxor.StoreWatchMixin` 41 | * `Carousel` - The actual image carousel itself; all data is passed as props, and user interaction is propagated back to `Application` 42 | * `ImageForm` - A small form for adding images to the carousel; user interaction is propagated back to `Application` 43 | 44 | Note that `ImageForm` has state outside of the data from the stores—however, it's localized to the form, and isn't part of the larger "application state." When the form is submitted, this local state is sent to the parent component through a property, where it is then sent to the dispatcher via the `addImage` action. 45 | 46 | --- 47 | 48 | In this small application, only the top level `Application` component requires access to the Flux data. However, since `Application` mixes in `Fluxxor.FluxMixin`, any descendants of `Application` with the `Fluxxor.FluxMixin` would automatically have access to the `Fluxxor.Flux` instance via `this.getFlux()`. 49 | 50 | 51 | 62 | -------------------------------------------------------------------------------- /site/contents/examples/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Examples 3 | template: page.ejs 4 | --- 5 | 6 | Examples 7 | ======== 8 | 9 | * [Basic Todos](/guides/quick-start.html) is a *very* simple todo app; build this app yourself with step-by-step instructions in the [quick-start guide](/guides/quick-start.html). 10 | * [Image Carousel](/examples/carousel/) is a simple image carousel that uses two stores. 11 | * [Asynchronous Data](/guides/async-data.html) demonstrates how to deal with asynchronous data loading in a flux application. 12 | * [React Router](/examples/react-router.html) demonstrates how to integrate Fluxxor with react-router, a React-based routing solution. 13 | -------------------------------------------------------------------------------- /site/contents/examples/react-router.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: React Router 3 | template: page.ejs 4 | --- 5 | 6 | React Router 7 | ============ 8 | 9 | This example demonstrates the techniques explained in [the routing guide](/guides/routing.html), utilizing [React Router](https://github.com/rackt/react-router) to provide a client-side routing solution. The code for this example can be found [on GitHub](https://github.com/BinaryMuse/fluxxor/tree/master/examples/react-router). 10 | 11 |
    12 |
    13 | 14 |
    15 | 16 | The route definition and run block for this app looks like this: 17 | 18 | ```javascript 19 | var routes = ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | var flux = new Fluxxor.Flux(...); 33 | 34 | Router.run(routes, function(Handler) { 35 | React.render( 36 | , 37 | document.getElementById("app") 38 | ); 39 | }); 40 | ``` 41 | 42 | -------------------------------------------------------------------------------- /site/contents/faq.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: FAQ 3 | template: page.ejs 4 | --- 5 | 6 | FAQ 7 | === 8 | 9 | **Q:** Does Fluxxor require React? 10 | 11 | **A:** Fluxxor works great with React, and if you want to use the mixins, you will of course need React, but Fluxxor has no dependency on React itself. 12 | 13 |
    14 | 15 | **Q:** Why does `Fluxxor.FluxMixin` constantly throw an error? 16 | 17 | **A:** `Fluxxor.FluxMixin` is a function that takes React as a parameter and returns the associated mixin: 18 | 19 | ```javascript 20 | var FluxMixin = Fluxxor.FluxMixin(React); 21 | 22 | React.createClass({ 23 | mixins: [FluxMixin], 24 | 25 | // ... 26 | }); 27 | ``` 28 | 29 |
    30 | 31 | **Q:** How do I deal with asynchronous operations in stores? 32 | 33 | **A:** Dispatches to stores are always synchronous, but stores can perform asynchronous operations. At the end of the async call, the store can dispatch a separate action that represents the *availability* of the asynchronously-obtained data. See [Dealing with Asynchronous Data](/guides/async-data.html) for more details. 34 | 35 |
    36 | 37 | **Q:** Why does dispatching an action while another action is in progress throw an error? 38 | 39 | **A:** Fluxxor prevents cascading updates where one action triggers another, and so on. See [What is Flux](/what-is-flux.html) and [Flux Application Architecture](http://facebook.github.io/react/docs/flux-overview.html) for more information on Flux. 40 | 41 |
    42 | 43 | **Q:** Why am I getting an error saying that I can't dispatch an action while another action is being dispatched if I'm dispatching an action from `componentWillMount` or `componentDidMount`? 44 | 45 | **A:** React automatically batches updates when they're triggered from inside its synthetic event system (e.g. from an `onKeyPress` or `onClick` handler), but otherwise you have to batch them yourself. See the "Batched Updates" section of [Using with React](/guides/react.html) page for a solution to this problem. 46 | 47 |
    48 | 49 | **Q:** Why is Fluxxor throwing an error saying an action is already being dispatched when I'm sending an action from an asynchronous operation? 50 | 51 | **A:** Some libraries will sometimes call callbacks on the same tick, for example if data is cached. You can wrap the action dispatch call in a `setTimeout` to ensure the function is asynchronous. For bonus points, notify the author of the offending library that [their asynchronous callbacks are sometimes synchronous](http://blog.ometer.com/2011/07/24/callbacks-synchronous-and-asynchronous/). 52 | 53 |
    54 | 55 | **Q:** Why do I see a warning that says "possible EventEmitter memory leak detected"? 56 | 57 | **A:** This warning is built in to the Node.js [EventEmitter](http://nodejs.org/api/events.html#events_class_events_eventemitter) to help avoid accidentally leaking handlers. If you know you're not actually leaking and you want to suppress this warning, you can call [`setMaxListeners(n)`](http://nodejs.org/api/events.html#events_emitter_setmaxlisteners_n) on your store (you can use a value of `0` for unlimited). 58 | 59 | As of version 1.4.2, Fluxxor uses [EventEmitter3](https://github.com/3rd-Eden/EventEmitter3) instead of the built-in Node.js EventEmitter, which should resolve this issue. 60 | -------------------------------------------------------------------------------- /site/contents/fluxbox.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 32 | 39 | 41 | 54 | 65 | 67 | 68 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /site/contents/fluxxor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/fluxxor.png -------------------------------------------------------------------------------- /site/contents/fluxxor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 29 | 32 | 39 | 41 | 43 | 54 | 63 | 64 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | -------------------------------------------------------------------------------- /site/contents/getting-started/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Getting Started 3 | template: page.ejs 4 | --- 5 | 6 | Getting Started 7 | =============== 8 | 9 | 10 | 11 | [This content has moved. Click here if you are not automatically redirected.](/guides/) 12 | -------------------------------------------------------------------------------- /site/contents/getting-started/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | template: page.ejs 4 | --- 5 | 6 | Installation 7 | ============ 8 | 9 | 10 | 11 | [This content has moved. Click here if you are not automatically redirected.](/guides/installation.html) 12 | -------------------------------------------------------------------------------- /site/contents/getting-started/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quick-Start Guide 3 | template: page.ejs 4 | --- 5 | 6 | Quick-Start Guide / Basic Todos Example 7 | ======================================= 8 | 9 | 10 | 11 | [This content has moved. Click here if you are not automatically redirected.](/guides/quick-start.html) 12 | -------------------------------------------------------------------------------- /site/contents/guides/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Guides 3 | template: page.ejs 4 | --- 5 | 6 | Guides 7 | ====== 8 | 9 | * [Installation](/guides/installation.html) 10 | * [Quick-Start Guide](/guides/quick-start.html) 11 | * [Dealing with Asynchronous Data](/guides/async-data.html) 12 | -------------------------------------------------------------------------------- /site/contents/guides/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Installation 3 | template: page.ejs 4 | --- 5 | 6 | Installation 7 | ============ 8 | 9 | You have a few options when it comes to installing Fluxxor. Keep in mind that, while Fluxxor works great with [React](http://facebook.github.io/react/), React is not required for Fluxxor. 10 | 11 | CommonJS Module Bundlers 12 | ------------------------ 13 | 14 | Fluxxor is distributed [on npm](https://www.npmjs.org/package/fluxxor). You can install it with 15 | 16 | `npm install [--save] fluxxor` 17 | 18 | If you're using a client-side module bundler like [Browserify](http://browserify.org/) or [Webpack](http://webpack.github.io/), you're done! Simply `require("fluxxor")` to get a reference to the library. 19 | 20 | Browser Builds 21 | -------------- 22 | 23 | Browser builds and corresponding source map files can be downloaded from [the Fluxxor releases page](https://github.com/BinaryMuse/fluxxor/releases) or installed via [Bower](http://bower.io/) via `bower install fluxxor`. Browser builds use a universal module definition, and should work in the following environments: 24 | 25 | ### CommonJS 26 | 27 | ```javascript 28 | var Fluxxor = require("path/to/fluxxor"); 29 | ``` 30 | 31 | ### AMD 32 | 33 | ```javascript 34 | define("someModule", ["Fluxxor"], function(Fluxxor) { 35 | // ... 36 | }); 37 | ``` 38 | 39 | ### Standalone 40 | 41 | ```html 42 | 43 | ``` 44 | 45 | ```javascript 46 | window.Fluxxor.createStore({ ... }); 47 | ``` 48 | 49 | Third-Party Releases 50 | -------------------- 51 | 52 | The following releases are maintained by third parties, and support inquiries should be directed to their maintainers. 53 | 54 | ### WebJar 55 | 56 | For JVM languages, there are [WebJar](http://www.webjars.org) packages available on Maven Central and jsDelivr as the following: 57 | 58 | SBT/Play Framework 2: 59 | 60 | ```scala 61 | "org.webjars" % "fluxxor" % fluxxorVersion 62 | ``` 63 | 64 | Maven: 65 | 66 | ```xml 67 | 68 | org.webjars 69 | fluxxor 70 | ${fluxxor.version} 71 | 72 | ``` 73 | 74 | For detailed instructions, refer to the [WebJars documentation](http://www.webjars.org/documentation). For update requests, open a pull request on the [Fluxxor WebJar repository on Github](https://github.com/webjars/fluxxor). 75 | 76 | Browser Compatibility 77 | --------------------- 78 | 79 | Fluxxor is compatible with any [ES5-compliant browser](http://kangax.github.io/compat-table/es5/) (IE 9+, FF 4+, Safari 5.1.4+, Chrome 19+, Opera 12.10+). You can use [es5-shim](https://github.com/es-shims/es5-shim) for other browsers. 80 | 81 | Getting Started 82 | --------------- 83 | 84 | Check out [the quick-start guide](/guides/quick-start.html) to get up-to-speed building apps with Fluxxor in no time. 85 | -------------------------------------------------------------------------------- /site/contents/guides/quick-start.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Quick-Start Guide 3 | template: page.ejs 4 | --- 5 | 6 | Quick-Start Guide / Basic Todos Example 7 | ======================================= 8 | 9 | Once you have Fluxxor and React [installed](/guides/installation.html), it's time to build an app! Since this guide is designed to cover the basics quickly, we'll start with a *very* basic todo app—namely, this one right here: 10 | 11 |
    12 | 13 | Stores and Actions 14 | ------------------ 15 | 16 | First, let's create a store to keep track of our todo items. It will respond to the following actions: 17 | 18 | * `"ADD_TODO"` - adds a new todo 19 | * `"TOGGLE_TODO"` - completes or uncompletes a specific todo item 20 | * `"CLEAR_TODOS"` - removes all complete todo items 21 | 22 | ```javascript 23 | var constants = { 24 | ADD_TODO: "ADD_TODO", 25 | TOGGLE_TODO: "TOGGLE_TODO", 26 | CLEAR_TODOS: "CLEAR_TODOS" 27 | }; 28 | 29 | var TodoStore = Fluxxor.createStore({ 30 | initialize: function() { 31 | this.todoId = 0; 32 | this.todos = {}; 33 | 34 | this.bindActions( 35 | constants.ADD_TODO, this.onAddTodo, 36 | constants.TOGGLE_TODO, this.onToggleTodo, 37 | constants.CLEAR_TODOS, this.onClearTodos 38 | ); 39 | }, 40 | 41 | onAddTodo: function(payload) { 42 | var id = this._nextTodoId(); 43 | var todo = { 44 | id: id, 45 | text: payload.text, 46 | complete: false 47 | }; 48 | this.todos[id] = todo; 49 | this.emit("change"); 50 | }, 51 | 52 | onToggleTodo: function(payload) { 53 | var id = payload.id; 54 | this.todos[id].complete = !this.todos[id].complete; 55 | this.emit("change"); 56 | }, 57 | 58 | onClearTodos: function() { 59 | var todos = this.todos; 60 | 61 | Object.keys(todos).forEach(function(key) { 62 | if(todos[key].complete) { 63 | delete todos[key]; 64 | } 65 | }); 66 | 67 | this.emit("change"); 68 | }, 69 | 70 | getState: function() { 71 | return { 72 | todos: this.todos 73 | }; 74 | }, 75 | 76 | _nextTodoId: function() { 77 | return ++this.todoId; 78 | } 79 | }); 80 | ``` 81 | 82 | Let's create a few semantic actions to go along with our action types. 83 | 84 | ```javascript 85 | var actions = { 86 | addTodo: function(text) { 87 | this.dispatch(constants.ADD_TODO, {text: text}); 88 | }, 89 | 90 | toggleTodo: function(id) { 91 | this.dispatch(constants.TOGGLE_TODO, {id: id}); 92 | }, 93 | 94 | clearTodos: function() { 95 | this.dispatch(constants.CLEAR_TODOS); 96 | } 97 | }; 98 | ``` 99 | 100 | Now we can instantiate our store and build a `Flux` instance: 101 | 102 | ```javascript 103 | var stores = { 104 | TodoStore: new TodoStore() 105 | }; 106 | 107 | var flux = new Fluxxor.Flux(stores, actions); 108 | ``` 109 | 110 | Finally, let's use the `"dispatch"` event to add some logging: 111 | 112 | ```javascript 113 | flux.on("dispatch", function(type, payload) { 114 | if (console && console.log) { 115 | console.log("[Dispatch]", type, payload); 116 | } 117 | }); 118 | ``` 119 | 120 | React Application 121 | ----------------- 122 | 123 | Let's build out our UI with React. 124 | 125 | Our top-level `Application` component will use the [FluxMixin](/documentation/flux-mixin.html) as well as the [StoreWatchMixin](/documentation/store-watch-mixin.html) to make our lives a bit easier. The component will iterate over the array of todos and emit a `TodoItem` component for each one. 126 | 127 | We'll also add a quick form for adding new todo items, and a button for clearing completed todos. 128 | 129 | ```javascript 130 | var FluxMixin = Fluxxor.FluxMixin(React), 131 | StoreWatchMixin = Fluxxor.StoreWatchMixin; 132 | 133 | var Application = React.createClass({ 134 | mixins: [FluxMixin, StoreWatchMixin("TodoStore")], 135 | 136 | getInitialState: function() { 137 | return { newTodoText: "" }; 138 | }, 139 | 140 | getStateFromFlux: function() { 141 | var flux = this.getFlux(); 142 | // Our entire state is made up of the TodoStore data. In a larger 143 | // application, you will likely return data from multiple stores, e.g.: 144 | // 145 | // return { 146 | // todoData: flux.store("TodoStore").getState(), 147 | // userData: flux.store("UserStore").getData(), 148 | // fooBarData: flux.store("FooBarStore").someMoreData() 149 | // }; 150 | return flux.store("TodoStore").getState(); 151 | }, 152 | 153 | render: function() { 154 | var todos = this.state.todos; 155 | return ( 156 |
    157 |
      158 | {Object.keys(todos).map(function(id) { 159 | return
    • ; 160 | })} 161 |
    162 |
    163 | 166 | 167 |
    168 | 169 |
    170 | ); 171 | }, 172 | 173 | handleTodoTextChange: function(e) { 174 | this.setState({newTodoText: e.target.value}); 175 | }, 176 | 177 | onSubmitForm: function(e) { 178 | e.preventDefault(); 179 | if (this.state.newTodoText.trim()) { 180 | this.getFlux().actions.addTodo(this.state.newTodoText); 181 | this.setState({newTodoText: ""}); 182 | } 183 | }, 184 | 185 | clearCompletedTodos: function(e) { 186 | this.getFlux().actions.clearTodos(); 187 | } 188 | }); 189 | ``` 190 | 191 | The `TodoItem` component will display and style itself based on the completion of the todo, and will dispatch an action indicating its intent to toggle its completion state. 192 | 193 | ```javascript 194 | var TodoItem = React.createClass({ 195 | mixins: [FluxMixin], 196 | 197 | propTypes: { 198 | todo: React.PropTypes.object.isRequired 199 | }, 200 | 201 | render: function() { 202 | var style = { 203 | textDecoration: this.props.todo.complete ? "line-through" : "" 204 | }; 205 | 206 | return {this.props.todo.text}; 207 | }, 208 | 209 | onClick: function() { 210 | this.getFlux().actions.toggleTodo(this.props.todo.id); 211 | } 212 | }); 213 | ``` 214 | 215 | Bringing it Together 216 | -------------------- 217 | 218 | Now that we have a `Flux` instance and all our components are defined, we can finally render our app. We'll put it inside a `div` in our HTML with an ID of "app". 219 | 220 | ```javascript 221 | React.render(, document.getElementById("app")); 222 | ``` 223 | 224 | And that's it! We've created a (super simple) Flux application with React and Fluxxor. You can find the full source code [on GitHub](https://github.com/BinaryMuse/fluxxor/tree/master/examples/todo-basic). 225 | 226 | 227 | -------------------------------------------------------------------------------- /site/contents/guides/react.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Using with React 3 | template: page.ejs 4 | --- 5 | 6 | Using with React 7 | ================ 8 | 9 | Fluxxor can be used with any JavaScript framework (or no framework at all!), but it's designed to work well with [React](http://facebook.github.io/react/). 10 | 11 | Batched Updates 12 | --------------- 13 | 14 | Starting in React v0.12, React exposed an addon called `batchedUpdates` that allows you to manually batch React updates when outside of the normal React synthetic event lifecycle. This can be useful to ensure you don't get cascading dispatch errors with Fluxxor if you dispatch actions from within `componentWillMount` or `componentDidMount`. In React v0.14, this was moved to the `react-dom` package, and renamed to `unstable_batchedUpdates`. 15 | 16 | As of Fluxxor v1.6.0, there is a new method available you can use to tie Fluxxor action dispatches to the React batched update addon. To use `unstable_batchedUpdates` with Fluxxor, use `Flux#setDispatchInterceptor` with `ReactDOM.unstable_batchedUpdates`; for example: 17 | 18 | ```javascript 19 | var ReactDOM = require("react-dom"), 20 | Fluxxor = require("fluxxor"); 21 | 22 | var flux = new Fluxxor.Flux(stores, actions); 23 | flux.setDispatchInterceptor(function(action, dispatch) { 24 | ReactDOM.unstable_batchedUpdates(function() { 25 | dispatch(action); 26 | }); 27 | }); 28 | ``` 29 | 30 | Note that `ReactDOM.unstable_batchedUpdates` is specific to the DOM renderer, and is not appropriate for use in React Native apps, or apps that use other custom renderers. 31 | 32 | Mixins 33 | ------ 34 | 35 | Fluxxor includes a couple mixins to make interop with React easier; check out the documentation on [Fluxxor.FluxMixin](/documentation/flux-mixin.html) and [Fluxxor.StoreWatchMixin](/documentation/store-watch-mixin.html) for more information. 36 | -------------------------------------------------------------------------------- /site/contents/guides/routing.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Routing 3 | template: page.ejs 4 | --- 5 | 6 | Routing 7 | ======= 8 | 9 | When using a client-side router with Fluxxor, you need to find a way to get your `Flux` instance into the instantiated component. The [React Router project](https://github.com/rackt/react-router) makes this a cinch. 10 | 11 | First, define your routes: 12 | 13 | ```javascript 14 | var routes = ( 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | ``` 28 | 29 | For each component you list as a route handler (`App`, `Inbox`, `Message`, `InboxStats`, `Calendar`, and `Dashboard` in this example), be sure to mix in [Fluxxor's `FluxMixin`](/documentation/flux-mixin.html). 30 | 31 | Finally, run your router, and when you instantiate your component, pass in `flux` as a property. Since each defined route handler uses `FluxMixin`, the flux object will be propagated throughout your application. 32 | 33 | ```javascript 34 | Router.run(routes, function(Handler) { 35 | React.render(, document.body); 36 | }); 37 | ``` 38 | 39 | Be sure to check out [the React Router example](/examples/react-router.html) for a working demonstration of this technique. 40 | -------------------------------------------------------------------------------- /site/contents/images/flux-complex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/images/flux-complex.png -------------------------------------------------------------------------------- /site/contents/images/flux-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/images/flux-diagram.png -------------------------------------------------------------------------------- /site/contents/images/flux-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/images/flux-simple.png -------------------------------------------------------------------------------- /site/contents/images/mvc-complex.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/images/mvc-complex.png -------------------------------------------------------------------------------- /site/contents/images/mvc-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/images/mvc-diagram.png -------------------------------------------------------------------------------- /site/contents/images/mvc-simple.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryMuse/fluxxor/d11e52a4e6c99909e9584a8cd84b0a709ffb3d22/site/contents/images/mvc-simple.png -------------------------------------------------------------------------------- /site/contents/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Home 3 | template: index.ejs 4 | --- 5 | 6 |
    7 | Fluxxor 8 |
    9 | 10 |
    11 |

    Fluxxor is a set of tools to facilitate building JavaScript data layers using the Flux architecture by reifying many of the core Flux concepts. It works particularly well in conjunction with React as the view layer, and contains a few helpers to make integration with React applications easier.

    12 | 13 |

    The Flux architecture...

    14 | 15 |
      16 |
    • ...makes it easier to reason about changes to your application's data
    • 17 |
    • ...eschews complex MVC hierarchies in favor of a one-way data flow
    • 18 |
    • ...helps improve data consistency
    • 19 |
    • ...prevents hard-to-debug cascading updates
    • 20 |
    • ...works great with React and complements its reactive data flow
    • 21 |
    22 | 23 |

    **Want to learn more?** Start by checking out What is Flux.

    24 | 25 |

    To get started, npm install fluxxor or see other ways to install.

    26 | 27 |
    28 | 30 | 31 |
    32 | 33 |
    34 |
    35 |
    36 | 37 |
    38 |
    39 | Learn More about Flux 40 | 41 |
    42 | 43 |
    44 | Get Up and Running 45 | 46 |
    47 | 48 |
    49 | See some Examples 50 | 51 |
    52 | 53 |
    54 | Dive Into the API 55 | 56 |
    57 |
    58 | -------------------------------------------------------------------------------- /site/contents/style/main.less: -------------------------------------------------------------------------------- 1 | @import url(http://fonts.googleapis.com/css?family=Lato:300,400,700); 2 | 3 | body { 4 | font: 15px/1.5 "Helvetica Neue", Helvetica, Arial, sans-serif; 5 | color: #000; 6 | font-weight: 300; 7 | padding: 10px; 8 | } 9 | 10 | h1, h2, h3, h4, h5, h6 { 11 | color: #000; 12 | } 13 | 14 | h1 { 15 | font-size: 30px; 16 | padding-bottom: 5px; 17 | border-bottom: 1px solid #ccc; 18 | } 19 | 20 | h1, h2, h3 { 21 | line-height: 1.1; 22 | } 23 | 24 | h1, h2, h3, h4, h5, h6 { 25 | position: relative; 26 | 27 | .anchor-symbol { 28 | visibility: hidden; 29 | margin-left: 10px; 30 | color: #999; 31 | } 32 | 33 | &:hover { 34 | .anchor-symbol { 35 | visibility: visible; 36 | } 37 | } 38 | } 39 | 40 | a { 41 | color: #39c; 42 | text-decoration: none; 43 | font-weight: 400; 44 | } 45 | 46 | a:visited { 47 | color: #7D33CC; 48 | } 49 | 50 | strong a { 51 | font-weight: bold; 52 | } 53 | 54 | .logo-container { 55 | text-align: right; 56 | float: left; 57 | padding-right: 20px; 58 | width: 350px; 59 | 60 | .logo-svg { 61 | margin-top: 3px; 62 | height: 420px; 63 | } 64 | } 65 | 66 | .logo-container-sidebar { 67 | padding-left: 15px; 68 | 69 | .logo-svg { 70 | max-height: 150px; 71 | } 72 | } 73 | 74 | .homepage-callout { 75 | float: left; 76 | width: 520px; 77 | padding-top: 0; 78 | } 79 | 80 | code.install { 81 | border: 1px solid #ccc; 82 | background-color: #eee; 83 | padding: 5px; 84 | border-radius: 5px; 85 | } 86 | 87 | .wrapper { 88 | width: 910px; 89 | margin: 0 auto; 90 | } 91 | 92 | figure { 93 | padding: 0; 94 | margin: 0; 95 | 96 | figcaption { 97 | font-style: italic; 98 | margin-left: 20px; 99 | font-size: 90%; 100 | } 101 | } 102 | 103 | header { 104 | width: 225px; 105 | float: left; 106 | position: fixed; 107 | overflow-y: auto; 108 | height: 100%; 109 | padding-bottom: 50px; 110 | 111 | @media screen and (max-height: 800px) { 112 | position: static; 113 | } 114 | 115 | ul { 116 | padding-left: 20px; 117 | } 118 | } 119 | 120 | .tweet-button-container { 121 | display: inline-block; 122 | 123 | iframe { 124 | margin-bottom: 2px; 125 | } 126 | } 127 | 128 | section { 129 | width: 660px; 130 | float: right; 131 | padding-bottom: 50px; 132 | 133 | > h1:first-child, > h2:first-child, > h3:first-child { 134 | padding-top: 0; 135 | margin-top: 0; 136 | } 137 | 138 | img { 139 | max-width: 100%; 140 | } 141 | } 142 | 143 | .bottom-button-container { 144 | padding-top: 50px; 145 | } 146 | 147 | .index-button-container { 148 | width: 222px; 149 | display: inline-block; 150 | text-align: center; 151 | 152 | .cta { 153 | font-weight: 700; 154 | } 155 | 156 | a { 157 | display: inline-block; 158 | width: 150px; 159 | padding: 10px; 160 | margin-top: 5px; 161 | background-color: #ccc; 162 | border-radius: 4px; 163 | color: black; 164 | font-weight: 700; 165 | color: white; 166 | } 167 | } 168 | 169 | .colored-button(@color) { 170 | background-color: @color; 171 | border: 2px solid darken(@color, 15%); 172 | 173 | &:hover { 174 | background-color: darken(@color, 5%); 175 | } 176 | } 177 | 178 | .index-button-container.teal a { 179 | .colored-button(#009A93); 180 | } 181 | 182 | .index-button-container.orange a { 183 | .colored-button(#E96633); 184 | } 185 | 186 | .index-button-container.red a { 187 | .colored-button(#D95C5C); 188 | } 189 | 190 | .index-button-container.purple a { 191 | .colored-button(#3E3773); 192 | } 193 | 194 | .hljs { 195 | background: transparent; 196 | font-size: 14px; 197 | 198 | .xml { 199 | opacity: 1; 200 | } 201 | } 202 | 203 | .show-hide-nav { 204 | display: none; 205 | } 206 | 207 | pre { 208 | overflow-x: auto; 209 | } 210 | 211 | .footer-info { 212 | display: none; 213 | } 214 | 215 | @media screen and (max-width: 1060px) { 216 | .github-banner { 217 | display: none; 218 | } 219 | } 220 | 221 | @media screen and (max-width: 940px) { 222 | .wrapper { 223 | width: auto; 224 | } 225 | 226 | .logo-container, .logo-container-sidebar { 227 | width: auto; 228 | float: none; 229 | text-align: center; 230 | padding-right: 0; 231 | } 232 | 233 | .logo-container .logo-svg { 234 | max-width: 100%; 235 | } 236 | 237 | .logo-container-sidebar { 238 | text-align: left; 239 | } 240 | 241 | .homepage-callout { 242 | width: auto; 243 | padding-top: 0; 244 | } 245 | 246 | header { 247 | float: none; 248 | display: block; 249 | position: relative; 250 | width: auto; 251 | padding-bottom: 20px; 252 | 253 | .nav-list { 254 | display: none; 255 | } 256 | 257 | .show-hide-nav { 258 | display: block; 259 | padding-left: 15px; 260 | 261 | a { 262 | display: block; 263 | } 264 | 265 | a.hide-nav { 266 | display: none; 267 | } 268 | } 269 | } 270 | 271 | section { 272 | width: auto; 273 | float: none; 274 | padding-bottom: 0; 275 | } 276 | 277 | .index-button-container { 278 | width: auto; 279 | display: block; 280 | padding-bottom: 20px; 281 | } 282 | 283 | .github-banner { 284 | display: none; 285 | } 286 | 287 | .sidebar-info { 288 | display: none; 289 | } 290 | 291 | .footer-info { 292 | display: block; 293 | } 294 | } 295 | 296 | #react-router-example-section { 297 | margin-top: 25px; 298 | margin-bottom: 50px; 299 | padding-left: 10px; 300 | border-left: 10px solid #ccc; 301 | 302 | #app h1 { 303 | padding-top: 0; 304 | margin-top: 0; 305 | } 306 | } 307 | -------------------------------------------------------------------------------- /site/contents/what-is-flux.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: What is Flux? 3 | template: page.ejs 4 | --- 5 | 6 | What is Flux? 7 | ============= 8 | 9 | Flux is an architecture for creating data layers in JavaScript applications. It was designed at Facebook along with the [React](http://facebook.github.io/react/) view library. It places a focus on creating **explicit and understandable update paths** for your application's data, which makes **tracing changes during development simpler** and makes **bugs easier to track down and fix**. 10 | 11 | Fluxxor is an implementation of the flux architecture pattern. 12 | 13 | Overview 14 | -------- 15 | 16 | To best describe flux, we will compare it to one of the leading client-side architectures: MVC. In a client-side MVC application, a user interaction triggers code in a controller. The controller knows how to coordinate changes to one or more models by calling methods on the models. When the models change, they notify one or more views, which in turn read the new data from the models and update themselves accordingly so that the user can see that new data. 17 | 18 |
    19 | ![Simple MVC](/images/mvc-simple.png?1) 20 |
    A simple MVC flow
    21 |
    22 | 23 | As an MVC application grows and controllers, models, and views are added, the dependencies become more complex. 24 | 25 |
    26 | ![Complex MVC](/images/mvc-complex.png?1) 27 |
    A more complex MVC flow
    28 |
    29 | 30 | With the addition of just three views, one controller, and one model, the dependency graph is already harder to trace. When the user interacts with the UI, multiple branching code paths are executed, and debugging problems in application state becomes an exercise in figuring out which module (or modules) in one (or more) of these potential code paths contains a bug. In the worst cases, a user interaction will trigger updates which in turn trigger additional updates, resulting in error-prone and difficult-to-debug cascading effects along several of these, sometimes overlapping, paths. 31 | 32 | Flux eschews this design in favor of a one-way data flow. All user interactions within a view call an *action creator*, which causes an *action* event to be emitted from a singleton *dispatcher*. The dispatcher is a single-point-of-emission for all actions in a flux application. The action is sent from the dispatcher to *stores*, which update themselves in response to the action. 33 | 34 |
    35 | ![Simple Flux](/images/flux-simple.png?1) 36 |
    A simple flux flow
    37 |
    38 | 39 | The flow doesn't change significantly for additional stores or views. The dispatcher simply sends every action to *all* the stores in the application. Note that it does not contain knowledge about how to actually update the stores—the stores themselves contain this business logic. Each store is responsible for a domain of the application, and only update themselves in response to actions. 40 | 41 |
    42 | ![Complex Flux](/images/flux-complex.png?1) 43 |
    A more complex flux flow
    44 |
    45 | 46 | When a store updates, it emits a change event. In many React applications, special views (known sometimes as "controller-views") are responsible for watching for this change event, reading the stores' new data, and passing that data through properties to child views. It's not uncommon in a React application for a store change event to trigger a re-render of the top-level view, effectively re-rendering the entire view hierarchy (which React handles in an efficient manner). This completely avoids complex bugs and performance problems that can arise out of trying to watch for specific property changes on models and modifying parts of views only slightly. 47 | 48 | Key Properties 49 | -------------- 50 | 51 | The flux architecture has a few key properties that make it unique and provide important guarantees, all of which are centered around making the flow of data explicit and easily understandable and increasing the locality of bugs (so that you don't have to hunt down many code paths to find incorrect logic). There are many flux and flux-like implementations available; these properties are, to me, most important to the flux architecture. 52 | 53 | ### Enforced Synchrony 54 | 55 | Action dispatches and their handlers inside the stores are synchronous. All asynchronous operations should trigger an action dispatch that tells the system about the result of the operation (see the [async data guide](/guides/async-data.html) for more details). While action creators can call out to asynchronous APIs, action handlers in the stores will ideally not do so. This rule makes the flow of information in the application extremely explicit; debugging errors in application state simply involves figuring out which action caused the bad state and then finding the incorrect logic that responded to that action. 56 | 57 | ### Inversion of Control 58 | 59 | Since stores update themselves internally in response to actions (rather than being updated from outside by a controller or a similar module), no other piece of the system needs to know how to modify the application's state. All the logic for updating the state is contained within the store itself. And, since stores only ever update in response to actions, and only synchronously, testing stores simply becomes a matter of putting them in an initial state, sending them an action, and testing to see if they're in the correct final state. 60 | 61 | ### Semantic Actions 62 | 63 | Since the stores need to update themselves in response to actions, the actions tend to be semantically descriptive. For example, in a flux forum application, to mark a thread as read, you might dispatch an action with a type of `MARK_THREAD_READ`. The action (and the component generating the action) doesn't know *how* to perform the update, but *describes* what it wants to happen. 64 | 65 | Because of this property, you rarely have to change your action types, only how the stores respond to them. As long as your application has a concept of a "thread" and you have a button or other interaction that should mark a thread as read, the `MARK_THREAD_READ` action type is semantically valid. 66 | 67 | ### No Cascading Actions 68 | 69 | Flux disallows dispatching a second action as a result of dispatching an action. This helps prevent hard-to-debug cascading updates and helps you think about interactions in your application in terms of semantic actions. 70 | 71 | Further Reading 72 | --------------- 73 | 74 | For a more in-depth look at the Flux architecture, check out [Flux Application Architecture](http://facebook.github.io/flux/docs/overview.html) on the official Flux site, and be sure to check out [Rethinking Web App Development at Facebook](https://www.youtube.com/watch?v=nYkdrAPrdcw) from F8 on YouTube to hear Jing Chen talk more about Flux. 75 | 76 | Ready to Try it Out? 77 | -------------------- 78 | 79 | Check out the [installation](/guides/installation.html) and [getting started](/guides/quick-start.html) guides to get up and running with Fluxxor! 80 | -------------------------------------------------------------------------------- /site/templates/index.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | <%= locals.title %> - <%= page.title %> 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Fork me on GitHub 32 | 33 |
    34 | <%- page.html %> 35 |
    36 | 37 | 48 | 49 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /site/templates/page.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | <%= locals.title %> - <%= page.title %> 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | Fork me on GitHub 33 | 34 |
    35 |
    36 |
    37 | Fluxxor 38 |
    39 | 43 | 79 | 84 |
    85 |
    86 | <%- page.html %> 87 |
    88 | See a typo? Something still not clear? 89 | Report an issue on GitHub. 90 |
    91 | 92 | 98 |
    99 | 100 | 115 | 116 | 127 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /test/unit/test_dispatch_interceptor.js: -------------------------------------------------------------------------------- 1 | var Fluxxor = require("../../"), 2 | jsdom = require("jsdom"); 3 | 4 | var chai = require("chai"), 5 | expect = chai.expect; 6 | 7 | var Store = Fluxxor.createStore({ 8 | actions: { 9 | "ACTIVATE": "handleActivate", 10 | "LOAD_INITIAL_VALUE": "handleLoadInitialValue" 11 | }, 12 | 13 | initialize: function() { 14 | this.activated = false; 15 | this.value = null; 16 | }, 17 | 18 | handleActivate: function() { 19 | this.activated = true; 20 | this.emit("change"); 21 | }, 22 | 23 | handleLoadInitialValue: function() { 24 | this.value = "testing"; 25 | this.emit("change"); 26 | } 27 | }); 28 | 29 | var actions = { 30 | activate: function(callback) { 31 | setTimeout(function() { 32 | try { 33 | this.dispatch("ACTIVATE"); 34 | callback(); 35 | } catch (ex) { 36 | if (ex instanceof chai.AssertionError) { 37 | throw ex; 38 | } else { 39 | callback(ex); 40 | } 41 | } 42 | }.bind(this)); 43 | }, 44 | 45 | loadInitialValue: function() { 46 | this.dispatch("LOAD_INITIAL_VALUE"); 47 | } 48 | }; 49 | 50 | describe("Dispatch interceptor", function() { 51 | var React, TestUtils; 52 | var flux, App, ComponentA, ComponentB; 53 | 54 | beforeEach(function() { 55 | var doc = jsdom.jsdom(""); 56 | global.window = doc.defaultView; 57 | global.document = window.document; 58 | global.navigator = window.navigator; 59 | React = require("react/addons"); 60 | TestUtils = React.addons.TestUtils; 61 | 62 | flux = new Fluxxor.Flux({store: new Store()}, actions); 63 | 64 | App = React.createFactory(React.createClass({ 65 | displayName: "App", 66 | mixins: [Fluxxor.FluxMixin(React), Fluxxor.StoreWatchMixin("store")], 67 | 68 | getStateFromFlux: function() { 69 | return { 70 | activated: this.getFlux().store("store").activated 71 | }; 72 | }, 73 | 74 | render: function() { 75 | return React.DOM.div({}, this.renderChild()); 76 | }, 77 | 78 | renderChild: function() { 79 | if (!this.state.activated) { 80 | return ComponentA(); 81 | } else { 82 | return ComponentB(); 83 | } 84 | } 85 | })); 86 | 87 | ComponentA = React.createFactory(React.createClass({ 88 | displayName: "ComponentA", 89 | mixins: [ 90 | Fluxxor.FluxMixin(React) 91 | ], 92 | 93 | render: function() { 94 | return React.DOM.div(); 95 | } 96 | })); 97 | 98 | ComponentB = React.createFactory(React.createClass({ 99 | displayName: "ComponentB", 100 | mixins: [ 101 | Fluxxor.FluxMixin(React), 102 | Fluxxor.StoreWatchMixin("store") 103 | ], 104 | 105 | getStateFromFlux: function() { 106 | return { 107 | value: this.getFlux().store("store").value 108 | }; 109 | }, 110 | 111 | componentWillMount: function() { 112 | this.getFlux().actions.loadInitialValue(); 113 | }, 114 | 115 | render: function() { 116 | return React.DOM.div(); 117 | }, 118 | })); 119 | }); 120 | 121 | afterEach(function() { 122 | delete global.window; 123 | delete global.document; 124 | delete global.navigator; 125 | for (var i in require.cache) { 126 | if (require.cache.hasOwnProperty(i)) { 127 | delete require.cache[i]; // ugh react why 128 | } 129 | } 130 | }); 131 | 132 | it("doesn't intercept by default", function(done) { 133 | /* jshint expr:true */ 134 | TestUtils.renderIntoDocument(App({flux: flux})); 135 | flux.actions.activate(function(err) { 136 | expect(err).to.match(/dispatch.*another action/); 137 | done(); 138 | }); 139 | }); 140 | 141 | it("allows intercepting", function(done) { 142 | /* jshint expr:true */ 143 | flux.setDispatchInterceptor(function(action, dispatch) { 144 | React.addons.batchedUpdates(function() { 145 | dispatch(action); 146 | }); 147 | }); 148 | 149 | TestUtils.renderIntoDocument(App({flux: flux})); 150 | flux.actions.activate(function(err) { 151 | expect(err).to.be.undefined; 152 | done(); 153 | }); 154 | }); 155 | 156 | it("allows nested interceptors", function(done) { 157 | var dispatches = 0; 158 | /* jshint expr:true */ 159 | flux.setDispatchInterceptor(function(action, dispatch) { 160 | dispatches++; 161 | React.addons.batchedUpdates(function() { 162 | dispatch(action); 163 | }); 164 | }); 165 | 166 | TestUtils.renderIntoDocument(App({flux: flux})); 167 | flux.actions.activate(function(err) { 168 | expect(err).to.be.undefined; 169 | expect(dispatches).to.eql(2); 170 | done(); 171 | }); 172 | }); 173 | 174 | it("allows completely custom interceptors", function(done) { 175 | var dispatches = 0; 176 | /* jshint expr:true */ 177 | flux.setDispatchInterceptor(function() { 178 | dispatches++; 179 | }); 180 | 181 | TestUtils.renderIntoDocument(App({flux: flux})); 182 | flux.actions.activate(function(err) { 183 | expect(err).to.be.defined; 184 | expect(dispatches).to.eql(1); 185 | done(); 186 | }); 187 | }); 188 | }); 189 | -------------------------------------------------------------------------------- /test/unit/test_dispatcher.js: -------------------------------------------------------------------------------- 1 | var Fluxxor = require("../../"); 2 | 3 | var chai = require("chai"), 4 | expect = chai.expect, 5 | sinon = require("sinon"), 6 | sinonChai = require("sinon-chai"); 7 | 8 | chai.use(sinonChai); 9 | 10 | describe("Dispatcher", function() { 11 | var store1, store2, dispatcher; 12 | 13 | beforeEach(function() { 14 | var handleActionStub = sinon.stub(); 15 | handleActionStub.returns(true); 16 | 17 | store1 = { __handleAction__: handleActionStub }; 18 | store2 = { __handleAction__: sinon.spy() }; 19 | dispatcher = new Fluxxor.Dispatcher({Store1: store1, Store2: store2}); 20 | }); 21 | 22 | it("dispatches actions to every store", function() { 23 | var action = {type: "ACTION", payload: {val: 123}}; 24 | dispatcher.dispatch(action); 25 | expect(store1.__handleAction__).to.have.been.calledWith(action); 26 | expect(store2.__handleAction__).to.have.been.calledWith(action); 27 | }); 28 | 29 | it("does not allow cascading dispatches", function(done) { 30 | store1.__handleAction__ = function() { 31 | expect(function() { 32 | dispatcher.dispatch({type:"action2"}); 33 | }).to.throw(/action2.*another action.*action1/); 34 | done(); 35 | return true; 36 | }; 37 | dispatcher.dispatch({type:"action1"}); 38 | }); 39 | 40 | it("allows back-to-back dispatches on the same tick", function() { 41 | dispatcher.dispatch({type:"action"}); 42 | expect(function() { 43 | dispatcher.dispatch({type:"action"}); 44 | }).not.to.throw(); 45 | }); 46 | 47 | it("gracefully handles exceptions in the action handlers", function() { 48 | var thrw = true; 49 | store1.__handleAction__ = function() { 50 | if (thrw) { 51 | throw new Error("omg"); 52 | } 53 | return true; 54 | }; 55 | 56 | expect(function() { 57 | dispatcher.dispatch({type:"action"}); 58 | }).to.throw("omg"); 59 | 60 | expect(function() { 61 | thrw = false; 62 | dispatcher.dispatch({type:"action"}); 63 | }).not.to.throw(); 64 | }); 65 | 66 | it("throws when asked to dispatch an action with to 'type' property", function() { 67 | expect(function() { 68 | dispatcher.dispatch(); 69 | }).to.throw(/dispatch.*type/); 70 | 71 | expect(function() { 72 | dispatcher.dispatch(false); 73 | }).to.throw(/dispatch.*type/); 74 | 75 | expect(function() { 76 | dispatcher.dispatch(""); 77 | }).to.throw(/dispatch.*type/); 78 | 79 | expect(function() { 80 | dispatcher.dispatch(null); 81 | }).to.throw(/dispatch.*type/); 82 | 83 | expect(function() { 84 | dispatcher.dispatch({}); 85 | }).to.throw(/dispatch.*type/); 86 | }); 87 | 88 | it("allows stores to wait on other stores", function() { 89 | var callCount = 0; 90 | var Store1 = Fluxxor.createStore({ 91 | actions: { "ACTION": "handleAction" }, 92 | handleAction: function() { 93 | this.waitFor(["Store2"], function() { 94 | this.value = ++callCount; 95 | }); 96 | } 97 | }); 98 | var Store2 = Fluxxor.createStore({ 99 | actions: { "ACTION": "handleAction" }, 100 | handleAction: function() { 101 | this.value = ++callCount; 102 | } 103 | }); 104 | store1 = new Store1(); 105 | store2 = new Store2(); 106 | dispatcher = new Fluxxor.Dispatcher({Store1: store1, Store2: store2}); 107 | dispatcher.dispatch({type: "ACTION"}); 108 | expect(store1.value).to.equal(2); 109 | expect(store2.value).to.equal(1); 110 | }); 111 | 112 | it("does not allow stores to wait unless an action is being dispatched", function() { 113 | expect(function() { 114 | dispatcher.waitForStores(); 115 | }).to.throw(/unless.*action.*dispatch/); 116 | }); 117 | 118 | it("does not allow a store to wait on itself", function() { 119 | var Store = Fluxxor.createStore({ 120 | actions: { "ACTION": "handleAction" }, 121 | handleAction: function() { 122 | this.waitFor(["Store"], function() { 123 | }); 124 | } 125 | }); 126 | var store = new Store(); 127 | dispatcher = new Fluxxor.Dispatcher({Store: store}); 128 | expect(function() { 129 | dispatcher.dispatch({type: "ACTION"}); 130 | }).to.throw(/wait.*itself/); 131 | }); 132 | 133 | it("does not allow a store to wait more than once in the same loop", function() { 134 | var Store1 = Fluxxor.createStore({ 135 | actions: { "ACTION": "handleAction" }, 136 | handleAction: function() { 137 | this.waitFor(["Store2"], sinon.spy()); 138 | this.waitFor(["Store2"], sinon.spy()); 139 | } 140 | }); 141 | var Store2 = Fluxxor.createStore({}); 142 | store1 = new Store1(); 143 | store2 = new Store2(); 144 | dispatcher = new Fluxxor.Dispatcher({Store1: store1, Store2: store2}); 145 | expect(function() { 146 | dispatcher.dispatch({type: "ACTION"}); 147 | }).to.throw(/already.*waiting/); 148 | }); 149 | 150 | it("allows a store to wait on a store more than once in different loops", function() { 151 | var Store1 = Fluxxor.createStore({ 152 | actions: { "ACTION": "handleAction" }, 153 | handleAction: function() { 154 | this.waitFor(["Store2"], function() { 155 | this.waitFor(["Store2"], function(store2) { 156 | this.value = store2.value; 157 | }); 158 | }); 159 | } 160 | }); 161 | var Store2 = Fluxxor.createStore({ 162 | actions: { "ACTION": "handleAction" }, 163 | handleAction: function() { 164 | this.value = 42; 165 | } 166 | }); 167 | store1 = new Store1(); 168 | store2 = new Store2(); 169 | dispatcher = new Fluxxor.Dispatcher({Store1: store1, Store2: store2}); 170 | dispatcher.dispatch({type: "ACTION"}); 171 | expect(store1.value).to.equal(42); 172 | }); 173 | 174 | it("does not allow waiting on non-existant stores", function() { 175 | var Store = Fluxxor.createStore({ 176 | actions: { "ACTION": "handleAction" }, 177 | handleAction: function() { 178 | this.waitFor(["StoreFake"], sinon.spy()); 179 | } 180 | }); 181 | var store = new Store(); 182 | dispatcher = new Fluxxor.Dispatcher({Store: store}); 183 | expect(function() { 184 | dispatcher.dispatch({type: "ACTION"}); 185 | }).to.throw(/wait.*StoreFake/); 186 | }); 187 | 188 | it("detects direct circular dependencies between stores", function() { 189 | var Store1 = Fluxxor.createStore({ 190 | actions: { "ACTION": "handleAction" }, 191 | handleAction: function() { 192 | this.waitFor(["Store2"], sinon.spy()); 193 | } 194 | }); 195 | var Store2 = Fluxxor.createStore({ 196 | actions: { "ACTION": "handleAction" }, 197 | handleAction: function() { 198 | this.waitFor(["Store1"], sinon.spy()); 199 | } 200 | }); 201 | store1 = new Store1(); 202 | store2 = new Store2(); 203 | dispatcher = new Fluxxor.Dispatcher({Store1: store1, Store2: store2}); 204 | expect(function() { 205 | dispatcher.dispatch({type: "ACTION"}); 206 | }).to.throw(/circular.*Store2.*Store1/i); 207 | }); 208 | 209 | it("detects indirect circular dependencies between stores", function() { 210 | var Store1 = Fluxxor.createStore({ 211 | actions: { "ACTION": "handleAction" }, 212 | handleAction: function() { 213 | this.waitFor(["Store2"], sinon.spy()); 214 | } 215 | }); 216 | var Store2 = Fluxxor.createStore({ 217 | actions: { "ACTION": "handleAction" }, 218 | handleAction: function() { 219 | this.waitFor(["Store3"], sinon.spy()); 220 | } 221 | }); 222 | var Store3 = Fluxxor.createStore({ 223 | actions: { "ACTION": "handleAction" }, 224 | handleAction: function() { 225 | this.waitFor(["Store1"], sinon.spy()); 226 | } 227 | }); 228 | store1 = new Store1(); 229 | store2 = new Store2(); 230 | var store3 = new Store3(); 231 | dispatcher = new Fluxxor.Dispatcher({Store1: store1, Store2: store2, Store3: store3}); 232 | expect(function() { 233 | dispatcher.dispatch({type: "ACTION"}); 234 | }).to.throw(/circular.*Store1.*Store2.*Store3/i); 235 | }); 236 | 237 | describe("unhandled dispatch warnings", function() { 238 | var warnSpy; 239 | 240 | beforeEach(function() { 241 | warnSpy = sinon.stub(console, "warn"); 242 | }); 243 | 244 | afterEach(function() { 245 | warnSpy.restore(); 246 | }); 247 | 248 | it("warns if a dispatched action is not handled by any store", function() { 249 | /* jshint -W030 */ 250 | var Store1 = Fluxxor.createStore({}); 251 | var Store2 = Fluxxor.createStore({}); 252 | 253 | store1 = new Store1(); 254 | store2 = new Store2(); 255 | dispatcher = new Fluxxor.Dispatcher({Store1: store1, Store2: store2}); 256 | dispatcher.dispatch({type: "ACTION_TYPE"}); 257 | 258 | expect(warnSpy).to.have.been.calledOnce; 259 | expect(warnSpy).to.have.been.calledWithMatch(/ACTION_TYPE.*no store/); 260 | }); 261 | 262 | it("doesn't warn if a dispatched action is handled by any store", function() { 263 | /* jshint -W030 */ 264 | var Store1 = Fluxxor.createStore({ 265 | actions: { "ACTION_TYPE": "handleAction" }, 266 | handleAction: function() {} 267 | }); 268 | var Store2 = Fluxxor.createStore({}); 269 | 270 | store1 = new Store1(); 271 | store2 = new Store2(); 272 | dispatcher = new Fluxxor.Dispatcher({Store1: store1, Store2: store2}); 273 | dispatcher.dispatch({type: "ACTION_TYPE"}); 274 | 275 | expect(warnSpy).to.have.not.been.called; 276 | }); 277 | }); 278 | }); 279 | -------------------------------------------------------------------------------- /test/unit/test_flux_mixin.js: -------------------------------------------------------------------------------- 1 | var Fluxxor = require("../../"), 2 | jsdom = require("jsdom"); 3 | 4 | var chai = require("chai"), 5 | expect = chai.expect; 6 | 7 | function createComponent(React, FluxMixin, FluxChildMixin) { 8 | var Parent = React.createClass({ 9 | displayName: "Parent", 10 | mixins: [FluxMixin], 11 | 12 | render: function() { 13 | return React.createElement(Child); 14 | } 15 | }); 16 | 17 | var Child = React.createClass({ 18 | displayName: "Child", 19 | render: function() { 20 | return React.createElement(Grandchild); 21 | } 22 | }); 23 | 24 | var Grandchild = React.createClass({ 25 | displayName: "Grandchild", 26 | mixins: [FluxChildMixin], 27 | 28 | render: function() { 29 | return React.createElement(GreatGrandchild); 30 | } 31 | }); 32 | 33 | var GreatGrandchild = React.createClass({ 34 | displayName: "GreatGrandchild", 35 | mixins: [FluxMixin], 36 | 37 | render: function() { 38 | return React.DOM.div(); 39 | } 40 | }); 41 | 42 | return { 43 | Parent: Parent, 44 | Child: Child, 45 | Grandchild: Grandchild, 46 | GreatGrandchild: GreatGrandchild 47 | }; 48 | } 49 | 50 | describe("FluxMixin", function() { 51 | var React, TestUtils, FluxMixin, FluxChildMixin; 52 | var Parent, Child, Grandchild, GreatGrandchild, flux, objs; 53 | 54 | beforeEach(function() { 55 | console._warn = console.warn; 56 | console.warn = function() {}; 57 | 58 | var doc = jsdom.jsdom(''); 59 | global.window = doc.defaultView; 60 | global.document = window.document; 61 | global.navigator = window.navigator; 62 | React = require("react/addons"); 63 | TestUtils = React.addons.TestUtils; 64 | FluxMixin = Fluxxor.FluxMixin(React); 65 | FluxChildMixin = Fluxxor.FluxChildMixin(React); 66 | objs = createComponent(React, FluxMixin, FluxChildMixin); 67 | Parent = objs.Parent; 68 | Child = objs.Child; 69 | Grandchild = objs.Grandchild; 70 | GreatGrandchild = objs.GreatGrandchild; 71 | flux = new Fluxxor.Flux({}, {}); 72 | }); 73 | 74 | afterEach(function() { 75 | delete global.window; 76 | delete global.document; 77 | delete global.navigator; 78 | console.warn = console._warn; 79 | }); 80 | 81 | it("passes flux via getFlux() to descendants who ask for it", function() { 82 | /* jshint expr:true */ 83 | var tree = TestUtils.renderIntoDocument(React.createElement(Parent, {flux: flux})); 84 | expect(tree.getFlux()).to.equal(flux); 85 | var child = TestUtils.findRenderedComponentWithType(tree, Child); 86 | expect(child.getFlux).to.be.undefined; 87 | var grandchild = TestUtils.findRenderedComponentWithType(tree, Grandchild); 88 | expect(grandchild.getFlux()).to.equal(flux); 89 | var greatGrandchild = TestUtils.findRenderedComponentWithType(tree, GreatGrandchild); 90 | expect(greatGrandchild.getFlux()).to.equal(flux); 91 | }); 92 | 93 | it("throws when it can't find flux on the props or context", function() { 94 | var Comp = React.createFactory(React.createClass({ 95 | displayName: "TestComponent", 96 | mixins: [Fluxxor.FluxMixin(React)], 97 | render: function() { return React.DOM.div(); } 98 | })); 99 | expect(function() { 100 | React.renderToString(Comp()); 101 | }).to.throw(/Could not find flux.*TestComponent/); 102 | }); 103 | 104 | it("throws when attempting to mix in the function directly", function() { 105 | expect(function() { 106 | React.createClass({ 107 | mixins: [Fluxxor.FluxMixin], 108 | render: function() { return React.DOM.div(); } 109 | }); 110 | }).to.throw(/attempting to use a component class as a mixin/); 111 | }); 112 | 113 | it("gives a deprecation warning when using FluxChildMixin", function() { 114 | var warned = false; 115 | console.warn = function(text) { 116 | if (text.match(/FluxChildMixin.*CompName.*deprecated/)) { 117 | warned = true; 118 | } else { 119 | console._warn(text); 120 | } 121 | }; 122 | 123 | var Comp = React.createFactory(React.createClass({ 124 | mixins: [Fluxxor.FluxChildMixin(React)], 125 | displayName: "CompName", 126 | render: function() { return React.DOM.div(); } 127 | })); 128 | React.renderToString(Comp()); 129 | expect(warned).to.equal(true); 130 | }); 131 | 132 | it("throws when attempting to mix in the child function directly", function() { 133 | expect(function() { 134 | React.createClass({ 135 | mixins: [Fluxxor.FluxChildMixin], 136 | render: function() { return React.DOM.div(); } 137 | }); 138 | }).to.throw(/attempting to use a component class as a mixin/); 139 | }); 140 | }); 141 | -------------------------------------------------------------------------------- /test/unit/test_store.js: -------------------------------------------------------------------------------- 1 | var Fluxxor = require("../../"); 2 | 3 | var chai = require("chai"), 4 | expect = chai.expect, 5 | sinon = require("sinon"), 6 | sinonChai = require("sinon-chai"); 7 | 8 | chai.use(sinonChai); 9 | 10 | describe("Store", function() { 11 | it("passes one object from constructor to initialize", function(done) { 12 | /* jshint expr:true */ 13 | var Store = Fluxxor.createStore({ 14 | initialize: function(opt, nothing) { 15 | expect(opt).to.equal(42); 16 | expect(nothing).to.be.undefined; 17 | done(); 18 | } 19 | }); 20 | new Store(42, 100); 21 | }); 22 | 23 | it("copies properties from the spec", function() { 24 | var Store = Fluxxor.createStore({ 25 | answer: {is: 42} 26 | }); 27 | var store = new Store(); 28 | expect(store.answer).to.eql({is: 42}); 29 | }); 30 | 31 | it("disallows reserved property names", function() { 32 | expect(function() { 33 | Fluxxor.createStore({ 34 | flux: true 35 | }); 36 | }).to.throw(/reserved.*flux/i); 37 | 38 | expect(function() { 39 | Fluxxor.createStore({ 40 | waitFor: true 41 | }); 42 | }).to.throw(/reserved.*waitFor/i); 43 | }); 44 | 45 | it("allows registering actions via an actions hash", function() { 46 | var Store = Fluxxor.createStore({ 47 | actions: { 48 | "ACTION": "handleAction" 49 | }, 50 | 51 | handleAction: function() {} 52 | }); 53 | var store = new Store(); 54 | store.handleAction = sinon.spy(); 55 | var payload = {val: 42}; 56 | store.__handleAction__({type: "ACTION", payload: payload}); 57 | expect(store.handleAction).to.have.been.calledWith(payload, "ACTION"); 58 | }); 59 | 60 | describe("#bindActions", function() { 61 | it("allows registering actions via an argument list", function() { 62 | // also tests that methods are autobound to the store instance 63 | var Store = Fluxxor.createStore({ 64 | actions: { 65 | "ACTION": "handleAction" 66 | }, 67 | 68 | initialize: function() { 69 | this.bindActions("ACTION2", "handleAction2", 70 | "ACTION3", this.handleAction3); 71 | }, 72 | 73 | handleAction: function() {}, 74 | handleAction2: function() {}, 75 | handleAction3: function() { 76 | this.value = 42; 77 | } 78 | }); 79 | var store = new Store(); 80 | store.handleAction = sinon.spy(); 81 | store.handleAction2 = sinon.spy(); 82 | var payload = {val: 42}; 83 | store.__handleAction__({type: "ACTION", payload: payload}); 84 | expect(store.handleAction).to.have.been.calledWith(payload, "ACTION"); 85 | store.__handleAction__({type: "ACTION2", payload: payload}); 86 | expect(store.handleAction2).to.have.been.calledWith(payload, "ACTION2"); 87 | store.__handleAction__({type: "ACTION3", payload: payload}); 88 | expect(store.value).to.equal(42); 89 | }); 90 | 91 | it("allows registering actions via a hash", function() { 92 | var Store = Fluxxor.createStore({ 93 | actions: { 94 | "ACTION": "handleAction" 95 | }, 96 | 97 | initialize: function() { 98 | this.bindActions({ 99 | "ACTION2": "handleAction2", 100 | "ACTION3": this.handleAction3 101 | }); 102 | }, 103 | 104 | handleAction: function() {}, 105 | handleAction2: function() {}, 106 | handleAction3: function() { 107 | this.value = 42; 108 | } 109 | }); 110 | 111 | var store = new Store(); 112 | store.handleAction = sinon.spy(); 113 | store.handleAction2 = sinon.spy(); 114 | 115 | var payload = {val: 42}; 116 | store.__handleAction__({type: "ACTION", payload: payload}); 117 | expect(store.handleAction).to.have.been.calledWith(payload, "ACTION"); 118 | store.__handleAction__({type: "ACTION2", payload: payload}); 119 | expect(store.handleAction2).to.have.been.calledWith(payload, "ACTION2"); 120 | store.__handleAction__({type: "ACTION3", payload: payload}); 121 | expect(store.value).to.equal(42); 122 | }); 123 | }); 124 | 125 | it("throws when binding to a falsy action type", function() { 126 | var Store = Fluxxor.createStore({ 127 | initialize: function() { 128 | this.bindActions( 129 | "TYPE_ONE", "handleOne", 130 | null, "handleTwo" 131 | ); 132 | } 133 | }); 134 | 135 | expect(function() { 136 | new Store(); 137 | }).to.throw(/Argument 3.*bindActions.*falsy/); 138 | }); 139 | 140 | it("throws when using a non-function action handler", function() { 141 | var Store = Fluxxor.createStore({ 142 | actions: { 143 | "ACTION": "handleAction" 144 | } 145 | }); 146 | 147 | var store = new Store(); 148 | expect(function() { 149 | store.__handleAction__({type: "ACTION"}); 150 | }).to.throw(/handler.*type ACTION.*not.*function/); 151 | 152 | expect(function() { 153 | store.__handleAction__({type: "ACTION2"}); 154 | }).not.to.throw(); 155 | }); 156 | 157 | it("throws when binding an action type to a falsy handler", function() { 158 | var Store = Fluxxor.createStore({ 159 | actions: { 160 | "ACTION": this.handleAction 161 | }, 162 | 163 | handleAction: function() {} 164 | }); 165 | 166 | expect(function() { 167 | new Store(); 168 | }).to.throw(/handler.*type ACTION.*falsy/); 169 | }); 170 | }); 171 | -------------------------------------------------------------------------------- /test/unit/test_store_watch_mixin.js: -------------------------------------------------------------------------------- 1 | var Fluxxor = require("../../"), 2 | StoreWatchMixin = Fluxxor.StoreWatchMixin, 3 | jsdom = require("jsdom"); 4 | 5 | var chai = require("chai"), 6 | expect = chai.expect; 7 | 8 | describe("StoreWatchMixin", function() { 9 | var SwappedComponent, createComponent, React, TestUtils, Comp, FluxMixin, flux; 10 | 11 | beforeEach(function() { 12 | 13 | var doc = jsdom.jsdom(''); 14 | global.window = doc.defaultView; 15 | global.document = window.document; 16 | global.navigator = window.navigator; 17 | for (var i in require.cache) { 18 | if (require.cache.hasOwnProperty(i)) { 19 | delete require.cache[i]; 20 | } 21 | } 22 | React = require("react/addons"); 23 | TestUtils = React.addons.TestUtils; 24 | FluxMixin = Fluxxor.FluxMixin(React); 25 | 26 | SwappedComponent = React.createFactory(React.createClass({ 27 | mixins: [FluxMixin, StoreWatchMixin("Store1")], 28 | 29 | getStateFromFlux: function() { 30 | return { 31 | store1state: this.getFlux().store("Store1").getState(), 32 | }; 33 | }, 34 | 35 | render: function() { 36 | return React.DOM.div(null, [ 37 | React.DOM.span({key: 1}, String(this.state.store1state.value)), 38 | ]); 39 | } 40 | })); 41 | 42 | createComponent = function createComponent(React) { 43 | var Component = React.createFactory(React.createClass({ 44 | mixins: [FluxMixin, StoreWatchMixin("Store1", "Store2")], 45 | 46 | getStateFromFlux: function() { 47 | this.getStateCalls = this.getStateCalls || 0; 48 | this.getStateCalls++; 49 | return { 50 | store1state: this.getFlux().store("Store1").getState(), 51 | store2state: this.getFlux().store("Store2").getState() 52 | }; 53 | }, 54 | 55 | render: function() { 56 | if(this.state.store1state.value === 0) { 57 | return React.DOM.div(null, SwappedComponent()); 58 | } 59 | return React.DOM.div(null, [ 60 | React.DOM.span({key: 1}, String(this.state.store1state.value)), 61 | React.DOM.span({key: 2}, String(this.state.store2state.value)) 62 | ]); 63 | } 64 | })); 65 | 66 | return Component; 67 | }; 68 | 69 | var Store = Fluxxor.createStore({ 70 | actions: { 71 | "ACTION": "handleAction" 72 | }, 73 | 74 | initialize: function() { 75 | this.value = 0; 76 | }, 77 | 78 | handleAction: function() { 79 | this.value++; 80 | this.emit("change"); 81 | }, 82 | 83 | getState: function() { 84 | return { value: this.value }; 85 | } 86 | }); 87 | 88 | var stores = { 89 | Store1: new Store(), 90 | Store2: new Store() 91 | }; 92 | var actions = { 93 | act: function() { 94 | this.dispatch("ACTION", {}); 95 | } 96 | }; 97 | 98 | flux = new Fluxxor.Flux(stores, actions); 99 | 100 | Comp = createComponent(React); 101 | }); 102 | 103 | afterEach(function() { 104 | delete global.window; 105 | delete global.document; 106 | delete global.navigator; 107 | }); 108 | 109 | it("watches for store change events until the component is unmounted", function(done) { 110 | var tree = TestUtils.renderIntoDocument(Comp({flux: flux})); 111 | expect(tree.getStateCalls).to.eql(1); 112 | expect(tree.state).to.eql({store1state: {value: 0}, store2state: {value: 0}}); 113 | flux.actions.act(); 114 | expect(tree.getStateCalls).to.eql(3); 115 | expect(tree.state).to.eql({store1state: {value: 1}, store2state: {value: 1}}); 116 | React.unmountComponentAtNode(tree.getDOMNode().parentNode); 117 | setTimeout(function() { 118 | flux.actions.act(); 119 | expect(tree.getStateCalls).to.eql(3); 120 | expect(tree.state).to.eql({store1state: {value: 1}, store2state: {value: 1}}); 121 | done(); 122 | }); 123 | }); 124 | 125 | it("throws when attempting to mix in the function directly", function() { 126 | expect(function() { 127 | React.createFactory(React.createClass({ 128 | mixins: [Fluxxor.StoreWatchMixin], 129 | render: function() { return React.DOM.div(); } 130 | })); 131 | }).to.throw(/attempting to use a component class as a mixin/); 132 | }); 133 | 134 | }); 135 | -------------------------------------------------------------------------------- /version.js: -------------------------------------------------------------------------------- 1 | module.exports = "1.7.3" -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require("webpack"); 2 | 3 | module.exports = { 4 | cache: true, 5 | entry: "./index.js", 6 | sourceMapFilename: "fluxxor.js.map", 7 | output: { 8 | path: __dirname + "/build", 9 | filename: "fluxxor.js", 10 | library: "Fluxxor", 11 | libraryTarget: "umd" 12 | }, 13 | devtool: "source-map", 14 | module: { 15 | loaders: [ 16 | { test: /\.less$/, loader: "style!css!less" }, 17 | { test: /\.jsx$/, loader: "jsx-loader" }, 18 | { test: /\.json$/, loader: "json" } 19 | ] 20 | } 21 | }; 22 | --------------------------------------------------------------------------------