├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── .travis.yml ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Makefile ├── README.md ├── UPGRADE_GUIDE.md ├── component.js ├── connect.js ├── dist ├── flummox.js └── flummox.min.js ├── docs ├── .gitignore ├── Makefile ├── README.md ├── dist │ └── flummox │ │ ├── css │ │ └── app.min.css │ │ ├── data │ │ ├── allDocs.json │ │ └── docs │ │ │ ├── api │ │ │ ├── actions.json │ │ │ ├── flux.json │ │ │ ├── fluxcomponent.json │ │ │ ├── fluxmixin.json │ │ │ ├── higher-order-component.json │ │ │ ├── index.json │ │ │ └── store.json │ │ │ ├── guides │ │ │ ├── index.json │ │ │ ├── quick-start.json │ │ │ ├── react-integration.json │ │ │ └── why-flux-component-is-better-than-flux-mixin.json │ │ │ └── index.json │ │ ├── docs │ │ ├── api.md │ │ │ └── index.html │ │ ├── api │ │ │ ├── actions.md │ │ │ │ └── index.html │ │ │ ├── actions │ │ │ │ └── index.html │ │ │ ├── flux.md │ │ │ │ └── index.html │ │ │ ├── flux │ │ │ │ └── index.html │ │ │ ├── fluxcomponent.md │ │ │ │ └── index.html │ │ │ ├── fluxcomponent │ │ │ │ └── index.html │ │ │ ├── fluxmixin.md │ │ │ │ └── index.html │ │ │ ├── fluxmixin │ │ │ │ └── index.html │ │ │ ├── higher-order-component.md │ │ │ │ └── index.html │ │ │ ├── higher-order-component │ │ │ │ └── index.html │ │ │ ├── index.html │ │ │ ├── store.md │ │ │ │ └── index.html │ │ │ └── store │ │ │ │ └── index.html │ │ └── guides │ │ │ ├── index.html │ │ │ ├── quick-start.md │ │ │ └── index.html │ │ │ ├── quick-start │ │ │ └── index.html │ │ │ ├── react-integration.md │ │ │ └── index.html │ │ │ ├── react-integration │ │ │ └── index.html │ │ │ ├── why-flux-component-is-better-than-flux-mixin.md │ │ │ └── index.html │ │ │ └── why-flux-component-is-better-than-flux-mixin │ │ │ └── index.html │ │ ├── guides │ │ └── react-integration.md │ │ │ └── index.html │ │ ├── index.html │ │ └── js │ │ └── app.min.js ├── docs │ ├── api │ │ ├── actions.md │ │ ├── flux.md │ │ ├── fluxcomponent.md │ │ ├── fluxmixin.md │ │ ├── higher-order-component.md │ │ ├── index.md │ │ └── store.md │ ├── guides │ │ ├── index.md │ │ ├── quick-start.md │ │ ├── react-integration.md │ │ └── why-flux-component-is-better-than-flux-mixin.md │ └── index.md ├── nodemon.json ├── package.json ├── sass │ ├── _dependencies.scss │ ├── _utils.scss │ ├── _vendor.scss │ ├── app.scss │ ├── dependencies │ │ ├── _breakpoints.scss │ │ ├── _colors.scss │ │ ├── _theme.scss │ │ ├── _typography.scss │ │ └── _z-indices.scss │ ├── utils │ │ └── _vertical-rhythm.scss │ └── vendor │ │ ├── _normalize.scss │ │ └── _prism.scss ├── src │ ├── client │ │ ├── app.js │ │ ├── init.js │ │ └── vendor │ │ │ └── prism.js │ ├── scripts │ │ ├── build-docs.js │ │ └── build-static-site.js │ ├── server │ │ ├── appView.js │ │ ├── index.js │ │ └── webpack.js │ └── shared │ │ ├── Flux.js │ │ ├── actions │ │ └── DocActions.js │ │ ├── components │ │ ├── AppHandler.js │ │ ├── AppNav.js │ │ ├── Container.js │ │ ├── Doc.js │ │ ├── DocHandler.js │ │ ├── HomeHandler.js │ │ ├── Markdown.js │ │ └── View.js │ │ ├── init.js │ │ ├── routes.js │ │ ├── stores │ │ └── DocStore.js │ │ ├── theme.js │ │ └── utils │ │ ├── UrlUtils.js │ │ └── performRouteHandlerStaticMethod.js ├── views │ └── index.html ├── webpack.config.dev.js └── webpack.config.js ├── mixin.js ├── package.json ├── src ├── Actions.js ├── Flux.js ├── Store.js ├── __tests__ │ ├── Actions-test.js │ ├── Flux-test.js │ ├── Store-test.js │ └── exampleFlux-test.js ├── addons │ ├── FluxComponent.js │ ├── TestUtils.js │ ├── __tests__ │ │ ├── FluxComponent-test.js │ │ ├── TestUtils-test.js │ │ ├── addContext.js │ │ ├── connectToStores-test.js │ │ └── fluxMixin-test.js │ ├── connectToStores.js │ ├── fluxMixin.js │ └── reactComponentMethods.js └── test │ └── init.js ├── test-utils.js └── webpack.config.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "es2015-loose", 4 | "react", 5 | "stage-0" 6 | ], 7 | "plugins": [ 8 | "transform-es3-member-expression-literals", 9 | "transform-es3-property-literals" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "mocha": true 7 | }, 8 | "rules": { 9 | "strict": 0, 10 | "quotes": [2, "single"], 11 | "curly": [2, "multi-line"], 12 | "no-underscore-dangle": 0, 13 | "comma-dangle": 0, 14 | "consistent-return": 0, 15 | 16 | // Doesn't work with classes 17 | // https://github.com/babel/babel-eslint/issues/8 18 | "no-undef": 0, 19 | 20 | // Doesn't work inside ES6 template strings 21 | "comma-spacing": 0, 22 | 23 | // Doesn't work with await 24 | // https://github.com/babel/babel-eslint/issues/22 25 | "no-unused-expressions": 0, 26 | 27 | // Doesn't work with ES6 classes 28 | // https://github.com/babel/babel-eslint/issues/8 29 | "no-unused-vars": 0, 30 | 31 | "no-empty": 0, 32 | "no-use-before-define": [2, "nofunc"] 33 | }, 34 | "globals": { 35 | "expect": true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | /lib 30 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | /docs 3 | /lib/__tests__ 4 | /lib/addons/__tests__ 5 | /lib/test 6 | /src 7 | .eslintrc 8 | .travis.yml 9 | Makefile 10 | webpack.config.js 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 4 4 | - 6 5 | - 8 6 | before_script: "npm install -g codeclimate-test-reporter" 7 | script: "make fast-js test-cov" 8 | after_script: "cat coverage/lcov.info | codeclimate" 9 | addons: 10 | code_climate: 11 | repo_token: 59cf6bc8ac2edd88c1436f101e867b098e5b6b6bb03ac8c50812ca954924e84a 12 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | **Please make sure your PR includes both tests and documentation.** 4 | 5 | ## File organization 6 | 7 | All code is written in next-generation JavaScript and transpiled using Babel, including tests. Source files are located in `src` and transpiled to `lib`, which is gitignored. `dist` is for browser builds, and is not ignored. Add-ons (modules that are not part of core) are located in `src/addons`. 8 | 9 | Tests for a module should be placed in a `__tests__` subdirectory and named with a `-test.js` suffix. For example, the test suite for the module at `foo/bar.js` should be located at `foo/__tests__/bar-test.js`. 10 | 11 | 12 | ## Building 13 | 14 | Babel is used for transpilation, but refrain from using any features that require an ES6 or above polyfill, as this will increase the bundled size of the library — e.g. async/await or symbols. This does not apply to tests, however, as they do not affect the bundle size. 15 | 16 | To transpile the source files: 17 | 18 | ``` 19 | make js 20 | ``` 21 | 22 | (If it's the first time you're building the `lib` directory, or if you've just run the clean task, you should run `make fast-js` to transpile all the files at once. Subsequent builds should use `make js`.) 23 | 24 | Browser builds, one uncompressed and one compressed, are built using webpack. These should only be built right before a new release, not on every commit. These are not recommended for actual use — use a module bundler like webpack or browserify instead. They exist primarily so we can keep an eye on the overall size of the library. 25 | 26 | To build for the browser: 27 | 28 | ``` 29 | make browser 30 | ``` 31 | 32 | To transpile and build for browser: 33 | 34 | ``` 35 | make build 36 | ``` 37 | 38 | To clean all generated files 39 | 40 | ``` 41 | make clean 42 | ``` 43 | 44 | ## Tests 45 | 46 | To run the test suite: 47 | 48 | ``` 49 | make test 50 | ``` 51 | 52 | (As in the previous section, you should run `make fast-js` for the first run. You can chain make tasks on the command line like so: `make fast-js test`). 53 | 54 | Tests are run on the transpiled code, not the source files. If you rename or delete a source file, make sure the transpiled file is also deleted. You can always run `make clean` to clear out all generated files. 55 | 56 | Tests are written using mocha and chai. chai-as-promised and async/await should be used for testing asynchronous operations. A browser environment is provided for React tests via jsdom. Continuous integration tests are run on Travis CI. 57 | 58 | ## Documentation 59 | 60 | New features or API changes should be documented. Docs are located in the `docs` folder. Refer to the docs [README](docs/README.md) for information on how to build the docs. Please do not commit changes `dist/docs` — I will add those before deploying to GitHub pages. 61 | 62 | ## Code style 63 | 64 | Code is linted using ESLint and babel-eslint. Rules are located in `.eslintrc`. Even if linting passes, please do your best to maintain the existing code style. 65 | 66 | Linting is always run before testing. To run the lint task separately: 67 | 68 | ``` 69 | make lint 70 | ``` 71 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BABEL_CMD = node_modules/.bin/babel 2 | MOCHA_CMD = node_modules/.bin/mocha 3 | ISTANBUL_CMD = node_modules/.bin/istanbul 4 | ESLINT_CMD = node_modules/.bin/eslint 5 | WEBPACK_CMD = node_modules/.bin/webpack 6 | 7 | SRC_JS = $(shell find src -name "*.js") 8 | LIB_JS = $(patsubst src/%.js,lib/%.js,$(SRC_JS)) 9 | TEST_JS = $(shell find lib -name "*-test.js") 10 | 11 | BABEL_ARGS = --source-maps inline 12 | MOCHA_ARGS = --harmony --require lib/test/init.js $(TEST_JS) 13 | 14 | # Build application 15 | build: js browser 16 | 17 | clean: 18 | rm -rf lib/ 19 | rm -rf dist/ 20 | 21 | # Test 22 | test: lint js 23 | @NODE_ENV=test $(MOCHA_CMD) $(MOCHA_ARGS) 24 | 25 | test-cov: js 26 | @NODE_ENV=test $(ISTANBUL_CMD) cover node_modules/.bin/_mocha -- $(MOCHA_ARGS) 27 | 28 | lint: 29 | $(ESLINT_CMD) $(SRC_JS) 30 | 31 | 32 | # Build application quickly 33 | # Faster on first build, but not after that 34 | fast-build: fast-js build 35 | 36 | # Publish docs to GitHub Pages 37 | publish-docs: 38 | git subtree push --prefix docs/dist/flummox origin gh-pages 39 | 40 | # Transpile JavaScript using Babel 41 | js: $(LIB_JS) 42 | 43 | $(LIB_JS): lib/%.js: src/%.js 44 | mkdir -p $(dir $@) 45 | $(BABEL_CMD) $< -o $@ $(BABEL_ARGS) 46 | 47 | fast-js: 48 | $(BABEL_CMD) src -d lib $(BABEL_ARGS) 49 | 50 | watch-js: 51 | $(BABEL_CMD) src -d lib $(BABEL_ARGS) -w 52 | 53 | browser: $(SRC_JS) 54 | mkdir -p dist 55 | $(WEBPACK_CMD) src/Flux.js dist/flummox.js 56 | NODE_ENV=production $(WEBPACK_CMD) src/Flux.js dist/flummox.min.js 57 | 58 | .PHONY: build clean test test-cov lin fast-build js fast-js watch-js browser 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Flummox 2 | ======= 3 | 4 | [![build status](https://img.shields.io/travis/acdlite/flummox.svg?style=flat-square)](https://travis-ci.org/acdlite/flummox) 5 | [![Test Coverage](https://img.shields.io/codeclimate/coverage/github/acdlite/flummox.svg?style=flat-square)](https://codeclimate.com/github/acdlite/flummox) 6 | [![npm downloads](https://img.shields.io/npm/dm/flummox.svg?style=flat-square)](https://www.npmjs.com/package/flummox) 7 | [![npm version](https://img.shields.io/npm/v/flummox.svg?style=flat-square)](https://www.npmjs.com/package/flummox) 8 | 9 | Idiomatic, modular, testable, isomorphic Flux. No singletons required. 10 | 11 | ``` 12 | $ npm install --save flummox 13 | ``` 14 | 15 | ## Versions 16 | 17 | ### Stable (3.x) 18 | 19 | Current stable Flummox's version with latest [React.js](https://facebook.github.io/react/index.html) support is **3.6.x**. If you're happy enough with what you have right now then **you can safely stay with this version**. It will be maintained but we don't think that new features will be added. 20 | 21 | ### Non-stable (4.x) 22 | 23 | **Eventually 4.x should be the last major release but it never happened**. If you want the latest features then use [Redux](https://github.com/gaearon/redux) instead. It's really great. 24 | 25 | _We know that churn can be frustrating but we feel it would be irresponsible for us to continue recommending Flummox when Redux exists which is a significant improvement over classical Flux._ 26 | 27 | **Check out [redux-actions](https://github.com/acdlite/redux-actions) and [redux-promise](https://github.com/acdlite/redux-promise), which implement much of the convenience of Flummox as Redux extensions.** 28 | 29 | --- 30 | 31 | Join the **#flummox** channel of the [Reactiflux](http://reactiflux.com/) Slack community. 32 | 33 | [![Join the chat at https://gitter.im/acdlite/flummox](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/acdlite/flummox?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 34 | 35 | The documentation is hosted at [acdlite.github.io/flummox](http://acdlite.github.io/flummox). It is a pre-rendered, isomorphic app built with Flummox and React. Checkout the [source](https://github.com/acdlite/flummox/tree/master/docs). 36 | -------------------------------------------------------------------------------- /UPGRADE_GUIDE.md: -------------------------------------------------------------------------------- 1 | Upgrade Guide 2 | ============= 3 | 4 | Please refer to the [changelog](/CHANGELOG.md) for a full list of breaking changes. 5 | 6 | 2.x -> 3.x 7 | ---------- 8 | 9 | ### Upgrade to React 0.13 10 | 11 | FluxComponent and fluxMixin have seen some significant updates, and now require React 0.13. If you're still on React 0.12, keep using Flummox 2.13.1 until you're able to upgrade. 12 | 13 | ### Accessing props inside state getters 14 | 15 | State getters passed to `connectToStores()` are no longer auto-bound to the component. Instead, `props` are passed as the second parameter: 16 | 17 | ```js 18 | // Before: 2.x 19 | const MyComponent = React.createClass({ 20 | mixins: [fluxMixin({ 21 | posts: function(store) { 22 | return { 23 | post: store.getPost(this.props.postId), 24 | }; 25 | } 26 | })] 27 | }); 28 | 29 | // After: 3.x 30 | // Or, you know, just use FluxComponent :) 31 | const MyComponent = React.createClass({ 32 | mixins: [fluxMixin({ 33 | posts: (store, props) => ({ 34 | post: store.getPost(props.postId), 35 | }) 36 | })] 37 | }) 38 | ``` 39 | 40 | Aside from being more a functional interface, this allows us to do optimizations at the library level, and prevents anti-patterns such using state inside a state getter. 41 | 42 | ### Directly-nested FluxComponents 43 | 44 | Previously it was suggested that you could directly nest FluxComponents as a way to get store state based on other store state. This is now considered an anti-pattern. Use the `render` prop for custom rendering instead. 45 | 46 | ```js 47 | // Before: 2.x 48 | ({ 50 | mostRecentPost: store.getMostRecentPost(), 51 | }) 52 | }}> 53 | 60 | // has props "mostRecentPosts" and "mostRecentComments" 61 | 62 | 63 | 64 | // After: 3.x 65 | ({ 68 | mostRecentPost: store.getMostRecentPost(), 69 | }) 70 | }} 71 | render={storeState => 72 | ({ 74 | mostRecentComments: store.getMostRecentComments(storeState.mostRecentPost.id) 75 | }) 76 | }}> 77 | // has props "mostRecentPosts" and "mostRecentComments" 78 | 79 | } 80 | /> 81 | 82 | ``` 83 | -------------------------------------------------------------------------------- /component.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/addons/FluxComponent'); 2 | -------------------------------------------------------------------------------- /connect.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/addons/connectToStores'); 2 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # node-waf configuration 20 | .lock-wscript 21 | 22 | # Compiled binary addons (http://nodejs.org/api/addons.html) 23 | build/Release 24 | 25 | # Dependency directory 26 | # https://www.npmjs.org/doc/misc/npm-faq.html#should-i-check-my-node_modules-folder-into-git- 27 | node_modules 28 | 29 | /lib 30 | /public 31 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | BIN=node_modules/.bin 2 | 3 | WEBPACK_CMD = node_modules/.bin/webpack 4 | SASS_CMD = sassc 5 | WATCH_CMD = node_modules/.bin/watch 6 | AUTOPREFIXER_CMD = node_modules/.bin/autoprefixer 7 | CLEANCSS_CMD = node_modules/.bin/cleancss 8 | JSON_SASS_CMD = node_modules/.bin/json-sass 9 | 10 | BABEL_ARGS = --experimental --source-maps-inline 11 | 12 | SRC_JS = $(shell find src -name "*.js") 13 | LIB_JS = $(patsubst src/%.js,lib/%.js,$(SRC_JS)) 14 | 15 | DOCS_MD = $(shell find docs -name "*.md") 16 | 17 | build: build-dev minify-css 18 | 19 | build-dev: js webpack css build-docs 20 | 21 | # Build application quickly 22 | # Faster on first build, but not after that 23 | fast-build: fast-js build 24 | 25 | # Watch for changes 26 | watch: minify-css build-docs 27 | @NODE_ENV=development $(MAKE) -j5 dev-server webpack-server watch-css watch-js 28 | 29 | clean: 30 | rm -rf public/flummox/ 31 | rm -rf lib/ 32 | rm -rf dist/ 33 | 34 | # Transpile JavaScript using Babel 35 | js: $(LIB_JS) 36 | 37 | $(LIB_JS): lib/%.js: src/%.js 38 | mkdir -p $(dir $@) 39 | $(BIN)/babel $< -o $@ $(BABEL_ARGS) 40 | 41 | fast-js: 42 | $(BIN)/babel src -d lib $(BABEL_ARGS) 43 | 44 | watch-js: 45 | $(BIN)/babel src -d lib $(BABEL_ARGS) -w 46 | 47 | build-docs: $(DOCS_MD) js 48 | node lib/scripts/build-docs.js 49 | 50 | build-static-site: clean fast-build 51 | rm -rf dist/ 52 | node lib/scripts/build-static-site.js 53 | 54 | dev-server: $(LIB_JS) 55 | nodemon ./lib/server 56 | 57 | webpack-server: $(LIB_JS) 58 | node ./lib/server/webpack 59 | 60 | webpack: public/js/flummox/app.js 61 | 62 | public/js/flummox/app.js: $(SRC_JS) 63 | $(BIN)/webpack 64 | 65 | css: public/flummox/css/app.css 66 | 67 | minify-css: css public/flummox/css/app.min.css 68 | 69 | public/flummox/css/app.css: sass/app.scss theme 70 | mkdir -p $(dir $@) && sassc -m $< | $(BIN)/autoprefixer > $@ 71 | 72 | public/flummox/css/app.min.css: public/flummox/css/app.css 73 | $(BIN)/cleancss $< > $@ 74 | 75 | watch-css: 76 | $(BIN)/watch "mkdir -p public/flummox/css && sassc -m sass/app.scss | $(BIN)/autoprefixer > public/flummox/css/app.css" sass 77 | 78 | theme: sass/dependencies/_theme.scss 79 | 80 | sass/dependencies/_theme.scss: lib/shared/theme.js 81 | mkdir -p $(dir $@) && $(BIN)/json-sass -i $< \ 82 | | sed '1s/^/$$theme: /' \ 83 | > $@ 84 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | Documentation 2 | ============= 3 | 4 | The docs are built by pre-rendering an isomorphic React + Flummox app. Naturally, it also serves as a demo app. 5 | 6 | - The docs are written as a tree of Markdown files in `/docs` (probably should rename to distinguish from top-level docs folder). 7 | - A script converts the Markdown into JSON files, which can be served statically and fetched via AJAX. 8 | - A React app renders the site. 9 | - The app is served by iojs/koa, then converted to static files using wget. 10 | - The static files are committed into version control, then pushed to the `gh-pages` branch. 11 | 12 | View the site here: [http://acdlite.github.io/flummox](http://acdlite.github.io/flummox) 13 | 14 | For purposes of illustration, it's a bit over-engineered in some places. For instance, data is fetched via AJAX, even though it would be easier to just embed the payload directly in the HTML source. 15 | 16 | Right now the docs themselves have mostly just been copied over from the old `docs` directory that this replaces. Many of these docs are in need of an overhaul, and some are missing. (The `connectToStores` higher-order component isn't documented at all!) Going forward, docs should link heavily to source of this app to provide examples when appropriate. 17 | 18 | Pull requests welcome :) 19 | 20 | Tools and libraries used 21 | ------------------------ 22 | 23 | - [React](http://facebook.github.io/react/) 24 | - [Flummox](http://acdlite.github.io/flummox) 25 | - [iojs](http://iojs.org) 26 | - [koa](http://koajs.com/) 27 | - [Sass/libsass](http://sass-lang.com) 28 | - [React Router](https://github.com/rackt/react-router) 29 | - [isomorphic-fetch](https://github.com/matthew-andrews/isomorphic-fetch) 30 | - [React Hot Loader](http://gaearon.github.io/react-hot-loader/getstarted/) 31 | - [Remarkable](https://github.com/jonschlinkert/remarkable) 32 | - [front-matter](https://github.com/jxson/front-matter) 33 | 34 | ...and more. 35 | -------------------------------------------------------------------------------- /docs/dist/flummox/css/app.min.css: -------------------------------------------------------------------------------- 1 | img,legend{border:0}legend,td,th{padding:0}h1,h2,h3,h4,h5,h6,html{font-family:Source Sans Pro}.View,sub,sup{position:relative}@font-face{font-family:'Source Code Pro';font-style:normal;font-weight:400;src:local('Source Code Pro'),local('SourceCodePro-Regular'),url(http://fonts.gstatic.com/s/sourcecodepro/v6/mrl8jkM18OlOQN8JLgasD9zbP97U9sKh0jjxbPbfOKg.ttf)format('truetype')}@font-face{font-family:'Source Sans Pro';font-style:normal;font-weight:400;src:local('Source Sans Pro'),local('SourceSansPro-Regular'),url(http://fonts.gstatic.com/s/sourcesanspro/v9/ODelI1aHBYDBqgeIAH2zlNzbP97U9sKh0jjxbPbfOKg.ttf)format('truetype')}@font-face{font-family:'Source Sans Pro';font-style:normal;font-weight:700;src:local('Source Sans Pro Bold'),local('SourceSansPro-Bold'),url(http://fonts.gstatic.com/s/sourcesanspro/v9/toadOcfmlt9b38dHJxOBGLsbIrGiHa6JIepkyt5c0A0.ttf)format('truetype')}@font-face{font-family:'Source Sans Pro';font-style:italic;font-weight:700;src:local('Source Sans Pro Bold Italic'),local('SourceSansPro-BoldIt'),url(http://fonts.gstatic.com/s/sourcesanspro/v9/fpTVHK8qsXbIeTHTrnQH6Edtd7Dq2ZflsctMEexj2lw.ttf)format('truetype')}/*! normalize.css v3.0.2 | MIT License | git.io/normalize */body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,menu,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background-color:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,optgroup,strong{font-weight:700}dfn{font-style:italic}h1{font-size:2em;margin:.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;vertical-align:baseline}sup{top:-.5em}sub{bottom:-.25em}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{box-sizing:content-box;height:0}pre,textarea{overflow:auto}code,kbd,pre,samp{font-family:monospace,monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type=button],input[type=reset],input[type=submit]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type=checkbox],input[type=radio]{box-sizing:border-box;padding:0}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{height:auto}input[type=search]{-webkit-appearance:textfield;box-sizing:content-box}*,.View{box-sizing:border-box}input[type=search]::-webkit-search-cancel-button,input[type=search]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid silver;margin:0 2px;padding:.35em .625em .75em}table{border-collapse:collapse;border-spacing:0}code[class*=language-],pre[class*=language-]{color:#000;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono',monospace;direction:ltr;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f5f2f0}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#a67f59;background:rgba(255,255,255,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.function{color:#DD4A68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}html{-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%;font-size:18px;line-height:1.5;background:#1C4467;color:#fff;min-width:500px}a{color:#ED4F81}.Doc-content pre ::-moz-selection,.HTMLContent pre ::-moz-selection,::-moz-selection{background:rgba(237,79,129,.3)}.Doc-content pre ::selection,.HTMLContent pre ::selection,::selection{background:rgba(237,79,129,.3)}code,pre{font-family:Source Code Pro}.View{display:-webkit-box;display:-webkit-flex;display:-ms-flexbox;display:flex}.View--flexDirectionColumn{-webkit-box-orient:vertical;-webkit-box-direction:normal;-webkit-flex-direction:column;-ms-flex-direction:column;flex-direction:column}.View--alignItemsFlexEnd{-webkit-box-align:end;-webkit-align-items:flex-end;-ms-flex-align:end;align-items:flex-end}.View--justifyContentFlexEnd{-webkit-box-pack:end;-webkit-justify-content:flex-end;-ms-flex-pack:end;justify-content:flex-end}.View--flexGrow{-webkit-box-flex:1;-webkit-flex-grow:1;-ms-flex-positive:1;flex-grow:1}.Container{max-width:950px;margin:0 auto;padding:0 1.5rem}.Doc-content a,.HTMLContent a{text-decoration:none;font-weight:700}.Doc-content a:hover,.HTMLContent a:hover{border-bottom:2px solid}.Doc-content h1,.Doc-content h2,.Doc-content h3,.Doc-content h4,.Doc-content h5,.Doc-content h6,.HTMLContent h1,.HTMLContent h2,.HTMLContent h3,.HTMLContent h4,.HTMLContent h5,.HTMLContent h6{margin-top:4.5rem;margin-bottom:1.5rem}.Doc-content h1:first-child,.Doc-content h2:first-child,.Doc-content h3:first-child,.Doc-content h4:first-child,.Doc-content h5:first-child,.Doc-content h6:first-child,.HTMLContent h1:first-child,.HTMLContent h2:first-child,.HTMLContent h3:first-child,.HTMLContent h4:first-child,.HTMLContent h5:first-child,.HTMLContent h6:first-child{margin-top:0}.Doc-content h3,.Doc-content h4,.Doc-content h5,.Doc-content h6,.HTMLContent h3,.HTMLContent h4,.HTMLContent h5,.HTMLContent h6{margin-top:3rem}.Doc-content p,.Doc-content pre,.HTMLContent p,.HTMLContent pre{margin:1.5rem 0}.Doc-content pre,.HTMLContent pre{padding:.75rem 1.5rem;background:#fff;font-size:90%;border-left:solid .375rem #ED4F81;-webkit-transition:border-color 250ms;transition:border-color 250ms;color:#000}@media screen and (min-width:950px){.Doc-content pre,.HTMLContent pre{padding-left:1.125rem;margin-left:-1.5rem;margin-right:-1.5rem}}.Doc-content hr,.HTMLContent hr{border:2px solid rgba(255,255,255,.1);margin:3rem}.RouteTransition-enter,.RouteTransition-leave{z-index:999;position:absolute;width:100%;-webkit-backface-visibility:hidden;backface-visibility:hidden;-webkit-transition:opacity 150ms ease-in;transition:opacity 150ms ease-in}.RouteTransition-enter{opacity:.01}.RouteTransition-enter.RouteTransition-enter-active,.RouteTransition-leave{opacity:1}.RouteTransition-leave.RouteTransition-leave-active{opacity:.01} -------------------------------------------------------------------------------- /docs/dist/flummox/data/docs/api/actions.json: -------------------------------------------------------------------------------- 1 | {"path":"api/actions","content":"`Actions`\n=========\n\nCreate actions by extending from the base `Actions` class.\n\n```js\nclass MessageActions extends Actions {\n\n // Methods on the prototype are automatically converted into actions\n newMessage(content) {\n\n // The return value from the action is sent to the dispatcher.\n // It is also returned to the caller.\n return content;\n }\n\n // Asynchronous functions are also supported: just return a promise\n // This is easy using async-await\n async createMessage(messageContent) {\n const response = await serverCreateMessage(messageContent);\n return await response.json();\n }\n\n}\n```\n\nYou can also use a plain JavaScript object. When passed to `flux.createActions`, it will be converted into an Actions class.\n\n```js\n// Same as previous example\nconst MessageActions = {\n newMessage(content) {\n return content;\n },\n\n async createMessage(messageContent) {\n const response = await serverCreateMessage(messageContent);\n return await response.json();\n }\n}\n```\n\nTesting\n-------\n\nThe return value of an action is dispatched automatically. It's also returned to the caller. This means it's possible to test actions completely independently from a Flux or Store instance. Here's how you'd test the example MessageActions from above:\n\n```js\n// Using mocha and chai-as-promised\nconst actions = new MessageActions();\n\nexpect(actions.newMessage('Hello world!')).to.equal('Hello world');\n\n// Assuming `serverCreateMessage()` has been mocked\nexpect(actions.createMessage('Hello world!')).to.eventually.deep.equal({\n id: 1,\n content: 'Hello world!',\n});\n```\n\n\nAsynchronous actions\n--------------------\n\nAsynchronous actions are actions that return promises. Unlike synchronous actions, async actions fire the dispatcher twice: at the beginning and at the end of the action. Refer to the [Store API](store.md) for information on how to register handlers for asynchronous actions.\n\nMethods\n-------\n\n### getActionIds\n\n```js\nobject getActionIds()\n```\n\nReturns an object of action ids, keyed by action name. (In most cases, it's probably more convenient to use `Flux#getActionIds()` instead.)\n\n\nAlso available as `getConstants()`\n"} -------------------------------------------------------------------------------- /docs/dist/flummox/data/docs/api/flux.json: -------------------------------------------------------------------------------- 1 | {"path":"api/flux","content":"`Flux`\n======\n\nCreate Flux containers by extending from the base `Flux` (or `Flummox`) class.\n\n```js\nclass Flux extends Flummox {\n constructor() {\n super();\n\n // Create actions first so our store can reference them in\n // its constructor\n this.createActions('messages', MessageActions);\n\n // Extra arguments are sent to the store's constructor. Here, we're\n // passing a reference to this Flux instance\n this.createStore('messages', MessageStore, this);\n }\n}\n```\n\nEncapsulate your stores and actions\n-------------------------------------\n\nFlummox is designed to be used without singletons. Instead, create a Flux class that encapsulates the creation of all your application's stores and actions, so that you can create new instances on the fly.\n\n```js\nconst flux = new Flux();\n```\n\n(Note that there's nothing technically stopping you from using singletons if you wish, but why would you want to?)\n\n\nDebugging\n---------\n\nLike Stores, Flux instances are EventEmitters. A `dispatch` event is emitted on every dispatch. Listeners are sent the dispatched payload. This can be used during development for debugging.\n\n```js\nflux.addListener('dispatch', payload => {\n console.log('Dispatch: ', payload);\n});\n```\n\nAdditionally, an `error` event is emitted when errors occur as a result of an async action. This is both for convenience and to prevent error gobbling.\n\nMethods\n-------\n\n### createActions\n\n```js\nActions createActions(string key, function ActionsClass [, ...constructorArgs])\n```\n\nCreates an instance of `ActionsClass` and saves a reference to it. `constructorArgs` are passed to the constructor of `ActionsClass` on creation.\n\n### createStore\n\n```js\nStore createStore(string key, function StoreClass [, ...constructorArgs])\n```\n\nCreates an instance of `StoreClass`, registers the store's handlers with the dispatcher, and saves a reference to it. `constructorArgs` are passed to the constructor of `ActionsClass` on creation.\n\n### getStore\n\n```js\nStore getStore(string key)\n```\n\nGets an store instance by key.\n\n### removeStore\n\n```js\nStore removeStore(string key)\n```\n\nDeletes an instance of `StoreClass`, unregisters the store's handlers from dispatcher, and removes all store listeners.\n\n### getActions\n\n```js\nActions getActions(string key)\n```\n\nGets an actions instance by key.\n\n### getActionIds\n\n```js\nActions getActionIds(string key)\n```\n\nGets action ids for the given actions key. Internally calls `Actions#getActionIds`.\n\nAlso available as `getConstants()`.\n\n### removeActions\n\n```js\nActions removeActions(string key)\n```\n\nDeletes an actions instance by key.\n\nDispatcher methods\n------------------\n\nEvery Flux instance has its own dispatcher. You should try to avoid interacting with the dispatcher directly, but it is available (primarily for testing purposes) as `this.dispatcher`. Some convenience methods are also provided:\n\n### dispatch\n```\ndispatch(string actionId [, * body])\n```\n\nSimilar to the `dispatch()` method of the dispatcher itself, except instead of passing a payload, the payload is constructed for you, in the form:\n\n```js\n{\n actionId,\n body\n}\n```\n\nThis is used internally by Flummox: the `actionId` field is used to identify the source action, and `body` contains the value passed to store handlers. In your tests, you can use it to simulate a dispatch to your stores.\n\n### waitFor\n\n```\nwaitFor(Store store)\n```\n\nSimilar to the `waitFor()` method of the dispatcher itself, this can be used within a handler to wait for a different store to respond to the dispatcher before continuing. The operation is synchronous.\n\nInstead of passing a store, you can also pass a dispatcher token, or an array of tokens and stores.\n\nUsing a custom dispatcher\n-------------------------\n\n**tl;dr** Flummox uses the flux dispatcher from Facebook, but you can switch out whatever api compatible dispatcher you want.\n\n***\n\nUsually the dispatcher provided by Facebook is sufficient, but you aren't limited to using it if you find you need more than it provides. If you want to have custom behavior when dispatching actions, you can provide a wrapper for the Facebook dispatcher that does what you want. Or use something else entirely. It's up to you.\n\nTo substitute a different dispatcher object just change the `constructor()` function of your flux object like this:\n\n```js\n\nclass Flux extends Flummox {\n constructor() {\n super();\n\n this.dispatcher = new MyCustomDispatcher();\n }\n}\n\n```\n\nJust remember, whatever object you provide has to follow the same api as the dispatcher from Facebook. The easiest way to do that is to extend the Facebook dispatcher in a new class, and then provide whatever alternate or extended functionality you desire.\n\nFor instance, say you want to allow the dispatcher to receive actions for dispatching while it is in the middle of another action dispatch. The standard dispatcher will complain that you cannot dispatch an action during another action. There are good reasons for this, but perhaps you just want to queue up that action and have it execute when the current action is completed. One easy way to do this would be to use `setTimeout()`. To do this you would provide a dispatcher with slightly different dispatch functionality, like this:\n\n```js\n\nclass MyCustomDispatcher extends Dispatcher {\n dispatch(...args) {\n if (!this.isDispatching()) {\n super.dispatch(...args); // This will execute the Facebook dispatcher's dispatch function.\n } else {\n setTimeout(() => { // We are currently dispatching, so delay this action using setTimeout\n super.dispatch(...args);\n }, 0);\n }\n }\n}\n\n```\n\nSerialization/deserialization\n-------------------------------\n\nIf you're building an isomorphic application, it's often a good idea pass the initial state of your application from the server to the client to avoid unecessary/duplicate HTTP requests. This is easy with Flux, since all of your application state is located in your stores.\n\nThis feature is opt-in on a store-by-store basis, and requires some additional set-up.\n\n### serialize\n\n```\nstring serialize()\n```\n\nReturns a serialized string describing the entire state of your Flux application.\n\nInternally, it passes each store's current state to the store's static method `Store.serialize()`. The return value must be a string representing the given state. If a store does not have a static method `serialize()`, or if it returns a non-string, it is ignored.\n\n### deserialize\n\n```\ndeserialize(string stateString)\n```\n\nConverts a serialized state string (as returned from `Flux#serialize()`) to application state and updates the stores.\n\nInternally, it passes the state string for each store (as returned from `Store.serialize()`) to the store's static method `Store.deserialize()`. The return value must be a state object. It will be passed to `Store#replaceState()`. If a store does not have a static method `deserialize()`, it is ignored.\n"} -------------------------------------------------------------------------------- /docs/dist/flummox/data/docs/api/fluxcomponent.json: -------------------------------------------------------------------------------- 1 | {"path":"api/fluxcomponent","content":"`FluxComponent`\n===============\n\n**In v3.0, FluxComponent requires React 0.13. If you're still on React 0.12, keep using Flummox 2.x until you're able to upgrade.**\n\nAccess the Flux instance and subscribe to store updates. Refer to the [React integration guide](../guides/react-integration.md) for more information.\n\n\n```js\n ({\n post: store.getPost(this.props.post.id),\n }),\n comments: store => ({\n comments: store.getCommentsForPost(this.props.post.id),\n })\n}}>\n \n\n```\n\nIn general, [it's recommended to use FluxComponent instead of fluxMixin](../guides/why-flux-component-is-better-than-flux-mixin.md).\n\nState getters\n-------------\n\nThe `stateGetter` prop can be used to control how state from stores are transformed into props:\n\n```js\n ({\n posts: store.getPostForUser(sessionStore.getCurrentUserId())\n })}\n}}>\n \n\n```\n\nThe `stateGetter` prop behaves differently depending on the value passed to the `connectToStores` prop, refer to [fluxMixin](fluxmixin.md) for more details.\n\n\nAccess flux with `this.props.flux`\n----------------------------------\n\nIn the child component, you can access the Flux instance with `this.props.flux`. For example, to perform an action:\n\n```js\nonClick(e) {\n e.preventDefault();\n this.props.flux.getActions('actionsKey').someAction();\n}\n```\n\nCustom rendering\n----------------\n\nWith FluxComponent, state from your stores is automatically passed as props to its children. This is nice for simple cases, especially when there's only a single child. But for more complex cases, or if you want direct control over rendering, you can pass a custom render function prop to FluxComponent:\n\n```js\n// Using children\n ({\n post: store.getPost(this.props.postId),\n })\n}}>\n \n\n\n// Using custom `render` function\n ({\n post: store.getPost(this.props.postId),\n })\n }}\n render={storeState => {\n // Render whatever you want\n return ;\n }}\n/>\n```\n\nProps\n-----\n\n### `flux`\n\nIndicates the [Flux instance](flux.md) to be used. It will be added to the context of all its nested components. If unset, it'll try to infer it from the context.\n\n### `connectToStores`\n\nThis prop has the same effect as passing the first argument to [fluxMixin](fluxmixin.md)'s `connectToStores()`.\n\n### `stateGetter`\n\nThis prop has the same effect as passing the second argument to [fluxMixin](fluxmixin.md)'s `connectToStores()`.\n\n### `render`\n\nOptionally overrides the rendering function, useful to control what state is passed down as props to components.\n"} -------------------------------------------------------------------------------- /docs/dist/flummox/data/docs/api/fluxmixin.json: -------------------------------------------------------------------------------- 1 | {"path":"api/fluxmixin","content":"`fluxMixin`\n===========\n\nAccess the Flux instance and subscribe to store updates. Refer to the [React integration guide](../guides/react-integration.md) for more information.\n\nNote that fluxMixin is actually a function that returns a mixin, as the example below shows. The parameters are the same as those for `connectToStores()`, described below. On component initialization, `connectToStores()` is called and used to set the initial state of the component.\n\n```js\nimport fluxMixin from 'flummox/mixin';\n\nlet MyComponent = React.createClass({\n\n mixins[fluxMixin(['storeA', 'storeB'])],\n\n ...\n});\n```\n\nIn general, [it's recommended to use FluxComponent instead of fluxMixin](../guides/why-flux-component-is-better-than-flux-mixin.md).\n\nState getters\n-------------\n\nWhen connecting to stores with fluxMixin (and FluxComponent), you'll usually want to specify custom state getters.\n\nA state getter is a function which returns a state object for a given store. The state object is merged into the component state using `setState()`.\n\nThe default state getter returns the entire store state. You can specify null as a state getter to use the default state getter.\n\nHere's an example of a state getter map you would pass to either `fluxMixin()` or `connectToStores()`. The keys are store keys, and the values are state getter functions.\n\n```js\nfluxMixin({\n posts: (store, props) =>({\n storeA: store.getPost(props.post.id),\n }),\n comments: (store, props) => ({\n comments: store.getCommentsForPost(props.post.id),\n })\n});\n```\n\nIn some situations the data retrieved from one store depends on the state from another, you can use an array of store keys with a custom state getter to ensure the components state is updated when either store changes:\n\n```js\nfluxMixin(\n ['posts', 'session'],\n\n // An array of store instances are passed to the state getter; Instead of indexing\n // into the stores array, ES6 array destructuring is used to access each store\n // as a variable.\n ([postStore, sessionStore]) => ({\n posts: store.getPostForUser(sessionStore.getCurrentUserId())\n })\n);\n```\n\nAccess flux with `this.flux`\n----------------------------\n\nYou can access the Flux instance with `this.flux`. For example, to perform an action:\n\n```js\nonClick(e) {\n e.preventDefault();\n this.flux.getActions('actionsKey').someAction();\n}\n```\n\n\nMethods\n-------\n\n### connectToStores\n\n```js\nconnectToStores(string storeKey [, function stateGetter])\nconnectToStores(Array storeKeys [, function stateGetter])\nconnectToStores(object stateGetterMap [, function stateGetter])\n```\n\nSynchronize component state with state from Flux stores. Pass a single store key, an array of store keys, or a map of store keys to getter functions. You can also specify a custom state getter as the second argument, the default state getter will return the entire store state (a reduce is performed on the entire store state when using an array or store keys).\n\nWhen using an array of store keys, the custom state getter is called with an array of store instances (same order as keys) as the first argument; An array is used instead of separate arguments to allow for future versions of flummox to pass additional arguments to the stateGetter eg. `props`.\n\nOtherwise only a single store instance is passed to the custom state getter.\n\nReturns the initial combined state of the specified stores.\n\n**Usage note**: Generally, you should avoid calling this directly and instead pass arguments to `fluxMixin()`, which calls this internally.\n\n### getStoreState\n\n```\ngetStoreState()\n```\n\nReturns current combined state of connected stores.\n"} -------------------------------------------------------------------------------- /docs/dist/flummox/data/docs/api/higher-order-component.json: -------------------------------------------------------------------------------- 1 | {"path":"api/higher-order-component","content":"Higher-order Flux Component\n===========================\n\n**Requires React 0.13. If you're still on React 0.12, use FluxComponent instead.**\n\nA higher-order component form of [FluxComponent](fluxcomponent.md). Here's an example from the [Flummox documentation app](https://github.com/acdlite/flummox/blob/master/docs/src/shared/components/HomeHandler.js#L15-L19):\n\n```js\nclass HomeHandler extends React.Component {\n render() {\n const { doc } = this.props;\n\n if (!doc) return ;\n\n return ;\n }\n}\n\nHomeHandler = connectToStores(HomeHandler, {\n docs: store => ({\n doc: store.getDoc('index')\n })\n});\n```\n\n**Note**: FluxComponent, fluxMixin, and the higher-order component implement the same [interface](https://github.com/acdlite/flummox/blob/master/src/addons/reactComponentMethods.js). Eventually the docs will updated to make this clearer.\n"} -------------------------------------------------------------------------------- /docs/dist/flummox/data/docs/api/index.json: -------------------------------------------------------------------------------- 1 | {"path":"api/index","content":"API\n===\n\nFlummox has three exports:\n\n* [Actions](api/actions.md)\n* [Store](api/store.md)\n* [Flux](api/flux.md) (also available as `Flummox`) This is the default export.\n\nIf you're using ES6 module syntax:\n\n```js\nimport { Flux } from 'flummox';\n```\n\nOr multiple classes at once:\n\n```js\nimport { Flux, Store, Actions } from 'flummox';\n```\n\nAddons\n------\n\nFlummox also comes with some addons. These are not part of the main export. That way, if you don't use them, they won't increase the size of your bundle.\n\n* [FluxComponent](api/fluxcomponent.md)\n* [fluxMixin](api/fluxmixin.md)\n* [Higher-order component](api/higher-order-component.md)\n\nRefer to the [React integration guide](../guides/react-integration.md) for details.\n\n```js\nimport fluxMixin from 'flummox/mixin';\nimport FluxComponent from 'flummox/component';\nimport connectToStores from 'flummox/connect';\n```\n"} -------------------------------------------------------------------------------- /docs/dist/flummox/data/docs/api/store.json: -------------------------------------------------------------------------------- 1 | {"path":"api/store","content":"`Store`\n=======\n\nCreate stores by extending from the base `Store` class.\n\n```js\nclass MessageStore extends Store {\n\n // Note that passing a flux instance to the constructor is not required;\n // we do it here so we have access to any action ids we're interested in.\n constructor(flux) {\n\n // Don't forget to call the super constructor\n super();\n\n // Register handlers with the dispatcher using action ids, or the\n // actions themselves\n const messageActions = flux.getActions('messages');\n this.register(messageActions.newMessage, this.handleNewMessage);\n\n // Set initial state using assignment\n this.state = {};\n }\n\n // Define handlers as methods...\n handleNewMessage() { ... }\n\n // It's also useful to define accessor-style methods that can be used by\n // your views on state changes\n getMessage(id) { ... }\n}\n```\n\nManaging state\n--------------\n\nIn a Flux application, all application state is contained inside stores, and the state of a store can only be changed by the store itself. The sole point of entry for data in a store is via the dispatcher, but information sent through the dispatcher should be thought of not as data, but as messages *describing* data. This may seem like a distinction without a difference, but by sticking to this concept, it ensures that your stores remain isolated and free of the complicated dependencies you often find in MVC-style apps.\n\nBecause state is so crucial, Flummox is a little more opinionated than some other Flux libraries about how to manage it: for instance, all state must be stored in the `this.state` object. That being said, Flummox places no restrictions on the types of data you store, so interoperability with libraries like Immutable.js is a non-issue. If this sounds familiar, it's because the store API is heavily influenced by the [React component API](http://facebook.github.io/react/docs/component-api.html):\n\n* All state mutations must be made via `setState()`.\n* `this.state` is used to access the current state, but it should **never** be mutated directly. Treat `this.state` as if it were immutable.\n* As in React +0.13, initial state is set by assignment in the constructor.\n\nPerforming optimistic updates\n-----------------------------\n\nA common pattern when performing server operations is to update the application's UI optimistically — before receiving a response from the server — then responding appropriately if the server returns an error. Use the method `registerAsync` to register separate handlers for the beginning of an asynchronous action and on success and failure. See below for details.\n\nCustomizing top-level state type\n--------------------------------\n\nThe default top-level state type (`this.state`) is a plain object. You can add any type of data to this structure, as in React. However, if you want even more control over state management, you can customize the top-level state type by overriding the static Store method `Store.assignState()`. This method is used internally to perform state changes. The default implementation is essentially a wrapper around `Object.assign()`:\n\n```js\n// Default implementation\nstatic assignState(oldState, newState) {\n return Object.assign({}, oldState, newState);\n}\n```\n\nThings to keep in mind when overriding `assignState()`:\n\n- Should be non-mutative.\n- `assignState(null, newState)` should not throw and should return a copy of `newState`.\n\nTo support React integration with FluxComponent, you should also override `Store#getStateAsObject()`, which returns a plain object representation of `this.state`. The default state getter uses the object returned by this function.\n\nMethods\n-------\n\n### register\n\n```js\nregister(function action | string actionId , function handler)\n```\n\nRegister a handler for a specific action. The handler will be automatically bound to the store instance.\n\nYou can register using either the action id or the action itself.\n\n**Usage note**: `register()` works for both async and sync actions. In the case of async actions, it receives the resolved value of the promise returned by the action.\n\n### registerAsync\n\n```js\nregisterAsync(function action | string actionId [, function begin, function success, function failure])\n```\n\nA register handler specifically for asynchronous actions (actions that return promises).\n\n- `beginHandler` is called at the beginning of the asynchronous action. It receives same arguments that were passed to the action.\n\n- `successHandler` works the same as if you registered an async action with `register()`: it is called if and when the asynchronous action resolves. It receives the resolved value of the promise returned by the action.\n\n- `failureHandler` is called if and when the asynchronous action is rejected. It receives the rejected value of the promise returned by the action (by convention, an error object).\n\nThis makes it easy perform to perform optimistic UI updates.\n\nIf any of the passed handlers are not functions, they are ignored.\n\n**Usage note**: `registerAsync(null, handler, null)` is functionally equivalent to `register(handler)`. If you don't need to respond to the beginning of an async action or respond to errors, then just use `register()`.\n\n### setState\n\n```js\nsetState(function|object nextState)\n```\n\nShallow merges `nextState` with the current state, then emits a change event so views know to update themselves.\n\nSimilar to React, multiple calls to `setState()` within the same handler are batched and applied at the end. Accessing `this.state` after calling `setState()` will return the existing value, not the updated value.\n\nYou can also do transactional state updates by passing a function:\n\n```js\nthis.setState(state => ({ counter: state.counter + 1 }));\n```\n\n### replaceState\n\n```js\nreplaceState(object nextState)\n```\n\nLike `setState()` but deletes any pre-existing state keys that are not in nextState.\n\n### forceUpdate\n\n```js\nforceUpdate()\n```\n\nEmits change event.\n\n**Usage note**: If you can, use `setState()` instead.\n\n\nEventEmitter methods\n--------------------\n\nFlummox stores are EventEmitters — specifically [eventemitter3](https://github.com/primus/eventemitter3) — so you can use any of the EventEmitter methods, the important ones being `addListener()` and `removeListener()`. Use these in your controller-views to subscribe to changes.\n\n**Usage note**: A `change` event is emitted automatically whenever state changes. Generally, this is the only event views should need to subscribe to. Unlike in MVC, Flux store events don't pass data around to different parts of your application; they merely broadcast that a change has occurred within a store, and interested parties should synchronize their state accordingly.\n\nDispatcher methods\n------------------\n\n### waitFor\n\n```js\nwaitFor(Store store)\n```\n\nWithin a handler, this waits for a different store to respond to the dispatcher before continuing. The operation is synchronous. E.g.\n\n```js\nsomeActionHandler() {\n this.waitFor(someStore);\n // someStore has completed, continue...\n}\n```\n\nInternally, it calls [`Dispatcher#waitFor()`](http://facebook.github.io/flux/docs/dispatcher.html#content).\n\nInstead of passing a store, you can also pass a dispatcher token, or an array of tokens and stores.\n\n**Usage note**: Because this method introduces dependencies between stores, you should generally try to avoid using it. Stores should be as isolated as possible from the outside world. If you find yourself relying on `waitFor()` often, consider rethinking how data flows through your app.\n\nStatic Methods\n-------\n\n### serialize(state)\n\nIf you use `Flux.serialize`, Flummox will try to call the static method `serialize` on all your stores. Flummox will pass the state object of the store to the method and expects a String\n\n### deserialize(state)\n\nIf you use `Flux.deserialize`, Flummox will try to call the static method `deserialize` on all your stores. Flummox will pass the appropriate serialized representation and expects an object, with which Flummox will call `replaceState` on your store.\n"} -------------------------------------------------------------------------------- /docs/dist/flummox/data/docs/guides/index.json: -------------------------------------------------------------------------------- 1 | {"path":"guides/index","content":"Guides\n======\n\n- [Quick start](guides/quick-start)\n- [React integration](guides/react-integration)\n- [Why FluxComponent > fluxMixin](guides/why-flux-component-is-better-than-flux-mixin)\n"} -------------------------------------------------------------------------------- /docs/dist/flummox/data/docs/guides/react-integration.json: -------------------------------------------------------------------------------- 1 | {"path":"guides/react-integration","content":"# React integration guide\n\nIf you're using Flummox, you're probably also using React. To make React integration incredibly simple, Flummox comes with some optional goodies: [FluxComponent](/flummox/docs/api/fluxcomponent) and [fluxMixin](/flummox/docs/api/fluxmixin). Both have essentially the same functionality — in fact, the component is mostly just a wrapper around the mixin. However, in the spirit of React, the component form is preferred. (Read more about [why FluxComponent is preferred](why-flux-component-is-better-than-flux-mixin).)\n\n```js\nimport FluxComponent from 'flummox/component';\nimport fluxMixin from 'flummox/mixin';\n```\n\nThis guide discusses how to use FluxComponent to integrate Flummox with React.\n\n**In v3.0, FluxComponent requires React 0.13. If you're still on React 0.12, keep using Flummox 2.x until you're able to upgrade.**\n\n## Accessing the Flux instance\n\n**tl;dr** FluxComponent gives you easy access to your Flux instance from anywhere in your component tree using only components and props.\n\n***\n\nUnlike most other Flux libraries, Flummox doesn't rely on singletons, because singletons don't work well on the server. The one downside of this approach is that in order to access your stores and actions, you can't just require a module: you have to pass your Flux instance through your component tree. You could do this by manually passing props from one component to the next, but that's not a great solution for components that are nested more than a few levels down.\n\nA better approach is to use context, which exposes data to arbitrarily deep components. Context in React is currently undocumented, but don't worry: it's a widely-used, safe part of React. (React Router uses context extensively.)\n\nContext is kind of weird, though. It's awkward to use and easy to abuse, which is probably why it's as-yet undocumented.\n\nFluxComponent treats context as an implementation detail, so you don't have to deal with it. Pass your Flux instance as a prop, and it will be added to the context of all its nested components.\n\nAdditionally, the immediate children of FluxComponent will be injected with a `flux` prop for easy access.\n\n```js\n\n // Immediate children have flux prop\n // flux has been added to context\n\n```\n\nIf flux is already part of the context, you can omit the flux prop on FluxComponent:\n\n```js\n\n // Same as last time: immediate children have flux prop\n // flux is already part of context, and remains so\n\n```\n\nSo if you pass a flux instance as a prop to a FluxComponent near the top of your app hierarchy, any FluxComponents further down the tree will automatically have access to it:\n\n```js\nReact.render(\n \n \n ,\n document.getElementById('app')\n)\n```\n\nPretty simple, right?\n\n## Subscribing to store updates\n\n**tl;dr** FluxComponent synchronizes with the state of your Flux stores and injects the state into its children as props.\n***\n\nStores are EventEmitters that emit change events whenever their state changes. To keep up to date, components must get the intial state, add an event listener, save a reference to the listener, and then remove the listener before unmounting to prevent memory leaks.\n\nThis sucks. And it's easy to mess up.\n\nFluxComponent hides all of these concerns behind a simple component interface. The prop `connectToStores` specifies which stores you want to stay in sync with. FluxComponents's immediate children will be injected with props corresponding to the state of those stores.\n\n```js\nclass OuterComponent extends React.Component {\n render() {\n return (\n // Pass an array of store keys\n \n \n \n );\n }\n}\n```\n\nIf `storeA` has state `{foo: 'bar'}` and `storeB` has state `{bar: 'baz'}`, then InnerComponent has props `foo=\"bar\"` and `bar=\"baz\"`. Whenever the stores change, so do the props.\n\n`connectToStores` will accept a single store key, an array of store keys, or a map of store keys to getter functions. A getter function is a function which takes a single parameter, the store, and returns an object of props to be injected into the children of FluxComponent. If a null is specified as a getter, the default getter is used instead, which simply returns the entire store state (like in the example above).\n\nSo, in just a few short lines, we can specify the initialization logic, update logic, and listening/unlistening logic for our component.\n\n```js\n// Pass an object of store keys mapped to getter functions\n ({\n post: store.getPost(this.props.post.id),\n }),\n comments: store => ({\n comments: store.getCommentsForPost(this.props.post.id),\n })\n}}>\n \n\n```\n\nIn this example, InnerComponent has props `post` and `comments`. If this auto-magic prop passing feels weird, or if you want direct control over rendering, you can pass a custom render function instead. Refer to the [FluxComponent](/flummox/docs/api/fluxcomponent) docs for more information.\n\n## Using fluxMixin\n\n**tl;dr** Just use FluxComponent. (Unless you don't want to. Up to you.) Read a longer explanation for [why FluxComponent is preferred](why-flux-component-is-better-than-flux-mixin).\n\n***\n\nFluxComponent is really just a wrapper around fluxMixin. (Seriously, check out the source.) But if you want to use fluxMixin directly, you can.\n\nLike FluxComponent, fluxMixin expects that the component you're mixing it into has access to a Flux instance via either a prop or context. It adds the Flux instance to the child context.\n\nUnlike FluxComponent, it does not inject props into its children. You can, however, access the instance with `this.flux`.\n\nfluxMixin adds a single method, `connectToStores()`. This is exactly like the `connectToStores` prop of FluxComponent. You can pass a single store key, an array of store keys, or a map of store keys to getter functions. In the single store key form, you can also pass a getter function as the second argument. (This form is not available to FluxComponent because props are single values.)\n\nfluxMixin does not inject store state as props into its children. Instead, it merges it into component state using `setState()`.\n\nWhen you call `connectToStores()`, it returns the current combined state of the stores (as specified by the getters). This is so you can use it within `getInitialState()`.\n\nHowever, there is a better way. fluxMixin is actually a function that returns a mixin object. Arguments passed to `fluxMixin()` are automatically sent to `connectToStores()` and used to set the initial state of the component.\n\n```js\n\nconst MyComponent = React.createClass({\n\n // Remember, you can also use the single key or object forms\n mixins[fluxMixin(['storeA', 'storeB'])],\n\n ...\n});\n\n```\n\nIf `storeA` has state `{foo: 'bar'}` and `storeB` has state `{bar: 'baz'}`, then MyComponent has state `{foo: 'bar', bar: 'baz'}`. Whenever the stores change, so does MyComponent.\n"} -------------------------------------------------------------------------------- /docs/dist/flummox/data/docs/guides/why-flux-component-is-better-than-flux-mixin.json: -------------------------------------------------------------------------------- 1 | {"path":"guides/why-flux-component-is-better-than-flux-mixin","content":"Why FluxComponent > fluxMixin\n=============================\n\nIn the [React integration guide](react-integration), I suggest that using [FluxComponent](/flummox/docs/api/fluxcomponent) is better than using [fluxMixin](/flummox/docs/api/fluxmixin), even though they do essentially the same thing. A few people have told me they like the mixin form more, so allow me to explain.\n\nMy argument can be broken down into three basic points. Note that these aren't my original ideas, nor are they unique to Flummox — they are the \"React Way\":\n\n- Declarative > imperative\n- Composition > inheritance\n- State is evil\n\nDeclarative > imperative\n------------------------\n\nThis is a no brainer. Remember the days before React, when you had to write one piece of code to render your application and another to update it? HAHAHAHA. That was awful. React showed us that expressing our views declaratively leads to clearer, more predictable, and less error-prone applications.\n\nYou might feel like fluxMixin and FluxComponent are equally declarative. They do have similar interfaces: a single argument/prop, that does (almost) the same thing. Still, as nice as fluxMixin's interface is, there's no beating a component in terms of clarity. A good rule of thumb in React is that everything that can be expressed as a component, should be.\n\n\nComposition > inheritance\n-------------------------\n\nReact has a very clear opinion on composition vs. inheritance: composition wins. Pretty much everything awesome about React — components, unidirectional data flow, the reconciliation process — derives from the fact that a React app is just a giant tree of components composed of other components.\n\nComponents make your code easy to reason about. If you stick to the basics of using components and props in your React app, you don't have to guess where data is coming from. The answer is always *from the owner*.\n\nHowever, when you use fluxMixin, you're introducing data into your component that comes not from the owner, but from an external source — your stores. (This is also true of FluxComponent, but to a lesser extent, as we'll see later.) This can easily lead to trouble.\n\nFor instance, here's a component that renders a single blog post, based on the id of the post.\n\n```js\nconst BlogPost = React.createClass({\n mixins: [fluxMixin({\n posts: (store, props) => ({\n post: store.getPost(props.id),\n })\n })],\n\n render() {\n
\n

{this.state.post.title}

\n
{this.state.post.content}
\n
\n }\n});\n```\n\nCan you spot the problem? What happens when you want to re-use this same component to display a list of blog posts on your home page? Does it really make sense for each BlogPost component to separately retrieve its own post data from the store? Nope, not really.\n\nConsider that the owner component (BlogRoll, let's say) has to pass down an `id` prop in order for BlogPost to work properly. Where do you think BlogRoll is going to get that id from? The store, of course. Now you have BlogRoll *and* each of its children getting data from the store, each with their own event listeners, and each calling `setState()` every time the store changes. D'oh!\n\nA better approach is to separate the data fetching logic from the logic of rendering the post. Instead of having a prop `id`, BlogPost should have a prop `post`. It shouldn't concern itself with how the data is retrieved — that's the concern of the owner.\n\nAfter we rewrite BlogPost, it looks something like this:\n\n```js\nconst BlogPost = React.createClass({\n render() {\n
\n

{this.props.post.title}

\n
{this.props.post.content}
\n
\n }\n});\n```\n\nAnd its owner looks something like this:\n\n```js\nconst BlogPostPage = React.createClass({\n mixins: [fluxMixin({\n posts: (store, props) => ({\n post: store.getPost(props.id),\n })\n })],\n\n render() {\n
\n \n \n \n \n \n \n
\n }\n})\n```\n\n*For the sake of this example, let's just assume the `id` prop magically exists and is derived from the URL. In reality, we'd use something like React Router's [State mixin]( https://github.com/rackt/react-router/blob/master/docs/api/mixins/State.md).*\n\nThere's another problem, though. Every time the store changes, fluxMixin calls `setState()` on BlogPostPage, triggering a re-render of the *entire* component.\n\nWhich brings us to the final point...\n\nState is evil\n-------------\n\nOnce you've grokked the basics, this is perhaps the most important thing to know about React. To [think in React](http://facebook.github.io/react/blog/2013/11/05/thinking-in-react.html) is to find the minimal amount of state necessary to represent your app, and calculate everything based on that. This is because state is unpredictable. Props are, for the most part, derived from other props and state, but state can be anything. The more state in your application, the harder it is to reason about it. As much as possible, state in React should be an implementation detail — a necessary evil, not a crutch.\n\nOn an even more practical level, every time the state of a component changes, the entire component sub-tree is re-rendered. In our example from the previous section, BlogPostPage updates every time the `posts` store changes — including SiteNavigation, SiteSidebar, and SiteFooter, which don't need to re-render. Only BlogPost does. Imagine if you're listening to more than just one store. The problem is compounded.\n\nAlright, so we need to refactor once again so that fluxMixin is only updating what needs to be updated. We already learned that we shouldn't put the mixin inside BlogPost itself, because that makes the component less reusable. Our remaining option is to create a new component that wraps around BlogPost:\n\n```js\nconst BlogPostWrapper = React.createClass({\n mixins: [fluxMixin({\n posts: (store, props) => ({\n post: store.getPost(props.id),\n })\n ]\n\n render() {\n \n }\n});\n```\n\nThis works. But it's kind of tedious, right? Imagine creating a wrapper like this for every single component that requires data from a store.\n\nWouldn't it be great if there were a shortcut for this pattern — a convenient way to update specific parts of your app, without triggering unnecessary renders?\n\nYep! It's called FluxComponent.\n\n```js\nclass BlogPostPage extends React.Component {\n render() {\n
\n \n \n ({\n post: store.getPost(this.props.postId),\n })\n }}>\n \n \n \n \n \n
\n }\n}\n```\n\nThe state fetched by `connectToStores()` is transferred to the children of FluxComponent. If this auto-magic prop passing feels weird, or if you want direct control over rendering, you can pass a custom render function instead:\n\n```js\nclass BlogPostPage React.Component {\n render() {\n
\n \n \n ({\n post: store.getPost(this.props.postId),\n })\n }}\n render={storeState => {\n // render whatever you want\n return ;\n }}\n />\n \n \n \n
\n }\n}\n```\n\nDo what's right\n---------------\n\nIf I'm leaving you unconvinced, just do what you feel is right. I think components are generally preferable to mixins, but as with any rule, there are exceptions. For instance, [React Tween State](https://github.com/chenglou/react-tween-state) is a great project that wouldn't make sense as a component.\n\nEither way, both fluxMixin and FluxComponent are available for you to use, and both are pretty great :)\n\nIf you have any suggestions for how they could be improved, please let me know by submitting an issue.\n"} -------------------------------------------------------------------------------- /docs/dist/flummox/docs/api.md/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flummox | Minimal, isomorphic Flux 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

API

16 |

Flummox has three exports:

17 |
    18 |
  • Actions
  • 19 |
  • Store
  • 20 |
  • Flux (also available as Flummox) This is the default export.
  • 21 |
22 |

If you’re using ES6 module syntax:

23 |
import { Flux } from 'flummox';
24 | 
25 |

Or multiple classes at once:

26 |
import { Flux, Store, Actions } from 'flummox';
27 | 
28 |

Addons

29 |

Flummox also comes with some addons. These are not part of the main export. That way, if you don’t use them, they won’t increase the size of your bundle.

30 | 35 |

Refer to the React integration guide for details.

36 |
import fluxMixin from 'flummox/mixin';
37 | import FluxComponent from 'flummox/component';
38 | import connectToStores from 'flummox/connect';
39 | 
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/dist/flummox/docs/api/actions.md/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flummox | Minimal, isomorphic Flux 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Actions

16 |

Create actions by extending from the base Actions class.

17 |
class MessageActions extends Actions {
18 | 
19 |   // Methods on the prototype are automatically converted into actions
20 |   newMessage(content) {
21 | 
22 |     // The return value from the action is sent to the dispatcher.
23 |     // It is also returned to the caller.
24 |     return content;
25 |   }
26 | 
27 |   // Asynchronous functions are also supported: just return a promise
28 |   // This is easy using async-await
29 |   async createMessage(messageContent) {
30 |     const response = await serverCreateMessage(messageContent);
31 |     return await response.json();
32 |   }
33 | 
34 | }
35 | 
36 |

You can also use a plain JavaScript object. When passed to flux.createActions, it will be converted into an Actions class.

37 |
// Same as previous example
38 | const MessageActions = {
39 |   newMessage(content) {
40 |     return content;
41 |   },
42 | 
43 |   async createMessage(messageContent) {
44 |     const response = await serverCreateMessage(messageContent);
45 |     return await response.json();
46 |   }
47 | }
48 | 
49 |

Testing

50 |

The return value of an action is dispatched automatically. It’s also returned to the caller. This means it’s possible to test actions completely independently from a Flux or Store instance. Here’s how you’d test the example MessageActions from above:

51 |
// Using mocha and chai-as-promised
52 | const actions = new MessageActions();
53 | 
54 | expect(actions.newMessage('Hello world!')).to.equal('Hello world');
55 | 
56 | // Assuming `serverCreateMessage()` has been mocked
57 | expect(actions.createMessage('Hello world!')).to.eventually.deep.equal({
58 |   id: 1,
59 |   content: 'Hello world!',
60 | });
61 | 
62 |

Asynchronous actions

63 |

Asynchronous actions are actions that return promises. Unlike synchronous actions, async actions fire the dispatcher twice: at the beginning and at the end of the action. Refer to the Store API for information on how to register handlers for asynchronous actions.

64 |

Methods

65 |

getActionIds

66 |
object getActionIds()
67 | 
68 |

Returns an object of action ids, keyed by action name. (In most cases, it’s probably more convenient to use Flux#getActionIds() instead.)

69 |

Also available as getConstants()

70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /docs/dist/flummox/docs/api/actions/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flummox | Minimal, isomorphic Flux 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Actions

16 |

Create actions by extending from the base Actions class.

17 |
class MessageActions extends Actions {
18 | 
19 |   // Methods on the prototype are automatically converted into actions
20 |   newMessage(content) {
21 | 
22 |     // The return value from the action is sent to the dispatcher.
23 |     // It is also returned to the caller.
24 |     return content;
25 |   }
26 | 
27 |   // Asynchronous functions are also supported: just return a promise
28 |   // This is easy using async-await
29 |   async createMessage(messageContent) {
30 |     const response = await serverCreateMessage(messageContent);
31 |     return await response.json();
32 |   }
33 | 
34 | }
35 | 
36 |

You can also use a plain JavaScript object. When passed to flux.createActions, it will be converted into an Actions class.

37 |
// Same as previous example
38 | const MessageActions = {
39 |   newMessage(content) {
40 |     return content;
41 |   },
42 | 
43 |   async createMessage(messageContent) {
44 |     const response = await serverCreateMessage(messageContent);
45 |     return await response.json();
46 |   }
47 | }
48 | 
49 |

Testing

50 |

The return value of an action is dispatched automatically. It’s also returned to the caller. This means it’s possible to test actions completely independently from a Flux or Store instance. Here’s how you’d test the example MessageActions from above:

51 |
// Using mocha and chai-as-promised
52 | const actions = new MessageActions();
53 | 
54 | expect(actions.newMessage('Hello world!')).to.equal('Hello world');
55 | 
56 | // Assuming `serverCreateMessage()` has been mocked
57 | expect(actions.createMessage('Hello world!')).to.eventually.deep.equal({
58 |   id: 1,
59 |   content: 'Hello world!',
60 | });
61 | 
62 |

Asynchronous actions

63 |

Asynchronous actions are actions that return promises. Unlike synchronous actions, async actions fire the dispatcher twice: at the beginning and at the end of the action. Refer to the Store API for information on how to register handlers for asynchronous actions.

64 |

Methods

65 |

getActionIds

66 |
object getActionIds()
67 | 
68 |

Returns an object of action ids, keyed by action name. (In most cases, it’s probably more convenient to use Flux#getActionIds() instead.)

69 |

Also available as getConstants()

70 |
71 | 72 | 73 | 74 | 75 | 76 | 77 | -------------------------------------------------------------------------------- /docs/dist/flummox/docs/api/higher-order-component.md/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flummox | Minimal, isomorphic Flux 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Higher-order Flux Component

16 |

Requires React 0.13. If you’re still on React 0.12, use FluxComponent instead.

17 |

A higher-order component form of FluxComponent. Here’s an example from the Flummox documentation app:

18 |
class HomeHandler extends React.Component {
19 |   render() {
20 |     const { doc } = this.props;
21 | 
22 |     if (!doc) return <span />;
23 | 
24 |     return <Doc doc={doc} />;
25 |   }
26 | }
27 | 
28 | HomeHandler = connectToStores(HomeHandler, {
29 |   docs: store => ({
30 |     doc: store.getDoc('index')
31 |   })
32 | });
33 | 
34 |

Note: FluxComponent, fluxMixin, and the higher-order component implement the same interface. Eventually the docs will updated to make this clearer.

35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/dist/flummox/docs/api/higher-order-component/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flummox | Minimal, isomorphic Flux 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Higher-order Flux Component

16 |

Requires React 0.13. If you’re still on React 0.12, use FluxComponent instead.

17 |

A higher-order component form of FluxComponent. Here’s an example from the Flummox documentation app:

18 |
class HomeHandler extends React.Component {
19 |   render() {
20 |     const { doc } = this.props;
21 | 
22 |     if (!doc) return <span />;
23 | 
24 |     return <Doc doc={doc} />;
25 |   }
26 | }
27 | 
28 | HomeHandler = connectToStores(HomeHandler, {
29 |   docs: store => ({
30 |     doc: store.getDoc('index')
31 |   })
32 | });
33 | 
34 |

Note: FluxComponent, fluxMixin, and the higher-order component implement the same interface. Eventually the docs will updated to make this clearer.

35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /docs/dist/flummox/docs/api/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flummox | Minimal, isomorphic Flux 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |

API

16 |

Flummox has three exports:

17 |
    18 |
  • Actions
  • 19 |
  • Store
  • 20 |
  • Flux (also available as Flummox) This is the default export.
  • 21 |
22 |

If you’re using ES6 module syntax:

23 |
import { Flux } from 'flummox';
24 | 
25 |

Or multiple classes at once:

26 |
import { Flux, Store, Actions } from 'flummox';
27 | 
28 |

Addons

29 |

Flummox also comes with some addons. These are not part of the main export. That way, if you don’t use them, they won’t increase the size of your bundle.

30 | 35 |

Refer to the React integration guide for details.

36 |
import fluxMixin from 'flummox/mixin';
37 | import FluxComponent from 'flummox/component';
38 | import connectToStores from 'flummox/connect';
39 | 
40 |
41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /docs/dist/flummox/docs/guides/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flummox | Minimal, isomorphic Flux 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /docs/docs/api/actions.md: -------------------------------------------------------------------------------- 1 | `Actions` 2 | ========= 3 | 4 | Create actions by extending from the base `Actions` class. 5 | 6 | ```js 7 | class MessageActions extends Actions { 8 | 9 | // Methods on the prototype are automatically converted into actions 10 | newMessage(content) { 11 | 12 | // The return value from the action is sent to the dispatcher. 13 | // It is also returned to the caller. 14 | return content; 15 | } 16 | 17 | // Asynchronous functions are also supported: just return a promise 18 | // This is easy using async-await 19 | async createMessage(messageContent) { 20 | const response = await serverCreateMessage(messageContent); 21 | return await response.json(); 22 | } 23 | 24 | } 25 | ``` 26 | 27 | You can also use a plain JavaScript object. When passed to `flux.createActions`, it will be converted into an Actions class. 28 | 29 | ```js 30 | // Same as previous example 31 | const MessageActions = { 32 | newMessage(content) { 33 | return content; 34 | }, 35 | 36 | async createMessage(messageContent) { 37 | const response = await serverCreateMessage(messageContent); 38 | return await response.json(); 39 | } 40 | } 41 | ``` 42 | 43 | Testing 44 | ------- 45 | 46 | The return value of an action is dispatched automatically. It's also returned to the caller. This means it's possible to test actions completely independently from a Flux or Store instance. Here's how you'd test the example MessageActions from above: 47 | 48 | ```js 49 | // Using mocha and chai-as-promised 50 | const actions = new MessageActions(); 51 | 52 | expect(actions.newMessage('Hello world!')).to.equal('Hello world'); 53 | 54 | // Assuming `serverCreateMessage()` has been mocked 55 | expect(actions.createMessage('Hello world!')).to.eventually.deep.equal({ 56 | id: 1, 57 | content: 'Hello world!', 58 | }); 59 | ``` 60 | 61 | 62 | Asynchronous actions 63 | -------------------- 64 | 65 | Asynchronous actions are actions that return promises. Unlike synchronous actions, async actions fire the dispatcher twice: at the beginning and at the end of the action. Refer to the [Store API](store.md) for information on how to register handlers for asynchronous actions. 66 | 67 | Methods 68 | ------- 69 | 70 | ### getActionIds 71 | 72 | ```js 73 | object getActionIds() 74 | ``` 75 | 76 | Returns an object of action ids, keyed by action name. (In most cases, it's probably more convenient to use `Flux#getActionIds()` instead.) 77 | 78 | 79 | Also available as `getConstants()` 80 | -------------------------------------------------------------------------------- /docs/docs/api/flux.md: -------------------------------------------------------------------------------- 1 | `Flux` 2 | ====== 3 | 4 | Create Flux containers by extending from the base `Flux` (or `Flummox`) class. 5 | 6 | ```js 7 | class Flux extends Flummox { 8 | constructor() { 9 | super(); 10 | 11 | // Create actions first so our store can reference them in 12 | // its constructor 13 | this.createActions('messages', MessageActions); 14 | 15 | // Extra arguments are sent to the store's constructor. Here, we're 16 | // passing a reference to this Flux instance 17 | this.createStore('messages', MessageStore, this); 18 | } 19 | } 20 | ``` 21 | 22 | Encapsulate your stores and actions 23 | ------------------------------------- 24 | 25 | Flummox is designed to be used without singletons. Instead, create a Flux class that encapsulates the creation of all your application's stores and actions, so that you can create new instances on the fly. 26 | 27 | ```js 28 | const flux = new Flux(); 29 | ``` 30 | 31 | (Note that there's nothing technically stopping you from using singletons if you wish, but why would you want to?) 32 | 33 | 34 | Debugging 35 | --------- 36 | 37 | Like Stores, Flux instances are EventEmitters. A `dispatch` event is emitted on every dispatch. Listeners are sent the dispatched payload. This can be used during development for debugging. 38 | 39 | ```js 40 | flux.addListener('dispatch', payload => { 41 | console.log('Dispatch: ', payload); 42 | }); 43 | ``` 44 | 45 | Additionally, an `error` event is emitted when errors occur as a result of an async action. This is both for convenience and to prevent error gobbling. 46 | 47 | Methods 48 | ------- 49 | 50 | ### createActions 51 | 52 | ```js 53 | Actions createActions(string key, function ActionsClass [, ...constructorArgs]) 54 | ``` 55 | 56 | Creates an instance of `ActionsClass` and saves a reference to it. `constructorArgs` are passed to the constructor of `ActionsClass` on creation. 57 | 58 | ### createStore 59 | 60 | ```js 61 | Store createStore(string key, function StoreClass [, ...constructorArgs]) 62 | ``` 63 | 64 | Creates an instance of `StoreClass`, registers the store's handlers with the dispatcher, and saves a reference to it. `constructorArgs` are passed to the constructor of `ActionsClass` on creation. 65 | 66 | ### getStore 67 | 68 | ```js 69 | Store getStore(string key) 70 | ``` 71 | 72 | Gets an store instance by key. 73 | 74 | ### removeStore 75 | 76 | ```js 77 | Store removeStore(string key) 78 | ``` 79 | 80 | Deletes an instance of `StoreClass`, unregisters the store's handlers from dispatcher, and removes all store listeners. 81 | 82 | ### getActions 83 | 84 | ```js 85 | Actions getActions(string key) 86 | ``` 87 | 88 | Gets an actions instance by key. 89 | 90 | ### getActionIds 91 | 92 | ```js 93 | Actions getActionIds(string key) 94 | ``` 95 | 96 | Gets action ids for the given actions key. Internally calls `Actions#getActionIds`. 97 | 98 | Also available as `getConstants()`. 99 | 100 | ### removeActions 101 | 102 | ```js 103 | Actions removeActions(string key) 104 | ``` 105 | 106 | Deletes an actions instance by key. 107 | 108 | Dispatcher methods 109 | ------------------ 110 | 111 | Every Flux instance has its own dispatcher. You should try to avoid interacting with the dispatcher directly, but it is available (primarily for testing purposes) as `this.dispatcher`. Some convenience methods are also provided: 112 | 113 | ### dispatch 114 | ``` 115 | dispatch(string actionId [, * body]) 116 | ``` 117 | 118 | Similar to the `dispatch()` method of the dispatcher itself, except instead of passing a payload, the payload is constructed for you, in the form: 119 | 120 | ```js 121 | { 122 | actionId, 123 | body 124 | } 125 | ``` 126 | 127 | This is used internally by Flummox: the `actionId` field is used to identify the source action, and `body` contains the value passed to store handlers. In your tests, you can use it to simulate a dispatch to your stores. 128 | 129 | ### waitFor 130 | 131 | ``` 132 | waitFor(Store store) 133 | ``` 134 | 135 | Similar to the `waitFor()` method of the dispatcher itself, this can be used within a handler to wait for a different store to respond to the dispatcher before continuing. The operation is synchronous. 136 | 137 | Instead of passing a store, you can also pass a dispatcher token, or an array of tokens and stores. 138 | 139 | Using a custom dispatcher 140 | ------------------------- 141 | 142 | **tl;dr** Flummox uses the flux dispatcher from Facebook, but you can switch out whatever api compatible dispatcher you want. 143 | 144 | *** 145 | 146 | Usually the dispatcher provided by Facebook is sufficient, but you aren't limited to using it if you find you need more than it provides. If you want to have custom behavior when dispatching actions, you can provide a wrapper for the Facebook dispatcher that does what you want. Or use something else entirely. It's up to you. 147 | 148 | To substitute a different dispatcher object just change the `constructor()` function of your flux object like this: 149 | 150 | ```js 151 | 152 | class Flux extends Flummox { 153 | constructor() { 154 | super(); 155 | 156 | this.dispatcher = new MyCustomDispatcher(); 157 | } 158 | } 159 | 160 | ``` 161 | 162 | Just remember, whatever object you provide has to follow the same api as the dispatcher from Facebook. The easiest way to do that is to extend the Facebook dispatcher in a new class, and then provide whatever alternate or extended functionality you desire. 163 | 164 | For instance, say you want to allow the dispatcher to receive actions for dispatching while it is in the middle of another action dispatch. The standard dispatcher will complain that you cannot dispatch an action during another action. There are good reasons for this, but perhaps you just want to queue up that action and have it execute when the current action is completed. One easy way to do this would be to use `setTimeout()`. To do this you would provide a dispatcher with slightly different dispatch functionality, like this: 165 | 166 | ```js 167 | 168 | class MyCustomDispatcher extends Dispatcher { 169 | dispatch(...args) { 170 | if (!this.isDispatching()) { 171 | super.dispatch(...args); // This will execute the Facebook dispatcher's dispatch function. 172 | } else { 173 | setTimeout(() => { // We are currently dispatching, so delay this action using setTimeout 174 | super.dispatch(...args); 175 | }, 0); 176 | } 177 | } 178 | } 179 | 180 | ``` 181 | 182 | Serialization/deserialization 183 | ------------------------------- 184 | 185 | If you're building an isomorphic application, it's often a good idea pass the initial state of your application from the server to the client to avoid unecessary/duplicate HTTP requests. This is easy with Flux, since all of your application state is located in your stores. 186 | 187 | This feature is opt-in on a store-by-store basis, and requires some additional set-up. 188 | 189 | ### serialize 190 | 191 | ``` 192 | string serialize() 193 | ``` 194 | 195 | Returns a serialized string describing the entire state of your Flux application. 196 | 197 | Internally, it passes each store's current state to the store's static method `Store.serialize()`. The return value must be a string representing the given state. If a store does not have a static method `serialize()`, or if it returns a non-string, it is ignored. 198 | 199 | ### deserialize 200 | 201 | ``` 202 | deserialize(string stateString) 203 | ``` 204 | 205 | Converts a serialized state string (as returned from `Flux#serialize()`) to application state and updates the stores. 206 | 207 | Internally, it passes the state string for each store (as returned from `Store.serialize()`) to the store's static method `Store.deserialize()`. The return value must be a state object. It will be passed to `Store#replaceState()`. If a store does not have a static method `deserialize()`, it is ignored. 208 | -------------------------------------------------------------------------------- /docs/docs/api/fluxcomponent.md: -------------------------------------------------------------------------------- 1 | `FluxComponent` 2 | =============== 3 | 4 | **In v3.0, FluxComponent requires React 0.13. If you're still on React 0.12, keep using Flummox 2.x until you're able to upgrade.** 5 | 6 | Access the Flux instance and subscribe to store updates. Refer to the [React integration guide](../guides/react-integration.md) for more information. 7 | 8 | 9 | ```js 10 | ({ 12 | post: store.getPost(this.props.post.id), 13 | }), 14 | comments: store => ({ 15 | comments: store.getCommentsForPost(this.props.post.id), 16 | }) 17 | }}> 18 | 19 | 20 | ``` 21 | 22 | In general, [it's recommended to use FluxComponent instead of fluxMixin](../guides/why-flux-component-is-better-than-flux-mixin.md). 23 | 24 | State getters 25 | ------------- 26 | 27 | The `stateGetter` prop can be used to control how state from stores are transformed into props: 28 | 29 | ```js 30 | ({ 33 | posts: store.getPostForUser(sessionStore.getCurrentUserId()) 34 | })} 35 | }}> 36 | 37 | 38 | ``` 39 | 40 | The `stateGetter` prop behaves differently depending on the value passed to the `connectToStores` prop, refer to [fluxMixin](fluxmixin.md) for more details. 41 | 42 | 43 | Access flux with `this.props.flux` 44 | ---------------------------------- 45 | 46 | In the child component, you can access the Flux instance with `this.props.flux`. For example, to perform an action: 47 | 48 | ```js 49 | onClick(e) { 50 | e.preventDefault(); 51 | this.props.flux.getActions('actionsKey').someAction(); 52 | } 53 | ``` 54 | 55 | Custom rendering 56 | ---------------- 57 | 58 | With FluxComponent, state from your stores is automatically passed as props to its children. This is nice for simple cases, especially when there's only a single child. But for more complex cases, or if you want direct control over rendering, you can pass a custom render function prop to FluxComponent: 59 | 60 | ```js 61 | // Using children 62 | ({ 64 | post: store.getPost(this.props.postId), 65 | }) 66 | }}> 67 | 68 | 69 | 70 | // Using custom `render` function 71 | ({ 74 | post: store.getPost(this.props.postId), 75 | }) 76 | }} 77 | render={storeState => { 78 | // Render whatever you want 79 | return ; 80 | }} 81 | /> 82 | ``` 83 | 84 | Props 85 | ----- 86 | 87 | ### `flux` 88 | 89 | Indicates the [Flux instance](flux.md) to be used. It will be added to the context of all its nested components. If unset, it'll try to infer it from the context. 90 | 91 | ### `connectToStores` 92 | 93 | This prop has the same effect as passing the first argument to [fluxMixin](fluxmixin.md)'s `connectToStores()`. 94 | 95 | ### `stateGetter` 96 | 97 | This prop has the same effect as passing the second argument to [fluxMixin](fluxmixin.md)'s `connectToStores()`. 98 | 99 | ### `render` 100 | 101 | Optionally overrides the rendering function, useful to control what state is passed down as props to components. 102 | -------------------------------------------------------------------------------- /docs/docs/api/fluxmixin.md: -------------------------------------------------------------------------------- 1 | `fluxMixin` 2 | =========== 3 | 4 | Access the Flux instance and subscribe to store updates. Refer to the [React integration guide](../guides/react-integration.md) for more information. 5 | 6 | Note that fluxMixin is actually a function that returns a mixin, as the example below shows. The parameters are the same as those for `connectToStores()`, described below. On component initialization, `connectToStores()` is called and used to set the initial state of the component. 7 | 8 | ```js 9 | import fluxMixin from 'flummox/mixin'; 10 | 11 | let MyComponent = React.createClass({ 12 | 13 | mixins[fluxMixin(['storeA', 'storeB'])], 14 | 15 | ... 16 | }); 17 | ``` 18 | 19 | In general, [it's recommended to use FluxComponent instead of fluxMixin](../guides/why-flux-component-is-better-than-flux-mixin.md). 20 | 21 | State getters 22 | ------------- 23 | 24 | When connecting to stores with fluxMixin (and FluxComponent), you'll usually want to specify custom state getters. 25 | 26 | A state getter is a function which returns a state object for a given store. The state object is merged into the component state using `setState()`. 27 | 28 | The default state getter returns the entire store state. You can specify null as a state getter to use the default state getter. 29 | 30 | Here's an example of a state getter map you would pass to either `fluxMixin()` or `connectToStores()`. The keys are store keys, and the values are state getter functions. 31 | 32 | ```js 33 | fluxMixin({ 34 | posts: (store, props) =>({ 35 | storeA: store.getPost(props.post.id), 36 | }), 37 | comments: (store, props) => ({ 38 | comments: store.getCommentsForPost(props.post.id), 39 | }) 40 | }); 41 | ``` 42 | 43 | In some situations the data retrieved from one store depends on the state from another, you can use an array of store keys with a custom state getter to ensure the components state is updated when either store changes: 44 | 45 | ```js 46 | fluxMixin( 47 | ['posts', 'session'], 48 | 49 | // An array of store instances are passed to the state getter; Instead of indexing 50 | // into the stores array, ES6 array destructuring is used to access each store 51 | // as a variable. 52 | ([postStore, sessionStore]) => ({ 53 | posts: store.getPostForUser(sessionStore.getCurrentUserId()) 54 | }) 55 | ); 56 | ``` 57 | 58 | Access flux with `this.flux` 59 | ---------------------------- 60 | 61 | You can access the Flux instance with `this.flux`. For example, to perform an action: 62 | 63 | ```js 64 | onClick(e) { 65 | e.preventDefault(); 66 | this.flux.getActions('actionsKey').someAction(); 67 | } 68 | ``` 69 | 70 | 71 | Methods 72 | ------- 73 | 74 | ### connectToStores 75 | 76 | ```js 77 | connectToStores(string storeKey [, function stateGetter]) 78 | connectToStores(Array storeKeys [, function stateGetter]) 79 | connectToStores(object stateGetterMap [, function stateGetter]) 80 | ``` 81 | 82 | Synchronize component state with state from Flux stores. Pass a single store key, an array of store keys, or a map of store keys to getter functions. You can also specify a custom state getter as the second argument, the default state getter will return the entire store state (a reduce is performed on the entire store state when using an array or store keys). 83 | 84 | When using an array of store keys, the custom state getter is called with an array of store instances (same order as keys) as the first argument; An array is used instead of separate arguments to allow for future versions of flummox to pass additional arguments to the stateGetter eg. `props`. 85 | 86 | Otherwise only a single store instance is passed to the custom state getter. 87 | 88 | Returns the initial combined state of the specified stores. 89 | 90 | **Usage note**: Generally, you should avoid calling this directly and instead pass arguments to `fluxMixin()`, which calls this internally. 91 | 92 | ### getStoreState 93 | 94 | ``` 95 | getStoreState() 96 | ``` 97 | 98 | Returns current combined state of connected stores. 99 | -------------------------------------------------------------------------------- /docs/docs/api/higher-order-component.md: -------------------------------------------------------------------------------- 1 | Higher-order Flux Component 2 | =========================== 3 | 4 | **Requires React 0.13. If you're still on React 0.12, use FluxComponent instead.** 5 | 6 | A higher-order component form of [FluxComponent](fluxcomponent.md). Here's an example from the [Flummox documentation app](https://github.com/acdlite/flummox/blob/master/docs/src/shared/components/HomeHandler.js#L15-L19): 7 | 8 | ```js 9 | class HomeHandler extends React.Component { 10 | render() { 11 | const { doc } = this.props; 12 | 13 | if (!doc) return ; 14 | 15 | return ; 16 | } 17 | } 18 | 19 | HomeHandler = connectToStores(HomeHandler, { 20 | docs: store => ({ 21 | doc: store.getDoc('index') 22 | }) 23 | }); 24 | ``` 25 | 26 | **Note**: FluxComponent, fluxMixin, and the higher-order component implement the same [interface](https://github.com/acdlite/flummox/blob/master/src/addons/reactComponentMethods.js). Eventually the docs will updated to make this clearer. 27 | -------------------------------------------------------------------------------- /docs/docs/api/index.md: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | Flummox has three exports: 5 | 6 | * [Actions](api/actions.md) 7 | * [Store](api/store.md) 8 | * [Flux](api/flux.md) (also available as `Flummox`) This is the default export. 9 | 10 | If you're using ES6 module syntax: 11 | 12 | ```js 13 | import { Flux } from 'flummox'; 14 | ``` 15 | 16 | Or multiple classes at once: 17 | 18 | ```js 19 | import { Flux, Store, Actions } from 'flummox'; 20 | ``` 21 | 22 | Addons 23 | ------ 24 | 25 | Flummox also comes with some addons. These are not part of the main export. That way, if you don't use them, they won't increase the size of your bundle. 26 | 27 | * [FluxComponent](api/fluxcomponent.md) 28 | * [fluxMixin](api/fluxmixin.md) 29 | * [Higher-order component](api/higher-order-component.md) 30 | 31 | Refer to the [React integration guide](guides/react-integration.md) for details. 32 | 33 | ```js 34 | import fluxMixin from 'flummox/mixin'; 35 | import FluxComponent from 'flummox/component'; 36 | import connectToStores from 'flummox/connect'; 37 | ``` 38 | -------------------------------------------------------------------------------- /docs/docs/guides/index.md: -------------------------------------------------------------------------------- 1 | Guides 2 | ====== 3 | 4 | - [Quick start](guides/quick-start) 5 | - [React integration](guides/react-integration) 6 | - [Why FluxComponent > fluxMixin](guides/why-flux-component-is-better-than-flux-mixin) 7 | -------------------------------------------------------------------------------- /docs/docs/guides/react-integration.md: -------------------------------------------------------------------------------- 1 | # React integration guide 2 | 3 | If you're using Flummox, you're probably also using React. To make React integration incredibly simple, Flummox comes with some optional goodies: [FluxComponent](/flummox/docs/api/fluxcomponent) and [fluxMixin](/flummox/docs/api/fluxmixin). Both have essentially the same functionality — in fact, the component is mostly just a wrapper around the mixin. However, in the spirit of React, the component form is preferred. (Read more about [why FluxComponent is preferred](why-flux-component-is-better-than-flux-mixin).) 4 | 5 | ```js 6 | import FluxComponent from 'flummox/component'; 7 | import fluxMixin from 'flummox/mixin'; 8 | ``` 9 | 10 | This guide discusses how to use FluxComponent to integrate Flummox with React. 11 | 12 | **In v3.0, FluxComponent requires React 0.13. If you're still on React 0.12, keep using Flummox 2.x until you're able to upgrade.** 13 | 14 | ## Accessing the Flux instance 15 | 16 | **tl;dr** FluxComponent gives you easy access to your Flux instance from anywhere in your component tree using only components and props. 17 | 18 | *** 19 | 20 | Unlike most other Flux libraries, Flummox doesn't rely on singletons, because singletons don't work well on the server. The one downside of this approach is that in order to access your stores and actions, you can't just require a module: you have to pass your Flux instance through your component tree. You could do this by manually passing props from one component to the next, but that's not a great solution for components that are nested more than a few levels down. 21 | 22 | A better approach is to use context, which exposes data to arbitrarily deep components. Context in React is currently undocumented, but don't worry: it's a widely-used, safe part of React. (React Router uses context extensively.) 23 | 24 | Context is kind of weird, though. It's awkward to use and easy to abuse, which is probably why it's as-yet undocumented. 25 | 26 | FluxComponent treats context as an implementation detail, so you don't have to deal with it. Pass your Flux instance as a prop, and it will be added to the context of all its nested components. 27 | 28 | Additionally, the immediate children of FluxComponent will be injected with a `flux` prop for easy access. 29 | 30 | ```js 31 | 32 | // Immediate children have flux prop 33 | // flux has been added to context 34 | 35 | ``` 36 | 37 | If flux is already part of the context, you can omit the flux prop on FluxComponent: 38 | 39 | ```js 40 | 41 | // Same as last time: immediate children have flux prop 42 | // flux is already part of context, and remains so 43 | 44 | ``` 45 | 46 | So if you pass a flux instance as a prop to a FluxComponent near the top of your app hierarchy, any FluxComponents further down the tree will automatically have access to it: 47 | 48 | ```js 49 | React.render( 50 | 51 | 52 | , 53 | document.getElementById('app') 54 | ) 55 | ``` 56 | 57 | Pretty simple, right? 58 | 59 | ## Subscribing to store updates 60 | 61 | **tl;dr** FluxComponent synchronizes with the state of your Flux stores and injects the state into its children as props. 62 | *** 63 | 64 | Stores are EventEmitters that emit change events whenever their state changes. To keep up to date, components must get the intial state, add an event listener, save a reference to the listener, and then remove the listener before unmounting to prevent memory leaks. 65 | 66 | This sucks. And it's easy to mess up. 67 | 68 | FluxComponent hides all of these concerns behind a simple component interface. The prop `connectToStores` specifies which stores you want to stay in sync with. FluxComponents's immediate children will be injected with props corresponding to the state of those stores. 69 | 70 | ```js 71 | class OuterComponent extends React.Component { 72 | render() { 73 | return ( 74 | // Pass an array of store keys 75 | 76 | 77 | 78 | ); 79 | } 80 | } 81 | ``` 82 | 83 | If `storeA` has state `{foo: 'bar'}` and `storeB` has state `{bar: 'baz'}`, then InnerComponent has props `foo="bar"` and `bar="baz"`. Whenever the stores change, so do the props. 84 | 85 | `connectToStores` will accept a single store key, an array of store keys, or a map of store keys to getter functions. A getter function is a function which takes a single parameter, the store, and returns an object of props to be injected into the children of FluxComponent. If a null is specified as a getter, the default getter is used instead, which simply returns the entire store state (like in the example above). 86 | 87 | So, in just a few short lines, we can specify the initialization logic, update logic, and listening/unlistening logic for our component. 88 | 89 | ```js 90 | // Pass an object of store keys mapped to getter functions 91 | ({ 93 | post: store.getPost(this.props.post.id), 94 | }), 95 | comments: store => ({ 96 | comments: store.getCommentsForPost(this.props.post.id), 97 | }) 98 | }}> 99 | 100 | 101 | ``` 102 | 103 | In this example, InnerComponent has props `post` and `comments`. If this auto-magic prop passing feels weird, or if you want direct control over rendering, you can pass a custom render function instead. Refer to the [FluxComponent](/flummox/docs/api/fluxcomponent) docs for more information. 104 | 105 | ## Using fluxMixin 106 | 107 | **tl;dr** Just use FluxComponent. (Unless you don't want to. Up to you.) Read a longer explanation for [why FluxComponent is preferred](why-flux-component-is-better-than-flux-mixin). 108 | 109 | *** 110 | 111 | FluxComponent is really just a wrapper around fluxMixin. (Seriously, check out the source.) But if you want to use fluxMixin directly, you can. 112 | 113 | Like FluxComponent, fluxMixin expects that the component you're mixing it into has access to a Flux instance via either a prop or context. It adds the Flux instance to the child context. 114 | 115 | Unlike FluxComponent, it does not inject props into its children. You can, however, access the instance with `this.flux`. 116 | 117 | fluxMixin adds a single method, `connectToStores()`. This is exactly like the `connectToStores` prop of FluxComponent. You can pass a single store key, an array of store keys, or a map of store keys to getter functions. In the single store key form, you can also pass a getter function as the second argument. (This form is not available to FluxComponent because props are single values.) 118 | 119 | fluxMixin does not inject store state as props into its children. Instead, it merges it into component state using `setState()`. 120 | 121 | When you call `connectToStores()`, it returns the current combined state of the stores (as specified by the getters). This is so you can use it within `getInitialState()`. 122 | 123 | However, there is a better way. fluxMixin is actually a function that returns a mixin object. Arguments passed to `fluxMixin()` are automatically sent to `connectToStores()` and used to set the initial state of the component. 124 | 125 | ```js 126 | 127 | const MyComponent = React.createClass({ 128 | 129 | // Remember, you can also use the single key or object forms 130 | mixins[fluxMixin(['storeA', 'storeB'])], 131 | 132 | ... 133 | }); 134 | 135 | ``` 136 | 137 | If `storeA` has state `{foo: 'bar'}` and `storeB` has state `{bar: 'baz'}`, then MyComponent has state `{foo: 'bar', bar: 'baz'}`. Whenever the stores change, so does MyComponent. 138 | -------------------------------------------------------------------------------- /docs/nodemon.json: -------------------------------------------------------------------------------- 1 | { 2 | "watch": [ 3 | "lib/", 4 | "views/", 5 | "data/" 6 | ], 7 | "ext": "js html json", 8 | "env": { 9 | "NODE_ENV": "development" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flummox-docs", 3 | "version": "1.0.0", 4 | "description": "Website for Flummox", 5 | "main": "lib/app.js", 6 | "scripts": { 7 | "prestart": "make fast-build", 8 | "start": "node --harmony lib/server" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/acdlite/flummox.git" 13 | }, 14 | "keywords": [ 15 | "flummox", 16 | "flux", 17 | "react", 18 | "reactjs" 19 | ], 20 | "author": "Andrew Clark ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/acdlite/flummox/issues" 24 | }, 25 | "homepage": "https://github.com/acdlite/flummox", 26 | "dependencies": { 27 | "case": "~1.2.1", 28 | "flummox": "~3.4.0", 29 | "highlight.js": "~8.4.0", 30 | "immutable": "~3.6.4", 31 | "isomorphic-fetch": "~2.0.0", 32 | "koa": "~0.18.1", 33 | "koa-static": "~1.4.9", 34 | "nunjucks": "~1.2.0", 35 | "react": "~0.13.1", 36 | "react-router": "~0.13.2", 37 | "react-style": "~0.5.1", 38 | "react-suitcss": "~1.1.1", 39 | "remarkable": "~1.6.0" 40 | }, 41 | "devDependencies": { 42 | "autoprefixer": "~5.1.0", 43 | "babel": "~4.7.16", 44 | "babel-core": "~4.7.16", 45 | "babel-eslint": "~2.0.2", 46 | "babel-loader": "~4.2.0", 47 | "clean-css": "~3.1.8", 48 | "css-loader": "~0.9.1", 49 | "eslint": "~0.18.0", 50 | "event-stream": "~3.3.0", 51 | "front-matter": "~1.0.0", 52 | "json-sass": "~1.3.3", 53 | "mkdirp": "~0.5.0", 54 | "mz": "~1.3.0", 55 | "react-hot-loader": "~1.2.4", 56 | "readdirp": "~1.3.0", 57 | "style-loader": "~0.9.0", 58 | "watch": "~0.14.0", 59 | "webpack": "~1.7.3", 60 | "webpack-dev-server": "~1.7.0" 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /docs/sass/_dependencies.scss: -------------------------------------------------------------------------------- 1 | @import "dependencies/theme"; 2 | @import "dependencies/typography"; 3 | @import "dependencies/colors"; 4 | @import "dependencies/breakpoints"; 5 | @import "dependencies/z-indices"; 6 | -------------------------------------------------------------------------------- /docs/sass/_utils.scss: -------------------------------------------------------------------------------- 1 | @import "utils/vertical-rhythm"; 2 | -------------------------------------------------------------------------------- /docs/sass/_vendor.scss: -------------------------------------------------------------------------------- 1 | @import url(//fonts.googleapis.com/css?family=Source+Sans+Pro:400,700,700italic|Source+Code+Pro); 2 | 3 | @import 'vendor/normalize'; 4 | @import 'vendor/prism'; 5 | -------------------------------------------------------------------------------- /docs/sass/app.scss: -------------------------------------------------------------------------------- 1 | @import "vendor"; 2 | @import "utils"; 3 | @import "dependencies"; 4 | 5 | * { 6 | box-sizing: border-box; 7 | } 8 | 9 | html { 10 | @include font-family(text); 11 | font-size: 18px; 12 | line-height: 1.5; 13 | background: theme-color(background); 14 | color: theme-color(text); 15 | 16 | // TODO: make site mobile-friendly 17 | min-width: bp(s); 18 | } 19 | 20 | a { 21 | color: theme-color(pink); 22 | } 23 | 24 | ::selection { 25 | background: transparentize(theme-color(pink), 0.7); 26 | } 27 | 28 | h1, h2, h3, h4, h5, h6 { 29 | @include font-family(display); 30 | } 31 | 32 | pre, code { 33 | @include font-family(code); 34 | } 35 | 36 | // Muahahahahaha 37 | .View { 38 | box-sizing: border-box; 39 | display: flex; 40 | position: relative; 41 | 42 | &--flexDirectionColumn { 43 | flex-direction: column; 44 | } 45 | 46 | &--alignItemsFlexEnd { 47 | align-items: flex-end; 48 | } 49 | 50 | &--justifyContentFlexEnd { 51 | justify-content: flex-end; 52 | } 53 | 54 | &--flexGrow { 55 | flex-grow: 1; 56 | } 57 | } 58 | 59 | .Container { 60 | max-width: bp(l); 61 | margin: 0 auto; 62 | padding: 0 rhythm(1); 63 | } 64 | 65 | .HTMLContent { 66 | a { 67 | text-decoration: none; 68 | font-weight: bold; 69 | 70 | &:hover { 71 | border-bottom: 2px solid; 72 | } 73 | } 74 | 75 | h1, h2, h3, h4, h5, h6 { 76 | margin-top: rhythm(3); 77 | margin-bottom: rhythm(1); 78 | 79 | &:first-child { 80 | margin-top: 0; 81 | } 82 | } 83 | 84 | h3, h4, h5, h6 { 85 | margin-top: rhythm(2); 86 | } 87 | 88 | p, pre { 89 | margin: rhythm(1) 0; 90 | } 91 | 92 | pre { 93 | padding: rhythm(1/2 1); 94 | background: white; 95 | font-size: 90%; 96 | border-left: solid rhythm(1/4) theme-color(pink); 97 | transition: border-color 250ms; 98 | color: black; 99 | 100 | @include bp(l) { 101 | padding-left: rhythm(3/4); 102 | margin-left: rhythm(-1); 103 | margin-right: rhythm(-1); 104 | } 105 | 106 | ::selection { 107 | @extend ::selection; 108 | } 109 | } 110 | 111 | hr { 112 | border: 2px solid transparentize(white, 0.9); 113 | margin: rhythm(2); 114 | } 115 | } 116 | 117 | .Doc { 118 | &-content { 119 | @extend .HTMLContent; 120 | } 121 | } 122 | 123 | .RouteTransition { 124 | $transition-duration: 150ms; 125 | 126 | &-enter, &-leave { 127 | @include z-index(RouteTransition); 128 | position: absolute; 129 | width: 100%; 130 | backface-visibility: hidden; 131 | transition: opacity $transition-duration ease-in; 132 | } 133 | 134 | &-enter { 135 | opacity: 0.01; 136 | } 137 | 138 | &-enter.RouteTransition-enter-active { 139 | opacity: 1; 140 | } 141 | 142 | &-leave { 143 | opacity: 1; 144 | } 145 | 146 | &-leave.RouteTransition-leave-active { 147 | opacity: 0.01; 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /docs/sass/dependencies/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | $breakpoints: map-get($theme, 'breakpoints'); 2 | $mediaQueries: map-get($theme, 'sassMediaQueries'); 3 | 4 | @function media-query($name) { 5 | @return unquote(map-get($mediaQueries, $name)); 6 | } 7 | 8 | @function bp($name) { 9 | @return map-get($breakpoints, $name) + px; 10 | } 11 | 12 | @mixin bp($name) { 13 | @media #{media-query($name)} { 14 | @content; 15 | } 16 | } 17 | 18 | $breakpoints: map-get($theme, 'breakpoints'); 19 | -------------------------------------------------------------------------------- /docs/sass/dependencies/_colors.scss: -------------------------------------------------------------------------------- 1 | $colors: map-get($theme, "colors"); 2 | 3 | @function theme-color($name) { 4 | @return map-get($colors, $name); 5 | } 6 | -------------------------------------------------------------------------------- /docs/sass/dependencies/_theme.scss: -------------------------------------------------------------------------------- 1 | $theme: ( 2 | colors: ( 3 | purple: #724685, 4 | darkPurple: #413247, 5 | pink: #ED4F81, 6 | blue: #2083E0, 7 | darkBlue: #1C4467, 8 | text: #fff, 9 | background: #1C4467 10 | ), 11 | zIndices: ( 12 | AppNav: 9999, 13 | RouteTransition: 999 14 | ), 15 | fontFamilies: ( 16 | sourceSans: Source Sans Pro, 17 | sourceCode: Source Code Pro, 18 | text: Source Sans Pro, 19 | display: Source Sans Pro, 20 | code: Source Code Pro 21 | ), 22 | sassFontFamilies: ( 23 | sourceSans: (Source Sans Pro), 24 | sourceCode: (Source Code Pro), 25 | text: (Source Sans Pro), 26 | display: (Source Sans Pro), 27 | code: (Source Code Pro) 28 | ), 29 | breakpoints: ( 30 | s: 500, 31 | m: 768, 32 | l: 950, 33 | xl: 1100, 34 | xxl: 1300 35 | ), 36 | mediaQueries: ( 37 | s: screen and (min-width:500px), 38 | m: screen and (min-width:768px), 39 | l: screen and (min-width:950px), 40 | xl: screen and (min-width:1100px), 41 | xxl: screen and (min-width:1300px) 42 | ), 43 | sassMediaQueries: ( 44 | s: 'screen and (min-width:500px)', 45 | m: 'screen and (min-width:768px)', 46 | l: 'screen and (min-width:950px)', 47 | xl: 'screen and (min-width:1100px)', 48 | xxl: 'screen and (min-width:1300px)' 49 | ) 50 | ); 51 | -------------------------------------------------------------------------------- /docs/sass/dependencies/_typography.scss: -------------------------------------------------------------------------------- 1 | $font-families: map-get($theme, sassFontFamilies); 2 | $font-sizes: map-get($theme, fontSizes); 3 | 4 | @function font-family($name) { 5 | @return map-get($font-families, $name); 6 | } 7 | 8 | @mixin font-family($name) { 9 | font-family: font-family($name); 10 | } 11 | 12 | @function font-size($name) { 13 | @return map-get($font-sizes, $name); 14 | } 15 | -------------------------------------------------------------------------------- /docs/sass/dependencies/_z-indices.scss: -------------------------------------------------------------------------------- 1 | $z-indices: map-get($theme, zIndices); 2 | 3 | @function z-index($name, $offset: 0) { 4 | @return map-get($z-indices, $name) + $offset; 5 | } 6 | 7 | @mixin z-index($args...) { 8 | z-index: z-index($args...); 9 | } 10 | -------------------------------------------------------------------------------- /docs/sass/utils/_vertical-rhythm.scss: -------------------------------------------------------------------------------- 1 | $base-font-size: 1rem !global; 2 | $base-line-height: $base-font-size * 1.5 !global; 3 | $round-to-nearest-half-line: true !global; 4 | 5 | /** 6 | * Convert a unitless value to vertical rhythm units. 7 | * Non-unitless numbers are ignored. 8 | * @param {number | list} $values - Number of lines 9 | * @return {number | list} Output 10 | */ 11 | @function rhythm($value) { 12 | @if type-of($value) == list { 13 | $list: $value; 14 | $separator: list-separator($list); 15 | $rhythm: (); 16 | 17 | @each $item in $list { 18 | $rhythm: append($rhythm, rhythm($item), $separator); 19 | } 20 | 21 | @return $rhythm; 22 | } 23 | 24 | @else if type-of($value) == number and unit($value) == "" and $value != 0 { 25 | $lines: $value; 26 | $value: $lines * $base-line-height; 27 | } 28 | 29 | @return $value; 30 | } 31 | -------------------------------------------------------------------------------- /docs/sass/vendor/_prism.scss: -------------------------------------------------------------------------------- 1 | /* http://prismjs.com/download.html?themes=prism&languages=markup+css+clike+javascript+jsx */ 2 | /** 3 | * prism.js default theme for JavaScript, CSS and HTML 4 | * Based on dabblet (http://dabblet.com) 5 | * @author Lea Verou 6 | */ 7 | 8 | code[class*="language-"], 9 | pre[class*="language-"] { 10 | color: black; 11 | text-shadow: 0 1px white; 12 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 13 | direction: ltr; 14 | text-align: left; 15 | white-space: pre; 16 | word-spacing: normal; 17 | word-break: normal; 18 | line-height: 1.5; 19 | 20 | -moz-tab-size: 4; 21 | -o-tab-size: 4; 22 | tab-size: 4; 23 | 24 | -webkit-hyphens: none; 25 | -moz-hyphens: none; 26 | -ms-hyphens: none; 27 | hyphens: none; 28 | } 29 | 30 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection, 31 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection { 32 | text-shadow: none; 33 | background: #b3d4fc; 34 | } 35 | 36 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection, 37 | code[class*="language-"]::selection, code[class*="language-"] ::selection { 38 | text-shadow: none; 39 | background: #b3d4fc; 40 | } 41 | 42 | @media print { 43 | code[class*="language-"], 44 | pre[class*="language-"] { 45 | text-shadow: none; 46 | } 47 | } 48 | 49 | /* Code blocks */ 50 | pre[class*="language-"] { 51 | padding: 1em; 52 | margin: .5em 0; 53 | overflow: auto; 54 | } 55 | 56 | :not(pre) > code[class*="language-"], 57 | pre[class*="language-"] { 58 | background: #f5f2f0; 59 | } 60 | 61 | /* Inline code */ 62 | :not(pre) > code[class*="language-"] { 63 | padding: .1em; 64 | border-radius: .3em; 65 | } 66 | 67 | .token.comment, 68 | .token.prolog, 69 | .token.doctype, 70 | .token.cdata { 71 | color: slategray; 72 | } 73 | 74 | .token.punctuation { 75 | color: #999; 76 | } 77 | 78 | .namespace { 79 | opacity: .7; 80 | } 81 | 82 | .token.property, 83 | .token.tag, 84 | .token.boolean, 85 | .token.number, 86 | .token.constant, 87 | .token.symbol, 88 | .token.deleted { 89 | color: #905; 90 | } 91 | 92 | .token.selector, 93 | .token.attr-name, 94 | .token.string, 95 | .token.char, 96 | .token.builtin, 97 | .token.inserted { 98 | color: #690; 99 | } 100 | 101 | .token.operator, 102 | .token.entity, 103 | .token.url, 104 | .language-css .token.string, 105 | .style .token.string { 106 | color: #a67f59; 107 | background: hsla(0, 0%, 100%, .5); 108 | } 109 | 110 | .token.atrule, 111 | .token.attr-value, 112 | .token.keyword { 113 | color: #07a; 114 | } 115 | 116 | .token.function { 117 | color: #DD4A68; 118 | } 119 | 120 | .token.regex, 121 | .token.important, 122 | .token.variable { 123 | color: #e90; 124 | } 125 | 126 | .token.important, 127 | .token.bold { 128 | font-weight: bold; 129 | } 130 | .token.italic { 131 | font-style: italic; 132 | } 133 | 134 | .token.entity { 135 | cursor: help; 136 | } 137 | 138 | -------------------------------------------------------------------------------- /docs/src/client/app.js: -------------------------------------------------------------------------------- 1 | require('../shared/init'); 2 | import './init'; 3 | 4 | import React from 'react'; 5 | import Router from 'react-router'; 6 | import FluxComponent from 'flummox/component'; 7 | import Flux from '../shared/Flux'; 8 | import routes from '../shared/routes'; 9 | import performRouteHandlerStaticMethod from '../shared/utils/performRouteHandlerStaticMethod'; 10 | import url from 'url'; 11 | 12 | // Initialize flux 13 | const flux = new Flux(); 14 | 15 | const router = Router.create({ 16 | routes: routes, 17 | location: Router.HistoryLocation 18 | }); 19 | 20 | // Render app 21 | router.run(async (Handler, state) => { 22 | const routeHandlerInfo = { state, flux }; 23 | 24 | await performRouteHandlerStaticMethod(state.routes, 'routerWillRun', routeHandlerInfo); 25 | 26 | React.render( 27 | 28 | 29 | , 30 | document.getElementById('app') 31 | ); 32 | }); 33 | 34 | // Intercept local route changes 35 | document.onclick = event => { 36 | const { toElement: target } = event; 37 | 38 | if (!target) return; 39 | 40 | if (target.tagName !== 'A') return; 41 | 42 | const href = target.getAttribute('href'); 43 | 44 | if (!href) return; 45 | 46 | const resolvedHref = url.resolve(window.location.href, href); 47 | const { host, path } = url.parse(resolvedHref); 48 | 49 | if (host === window.location.host) { 50 | event.preventDefault(); 51 | router.transitionTo(path); 52 | } 53 | }; 54 | -------------------------------------------------------------------------------- /docs/src/client/init.js: -------------------------------------------------------------------------------- 1 | import './vendor/prism'; 2 | -------------------------------------------------------------------------------- /docs/src/scripts/build-docs.js: -------------------------------------------------------------------------------- 1 | require('../shared/init'); 2 | 3 | import frontmatter from 'front-matter'; 4 | import { writeFile, readFile } from 'mz/fs'; 5 | import path from 'path'; 6 | import readdirp from 'readdirp'; 7 | import _mkdirp from 'mkdirp'; 8 | import { map, writeArray } from 'event-stream'; 9 | 10 | const docsRoot = path.join(process.cwd(), 'docs'); 11 | const dataRoot = path.join(process.cwd(), 'public/flummox/data'); 12 | 13 | const docStream = readdirp({ root: docsRoot, fileFilter: '*.md' }) 14 | .pipe(map((entry, cb) => { 15 | async () => { 16 | const { fullPath, path } = entry; 17 | 18 | const contents = await readFile(fullPath, 'utf8'); 19 | 20 | const { attributes, body } = frontmatter(contents); 21 | 22 | return { 23 | path: path.slice(0, -3), // remove .md extension 24 | content: body, 25 | ...attributes, 26 | }; 27 | }() 28 | .then( 29 | result => cb(null, result), 30 | err => cb(err) 31 | ); 32 | })); 33 | 34 | // Write to individual JSON document 35 | docStream.pipe(map((doc, cb) => { 36 | async () => { 37 | const dest = path.format({ 38 | root: '/', 39 | dir: path.join(dataRoot, 'docs', path.dirname(doc.path)), 40 | base: `${path.basename(doc.path)}.json`, 41 | }); 42 | 43 | const dir = path.dirname(dest); 44 | 45 | await mkdirp(dir); 46 | 47 | await writeFile(dest, JSON.stringify(doc)); 48 | }() 49 | .then( 50 | () => cb(null), 51 | err => cb(err) 52 | ); 53 | })); 54 | 55 | // Write combined JSON document 56 | docStream.pipe(writeArray((_, docs) => { 57 | writeFile(path.join(dataRoot, 'allDocs.json'), JSON.stringify(docs)); 58 | })); 59 | 60 | function mkdirp(dir) { 61 | return new Promise((resolve, reject) => _mkdirp(dir, (err) => { 62 | if (err) reject(err); 63 | resolve(); 64 | })); 65 | } 66 | -------------------------------------------------------------------------------- /docs/src/scripts/build-static-site.js: -------------------------------------------------------------------------------- 1 | require('../server'); 2 | 3 | import { exec } from 'mz/child_process'; 4 | import { rename } from 'mz/fs'; 5 | import _mkdirp from 'mkdirp'; 6 | import readdirp from 'readdirp'; 7 | import { map } from 'event-stream'; 8 | import path from 'path'; 9 | 10 | const outputRoot = path.resolve(process.cwd(), 'dist'); 11 | 12 | async () => { 13 | await exec('wget -p -P dist/ -m -nH -erobots=off --reject index.html --html-extension http://localhost:3000/flummox'); 14 | await exec('cp -a public/flummox/data dist/flummox/data'); 15 | 16 | const docStream = readdirp({ root: outputRoot, fileFilter: '*.html' }) 17 | .pipe(map((entry, cb) => { 18 | const { path: relativePath } = entry; 19 | const { name } = path.parse(relativePath); 20 | const originalPath = path.resolve(outputRoot, relativePath); 21 | const dir = path.resolve(originalPath, `../${name}`); 22 | const newPath = path.resolve(dir, 'index.html'); 23 | 24 | async () => { 25 | await mkdirp(dir); 26 | await rename(originalPath, newPath); 27 | cb(null); 28 | }().catch(error => cb(error)); 29 | })) 30 | .on('error', error => console.log(error)) 31 | .on('end', () => { 32 | process.exit(0); 33 | }); 34 | }(); 35 | 36 | function mkdirp(dir) { 37 | return new Promise((resolve, reject) => _mkdirp(dir, (err) => { 38 | if (err) reject(err); 39 | resolve(); 40 | })); 41 | } 42 | -------------------------------------------------------------------------------- /docs/src/server/appView.js: -------------------------------------------------------------------------------- 1 | // Render React app 2 | import React from 'react'; 3 | import Router from 'react-router'; 4 | import FluxComponent from 'flummox/component'; 5 | import Flux from '../shared/Flux'; 6 | import routes from '../shared/routes'; 7 | import performRouteHandlerStaticMethod from '../shared/utils/performRouteHandlerStaticMethod'; 8 | import nunjucks from 'nunjucks'; 9 | 10 | nunjucks.configure('views', { 11 | autoescape: true, 12 | }); 13 | 14 | export default function(app) { 15 | app.use(function *() { 16 | const router = Router.create({ 17 | routes: routes, 18 | location: this.url, 19 | onError: error => { 20 | throw error; 21 | }, 22 | onAbort: abortReason => { 23 | const error = new Error(); 24 | 25 | if (abortReason.constructor.name === 'Redirect') { 26 | const { to, params, query } = abortReason; 27 | const url = router.makePath(to, params, query); 28 | error.redirect = url; 29 | } 30 | 31 | throw error; 32 | } 33 | }); 34 | 35 | const flux = new Flux(); 36 | 37 | let appString; 38 | 39 | try { 40 | const { Handler, state } = yield new Promise((resolve, reject) => { 41 | router.run((_Handler, _state) => 42 | resolve({ Handler: _Handler, state: _state }) 43 | ); 44 | }); 45 | 46 | const routeHandlerInfo = { state, flux }; 47 | 48 | try { 49 | yield performRouteHandlerStaticMethod(state.routes, 'routerWillRun', routeHandlerInfo); 50 | } catch (error) {} 51 | 52 | 53 | appString = React.renderToString( 54 | 55 | 56 | 57 | ); 58 | } catch (error) { 59 | if (error.redirect) { 60 | return this.redirect(error.redirect); 61 | } 62 | 63 | throw error; 64 | } 65 | 66 | this.body = nunjucks.render('index.html', { 67 | appString, 68 | env: process.env, 69 | }); 70 | }); 71 | } 72 | -------------------------------------------------------------------------------- /docs/src/server/index.js: -------------------------------------------------------------------------------- 1 | require('../shared/init'); 2 | 3 | import koa from 'koa'; 4 | const app = koa(); 5 | export default app; 6 | 7 | // Serve static assets from `public` directory 8 | import serve from 'koa-static'; 9 | app.use(serve('public')); 10 | 11 | import appView from './appView'; 12 | appView(app); 13 | 14 | // Start listening 15 | const port = process.env.PORT || 3000; 16 | app.listen(port); 17 | console.log(`App started listening on port ${port}`); 18 | -------------------------------------------------------------------------------- /docs/src/server/webpack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('../shared/init'); 4 | 5 | // Start webpack server 6 | import webpack from 'webpack'; 7 | import WebpackDevServer from 'webpack-dev-server'; 8 | import config from '../../webpack.config.dev'; 9 | 10 | new WebpackDevServer(webpack(config), { 11 | publicPath: config.output.publicPath, 12 | hot: true, 13 | }) 14 | .listen(8081, 'localhost', function (err, result) { 15 | if (err) console.log(err); 16 | 17 | console.log('Dev server listening at localhost:8081'); 18 | }); 19 | -------------------------------------------------------------------------------- /docs/src/shared/Flux.js: -------------------------------------------------------------------------------- 1 | import { Flummox } from 'flummox'; 2 | import DocActions from './actions/DocActions'; 3 | import DocStore from './stores/DocStore'; 4 | 5 | export default class Flux extends Flummox { 6 | constructor() { 7 | super(); 8 | 9 | const docActions = this.createActions('docs', DocActions); 10 | this.createStore('docs', DocStore, { docActions }); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /docs/src/shared/actions/DocActions.js: -------------------------------------------------------------------------------- 1 | import { Actions } from 'flummox'; 2 | import { siteUrl } from '../utils/UrlUtils'; 3 | 4 | export default class DocActions extends Actions { 5 | async getDoc(path) { 6 | let response; 7 | 8 | try { 9 | const url = siteUrl(`/flummox/data/docs/${path}.json`); 10 | response = await fetch(url); 11 | return await response.json(); 12 | } catch (error) { 13 | const url = siteUrl(`/flummox/data/docs/${path}/index.json`); 14 | response = await fetch(url); 15 | return await response.json(); 16 | } 17 | } 18 | 19 | async getAllDocs() { 20 | const url = siteUrl('/flummox/data/allDocs.json'); 21 | const response = await fetch(url); 22 | return await response.json(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /docs/src/shared/components/AppHandler.js: -------------------------------------------------------------------------------- 1 | import React from 'react/addons'; 2 | import { RouteHandler } from 'react-router'; 3 | import AppNav from './AppNav'; 4 | 5 | const { CSSTransitionGroup } = React.addons; 6 | 7 | class AppHandler extends React.Component { 8 | static willTransitionTo(transition) { 9 | const { path } = transition; 10 | 11 | if (path !== '/' && path.endsWith('/')) { 12 | transition.redirect(path.substring(0, path.length - 1)); 13 | } 14 | } 15 | 16 | // Fetch all docs on start up, since there aren't that many 17 | static async routerWillRun({ flux }) { 18 | const docActions = flux.getActions('docs'); 19 | return await docActions.getAllDocs(); 20 | } 21 | 22 | render() { 23 | return ( 24 |
25 | 26 | 27 | 28 | 29 |
30 | ); 31 | } 32 | } 33 | 34 | export default AppHandler; 35 | -------------------------------------------------------------------------------- /docs/src/shared/components/AppNav.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Link } from 'react-router'; 3 | import connectToStores from 'flummox/connect'; 4 | import View from './View'; 5 | import { rhythm, color, zIndex } from '../theme'; 6 | import StyleSheet from 'react-style'; 7 | 8 | const navColor = color('blue'); 9 | 10 | const styles = StyleSheet.create({ 11 | navWrapper: { 12 | backgroundColor: navColor, 13 | position: 'fixed', 14 | width: '100%', 15 | zIndex: zIndex('AppNav'), 16 | }, 17 | 18 | subNav: { 19 | display: 'none', 20 | backgroundColor: navColor, 21 | position: 'absolute', 22 | right: 0, 23 | top: '100%', 24 | width: '16em', 25 | textAlign: 'right', 26 | }, 27 | 28 | text: { 29 | padding: `${rhythm(1 / 4)} ${rhythm(1 / 2)}`, 30 | color: '#fff', 31 | width: '100%', 32 | textDecoration: 'none', 33 | border: 'inherit solid 1px', 34 | } 35 | }); 36 | 37 | class AppNav extends React.Component { 38 | render() { 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | } 65 | } 66 | 67 | AppNav = connectToStores(AppNav, 'docs'); 68 | 69 | class AppNavLink extends React.Component { 70 | constructor() { 71 | super(); 72 | 73 | this.state = { 74 | hover: false, 75 | }; 76 | } 77 | 78 | render() { 79 | const { children, title, href, level, ...props } = this.props; 80 | 81 | let subNav; 82 | if (children) { 83 | 84 | const dynamicStyles = { 85 | display: this.state.hover ? null : 'none' 86 | }; 87 | 88 | subNav = ( 89 | 93 | {children} 94 | 95 | ); 96 | } 97 | 98 | const dynamicInlineLinkStyles = { 99 | backgroundColor: this.state.hover ? color('darkBlue') : null, 100 | borderColor: this.state.hover ? color('blue') : null, 101 | }; 102 | 103 | return ( 104 | this.setState({ hover: true })} 106 | onMouseLeave={() => this.setState({ hover: false })} 107 | {...props} 108 | > 109 | 110 | {title} 111 | 112 | {subNav} 113 | 114 | ); 115 | } 116 | } 117 | 118 | AppNavLink.defaultProps = { 119 | level: 1, 120 | }; 121 | 122 | export default AppNav; 123 | -------------------------------------------------------------------------------- /docs/src/shared/components/Container.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Container extends React.Component { 4 | render() { 5 | const { className, ...props } = this.props; 6 | 7 | return ( 8 |
12 | ); 13 | } 14 | } 15 | 16 | export default Container; 17 | -------------------------------------------------------------------------------- /docs/src/shared/components/Doc.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import Container from './Container'; 4 | import Markdown from './Markdown'; 5 | import { rhythm } from '../theme'; 6 | 7 | 8 | class Doc extends React.Component { 9 | render() { 10 | const { doc } = this.props; 11 | 12 | if (!doc) return null; 13 | 14 | return ( 15 |
18 | 19 | 20 | 21 |
22 | ); 23 | } 24 | } 25 | 26 | export default Doc; 27 | -------------------------------------------------------------------------------- /docs/src/shared/components/DocHandler.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Flux from 'flummox/component'; 3 | import View from './View'; 4 | import Doc from './Doc'; 5 | 6 | class DocHandler extends React.Component { 7 | static willTransitionTo(transition, params) { 8 | const { splat: docPath } = params; 9 | 10 | const canonicalPath = DocHandler.canonicalDocPath(docPath); 11 | 12 | if (docPath !== canonicalPath) transition.redirect(`/flummox/docs/${canonicalPath}`); 13 | } 14 | 15 | // Redundant since docs have already been fetched, but included for illustration 16 | static async routerWillRun({ flux, state }) { 17 | const docActions = flux.getActions('docs'); 18 | const { params: { splat: path } } = state; 19 | 20 | const canonicalPath = DocHandler.canonicalDocPath(path); 21 | 22 | return await docActions.getDoc(canonicalPath); 23 | } 24 | 25 | static canonicalDocPath(docPath) { 26 | docPath = docPath.replace(/\.md$/, ''); 27 | docPath = docPath.replace(/\/index\/?$/, ''); 28 | 29 | return docPath; 30 | } 31 | 32 | getDocPath() { 33 | const { params: { splat: docPath } } = this.props; 34 | 35 | return DocHandler.canonicalDocPath(docPath); 36 | } 37 | 38 | render() { 39 | const docPath = this.getDocPath(); 40 | 41 | return ( 42 |
43 | ({ 47 | doc: store.getDoc(props.docPath) 48 | }) 49 | }} 50 | render={({ doc }) => } 51 | /> 52 |
53 | ); 54 | } 55 | } 56 | 57 | export default DocHandler; 58 | -------------------------------------------------------------------------------- /docs/src/shared/components/HomeHandler.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import connectToStores from 'flummox/connect'; 3 | import Doc from './Doc'; 4 | 5 | class HomeHandler extends React.Component { 6 | render() { 7 | const { doc } = this.props; 8 | 9 | if (!doc) return ; 10 | 11 | return ; 12 | } 13 | } 14 | 15 | HomeHandler = connectToStores(HomeHandler, { 16 | docs: store => ({ 17 | doc: store.getDoc('index') 18 | }) 19 | }); 20 | 21 | export default HomeHandler; 22 | -------------------------------------------------------------------------------- /docs/src/shared/components/Markdown.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Remarkable from 'remarkable'; 3 | 4 | const md = new Remarkable({ 5 | html: true, 6 | linkify: true, 7 | typographer: true, 8 | highlight: (str, lang) => { 9 | if (typeof Prism === 'undefined') return; 10 | 11 | if (lang === 'js') lang = 'javascript'; 12 | 13 | if (!lang || !Prism.languages[lang]) return; 14 | 15 | return Prism.highlight(str, Prism.languages[lang]); 16 | } 17 | }); 18 | 19 | class Markdown extends React.Component { 20 | 21 | render() { 22 | const { src, ...props } = this.props; 23 | const html = md.render(src); 24 | 25 | return ( 26 |
30 | ); 31 | } 32 | } 33 | 34 | Markdown.defaultProps = { 35 | src: '', 36 | }; 37 | 38 | export default Markdown; 39 | -------------------------------------------------------------------------------- /docs/src/shared/components/View.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SuitCSS from 'react-suitcss'; 3 | import { pascal } from 'case'; 4 | 5 | const View = React.createClass({ 6 | render() { 7 | const { 8 | component, 9 | flexDirection, 10 | alignItems, 11 | justifyContent, 12 | ...props} = this.props; 13 | 14 | if (flexDirection) { 15 | props[`flexDirection${pascal(flexDirection)}`] = true; 16 | } 17 | 18 | if (alignItems) { 19 | props[`alignItems${pascal(alignItems)}`] = true; 20 | } 21 | 22 | if (justifyContent) { 23 | props[`justifyContent${pascal(justifyContent)}`] = true; 24 | } 25 | 26 | return ( 27 | 38 | ); 39 | } 40 | }); 41 | 42 | View.defaultProps = { 43 | component: 'div', 44 | }; 45 | 46 | export default View; 47 | -------------------------------------------------------------------------------- /docs/src/shared/init.js: -------------------------------------------------------------------------------- 1 | require('babel/external-helpers'); 2 | import 'babel/polyfill'; 3 | 4 | import 'isomorphic-fetch'; 5 | -------------------------------------------------------------------------------- /docs/src/shared/routes.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Route, Redirect, DefaultRoute } from 'react-router'; 3 | import AppHandler from './components/AppHandler'; 4 | import HomeHandler from './components/HomeHandler'; 5 | import DocHandler from './components/DocHandler'; 6 | 7 | export default ( 8 | 9 | 10 | 11 | 12 | 13 | ); 14 | -------------------------------------------------------------------------------- /docs/src/shared/stores/DocStore.js: -------------------------------------------------------------------------------- 1 | import { Store } from 'flummox'; 2 | import { Map } from 'immutable'; 3 | 4 | export default class DocStore extends Store { 5 | constructor({ docActions }) { 6 | super(); 7 | 8 | this.register(docActions.getDoc, this.handleNewDoc); 9 | this.register(docActions.getAllDocs, this.handleNewDocs); 10 | 11 | this.state = { 12 | docs: new Map() 13 | }; 14 | } 15 | 16 | handleNewDoc(newDoc) { 17 | const docs = { 18 | [newDoc.path]: newDoc 19 | }; 20 | 21 | this.setState({ 22 | docs: this.state.docs.merge(docs) 23 | }); 24 | } 25 | 26 | handleNewDocs(newDocs) { 27 | const docs = newDocs.reduce((result, doc) => { 28 | result[doc.path] = doc; 29 | return result; 30 | }, {}); 31 | 32 | this.setState({ 33 | docs: this.state.docs.merge(docs) 34 | }); 35 | } 36 | 37 | getDoc(path) { 38 | let doc = this.state.docs.find(doc => doc.get('path') === path); 39 | 40 | if (!doc) { 41 | doc = this.state.docs.find(doc => doc.get('path') === `${path}/index`); 42 | } 43 | 44 | return doc; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /docs/src/shared/theme.js: -------------------------------------------------------------------------------- 1 | export const colors = { 2 | purple: '#724685', 3 | darkPurple: '#413247', 4 | pink: '#ED4F81', 5 | blue: '#2083E0', 6 | darkBlue: '#1C4467', 7 | }; 8 | 9 | export const zIndices = { 10 | AppNav: 9999, 11 | RouteTransition: 999, 12 | }; 13 | 14 | export function zIndex(name, offset = 0) { 15 | return zIndices[name] + offset; 16 | } 17 | 18 | colors.text = '#fff'; 19 | colors.background = colors.darkBlue; 20 | 21 | export function color(name) { 22 | return colors[name]; 23 | } 24 | 25 | export const fontFamilies = { 26 | sourceSans: 'Source Sans Pro', 27 | sourceCode: 'Source Code Pro', 28 | }; 29 | 30 | fontFamilies.text = fontFamilies.sourceSans; 31 | fontFamilies.display = fontFamilies.sourceSans; 32 | fontFamilies.code = fontFamilies.sourceCode; 33 | 34 | export const sassFontFamilies = mapObject(fontFamilies, wrapSassExpression); 35 | 36 | export function rhythm(lines = 1) { 37 | return `${lines * 1.5}rem`; 38 | } 39 | 40 | export const breakpoints = { 41 | s: 500, 42 | m: 768, 43 | l: 950, 44 | xl: 1100, 45 | xxl: 1300, 46 | }; 47 | 48 | export const mediaQueries = mapObject(breakpoints, 49 | breakpoint => `screen and (min-width:${breakpoint}px)` 50 | ); 51 | 52 | export const sassMediaQueries = mapObject(mediaQueries, mq => `'${mq}'`); 53 | 54 | /** 55 | * Wrap a Sass string in parentheses. 56 | * @param {String} sassString 57 | * @returns {String} 58 | */ 59 | function wrapSassExpression(sassString) { 60 | return '(' + sassString + ')'; 61 | } 62 | 63 | /** 64 | * Takes an object and returns a new object by applying a transformation to 65 | * each value. 66 | * @param {object} object - Original object 67 | * @param {function} transform - Transformation function 68 | * @return {object} New object with transformed values 69 | */ 70 | function mapObject(object, transform) { 71 | return Object.keys(object).reduce((result, key) => { 72 | result[key] = transform(object[key]); 73 | return result; 74 | }, {}); 75 | } 76 | -------------------------------------------------------------------------------- /docs/src/shared/utils/UrlUtils.js: -------------------------------------------------------------------------------- 1 | import url from 'url'; 2 | 3 | let baseUrl; 4 | 5 | if (typeof window === 'undefined') { 6 | const port = process.env.PORT || 3000; 7 | baseUrl = `http://localhost:${port}`; 8 | } else { 9 | baseUrl = '/'; 10 | } 11 | 12 | export function siteUrl(to) { 13 | if (typeof to === 'undefined') return baseUrl; 14 | 15 | return url.resolve(baseUrl, to); 16 | } 17 | -------------------------------------------------------------------------------- /docs/src/shared/utils/performRouteHandlerStaticMethod.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Accepts an array of matched routes as returned from react-router's 3 | * `Router.run()` and calls the given static method on each. The methods may 4 | * return a promise. 5 | * 6 | * Returns a promise that resolves after any promises returned by the routes 7 | * resolve. The practical uptake is that you can wait for your data to be 8 | * fetched before continuing. Based off react-router's async-data example 9 | * https://github.com/rackt/react-router/blob/master/examples/async-data/app.js#L121 10 | * @param {array} routes - Matched routes 11 | * @param {string} methodName - Name of static method to call 12 | * @param {...any} ...args - Arguments to pass to the static method 13 | */ 14 | export default async function performRouteHandlerStaticMethod(routes, methodName, ...args) { 15 | return Promise.all(routes 16 | .map(route => route.handler[methodName]) 17 | .filter(method => typeof method === 'function') 18 | .map(method => method(...args)) 19 | ); 20 | } 21 | -------------------------------------------------------------------------------- /docs/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Flummox | Minimal, isomorphic Flux 7 | 8 | 9 | 10 | {% if env.NODE_ENV != 'development' %} 11 | 12 | {% endif %} 13 | 14 | 15 |
{{ appString | safe }}
16 | 17 | {% if env.NODE_ENV == 'development' %} 18 | 19 | {% else %} 20 | 21 | {% endif %} 22 | 23 | 24 | -------------------------------------------------------------------------------- /docs/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | var path = require('path'); 5 | 6 | module.exports = { 7 | devtool: 'inline-source-map', 8 | entry: [ 9 | 'webpack-dev-server/client?http://localhost:8081', 10 | 'webpack/hot/only-dev-server', 11 | './public/flummox/css/app.css', 12 | './src/client/app', 13 | ], 14 | output: { 15 | path: path.join(__dirname, '/public/flummox/js/'), 16 | filename: 'app.js', 17 | publicPath: 'http://localhost:8081/flummox/js/', 18 | }, 19 | plugins: [ 20 | new webpack.HotModuleReplacementPlugin(), 21 | new webpack.NoErrorsPlugin(), 22 | new webpack.DefinePlugin({ 23 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 24 | 'process.env.BASE_URL': JSON.stringify(process.env.BASE_URL), 25 | }), 26 | ], 27 | resolve: { 28 | extensions: ['', '.js'] 29 | }, 30 | module: { 31 | loaders: [ 32 | { test: /\.jsx?$/, loaders: ['react-hot', 'babel-loader?experimental&externalHelpers'], exclude: /node_modules/ }, 33 | { test: /\.css/, loader: "style-loader!css-loader" }, 34 | ] 35 | } 36 | }; 37 | -------------------------------------------------------------------------------- /docs/webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | var path = require('path'); 5 | 6 | module.exports = { 7 | entry: './src/client/app', 8 | output: { 9 | path: path.join(__dirname, '/public/flummox/js/'), 10 | filename: 'app.min.js', 11 | publicPath: '/flummox/js/' 12 | }, 13 | plugins: [ 14 | new webpack.DefinePlugin({ 15 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), 16 | }), 17 | new webpack.optimize.UglifyJsPlugin({ 18 | compress: { 19 | warnings: false 20 | } 21 | }), 22 | ], 23 | resolve: { 24 | extensions: ['', '.js'] 25 | }, 26 | module: { 27 | loaders: [ 28 | { test: /\.jsx?$/, loaders: ['babel-loader?experimental&externalHelpers'], exclude: /node_modules/ } 29 | ] 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /mixin.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/addons/fluxMixin'); 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "flummox", 3 | "version": "3.6.9", 4 | "description": "Idiomatic, modular, testable, isomorphic Flux. No singletons required.", 5 | "main": "lib/Flux.js", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/acdlite/flummox.git" 9 | }, 10 | "homepage": "https://github.com/acdlite/flummox/blob/latest/README.md", 11 | "bugs": "https://github.com/acdlite/flummox/issues", 12 | "scripts": { 13 | "test": "make fast-js test", 14 | "prepublish": "npm run test" 15 | }, 16 | "keywords": [ 17 | "flux", 18 | "isomorphic", 19 | "react", 20 | "reactjs", 21 | "facebook", 22 | "es6" 23 | ], 24 | "author": "Andrew Clark ", 25 | "contributors": [ 26 | "Dmitri Voronianski " 27 | ], 28 | "license": "MIT", 29 | "devDependencies": { 30 | "babel-cli": "^6.3.15", 31 | "babel-core": "^6.3.15", 32 | "babel-eslint": "^4.1.6", 33 | "babel-loader": "^6.2.0", 34 | "babel-plugin-transform-es3-member-expression-literals": "^6.3.13", 35 | "babel-plugin-transform-es3-property-literals": "^6.3.13", 36 | "babel-preset-es2015-loose": "^6.1.3", 37 | "babel-preset-react": "^6.3.13", 38 | "babel-preset-stage-0": "^6.3.13", 39 | "babel-runtime": "^5.8.34", 40 | "chai": "~1.10.0", 41 | "chai-as-promised": "4.3.0", 42 | "es6-promise": "~2.0.1", 43 | "eslint": "^1.10.2", 44 | "istanbul": "0.3.7", 45 | "jsdom": "~4.0.5", 46 | "mocha": "2.2.1", 47 | "react": "^0.14.3", 48 | "react-addons-test-utils": "^0.14.3", 49 | "react-dom": "^0.14.3", 50 | "sinon": "1.14.0", 51 | "source-map-support": "0.2.10", 52 | "webpack": "1.7.3" 53 | }, 54 | "dependencies": { 55 | "eventemitter3": "~0.1.6", 56 | "flux": "~2.0.1", 57 | "object-assign": "^4.0.1", 58 | "prop-types": "^15.5.10", 59 | "uniqueid": "~0.1.0" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Actions.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Actions 3 | * 4 | * Instances of the Actions class represent a set of actions. (In Flux parlance, 5 | * these might be more accurately denoted as Action Creators, while Action 6 | * refers to the payload sent to the dispatcher, but this is... confusing. We 7 | * will use Action to mean the function you call to trigger a dispatch.) 8 | * 9 | * Create actions by extending from the base Actions class and adding methods. 10 | * All methods on the prototype (except the constructor) will be 11 | * converted into actions. The return value of an action is used as the body 12 | * of the payload sent to the dispatcher. 13 | */ 14 | 15 | import uniqueId from 'uniqueid'; 16 | 17 | export default class Actions { 18 | 19 | constructor() { 20 | 21 | this._baseId = uniqueId(); 22 | 23 | const methodNames = this._getActionMethodNames(); 24 | for (let i = 0; i < methodNames.length; i++) { 25 | const methodName = methodNames[i]; 26 | this._wrapAction(methodName); 27 | } 28 | 29 | this.getConstants = this.getActionIds; 30 | } 31 | 32 | getActionIds() { 33 | return this._getActionMethodNames().reduce((result, actionName) => { 34 | result[actionName] = this[actionName]._id; 35 | return result; 36 | }, {}); 37 | } 38 | 39 | _getActionMethodNames(instance) { 40 | return Object.getOwnPropertyNames(this.constructor.prototype) 41 | .filter(name => 42 | name !== 'constructor' && 43 | typeof this[name] === 'function' 44 | ); 45 | } 46 | 47 | _wrapAction(methodName) { 48 | const originalMethod = this[methodName]; 49 | const actionId = this._createActionId(methodName); 50 | 51 | const action = (...args) => { 52 | const body = originalMethod.apply(this, args); 53 | 54 | if (isPromise(body)) { 55 | const promise = body; 56 | this._dispatchAsync(actionId, promise, args, methodName); 57 | } else { 58 | this._dispatch(actionId, body, args, methodName); 59 | } 60 | 61 | // Return original method's return value to caller 62 | return body; 63 | }; 64 | 65 | action._id = actionId; 66 | 67 | this[methodName] = action; 68 | } 69 | 70 | /** 71 | * Create unique string constant for an action method, using 72 | * @param {string} methodName - Name of the action method 73 | */ 74 | _createActionId(methodName) { 75 | return `${this._baseId}-${methodName}`; 76 | } 77 | 78 | _dispatch(actionId, body, args, methodName) { 79 | if (typeof this.dispatch === 'function') { 80 | if (typeof body !== 'undefined') { 81 | this.dispatch(actionId, body, args); 82 | } 83 | } else { 84 | if (process.env.NODE_ENV !== 'production') { 85 | console.warn( 86 | `You've attempted to perform the action ` 87 | + `${this.constructor.name}#${methodName}, but it hasn't been added ` 88 | + `to a Flux instance.` 89 | ); 90 | } 91 | } 92 | 93 | return body; 94 | } 95 | 96 | _dispatchAsync(actionId, promise, args, methodName) { 97 | if (typeof this.dispatchAsync === 'function') { 98 | this.dispatchAsync(actionId, promise, args); 99 | } else { 100 | if (process.env.NODE_ENV !== 'production') { 101 | console.warn( 102 | `You've attempted to perform the asynchronous action ` 103 | + `${this.constructor.name}#${methodName}, but it hasn't been added ` 104 | + `to a Flux instance.` 105 | ); 106 | } 107 | } 108 | } 109 | 110 | } 111 | 112 | function isPromise(value) { 113 | return value && typeof value.then === 'function'; 114 | } 115 | -------------------------------------------------------------------------------- /src/Store.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Store 3 | * 4 | * Stores hold application state. They respond to actions sent by the dispatcher 5 | * and broadcast change events to listeners, so they can grab the latest data. 6 | * The key thing to remember is that the only way stores receive information 7 | * from the outside world is via the dispatcher. 8 | */ 9 | 10 | import EventEmitter from 'eventemitter3'; 11 | import assign from 'object-assign'; 12 | 13 | export default class Store extends EventEmitter { 14 | 15 | /** 16 | * Stores are initialized with a reference 17 | * @type {Object} 18 | */ 19 | constructor() { 20 | super(); 21 | 22 | this.state = null; 23 | 24 | this._handlers = {}; 25 | this._asyncHandlers = {}; 26 | this._catchAllHandlers = []; 27 | this._catchAllAsyncHandlers = { 28 | begin: [], 29 | success: [], 30 | failure: [], 31 | }; 32 | } 33 | 34 | setState(newState) { 35 | // Do a transactional state update if a function is passed 36 | if (typeof newState === 'function') { 37 | const prevState = this._isHandlingDispatch 38 | ? this._pendingState 39 | : this.state; 40 | 41 | newState = newState(prevState); 42 | } 43 | 44 | if (this._isHandlingDispatch) { 45 | this._pendingState = this._assignState(this._pendingState, newState); 46 | this._emitChangeAfterHandlingDispatch = true; 47 | } else { 48 | this.state = this._assignState(this.state, newState); 49 | this.emit('change'); 50 | } 51 | } 52 | 53 | replaceState(newState) { 54 | if (this._isHandlingDispatch) { 55 | this._pendingState = this._assignState(undefined, newState); 56 | this._emitChangeAfterHandlingDispatch = true; 57 | } else { 58 | this.state = this._assignState(undefined, newState); 59 | this.emit('change'); 60 | } 61 | } 62 | 63 | getStateAsObject() { 64 | return this.state; 65 | } 66 | 67 | static assignState(oldState, newState) { 68 | return assign({}, oldState, newState); 69 | } 70 | 71 | _assignState(...args){ 72 | return (this.constructor.assignState || Store.assignState)(...args); 73 | } 74 | 75 | forceUpdate() { 76 | if (this._isHandlingDispatch) { 77 | this._emitChangeAfterHandlingDispatch = true; 78 | } else { 79 | this.emit('change'); 80 | } 81 | } 82 | 83 | register(actionId, handler) { 84 | actionId = ensureActionId(actionId); 85 | 86 | if (typeof handler !== 'function') return; 87 | 88 | this._handlers[actionId] = handler.bind(this); 89 | } 90 | 91 | registerAsync(actionId, beginHandler, successHandler, failureHandler) { 92 | actionId = ensureActionId(actionId); 93 | 94 | const asyncHandlers = this._bindAsyncHandlers({ 95 | begin: beginHandler, 96 | success: successHandler, 97 | failure: failureHandler, 98 | }); 99 | 100 | this._asyncHandlers[actionId] = asyncHandlers; 101 | } 102 | 103 | registerAll(handler) { 104 | if (typeof handler !== 'function') return; 105 | 106 | this._catchAllHandlers.push(handler.bind(this)); 107 | } 108 | 109 | registerAllAsync(beginHandler, successHandler, failureHandler) { 110 | const asyncHandlers = this._bindAsyncHandlers({ 111 | begin: beginHandler, 112 | success: successHandler, 113 | failure: failureHandler, 114 | }); 115 | 116 | Object.keys(asyncHandlers).forEach((key) => { 117 | this._catchAllAsyncHandlers[key].push( 118 | asyncHandlers[key] 119 | ); 120 | }); 121 | } 122 | 123 | _bindAsyncHandlers(asyncHandlers) { 124 | for (let key in asyncHandlers) { 125 | if (!asyncHandlers.hasOwnProperty(key)) continue; 126 | 127 | const handler = asyncHandlers[key]; 128 | 129 | if (typeof handler === 'function') { 130 | asyncHandlers[key] = handler.bind(this); 131 | } else { 132 | delete asyncHandlers[key]; 133 | } 134 | } 135 | 136 | return asyncHandlers; 137 | } 138 | 139 | waitFor(tokensOrStores) { 140 | this._waitFor(tokensOrStores); 141 | } 142 | 143 | handler(payload) { 144 | const { 145 | body, 146 | actionId, 147 | 'async': _async, 148 | actionArgs, 149 | error 150 | } = payload; 151 | 152 | const _allHandlers = this._catchAllHandlers; 153 | const _handler = this._handlers[actionId]; 154 | 155 | const _allAsyncHandlers = this._catchAllAsyncHandlers[_async]; 156 | const _asyncHandler = this._asyncHandlers[actionId] 157 | && this._asyncHandlers[actionId][_async]; 158 | 159 | if (_async) { 160 | let beginOrFailureHandlers = _allAsyncHandlers.concat([_asyncHandler]); 161 | 162 | switch (_async) { 163 | case 'begin': 164 | this._performHandler(beginOrFailureHandlers, actionArgs); 165 | return; 166 | case 'failure': 167 | this._performHandler(beginOrFailureHandlers, [error]); 168 | return; 169 | case 'success': 170 | this._performHandler(_allAsyncHandlers.concat([ 171 | (_asyncHandler || _handler) 172 | ].concat(_asyncHandler && [] || _allHandlers)), [body]); 173 | return; 174 | default: 175 | return; 176 | } 177 | } 178 | 179 | this._performHandler(_allHandlers.concat([_handler]), [body]); 180 | } 181 | 182 | _performHandler(_handlers, args) { 183 | this._isHandlingDispatch = true; 184 | this._pendingState = this._assignState(undefined, this.state); 185 | this._emitChangeAfterHandlingDispatch = false; 186 | 187 | try { 188 | this._performHandlers(_handlers, args); 189 | } finally { 190 | if (this._emitChangeAfterHandlingDispatch) { 191 | this.state = this._pendingState; 192 | this.emit('change'); 193 | } 194 | 195 | this._isHandlingDispatch = false; 196 | this._pendingState = undefined; 197 | this._emitChangeAfterHandlingDispatch = false; 198 | } 199 | } 200 | 201 | _performHandlers(_handlers, args) { 202 | _handlers.forEach(_handler => 203 | (typeof _handler === 'function') && _handler.apply(this, args)); 204 | } 205 | } 206 | 207 | function ensureActionId(actionOrActionId) { 208 | return typeof actionOrActionId === 'function' 209 | ? actionOrActionId._id 210 | : actionOrActionId; 211 | } 212 | -------------------------------------------------------------------------------- /src/__tests__/Actions-test.js: -------------------------------------------------------------------------------- 1 | import { Flux, Actions } from '../Flux'; 2 | import sinon from 'sinon'; 3 | 4 | describe('Actions', () => { 5 | 6 | class TestActions extends Actions { 7 | getFoo() { 8 | return { foo: 'bar' }; 9 | } 10 | 11 | getBar() { 12 | return { bar: 'baz' }; 13 | } 14 | 15 | getBaz() { 16 | return; 17 | } 18 | 19 | async asyncAction(returnValue) { 20 | return returnValue; 21 | } 22 | 23 | badAsyncAction() { 24 | return Promise.reject(new Error('some error')); 25 | } 26 | } 27 | 28 | describe('#getActionIds / #getConstants', () => { 29 | it('returns strings corresponding to action method names', () => { 30 | const actions = new TestActions(); 31 | 32 | const actionIds = actions.getActionIds(); 33 | 34 | expect(actionIds.getFoo).to.be.a('string'); 35 | expect(actionIds.getBar).to.be.a('string'); 36 | 37 | expect(actionIds.getFoo).to.be.a('string'); 38 | expect(actionIds.getBar).to.be.a('string'); 39 | }); 40 | 41 | }); 42 | 43 | describe('#[methodName]', () => { 44 | it('calls Flux dispatcher', () => { 45 | const actions = new TestActions(); 46 | 47 | // Attach mock flux instance 48 | const dispatch = sinon.spy(); 49 | actions.dispatch = dispatch; 50 | 51 | actions.getFoo(); 52 | expect(dispatch.firstCall.args[1]).to.deep.equal({ foo: 'bar' }); 53 | }); 54 | 55 | it('warns if actions have not been added to a Flux instance', () => { 56 | const actions = new TestActions(); 57 | const warn = sinon.spy(console, 'warn'); 58 | 59 | actions.getFoo(); 60 | 61 | expect(warn.firstCall.args[0]).to.equal( 62 | 'You\'ve attempted to perform the action TestActions#getFoo, but it ' 63 | + 'hasn\'t been added to a Flux instance.' 64 | ); 65 | 66 | actions.asyncAction(); 67 | 68 | expect(warn.secondCall.args[0]).to.equal( 69 | `You've attempted to perform the asynchronous action ` 70 | + `TestActions#asyncAction, but it hasn't been added ` 71 | + `to a Flux instance.` 72 | ); 73 | 74 | console.warn.restore(); 75 | }); 76 | 77 | it('sends return value to Flux dispatch', () => { 78 | const actions = new TestActions(); 79 | const actionId = actions.getActionIds().getFoo; 80 | const dispatch = sinon.spy(); 81 | actions.dispatch = dispatch; 82 | 83 | actions.getFoo(); 84 | 85 | expect(dispatch.firstCall.args[0]).to.equal(actionId); 86 | expect(dispatch.firstCall.args[1]).to.deep.equal({ foo: 'bar' }); 87 | }); 88 | 89 | it('send async return value to Flux#dispatchAsync', async function() { 90 | const actions = new TestActions(); 91 | const actionId = actions.getActionIds().asyncAction; 92 | const dispatch = sinon.stub().returns(Promise.resolve()); 93 | actions.dispatchAsync = dispatch; 94 | 95 | const response = actions.asyncAction('foobar'); 96 | 97 | expect(response.then).to.be.a('function'); 98 | 99 | await response; 100 | 101 | expect(dispatch.firstCall.args[0]).to.equal(actionId); 102 | expect(dispatch.firstCall.args[1]).to.be.an.instanceOf(Promise); 103 | }); 104 | 105 | it('skips disptach if return value is undefined', () => { 106 | const actions = new TestActions(); 107 | const dispatch = sinon.spy(); 108 | actions.dispatch = dispatch; 109 | 110 | actions.getBaz(); 111 | 112 | expect(dispatch.called).to.be.false; 113 | }); 114 | 115 | it('does not skip async dispatch, even if resolved value is undefined', () => { 116 | const actions = new TestActions(); 117 | const dispatch = sinon.stub().returns(Promise.resolve(undefined)); 118 | actions.dispatchAsync = dispatch; 119 | 120 | actions.asyncAction(); 121 | 122 | expect(dispatch.called).to.be.true; 123 | }); 124 | 125 | it('returns value from wrapped action', async function() { 126 | const flux = new Flux(); 127 | const actions = flux.createActions('test', TestActions); 128 | 129 | expect(actions.getFoo()).to.deep.equal({ foo: 'bar' }); 130 | await expect(actions.asyncAction('async result')) 131 | .to.eventually.equal('async result'); 132 | }); 133 | 134 | }); 135 | 136 | }); 137 | -------------------------------------------------------------------------------- /src/__tests__/exampleFlux-test.js: -------------------------------------------------------------------------------- 1 | import { Flummox, Store, Actions } from '../Flux'; 2 | 3 | describe('Examples:', () => { 4 | 5 | /** 6 | * A simple Flummox example 7 | */ 8 | describe('Messages', () => { 9 | 10 | /** 11 | * To create some actions, create a new class that extends from the base 12 | * Actions class. Methods on the class's prototype will be converted into 13 | * actions, each with its own action id. 14 | * 15 | * In this example, calling `newMessage` will fire the dispatcher, with 16 | * a payload containing the passed message content. Easy! 17 | */ 18 | class MessageActions extends Actions { 19 | newMessage(content) { 20 | 21 | // The return value from the action is sent to the dispatcher. 22 | return content; 23 | } 24 | } 25 | 26 | /** 27 | * Now we need to a Store that will receive payloads from the dispatcher 28 | * and update itself accordingly. Like before, create a new class that 29 | * extends from the Store class. 30 | * 31 | * Stores are automatically registered with the dispatcher, but rather than 32 | * using a giant `switch` statement to check for specific action types, we 33 | * register handlers with action ids, or with a reference to the action 34 | * itself. 35 | * 36 | * Stores have a React-inspired API for managing state. Use `this.setState` 37 | * to update state within your handlers. Multiple calls to `this.setState` 38 | * within the same handler will be batched. A change event will fire after 39 | * the batched updates are applied. Your view controllers can listen 40 | * for change events using the EventEmitter API. 41 | */ 42 | class MessageStore extends Store { 43 | 44 | // Note that passing a flux instance to the constructor is not required; 45 | // we do it here so we have access to any action ids we're interested in. 46 | constructor(flux) { 47 | 48 | // Don't forget to call the super constructor 49 | super(); 50 | 51 | const messageActions = flux.getActions('messages'); 52 | this.register(messageActions.newMessage, this.handleNewMessage); 53 | this.messageCounter = 0; 54 | 55 | this.state = {}; 56 | } 57 | 58 | handleNewMessage(content) { 59 | const id = this.messageCounter++; 60 | 61 | this.setState({ 62 | [id]: { 63 | content, 64 | id, 65 | }, 66 | }); 67 | } 68 | } 69 | 70 | 71 | /** 72 | * Here's where it all comes together. Extend from the base Flummox class 73 | * to create a class that encapsulates your entire flux set-up. 74 | */ 75 | class Flux extends Flummox { 76 | constructor() { 77 | super(); 78 | 79 | // Create actions first so our store can reference them in 80 | // its constructor 81 | this.createActions('messages', MessageActions); 82 | 83 | // Extra arguments are sent to the store's constructor. Here, we're 84 | // padding a reference to this flux instance 85 | this.createStore('messages', MessageStore, this); 86 | } 87 | } 88 | 89 | /** 90 | * And that's it! No need for singletons or global references -- just create 91 | * a new instance. 92 | * 93 | * Now let's test it. 94 | */ 95 | 96 | it('creates new messages', () => { 97 | const flux = new Flux(); 98 | const messageStore = flux.getStore('messages'); 99 | const messageActions = flux.getActions('messages'); 100 | 101 | expect(messageStore.state).to.deep.equal({}); 102 | 103 | messageActions.newMessage('Hello, world!'); 104 | expect(messageStore.state).to.deep.equal({ 105 | [0]: { 106 | content: 'Hello, world!', 107 | id: 0, 108 | }, 109 | }); 110 | }); 111 | }); 112 | 113 | }); 114 | -------------------------------------------------------------------------------- /src/addons/FluxComponent.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Flux Component 3 | * 4 | * Component interface to reactComponentMethods module. 5 | * 6 | * Children of FluxComponent are given access to the flux instance via 7 | * `context.flux`. Use this near the top of your app hierarchy and all children 8 | * will have easy access to the flux instance (including, of course, other 9 | * Flux components!): 10 | * 11 | * 12 | * ...the rest of your app 13 | * 14 | * 15 | * Now any child can access the flux instance again like this: 16 | * 17 | * 18 | * ...children 19 | * 20 | * 21 | * We don't need the flux prop this time because flux is already part of 22 | * the context. 23 | * 24 | * Additionally, immediate children are given a `flux` prop. 25 | * 26 | * The component has an optional prop `connectToStores`, which is passed to 27 | * `this.connectToStores` and used to set the initial state. The component's 28 | * state is injected as props to the child components. 29 | * 30 | * The practical upshot of all this is that fluxMixin, state changes, and 31 | * context are now simply implementation details. Among other things, this means 32 | * you can write your components as plain ES6 classes. Here's an example: 33 | * 34 | * class ParentComponent extends React.Component { 35 | * 36 | * render() { 37 | * 38 | * 39 | * 40 | * } 41 | * 42 | * } 43 | * 44 | * ChildComponent in this example has prop `flux` containing the flux instance, 45 | * and props that sync with each of the state keys of fooStore. 46 | */ 47 | 48 | import React from 'react'; 49 | import { instanceMethods, staticProperties } from './reactComponentMethods'; 50 | import assign from 'object-assign'; 51 | 52 | class FluxComponent extends React.Component { 53 | constructor(props, context) { 54 | super(props, context); 55 | 56 | this.initialize(); 57 | 58 | this.state = this.connectToStores(props.connectToStores, props.stateGetter); 59 | 60 | this.wrapChild = this.wrapChild.bind(this); 61 | } 62 | 63 | wrapChild(child) { 64 | return React.cloneElement( 65 | child, 66 | this.getChildProps() 67 | ); 68 | } 69 | 70 | getChildProps() { 71 | const { 72 | children, 73 | render, 74 | connectToStores, 75 | stateGetter, 76 | flux, 77 | ...extraProps } = this.props; 78 | 79 | return assign( 80 | { flux: this.getFlux() }, // TODO: remove in next major version 81 | this.state, 82 | extraProps 83 | ); 84 | } 85 | 86 | render() { 87 | let { children, render: internalRender } = this.props; 88 | 89 | if (typeof internalRender === 'function') { 90 | return internalRender(this.getChildProps(), this.getFlux()); 91 | } 92 | 93 | if (!children) return null; 94 | 95 | if (!Array.isArray(children)) { 96 | const child = children; 97 | return this.wrapChild(child); 98 | } else { 99 | return {React.Children.map(children, this.wrapChild)}; 100 | } 101 | } 102 | } 103 | 104 | assign( 105 | FluxComponent.prototype, 106 | instanceMethods 107 | ); 108 | 109 | assign(FluxComponent, staticProperties); 110 | 111 | export default FluxComponent; 112 | -------------------------------------------------------------------------------- /src/addons/TestUtils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Used for simulating actions on stores when testing. 3 | * 4 | */ 5 | export function simulateAction(store, action, body) { 6 | const actionId = ensureActionId(action); 7 | store.handler({ actionId, body }); 8 | } 9 | 10 | /** 11 | * Used for simulating asynchronous actions on stores when testing. 12 | * 13 | * asyncAction must be one of the following: begin, success or failure. 14 | * 15 | * When simulating the 'begin' action, all arguments after 'begin' will 16 | * be passed to the action handler in the store. 17 | * 18 | * @example 19 | * 20 | * TestUtils.simulateActionAsync(store, 'actionId', 'begin', 'arg1', 'arg2'); 21 | * TestUtils.simulateActionAsync(store, 'actionId', 'success', { foo: 'bar' }); 22 | * TestUtils.simulateActionAsync(store, 'actionId', 'failure', new Error('action failed')); 23 | */ 24 | export function simulateActionAsync(store, action, asyncAction, ...args) { 25 | const actionId = ensureActionId(action); 26 | const payload = { 27 | actionId, async: asyncAction 28 | }; 29 | 30 | switch(asyncAction) { 31 | case 'begin': 32 | if (args.length) { 33 | payload.actionArgs = args; 34 | } 35 | break; 36 | case 'success': 37 | payload.body = args[0]; 38 | break; 39 | case 'failure': 40 | payload.error = args[0]; 41 | break; 42 | default: 43 | throw new Error('asyncAction must be one of: begin, success or failure'); 44 | } 45 | 46 | store.handler(payload); 47 | } 48 | 49 | function ensureActionId(actionOrActionId) { 50 | return typeof actionOrActionId === 'function' 51 | ? actionOrActionId._id 52 | : actionOrActionId; 53 | } 54 | -------------------------------------------------------------------------------- /src/addons/__tests__/TestUtils-test.js: -------------------------------------------------------------------------------- 1 | import * as TestUtils from '../TestUtils'; 2 | import sinon from 'sinon'; 3 | 4 | 5 | describe('TestUtils', () => { 6 | describe('#simulateAction', () => { 7 | it('calls the stores handler', () => { 8 | const store = mockStore(); 9 | const actionFunc = function() {}; 10 | actionFunc._id = 'actionFunc'; 11 | 12 | TestUtils.simulateAction(store, 'foo', 'foo body'); 13 | TestUtils.simulateAction(store, actionFunc, 'actionFunc body'); 14 | 15 | expect(store.handler.calledTwice).to.be.true; 16 | 17 | expect(store.handler.getCall(0).args[0]).to.deep.equal({ 18 | actionId: 'foo', 19 | body: 'foo body' 20 | }); 21 | 22 | expect(store.handler.getCall(1).args[0]).to.deep.equal({ 23 | actionId: 'actionFunc', 24 | body: 'actionFunc body' 25 | }); 26 | }); 27 | }); 28 | 29 | describe('#simulateActionAsync', () => { 30 | it('it handles async begin', () => { 31 | const store = mockStore(); 32 | 33 | TestUtils.simulateActionAsync(store, 'foo', 'begin'); 34 | 35 | expect(store.handler.calledOnce).to.be.true; 36 | expect(store.handler.firstCall.args[0]).to.deep.equal({ 37 | actionId: 'foo', 38 | async: 'begin' 39 | }); 40 | }); 41 | 42 | it('it handles async begin w/ action args', () => { 43 | const store = mockStore(); 44 | 45 | TestUtils.simulateActionAsync(store, 'foo', 'begin', 'arg1', 'arg2'); 46 | 47 | expect(store.handler.calledOnce).to.be.true; 48 | expect(store.handler.firstCall.args[0]).to.deep.equal({ 49 | actionId: 'foo', 50 | async: 'begin', 51 | actionArgs: ['arg1', 'arg2'] 52 | }); 53 | }); 54 | 55 | it('it handles async success', () => { 56 | const store = mockStore(); 57 | 58 | TestUtils.simulateActionAsync(store, 'foo', 'success', { foo: 'bar' }); 59 | 60 | expect(store.handler.calledOnce).to.be.true; 61 | expect(store.handler.firstCall.args[0]).to.deep.equal({ 62 | actionId: 'foo', 63 | async: 'success', 64 | body: { 65 | foo: 'bar' 66 | } 67 | }); 68 | }); 69 | 70 | it('it handles async failure', () => { 71 | const store = mockStore(); 72 | 73 | TestUtils.simulateActionAsync(store, 'foo', 'failure', 'error message'); 74 | 75 | expect(store.handler.calledOnce).to.be.true; 76 | expect(store.handler.firstCall.args[0]).to.deep.equal({ 77 | actionId: 'foo', 78 | async: 'failure', 79 | error: 'error message' 80 | }); 81 | }); 82 | 83 | it('it throws error with invalid asyncAction', () => { 84 | const store = mockStore(); 85 | const simulate = () => TestUtils.simulateActionAsync(store, 'foo', 'fizbin'); 86 | 87 | expect(simulate).to.throw('asyncAction must be one of: begin, success or failure'); 88 | }); 89 | }); 90 | }); 91 | 92 | function mockStore() { 93 | return { 94 | handler: sinon.spy() 95 | }; 96 | } 97 | -------------------------------------------------------------------------------- /src/addons/__tests__/addContext.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default function addContext(Component, context, contextTypes) { 4 | return React.createClass({ 5 | childContextTypes: contextTypes, 6 | 7 | getChildContext() { 8 | return context; 9 | }, 10 | 11 | render() { 12 | return ; 13 | } 14 | }); 15 | } 16 | -------------------------------------------------------------------------------- /src/addons/__tests__/connectToStores-test.js: -------------------------------------------------------------------------------- 1 | import connectToStores from '../connectToStores'; 2 | import addContext from './addContext'; 3 | import { Actions, Store, Flummox } from '../../Flux'; 4 | import React from 'react'; 5 | import TestUtils from 'react-addons-test-utils'; 6 | import PropTypes from 'prop-types' 7 | 8 | class TestActions extends Actions { 9 | getSomething(something) { 10 | return something; 11 | } 12 | } 13 | 14 | class TestStore extends Store { 15 | constructor(flux) { 16 | super(); 17 | 18 | const testActions = flux.getActions('test'); 19 | this.register(testActions.getSomething, this.handleGetSomething); 20 | 21 | this.state = { 22 | something: null 23 | }; 24 | } 25 | 26 | handleGetSomething(something) { 27 | this.setState({ something }); 28 | } 29 | } 30 | 31 | class Flux extends Flummox { 32 | constructor() { 33 | super(); 34 | 35 | this.createActions('test', TestActions); 36 | this.createStore('test', TestStore, this); 37 | } 38 | } 39 | 40 | describe('connectToStores (HoC)', () => { 41 | it('gets Flux from either props or context', () => { 42 | const flux = new Flux(); 43 | let contextComponent, propsComponent; 44 | 45 | class BaseComponent extends React.Component { 46 | render() { 47 | return
; 48 | } 49 | } 50 | 51 | const ConnectedComponent = connectToStores(BaseComponent, 'test'); 52 | 53 | const ContextComponent = addContext( 54 | ConnectedComponent, 55 | { flux }, 56 | { flux: PropTypes.instanceOf(Flummox) } 57 | ); 58 | 59 | const tree = TestUtils.renderIntoDocument( 60 | 61 | ); 62 | 63 | contextComponent = TestUtils.findRenderedComponentWithType( 64 | tree, ConnectedComponent 65 | ); 66 | 67 | propsComponent = TestUtils.renderIntoDocument( 68 | 69 | ); 70 | 71 | expect(contextComponent.flux).to.be.an.instanceof(Flummox); 72 | expect(propsComponent.flux).to.be.an.instanceof(Flummox); 73 | }); 74 | 75 | it('transfers props', () => { 76 | const flux = new Flux(); 77 | 78 | class BaseComponent extends React.Component { 79 | render() { 80 | return
; 81 | } 82 | } 83 | 84 | const ConnectedComponent = connectToStores(BaseComponent, 'test'); 85 | 86 | const tree = TestUtils.renderIntoDocument( 87 | 88 | ); 89 | 90 | const component = TestUtils.findRenderedComponentWithType( 91 | tree, BaseComponent 92 | ); 93 | 94 | expect(component.props.foo).to.equal('bar'); 95 | expect(component.props.bar).to.equal('baz'); 96 | }); 97 | 98 | it('syncs with store after state change', () => { 99 | const flux = new Flux(); 100 | 101 | class BaseComponent extends React.Component { 102 | render() { 103 | return
; 104 | } 105 | } 106 | 107 | const ConnectedComponent = connectToStores(BaseComponent, 'test'); 108 | 109 | const tree = TestUtils.renderIntoDocument( 110 | 111 | ); 112 | 113 | const component = TestUtils.findRenderedComponentWithType( 114 | tree, BaseComponent 115 | ); 116 | 117 | const getSomething = flux.getActions('test').getSomething; 118 | 119 | expect(component.props.something).to.be.null; 120 | 121 | getSomething('do'); 122 | 123 | expect(component.props.something).to.equal('do'); 124 | 125 | getSomething('re'); 126 | 127 | expect(component.props.something).to.equal('re'); 128 | }); 129 | }); 130 | -------------------------------------------------------------------------------- /src/addons/connectToStores.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Higher-order component form of connectToStores 3 | */ 4 | 5 | import React from 'react'; 6 | import { instanceMethods, staticProperties } from './reactComponentMethods'; 7 | import assign from 'object-assign'; 8 | 9 | export default (BaseComponent, stores, stateGetter) => { 10 | const ConnectedComponent = class extends React.Component { 11 | constructor(props, context) { 12 | super(props, context); 13 | 14 | this.initialize(); 15 | 16 | this.state = this.connectToStores(stores, stateGetter); 17 | } 18 | 19 | render() { 20 | return ; 21 | } 22 | }; 23 | 24 | assign( 25 | ConnectedComponent.prototype, 26 | instanceMethods 27 | ); 28 | 29 | assign(ConnectedComponent, staticProperties); 30 | 31 | return ConnectedComponent; 32 | }; 33 | -------------------------------------------------------------------------------- /src/addons/fluxMixin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * fluxMixin 3 | * 4 | * Exports a function that creates a React component mixin. Implements methods 5 | * from reactComponentMethods. 6 | * 7 | * Any arguments passed to the mixin creator are passed to `connectToStores()` 8 | * and used as the return value of `getInitialState()`. This lets you handle 9 | * all of the state initialization and updates in a single place, while removing 10 | * the burden of manually adding and removing store listeners. 11 | * 12 | * @example 13 | * let Component = React.createClass({ 14 | * mixins: [fluxMixin({ 15 | * storeA: store => ({ 16 | * foo: store.state.a, 17 | * }), 18 | * storeB: store => ({ 19 | * bar: store.state.b, 20 | * }) 21 | * }] 22 | * }); 23 | */ 24 | 25 | import PropTypes from 'prop-types' 26 | import { Flux } from '../Flux'; 27 | import { instanceMethods, staticProperties } from './reactComponentMethods'; 28 | import assign from 'object-assign'; 29 | 30 | export default function fluxMixin(...args) { 31 | function getInitialState() { 32 | this.initialize(); 33 | return this.connectToStores(...args); 34 | } 35 | 36 | return assign( 37 | { getInitialState }, 38 | instanceMethods, 39 | staticProperties 40 | ); 41 | }; 42 | -------------------------------------------------------------------------------- /src/addons/reactComponentMethods.js: -------------------------------------------------------------------------------- 1 | /** 2 | * React Component methods. These are the primitives used to implement 3 | * fluxMixin and FluxComponent. 4 | * 5 | * Exposes a Flux instance as `this.flux`. This requires that flux be passed as 6 | * either context or as a prop (prop takes precedence). Children also are given 7 | * access to flux instance as `context.flux`. 8 | * 9 | * It also adds the method `connectToStores()`, which ensures that the component 10 | * state stays in sync with the specified Flux stores. See the inline docs 11 | * of `connectToStores` for details. 12 | */ 13 | 14 | import { default as React } from 'react'; 15 | import PropTypes from 'prop-types'; 16 | import { Flux } from '../Flux'; 17 | import assign from 'object-assign'; 18 | 19 | const instanceMethods = { 20 | 21 | getChildContext() { 22 | const flux = this.getFlux(); 23 | 24 | if (!flux) return {}; 25 | 26 | return { flux }; 27 | }, 28 | 29 | getFlux() { 30 | return this.props.flux || this.context.flux; 31 | }, 32 | 33 | initialize() { 34 | this._fluxStateGetters = []; 35 | this._fluxListeners = {}; 36 | this.flux = this.getFlux(); 37 | 38 | if (!(this.flux instanceof Flux)) { 39 | // TODO: print the actual class name here 40 | throw new Error( 41 | `fluxMixin: Could not find Flux instance. Ensure that your component ` 42 | + `has either \`this.context.flux\` or \`this.props.flux\`.` 43 | ); 44 | } 45 | }, 46 | 47 | componentWillUnmount() { 48 | const flux = this.getFlux(); 49 | 50 | for (let key in this._fluxListeners) { 51 | if (!this._fluxListeners.hasOwnProperty(key)) continue; 52 | 53 | const store = flux.getStore(key); 54 | if (typeof store === 'undefined') continue; 55 | 56 | const listener = this._fluxListeners[key]; 57 | 58 | store.removeListener('change', listener); 59 | } 60 | }, 61 | 62 | componentWillReceiveProps(nextProps) { 63 | this.updateStores(nextProps); 64 | }, 65 | 66 | updateStores(props = this.props) { 67 | const state = this.getStoreState(props); 68 | this.setState(state); 69 | }, 70 | 71 | getStoreState(props = this.props) { 72 | return this._fluxStateGetters.reduce( 73 | (result, stateGetter) => { 74 | const { getter, stores } = stateGetter; 75 | const stateFromStores = getter(stores, props); 76 | return assign(result, stateFromStores); 77 | }, {} 78 | ); 79 | }, 80 | 81 | /** 82 | * Connect component to stores, get the combined initial state, and 83 | * subscribe to future changes. There are three ways to call it. The 84 | * simplest is to pass a single store key and, optionally, a state getter. 85 | * The state getter is a function that takes the store as a parameter and 86 | * returns the state that should be passed to the component's `setState()`. 87 | * If no state getter is specified, the default getter is used, which simply 88 | * returns the entire store state. 89 | * 90 | * The second form accepts an array of store keys. With this form, the state 91 | * getter is called once with an array of store instances (in the same order 92 | * as the store keys). the default getter performance a reduce on the entire 93 | * state for each store. 94 | * 95 | * The last form accepts an object of store keys mapped to state getters. As 96 | * a shortcut, you can pass `null` as a state getter to use the default 97 | * state getter. 98 | * 99 | * Returns the combined initial state of all specified stores. 100 | * 101 | * This way you can write all the initialization and update logic in a single 102 | * location, without having to mess with adding/removing listeners. 103 | * 104 | * @type {string|array|object} stateGetterMap - map of keys to getters 105 | * @returns {object} Combined initial state of stores 106 | */ 107 | connectToStores(stateGetterMap = {}, stateGetter = null) { 108 | const flux = this.getFlux(); 109 | 110 | const getStore = (key) => { 111 | const store = flux.getStore(key); 112 | 113 | if (typeof store === 'undefined') { 114 | throw new Error( 115 | `connectToStores(): Store with key '${key}' does not exist.` 116 | ); 117 | } 118 | 119 | return store; 120 | }; 121 | 122 | if (typeof stateGetterMap === 'string') { 123 | const key = stateGetterMap; 124 | const store = getStore(key); 125 | const getter = stateGetter || defaultStateGetter; 126 | 127 | this._fluxStateGetters.push({ stores: store, getter }); 128 | const listener = createStoreListener(this, store, getter); 129 | 130 | store.addListener('change', listener); 131 | this._fluxListeners[key] = listener; 132 | } else if (Array.isArray(stateGetterMap)) { 133 | const stores = stateGetterMap.map(getStore); 134 | const getter = stateGetter || defaultReduceStateGetter; 135 | 136 | this._fluxStateGetters.push({ stores, getter }); 137 | const listener = createStoreListener(this, stores, getter); 138 | 139 | stateGetterMap.forEach((key, index) => { 140 | const store = stores[index]; 141 | store.addListener('change', listener); 142 | this._fluxListeners[key] = listener; 143 | }); 144 | 145 | } else { 146 | for (let key in stateGetterMap) { 147 | const store = getStore(key); 148 | const getter = stateGetterMap[key] || defaultStateGetter; 149 | 150 | this._fluxStateGetters.push({ stores: store, getter }); 151 | const listener = createStoreListener(this, store, getter); 152 | 153 | store.addListener('change', listener); 154 | this._fluxListeners[key] = listener; 155 | } 156 | } 157 | 158 | return this.getStoreState(); 159 | } 160 | 161 | }; 162 | 163 | const staticProperties = { 164 | contextTypes: { 165 | flux: PropTypes.instanceOf(Flux), 166 | }, 167 | 168 | childContextTypes: { 169 | flux: PropTypes.instanceOf(Flux), 170 | }, 171 | 172 | propTypes: { 173 | connectToStores: PropTypes.oneOfType([ 174 | PropTypes.string, 175 | PropTypes.arrayOf(PropTypes.string), 176 | PropTypes.object 177 | ]), 178 | flux: PropTypes.instanceOf(Flux), 179 | render: PropTypes.func, 180 | stateGetter: PropTypes.func, 181 | }, 182 | }; 183 | 184 | export { instanceMethods, staticProperties }; 185 | 186 | function createStoreListener(component, store, storeStateGetter) { 187 | return function() { 188 | const state = storeStateGetter(store, this.props); 189 | this.setState(state); 190 | }.bind(component); 191 | } 192 | 193 | function defaultStateGetter(store) { 194 | return store.getStateAsObject(); 195 | } 196 | 197 | function defaultReduceStateGetter(stores) { 198 | return stores.reduce( 199 | (result, store) => assign(result, store.getStateAsObject()), 200 | {} 201 | ); 202 | } 203 | -------------------------------------------------------------------------------- /src/test/init.js: -------------------------------------------------------------------------------- 1 | import chai from 'chai'; 2 | global.expect = chai.expect; 3 | 4 | import chaiAsPromised from 'chai-as-promised'; 5 | chai.use(chaiAsPromised); 6 | 7 | import { Promise } from 'es6-promise'; 8 | if (!global.Promise) global.Promise = Promise; 9 | 10 | import 'babel-runtime/regenerator/runtime'; 11 | 12 | import { jsdom as _jsdom } from 'jsdom'; 13 | 14 | global.document = _jsdom(''); 15 | global.window = document.defaultView; 16 | global.navigator = window.navigator; 17 | -------------------------------------------------------------------------------- /test-utils.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/addons/TestUtils'); 2 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var webpack = require('webpack'); 4 | 5 | var plugins = [ 6 | new webpack.DefinePlugin({ 7 | 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV) 8 | }), 9 | 10 | ]; 11 | 12 | if (process.env.NODE_ENV === 'production') { 13 | plugins.push( 14 | new webpack.optimize.UglifyJsPlugin({ 15 | compressor: { 16 | warnings: false 17 | } 18 | }) 19 | ); 20 | } 21 | 22 | module.exports = { 23 | 24 | output: { 25 | library: 'Flummox', 26 | libraryTarget: 'var' 27 | }, 28 | 29 | plugins: plugins, 30 | 31 | resolve: { 32 | extensions: ['', '.js'] 33 | }, 34 | 35 | module: { 36 | loaders: [ 37 | { test: /\.js$/, loaders: ['babel?presets[]=react,presets[]=es2015-loose,presets[]=stage-0'], exclude: /node_modules/ } 38 | ] 39 | } 40 | }; 41 | --------------------------------------------------------------------------------