├── docs ├── _config.yml ├── images │ ├── logo.png │ ├── logo_x2.png │ └── logo.svg ├── _site │ ├── images │ │ ├── logo.png │ │ ├── logo_x2.png │ │ └── logo.svg │ ├── css │ │ ├── common.css │ │ ├── landing.css │ │ ├── page.css │ │ └── syntax.css │ ├── index.html │ ├── react.html │ ├── middleware.html │ └── guide.html ├── css │ ├── common.css │ ├── landing.css │ ├── page.css │ └── syntax.css ├── index.md ├── _layouts │ ├── landing.html │ └── page.html ├── middleware.md ├── react.md └── guide.md ├── .gitignore ├── .npmignore ├── examples ├── dice-roller │ ├── .babelrc │ ├── package.json │ ├── index.html │ ├── stylesheet.css │ └── src │ │ └── index.js └── dice-roller-react │ ├── .babelrc │ ├── index.html │ ├── package.json │ ├── stylesheet.css │ └── src │ └── index.js ├── .babelrc ├── src ├── middleware │ ├── logger.js │ └── versioning.js ├── react.js └── index.js ├── test ├── middleware │ ├── versioning.js │ └── logger.js └── index.js ├── middleware ├── versioning.js └── logger.js ├── LICENSE ├── CHANGELOG.md ├── react.js ├── README.md ├── package.json └── index.js /docs/_config.yml: -------------------------------------------------------------------------------- 1 | baseurl: /elfi 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vimproject 3 | yarn-error.log 4 | 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .vimproject 3 | yarn-error.log 4 | 5 | -------------------------------------------------------------------------------- /examples/dice-roller/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madx/elfi/HEAD/docs/images/logo.png -------------------------------------------------------------------------------- /examples/dice-roller-react/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /docs/images/logo_x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madx/elfi/HEAD/docs/images/logo_x2.png -------------------------------------------------------------------------------- /docs/_site/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madx/elfi/HEAD/docs/_site/images/logo.png -------------------------------------------------------------------------------- /docs/_site/images/logo_x2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/madx/elfi/HEAD/docs/_site/images/logo_x2.png -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": true, 6 | "browsers": "last 2 versions" 7 | } 8 | }], 9 | "react" 10 | ], 11 | 12 | "plugins": [ 13 | "transform-class-properties" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /examples/dice-roller-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Dice roller 6 | 7 | 8 | 9 |
10 | 11 | 12 | 2 | 3 | 4 | 5 | Dice roller 6 | 7 | 8 | 9 |
10 |
11 |
12 | 13 | 14 | 15 |
16 |
17 | 18 | 19 | 20 | { 7 | t.plan(3) 8 | const _change = s => Object.assign({}, s, { x: s.x + 1 }) 9 | const store = createStore({ x: 0 }, [ createVersioningMiddleware() ]) 10 | 11 | store.dispatch(_change) 12 | t.equal(store.getState().x, 1, "should correctly dispatch the change") 13 | t.equal(store.getState().version, 1, "should initialize the version") 14 | store.dispatch(_change) 15 | t.equal(store.getState().version, 2, "should update the version") 16 | }) 17 | -------------------------------------------------------------------------------- /test/middleware/logger.js: -------------------------------------------------------------------------------- 1 | import test from "tape" 2 | 3 | import { createStore } from "../../src" 4 | import createLoggerMiddleware from "../../src/middleware/logger" 5 | 6 | test("createLoggerMiddleware(logger)", t => { 7 | t.plan(4) 8 | const _change = s => s + 1 9 | const logger = ({ oldState, newState, change, args }) => { 10 | t.equal(oldState, 1, "passes the new state to the logger function") 11 | t.equal(newState, 2, "passes the new state to the logger function") 12 | t.equal(args[0], "argument", "passes the arguments to the logger function") 13 | t.equal(change, _change, "passes the change to the logger function") 14 | } 15 | const store = createStore(1, [createLoggerMiddleware(logger)]) 16 | 17 | store.dispatch(_change, "argument") 18 | }) 19 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: landing 3 | title: An elegant state container for your JavaScript applications 4 | --- 5 | 6 | > *elfi* is a state container for JavaScript applications. It takes its roots 7 | > in libraries such as [Flux][flux] and [Redux][redux], but strives to remain 8 | > simple and avoid boilerplate code. 9 | 10 | --- 11 | 12 | * **[Guide][doc:guide]** 13 | * \| 14 | * **[Middleware][doc:middleware]** 15 | * \| 16 | * **[React][doc:react]** 17 | 18 | --- 19 | 20 | [Read the initial blog post!][blogpost] 21 | 22 | [blogpost]: http://madx.me/articles/a-simpler-alternative-to-flux-and-redux.html 23 | [flux]: https://github.com/facebook/flux 24 | [redux]: https://github.com/reactjs/redux 25 | [doc:guide]: /elfi/guide.html 26 | [doc:middleware]: /elfi/middleware.html 27 | [doc:react]: /elfi/react.html 28 | -------------------------------------------------------------------------------- /examples/dice-roller/stylesheet.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | } 4 | 5 | .App { 6 | text-align: center; 7 | } 8 | 9 | .Dice { 10 | padding: 2rem; 11 | font-size: 8rem; 12 | font-weight: bold; 13 | background-color: black; 14 | color: white; 15 | } 16 | 17 | .Buttons { 18 | padding: 2rem; 19 | font-size: 2rem; 20 | } 21 | 22 | .Buttons button { 23 | padding: 0.25rem 0.75rem; 24 | font-size: 2rem; 25 | border: 1px solid #888; 26 | background-color: white; 27 | text-transform: uppercase; 28 | border-radius: 5px; 29 | margin-right: 1rem; 30 | } 31 | .Buttons button:last-child { 32 | margin-right: 0; 33 | } 34 | 35 | .Buttons button:hover { 36 | cursor: pointer; 37 | border-color: #444; 38 | } 39 | 40 | .Buttons button[data-action="roll-dice"] { 41 | background-color: black; 42 | color: white; 43 | } 44 | -------------------------------------------------------------------------------- /examples/dice-roller-react/stylesheet.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | } 4 | 5 | .App { 6 | text-align: center; 7 | } 8 | 9 | .Dice { 10 | padding: 2rem; 11 | font-size: 8rem; 12 | font-weight: bold; 13 | background-color: black; 14 | color: white; 15 | } 16 | 17 | .Buttons { 18 | padding: 2rem; 19 | font-size: 2rem; 20 | } 21 | 22 | .Buttons button { 23 | padding: 0.25rem 0.75rem; 24 | font-size: 2rem; 25 | border: 1px solid #888; 26 | background-color: white; 27 | text-transform: uppercase; 28 | border-radius: 5px; 29 | margin-right: 1rem; 30 | } 31 | .Buttons button:last-child { 32 | margin-right: 0; 33 | } 34 | 35 | .Buttons button:hover { 36 | cursor: pointer; 37 | border-color: #444; 38 | } 39 | 40 | .Buttons button[data-action="roll-dice"] { 41 | background-color: black; 42 | color: white; 43 | } 44 | -------------------------------------------------------------------------------- /middleware/versioning.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = createVersioningMiddleware; 7 | var DEFAULT_SETTER = function setter(newState) { 8 | newState.version = (newState.version || 0) + 1; 9 | return newState; 10 | }; 11 | 12 | function createVersioningMiddleware() { 13 | var setter = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : DEFAULT_SETTER; 14 | 15 | return function versioningMiddleware(next, state, change) { 16 | for (var _len = arguments.length, args = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) { 17 | args[_key - 3] = arguments[_key]; 18 | } 19 | 20 | var newState = next.apply(undefined, [state, change].concat(args)); 21 | 22 | if (newState !== state) { 23 | newState = setter(newState); 24 | } 25 | 26 | return newState; 27 | }; 28 | } -------------------------------------------------------------------------------- /middleware/logger.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.default = createLoggerMiddleware; 7 | var DEFAULT_LOGGER = function defaultLogger(_ref) { 8 | var oldState = _ref.oldState, 9 | newState = _ref.newState, 10 | change = _ref.change; 11 | 12 | console.log(change.name, oldState, newState); 13 | }; 14 | 15 | function createLoggerMiddleware() { 16 | var logger = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : DEFAULT_LOGGER; 17 | 18 | return function loggerMiddleware(next, oldState, change) { 19 | for (var _len = arguments.length, args = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) { 20 | args[_key - 3] = arguments[_key]; 21 | } 22 | 23 | var newState = next.apply(undefined, [oldState, change].concat(args)); 24 | logger({ oldState: oldState, newState: newState, change: change, args: args }); 25 | return newState; 26 | }; 27 | } -------------------------------------------------------------------------------- /src/react.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import PropTypes from "prop-types" 3 | 4 | export const ElfiContext = React.createContext(null) 5 | 6 | export const storeShape = PropTypes.shape({ 7 | dispatch: PropTypes.func.isRequired, 8 | getState: PropTypes.func.isRequired, 9 | subscribe: PropTypes.func.isRequired, 10 | }) 11 | 12 | export function connect( 13 | WrappedComponent, 14 | mapStateToProps = storeState => ({ 15 | storeState, 16 | }), 17 | ) { 18 | function ConnectedComponent(props) { 19 | return ( 20 | 21 | {store => { 22 | const propsFromStore = mapStateToProps(store.getState(), store) 23 | return ( 24 | 25 | ) 26 | }} 27 | 28 | ) 29 | } 30 | 31 | ConnectedComponent.displayName = `Connect$${WrappedComponent.name}` 32 | 33 | return ConnectedComponent 34 | } 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 François Vaux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # v2.0.0 2 | 3 | - core: Only throw an error when initialState is undefined 4 | - [BREAKING] react: Use the new Context API 5 | - Update docs 6 | 7 | # v1.4.1 8 | 9 | - logger: Pass the args to the logger function 10 | 11 | # v1.4.0 12 | 13 | - react: Listen for store updates in the `connect` function instead of the Provider 14 | - Use prettier for code formatting 15 | 16 | # v1.3.1 17 | 18 | - Switch to yarn 19 | - Update dependencies 20 | 21 | # v1.3.0 22 | 23 | - react: Pass the store state as a prop in the connect HOC 24 | 25 | # v1.2.0 26 | 27 | - react: Add a consumer HOC 28 | - react: Remove deprecation warnings for React 16 29 | 30 | # v1.1.0 31 | 32 | - react: Stop storing the store state in the `Provider`, use `forceUpdate` 33 | instead 34 | - Use the `latest` Babel preset 35 | - Remove the use of `setTimeout` when processing subscribers 36 | 37 | # v1.0.1 38 | 39 | - Add versioning and logger middleware 40 | - Add documentation and website 41 | - Add examples 42 | 43 | # v1.0.0 (broken, see v1.0.1) 44 | 45 | # v0.3.0 46 | 47 | - Don't wait for subscribers to execute when performing updates 48 | 49 | # v0.2.1 50 | 51 | - Use a named function for the final middleware 52 | 53 | # v0.2.0 54 | 55 | - Add middleware functionality 56 | - Use a set for storing subscribers 57 | 58 | # v0.1.2 59 | 60 | Initial release 61 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | export function createStore(initialState, middleware = []) { 2 | if (initialState === undefined) { 3 | throw new Error("Missing initial state in store creation") 4 | } 5 | let state = initialState 6 | const subscribers = new Set() 7 | 8 | // Final middleware, simply applies the change to the state 9 | middleware.push(function finalize(next, state, change, ...args) { 10 | return change(state, ...args) 11 | }) 12 | 13 | const updater = middleware 14 | .reduceRight((chain, mw) => { 15 | const next = chain[0] 16 | chain.unshift((state, change, ...args) => { 17 | return mw(next, state, change, ...args) 18 | }) 19 | return chain 20 | }, []) 21 | .shift() 22 | 23 | return { 24 | getState() { 25 | return state 26 | }, 27 | 28 | dispatch(change, ...args) { 29 | if (typeof change !== "function") { 30 | throw new Error("change must be a function") 31 | } 32 | 33 | const oldState = state 34 | state = updater(state, change, ...args) 35 | 36 | if (state === oldState) { 37 | return 38 | } 39 | 40 | subscribers.forEach(subscriber => subscriber(state, oldState)) 41 | }, 42 | 43 | subscribe(subscriber) { 44 | if (typeof subscriber !== "function") { 45 | throw new Error("subscriber must be a function") 46 | } 47 | 48 | subscribers.add(subscriber) 49 | 50 | return function unsubscribe() { 51 | if (subscribers.has(subscriber)) { 52 | subscribers.delete(subscriber) 53 | } 54 | } 55 | }, 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /docs/_layouts/landing.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | elfi - {{ page.title }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |

22 | elfi 23 |

24 |
25 |
26 | {{ content }} 27 |
28 | 33 | 34 | Fork me on GitHub 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /docs/_layouts/page.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | elfi - {{ page.title }} 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | elfi 25 | 26 |

27 |
28 |
29 | {{ content }} 30 |
31 | 36 | 37 | Fork me on GitHub 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /react.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.storeShape = exports.ElfiContext = undefined; 7 | 8 | var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; 9 | 10 | exports.connect = connect; 11 | 12 | var _react = require("react"); 13 | 14 | var _react2 = _interopRequireDefault(_react); 15 | 16 | var _propTypes = require("prop-types"); 17 | 18 | var _propTypes2 = _interopRequireDefault(_propTypes); 19 | 20 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 21 | 22 | var ElfiContext = exports.ElfiContext = _react2.default.createContext(null); 23 | 24 | var storeShape = exports.storeShape = _propTypes2.default.shape({ 25 | dispatch: _propTypes2.default.func.isRequired, 26 | getState: _propTypes2.default.func.isRequired, 27 | subscribe: _propTypes2.default.func.isRequired 28 | }); 29 | 30 | function connect(WrappedComponent) { 31 | var mapStateToProps = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : function (storeState) { 32 | return { 33 | storeState: storeState 34 | }; 35 | }; 36 | 37 | function ConnectedComponent(props) { 38 | return _react2.default.createElement( 39 | ElfiContext.Consumer, 40 | null, 41 | function (store) { 42 | var propsFromStore = mapStateToProps(store.getState(), store); 43 | return _react2.default.createElement(WrappedComponent, _extends({}, props, propsFromStore, { store: store })); 44 | } 45 | ); 46 | } 47 | 48 | ConnectedComponent.displayName = "Connect$" + WrappedComponent.name; 49 | 50 | return ConnectedComponent; 51 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [![elfi](https://rawgit.com/madx/elfi/master/docs/images/logo.svg)](https://github.com/madx/elfi) 2 | 3 | > An elegant state container for your JavaScript applications 4 | 5 | [![npm version](https://img.shields.io/npm/v/elfi.svg?style=flat-square)](https://github.com/madx/elfi/master/CHANGELOG.md) 6 | [![npm downloads](https://img.shields.io/npm/dm/elfi.svg?style=flat-square)](https://www.npmjs.com/package/elfi) 7 | [![Join the chat on Discord](https://img.shields.io/discord/433885849227100162.svg)](https://discord.gg/cMkzd4J) 8 | 9 | *elfi* is a state container for JavaScript applications. It takes its roots in 10 | libraries such as [Flux][flux] and [Redux][redux], but strives to remain simple 11 | and avoid boilerplate code. 12 | 13 | It only takes a few minutes to [learn][doc:guide], works great with 14 | [Immutable.js][immutable] and [React][doc:react], and is easy to extend using 15 | [middleware][doc:middleware]. 16 | 17 | --- 18 | 19 | **[Read the documentation][website]** 20 | 21 | --- 22 | 23 | ## Install 24 | 25 | ```console 26 | $ npm install elfi 27 | ``` 28 | 29 | ## Usage 30 | 31 | **[Read the documentation][doc:guide]** 32 | 33 | ## Contributing 34 | 35 | * ⇄ Pull Requests and ★ Stars are always welcome. 36 | * For bugs and feature requests, please create an issue. 37 | * Pull Requests must ensure that automated checks pass (`$ npm run check`). 38 | 39 | ## [License](LICENSE) 40 | 41 | --- 42 | 43 | If interested, you can also read the (now quite obsolete) **[initial blog post][blogpost]**. 44 | 45 | --- 46 | 47 | [website]: http://madx.github.io/elfi/ 48 | [blogpost]: http://madx.me/articles/a-simpler-alternative-to-flux-and-redux.html 49 | [flux]: https://github.com/facebook/flux 50 | [redux]: https://github.com/reactjs/redux 51 | [immutable]: https://facebook.github.io/immutable-js/ 52 | [doc:guide]: docs/guide.md 53 | [doc:middleware]: docs/middleware.md 54 | [doc:react]: docs/react.md 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "elfi", 3 | "version": "2.0.2", 4 | "description": "An elegant state container for your JavaScript applications", 5 | "main": "./index.js", 6 | "scripts": { 7 | "clean": "rimraf index.js react.js middleware/", 8 | "lint": "eslint src/ test/", 9 | "test": "babel-tape-runner test/*.js test/**/*.js", 10 | "build": "babel src --out-dir .", 11 | "check": "npm run lint && npm run test", 12 | "prepare": "npm run clean && npm run check && npm run build" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/madx/elfi.git" 17 | }, 18 | "files": [ 19 | "index.js", 20 | "react.js", 21 | "middleware/*", 22 | "src/*", 23 | "docs/*" 24 | ], 25 | "keywords": [ 26 | "flux", 27 | "redux", 28 | "state", 29 | "functional", 30 | "immutable" 31 | ], 32 | "author": "François Vaux (https://github.com/madx)", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "babel-cli": "^6.26.0", 36 | "babel-eslint": "^8.2.6", 37 | "babel-plugin-transform-class-properties": "^6.24.1", 38 | "babel-preset-env": "^1.7.0", 39 | "babel-preset-react": "^6.24.1", 40 | "babel-tape-runner": "^2.0.1", 41 | "eslint": "^5.3.0", 42 | "eslint-config-i-am-meticulous": "^11.0.0", 43 | "husky": "^0.14.3", 44 | "lint-staged": "^7.2.0", 45 | "prettier": "^1.14.2", 46 | "prop-types": "^15.6.2", 47 | "react": "^16.4.2", 48 | "rimraf": "^2.6.2", 49 | "tape": "^4.9.1" 50 | }, 51 | "eslintConfig": { 52 | "parser": "babel-eslint", 53 | "extends": [ 54 | "eslint-config-i-am-meticulous" 55 | ], 56 | "globals": { 57 | "Set": true 58 | } 59 | }, 60 | "prettier": { 61 | "semi": false, 62 | "trailingComma": "all", 63 | "bracketSpacing": true 64 | }, 65 | "peerDependencies": { 66 | "prop-types": "^15.6.0", 67 | "react": "^16.3.0" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /docs/middleware.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Built-in middleware 4 | --- 5 | 6 | # Built-in middleware 7 | 8 | _elfi_ provides some builtin middleware. You'll find documentation about them in 9 | this document. 10 | 11 | ## logger ([source](https://github.com/madx/elfi/blob/master/src/middleware/logger.js)) 12 | 13 | This middleware is used to log each change to whatever logging system you are 14 | using (a simple console.log by default) 15 | 16 | ```js 17 | import { createStore } from "elfi" 18 | import createLoggerMiddleware from "elfi/middleware/logger" 19 | 20 | const logger = createLoggerMiddleware() 21 | 22 | const store = createStore(1, [logger]) 23 | ``` 24 | 25 | You can define a custom logger by passing a function to 26 | `createLoggerMiddleware`. This function accepts a single argument which is 27 | object with the following shape `{oldState, newState, change, args}`. 28 | 29 | ```js 30 | import { createStore } from "elfi" 31 | import createLoggerMiddleware from "elfi/middleware/logger" 32 | 33 | function logOldState({ oldState }) { 34 | console.log(oldState) 35 | } 36 | 37 | const logger = createLoggerMiddleware(({ oldState }) => console.log(oldState)) 38 | 39 | const store = createStore(1, [logger]) 40 | ``` 41 | 42 | ## versioning ([source](https://github.com/madx/elfi/blob/master/src/middleware/versioning.js)) 43 | 44 | This middleware is used to add a version number to the state without triggering 45 | another state change. 46 | 47 | ```js 48 | import { createStore } from "elfi" 49 | import createVersioningMiddleware from "elfi/middleware/versioning" 50 | 51 | const versioning = createVersioningMiddleware() 52 | 53 | const store = createStore(1, [versioning]) 54 | ``` 55 | 56 | By default, it acts as if state is a simple object and sets the `version` field 57 | of that object. You will probably want to define how to set the version and for 58 | this you can pass a custom setter function to `createVersioningMiddleware`. 59 | 60 | It will be called with the new state as the sole argument. Here's an example 61 | with [Immutable.js][immutable]: 62 | 63 | ```js 64 | import Immutable from "immutable" 65 | import { createStore } from "elfi" 66 | import createVersioningMiddleware from "elfi/middleware/versioning" 67 | 68 | const versioning = createVersioningMiddleware(state => 69 | state.set((state.get("version") || 0) + 1), 70 | ) 71 | const store = createStore(new Immutable.Map(), [versioning]) 72 | ``` 73 | 74 | [immutable]: https://facebook.github.io/immutable-js/ 75 | -------------------------------------------------------------------------------- /docs/_site/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | elfi - An elegant state container for your JavaScript applications 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
21 |

22 | elfi 23 |

24 |
25 |
26 |
27 |

elfi is a state container for JavaScript applications. It takes its roots 28 | in libraries such as Flux and Redux, but strives to remain 29 | simple and avoid boilerplate code.

30 |
31 | 32 |
33 | 34 | 41 | 42 |
43 | 44 |

Read the initial blog post!

45 | 46 | 47 |
48 | 53 | 54 | Fork me on GitHub 55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /examples/dice-roller-react/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import ReactDOM from "react-dom" 3 | import {createStore} from "elfi" 4 | import createLoggerMiddleware from "elfi/middleware/logger" 5 | import {storeShape, Provider} from "elfi/react" 6 | import {Record, List} from "immutable" 7 | 8 | // Data types and constants 9 | const DICE = ["⚀", "⚁", "⚂", "⚃", "⚄", "⚅"] 10 | 11 | const AppState = Record({ 12 | diceCount: 1, 13 | roll: List.of(d6()) 14 | }, "AppState") 15 | 16 | function d6() { 17 | return DICE[~~(Math.random() * 6)] 18 | } 19 | 20 | // Changes 21 | const changes = { 22 | addDie(state, newDie) { 23 | if (state.diceCount === 6) { 24 | return state 25 | } 26 | 27 | return state 28 | .set("diceCount", state.diceCount + 1) 29 | .update("roll", r => r.push(newDie)) 30 | }, 31 | 32 | removeDie(state) { 33 | if (state.diceCount === 1) { 34 | return state 35 | } 36 | 37 | return state 38 | .set("diceCount", state.diceCount - 1) 39 | .update("roll", r => r.pop()) 40 | }, 41 | 42 | updateRoll(state, roll) { 43 | return state.set("roll", new List(roll)) 44 | }, 45 | } 46 | 47 | // React App 48 | function App(_, context) { 49 | const store = context.store 50 | const state = store.getState() 51 | 52 | function removeDie() { 53 | store.dispatch(changes.removeDie) 54 | } 55 | 56 | function rollDice() { 57 | const diceCount = state.diceCount 58 | const roll = Array.from({length: diceCount}, () => d6()) 59 | 60 | store.dispatch(changes.updateRoll, roll) 61 | } 62 | 63 | function addDie() { 64 | store.dispatch(changes.addDie, d6()) 65 | } 66 | 67 | return ( 68 |
69 |
70 | {state.roll.join(" ")} 71 |
72 |
73 | 74 | 75 | 76 |
77 |
78 | ) 79 | } 80 | App.contextTypes = { 81 | store: storeShape 82 | } 83 | 84 | 85 | // Initialize application state 86 | const logger = createLoggerMiddleware(({change, oldState, newState}) => { 87 | console.log(change.name, ":", oldState.toJS(), "=>", newState.toJS()) 88 | }) 89 | const initialState = new AppState() 90 | const store = createStore(initialState, [logger]) 91 | 92 | // Bootstrap 93 | ReactDOM.render( 94 | 95 | 96 | , 97 | document.getElementById("app-root") 98 | ) 99 | -------------------------------------------------------------------------------- /examples/dice-roller/src/index.js: -------------------------------------------------------------------------------- 1 | import {createStore} from "elfi" 2 | import createLoggerMiddleware from "elfi/middleware/logger" 3 | import {Record, List} from "immutable" 4 | 5 | // Data types and constants 6 | const DICE = ["⚀", "⚁", "⚂", "⚃", "⚄", "⚅"] 7 | 8 | const AppState = Record({ 9 | diceCount: 1, 10 | roll: List.of(d6()) 11 | }, "AppState") 12 | 13 | function d6() { 14 | return DICE[~~(Math.random() * 6)] 15 | } 16 | 17 | // UI elements 18 | const ui = { 19 | dice: document.querySelector(".Dice"), 20 | rollBtn: document.querySelector("button[data-action=\"roll-dice\"]"), 21 | addDieBtn: document.querySelector("button[data-action=\"add-die\"]"), 22 | removeDieBtn: document.querySelector("button[data-action=\"remove-die\"]"), 23 | } 24 | 25 | // Changes 26 | const changes = { 27 | addDie(state, newDie) { 28 | if (state.diceCount === 6) { 29 | return state 30 | } 31 | 32 | return state 33 | .set("diceCount", state.diceCount + 1) 34 | .update("roll", r => r.push(newDie)) 35 | }, 36 | 37 | removeDie(state) { 38 | if (state.diceCount === 1) { 39 | return state 40 | } 41 | 42 | return state 43 | .set("diceCount", state.diceCount - 1) 44 | .update("roll", r => r.pop()) 45 | }, 46 | 47 | updateRoll(state, roll) { 48 | return state.set("roll", new List(roll)) 49 | }, 50 | } 51 | 52 | // Helper function to perform a roll. The random generation must be done 53 | // *outside* of a change since it is not a pure action 54 | function rollDice(store) { 55 | const diceCount = store.getState().diceCount 56 | const roll = Array.from({length: diceCount}, () => d6()) 57 | 58 | store.dispatch(changes.updateRoll, roll) 59 | } 60 | 61 | // Initialize application state 62 | const logger = createLoggerMiddleware(({change, oldState, newState}) => { 63 | console.log(change.name, ":", oldState.toJS(), "=>", newState.toJS()) 64 | }) 65 | const initialState = new AppState() 66 | const store = createStore(initialState, [logger]) 67 | 68 | // Handle store updates 69 | store.subscribe(state => { 70 | ui.dice.textContent = state.roll.join(" ") 71 | }) 72 | 73 | // Bind actions to buttons 74 | ui.addDieBtn.addEventListener("click", (ev) => { 75 | ev.preventDefault() 76 | store.dispatch(changes.addDie, d6()) 77 | }) 78 | 79 | ui.removeDieBtn.addEventListener("click", (ev) => { 80 | ev.preventDefault() 81 | store.dispatch(changes.removeDie) 82 | }) 83 | 84 | ui.rollBtn.addEventListener("click", (ev) => { 85 | ev.preventDefault() 86 | rollDice(store) 87 | }) 88 | 89 | // Bootstrap 90 | ui.dice.textContent = store.getState().roll.join(" ") 91 | 92 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | import test from "tape" 2 | 3 | import { createStore } from "../src" 4 | 5 | function nextState(store) { 6 | return store.dispatch(() => store.getState() + 1) 7 | } 8 | 9 | test("createStore", t => { 10 | const initialState = {} 11 | const store = createStore(initialState) 12 | 13 | t.ok(store.getState() === initialState, 14 | "creates a store with an initial state") 15 | 16 | t.throws(() => createStore(), 17 | /missing initial state/i, 18 | "the initial state is mandatory") 19 | 20 | t.end() 21 | }) 22 | 23 | test("subscribe", t => { 24 | const store = createStore(1) 25 | t.plan(3) 26 | 27 | const unsubscribe = store.subscribe(() => { 28 | t.pass("adds a subscriber to be called upon dispatch") 29 | }) 30 | nextState(store) 31 | 32 | unsubscribe() 33 | nextState(store) 34 | t.pass("returns a function that can be used to stop subscribing") 35 | 36 | const subscriber = () => t.pass("ignores duplicate subscribers") 37 | store.subscribe(subscriber) 38 | store.subscribe(subscriber) 39 | nextState(store) 40 | }) 41 | 42 | test("dispatch", t => { 43 | const store = createStore(1) 44 | 45 | nextState(store) 46 | t.equal(2, store.getState(), 47 | "updates the state with the result of the dispatched change") 48 | 49 | store.dispatch(s => s - 1) 50 | t.equal(1, store.getState(), 51 | "passes the current state to the dispatched change") 52 | 53 | store.dispatch((s, i) => s + i, 1) 54 | t.equal(2, store.getState(), 55 | "passes extra arguments to the dispatched change") 56 | 57 | store.subscribe((newState, oldState) => { 58 | t.pass("notifies subscribers if the state changes") 59 | t.equal(3, newState, "passes the new to the subscriber") 60 | t.equal(2, oldState, "passes the old state to the subscriber") 61 | }) 62 | nextState(store) 63 | 64 | store.subscribe(() => { 65 | t.fail("does not notifies subscribers if the state does not change") 66 | }) 67 | store.dispatch(s => s) 68 | 69 | t.end() 70 | }) 71 | 72 | test("middleware", t => { 73 | t.plan(4) 74 | 75 | const setStateTo2 = () => 2 76 | const changeArg = "some argument" 77 | const middleware = (next, state, change, ...args) => { 78 | t.pass("is called when a change is dispatched") 79 | t.equal(state, store.getState(), 80 | "is passed the store state") 81 | t.equal(change, setStateTo2, 82 | "is passed the dispatched change") 83 | t.equal(args[0], changeArg, 84 | "is passed the dispatched change arguments") 85 | 86 | return next(state, change, ...args) 87 | } 88 | 89 | const store = createStore(1, [ middleware ]) 90 | store.dispatch(setStateTo2, changeArg) 91 | }) 92 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.createStore = createStore; 7 | 8 | function _toConsumableArray(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } else { return Array.from(arr); } } 9 | 10 | function createStore(initialState) { 11 | var middleware = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : []; 12 | 13 | if (initialState === undefined) { 14 | throw new Error("Missing initial state in store creation"); 15 | } 16 | var state = initialState; 17 | var subscribers = new Set(); 18 | 19 | // Final middleware, simply applies the change to the state 20 | middleware.push(function finalize(next, state, change) { 21 | for (var _len = arguments.length, args = Array(_len > 3 ? _len - 3 : 0), _key = 3; _key < _len; _key++) { 22 | args[_key - 3] = arguments[_key]; 23 | } 24 | 25 | return change.apply(undefined, [state].concat(args)); 26 | }); 27 | 28 | var updater = middleware.reduceRight(function (chain, mw) { 29 | var next = chain[0]; 30 | chain.unshift(function (state, change) { 31 | for (var _len2 = arguments.length, args = Array(_len2 > 2 ? _len2 - 2 : 0), _key2 = 2; _key2 < _len2; _key2++) { 32 | args[_key2 - 2] = arguments[_key2]; 33 | } 34 | 35 | return mw.apply(undefined, [next, state, change].concat(args)); 36 | }); 37 | return chain; 38 | }, []).shift(); 39 | 40 | return { 41 | getState: function getState() { 42 | return state; 43 | }, 44 | dispatch: function dispatch(change) { 45 | if (typeof change !== "function") { 46 | throw new Error("change must be a function"); 47 | } 48 | 49 | var oldState = state; 50 | 51 | for (var _len3 = arguments.length, args = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { 52 | args[_key3 - 1] = arguments[_key3]; 53 | } 54 | 55 | state = updater.apply(undefined, [state, change].concat(_toConsumableArray(args))); 56 | 57 | if (state === oldState) { 58 | return; 59 | } 60 | 61 | subscribers.forEach(function (subscriber) { 62 | return subscriber(state, oldState); 63 | }); 64 | }, 65 | subscribe: function subscribe(subscriber) { 66 | if (typeof subscriber !== "function") { 67 | throw new Error("subscriber must be a function"); 68 | } 69 | 70 | subscribers.add(subscriber); 71 | 72 | return function unsubscribe() { 73 | if (subscribers.has(subscriber)) { 74 | subscribers.delete(subscriber); 75 | } 76 | }; 77 | } 78 | }; 79 | } -------------------------------------------------------------------------------- /docs/react.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: React bindings 4 | --- 5 | 6 | # React bindings 7 | 8 | _elfi_ ships with some React bindings so you can quickly start working on an 9 | application using both of them. They are available in the `elfi/react` module. 10 | 11 | ## `storeShape` 12 | 13 | The `storeShape` object allows you to specify that a component's prop should be 14 | a store, which means it has the `dispatch`, `getState` and `subscribe` methods 15 | available. 16 | 17 | ```js 18 | import { storeShape } from "elfi/react" 19 | 20 | function Counter({ store }) { 21 | return

{store.getState()}

22 | } 23 | 24 | Counter.propTypes = { 25 | store: storeShape.isRequired, 26 | } 27 | ``` 28 | 29 | ## `ElfiContext` and `connect` 30 | 31 | _elfi_ ships with an integration for the official React Context API that's been 32 | available since React 16.3.0. To use it, you must wrap your app in the context 33 | provider `ElfiContext.Provider` and you can use `ElfiContext.Consumer` in your 34 | child components to consume store data. 35 | 36 | You'll also probably want to automatically trigger updates using the `subscribe` 37 | method from the store. Here is a typical app startup code using _elfi/react_: 38 | 39 | ```js 40 | // app.js 41 | import React from "react" 42 | import ReactDOM from "react-dom" 43 | import { createStore } from "elfi" 44 | import { ElfiContext } from "elfi/react" 45 | 46 | import Counter from "./Counter" 47 | 48 | const store = createStore(0) // Our state is a simple integer 49 | const root = document.getElementById("app-root") 50 | 51 | document.addEventListener("DOMContentLoaded", renderApp) 52 | store.subscribe(renderApp) 53 | 54 | function renderApp() { 55 | ReactDOM.render( 56 | 57 | 58 | , 59 | root, 60 | ) 61 | } 62 | ``` 63 | 64 | In order to simplify consumption of store data in your components, _elfi_ ships 65 | with a `connect` _higher order component_ that does all the dirty work for you. 66 | It takes a component as a first argument and an function that maps store data to 67 | props as a second argument. This mapping function takes the store state and the 68 | store itself as arguments, and must return an object of props. 69 | 70 | Using the previous app bootstrap code, here's an example usage of the `connect` 71 | HOC to build a component: 72 | 73 | ```js 74 | // Counter.js 75 | import React from "react" 76 | import { connect } from "elfi/react" 77 | 78 | function Counter({ value }) { 79 | return

Current value is {value}

80 | } 81 | 82 | export default connect( 83 | Counter, 84 | value => ({ value }), 85 | ) 86 | ``` 87 | 88 | ## Dispatching actions from components 89 | 90 | Dispatching actions from your React components is no different than using _elfi_ 91 | outside of the React environment. To continue on our counter example, here's how 92 | you would increment the counter's value when clicking on it: 93 | 94 | ```js 95 | // Counter.js 96 | import React from "react" 97 | import PropTypes from "prop-types" 98 | import { storeShape, connect } from "elfi/react" 99 | 100 | // Our increment action 101 | function increment(state) { 102 | return state + 1 103 | } 104 | 105 | class Counter extends React.Component { 106 | static propTypes = { 107 | store: storeShape.isRequired, 108 | value: PropTypes.number.isRequired, 109 | } 110 | 111 | handleClick = () => { 112 | const { store } = this.props 113 | store.dispatch(increment) 114 | } 115 | 116 | render() { 117 | const { value } = this.props 118 | return

Current value is {value}

119 | } 120 | } 121 | 122 | export default connect( 123 | Counter, 124 | value => ({ value }), 125 | ) 126 | ``` 127 | 128 | You can [try this demo online](https://codepen.io/madx/pen/MBLvPg) 129 | -------------------------------------------------------------------------------- /docs/css/syntax.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Name: Base16 Default Light 4 | Author: Chris Kempson (http://chriskempson.com) 5 | 6 | Pygments template by Jan T. Sott (https://github.com/idleberg) 7 | Created with Base16 Builder by Chris Kempson (https://github.com/chriskempson/base16-builder) 8 | 9 | */ 10 | .highlight .hll { background-color: #e0e0e0 } 11 | .highlight { background: #f5f5f5; color: #151515 } 12 | .highlight .c { color: #b0b0b0 } /* Comment */ 13 | .highlight .err { color: #ac4142 } /* Error */ 14 | .highlight .k { color: #aa759f } /* Keyword */ 15 | .highlight .l { color: #d28445 } /* Literal */ 16 | .highlight .n { color: #151515 } /* Name */ 17 | .highlight .o { color: #75b5aa } /* Operator */ 18 | .highlight .p { color: #151515 } /* Punctuation */ 19 | .highlight .cm { color: #b0b0b0 } /* Comment.Multiline */ 20 | .highlight .cp { color: #b0b0b0 } /* Comment.Preproc */ 21 | .highlight .c1 { color: #b0b0b0 } /* Comment.Single */ 22 | .highlight .cs { color: #b0b0b0 } /* Comment.Special */ 23 | .highlight .gd { color: #ac4142 } /* Generic.Deleted */ 24 | .highlight .ge { font-style: italic } /* Generic.Emph */ 25 | .highlight .gh { color: #151515; font-weight: bold } /* Generic.Heading */ 26 | .highlight .gi { color: #90a959 } /* Generic.Inserted */ 27 | .highlight .gp { color: #b0b0b0; font-weight: bold } /* Generic.Prompt */ 28 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 29 | .highlight .gu { color: #75b5aa; font-weight: bold } /* Generic.Subheading */ 30 | .highlight .kc { color: #aa759f } /* Keyword.Constant */ 31 | .highlight .kd { color: #aa759f } /* Keyword.Declaration */ 32 | .highlight .kn { color: #75b5aa } /* Keyword.Namespace */ 33 | .highlight .kp { color: #aa759f } /* Keyword.Pseudo */ 34 | .highlight .kr { color: #aa759f } /* Keyword.Reserved */ 35 | .highlight .kt { color: #f4bf75 } /* Keyword.Type */ 36 | .highlight .ld { color: #90a959 } /* Literal.Date */ 37 | .highlight .m { color: #d28445 } /* Literal.Number */ 38 | .highlight .s { color: #90a959 } /* Literal.String */ 39 | .highlight .na { color: #6a9fb5 } /* Name.Attribute */ 40 | .highlight .nb { color: #151515 } /* Name.Builtin */ 41 | .highlight .nc { color: #f4bf75 } /* Name.Class */ 42 | .highlight .no { color: #ac4142 } /* Name.Constant */ 43 | .highlight .nd { color: #75b5aa } /* Name.Decorator */ 44 | .highlight .ni { color: #151515 } /* Name.Entity */ 45 | .highlight .ne { color: #ac4142 } /* Name.Exception */ 46 | .highlight .nf { color: #6a9fb5 } /* Name.Function */ 47 | .highlight .nl { color: #151515 } /* Name.Label */ 48 | .highlight .nn { color: #f4bf75 } /* Name.Namespace */ 49 | .highlight .nx { color: #6a9fb5 } /* Name.Other */ 50 | .highlight .py { color: #151515 } /* Name.Property */ 51 | .highlight .nt { color: #75b5aa } /* Name.Tag */ 52 | .highlight .nv { color: #ac4142 } /* Name.Variable */ 53 | .highlight .ow { color: #75b5aa } /* Operator.Word */ 54 | .highlight .w { color: #151515 } /* Text.Whitespace */ 55 | .highlight .mf { color: #d28445 } /* Literal.Number.Float */ 56 | .highlight .mh { color: #d28445 } /* Literal.Number.Hex */ 57 | .highlight .mi { color: #d28445 } /* Literal.Number.Integer */ 58 | .highlight .mo { color: #d28445 } /* Literal.Number.Oct */ 59 | .highlight .sb { color: #90a959 } /* Literal.String.Backtick */ 60 | .highlight .sc { color: #151515 } /* Literal.String.Char */ 61 | .highlight .sd { color: #b0b0b0 } /* Literal.String.Doc */ 62 | .highlight .s2 { color: #90a959 } /* Literal.String.Double */ 63 | .highlight .se { color: #d28445 } /* Literal.String.Escape */ 64 | .highlight .sh { color: #90a959 } /* Literal.String.Heredoc */ 65 | .highlight .si { color: #d28445 } /* Literal.String.Interpol */ 66 | .highlight .sx { color: #90a959 } /* Literal.String.Other */ 67 | .highlight .sr { color: #90a959 } /* Literal.String.Regex */ 68 | .highlight .s1 { color: #90a959 } /* Literal.String.Single */ 69 | .highlight .ss { color: #90a959 } /* Literal.String.Symbol */ 70 | .highlight .bp { color: #151515 } /* Name.Builtin.Pseudo */ 71 | .highlight .vc { color: #ac4142 } /* Name.Variable.Class */ 72 | .highlight .vg { color: #ac4142 } /* Name.Variable.Global */ 73 | .highlight .vi { color: #ac4142 } /* Name.Variable.Instance */ 74 | .highlight .il { color: #d28445 } /* Literal.Number.Integer.Long */ 75 | -------------------------------------------------------------------------------- /docs/_site/css/syntax.css: -------------------------------------------------------------------------------- 1 | /* 2 | 3 | Name: Base16 Default Light 4 | Author: Chris Kempson (http://chriskempson.com) 5 | 6 | Pygments template by Jan T. Sott (https://github.com/idleberg) 7 | Created with Base16 Builder by Chris Kempson (https://github.com/chriskempson/base16-builder) 8 | 9 | */ 10 | .highlight .hll { background-color: #e0e0e0 } 11 | .highlight { background: #f5f5f5; color: #151515 } 12 | .highlight .c { color: #b0b0b0 } /* Comment */ 13 | .highlight .err { color: #ac4142 } /* Error */ 14 | .highlight .k { color: #aa759f } /* Keyword */ 15 | .highlight .l { color: #d28445 } /* Literal */ 16 | .highlight .n { color: #151515 } /* Name */ 17 | .highlight .o { color: #75b5aa } /* Operator */ 18 | .highlight .p { color: #151515 } /* Punctuation */ 19 | .highlight .cm { color: #b0b0b0 } /* Comment.Multiline */ 20 | .highlight .cp { color: #b0b0b0 } /* Comment.Preproc */ 21 | .highlight .c1 { color: #b0b0b0 } /* Comment.Single */ 22 | .highlight .cs { color: #b0b0b0 } /* Comment.Special */ 23 | .highlight .gd { color: #ac4142 } /* Generic.Deleted */ 24 | .highlight .ge { font-style: italic } /* Generic.Emph */ 25 | .highlight .gh { color: #151515; font-weight: bold } /* Generic.Heading */ 26 | .highlight .gi { color: #90a959 } /* Generic.Inserted */ 27 | .highlight .gp { color: #b0b0b0; font-weight: bold } /* Generic.Prompt */ 28 | .highlight .gs { font-weight: bold } /* Generic.Strong */ 29 | .highlight .gu { color: #75b5aa; font-weight: bold } /* Generic.Subheading */ 30 | .highlight .kc { color: #aa759f } /* Keyword.Constant */ 31 | .highlight .kd { color: #aa759f } /* Keyword.Declaration */ 32 | .highlight .kn { color: #75b5aa } /* Keyword.Namespace */ 33 | .highlight .kp { color: #aa759f } /* Keyword.Pseudo */ 34 | .highlight .kr { color: #aa759f } /* Keyword.Reserved */ 35 | .highlight .kt { color: #f4bf75 } /* Keyword.Type */ 36 | .highlight .ld { color: #90a959 } /* Literal.Date */ 37 | .highlight .m { color: #d28445 } /* Literal.Number */ 38 | .highlight .s { color: #90a959 } /* Literal.String */ 39 | .highlight .na { color: #6a9fb5 } /* Name.Attribute */ 40 | .highlight .nb { color: #151515 } /* Name.Builtin */ 41 | .highlight .nc { color: #f4bf75 } /* Name.Class */ 42 | .highlight .no { color: #ac4142 } /* Name.Constant */ 43 | .highlight .nd { color: #75b5aa } /* Name.Decorator */ 44 | .highlight .ni { color: #151515 } /* Name.Entity */ 45 | .highlight .ne { color: #ac4142 } /* Name.Exception */ 46 | .highlight .nf { color: #6a9fb5 } /* Name.Function */ 47 | .highlight .nl { color: #151515 } /* Name.Label */ 48 | .highlight .nn { color: #f4bf75 } /* Name.Namespace */ 49 | .highlight .nx { color: #6a9fb5 } /* Name.Other */ 50 | .highlight .py { color: #151515 } /* Name.Property */ 51 | .highlight .nt { color: #75b5aa } /* Name.Tag */ 52 | .highlight .nv { color: #ac4142 } /* Name.Variable */ 53 | .highlight .ow { color: #75b5aa } /* Operator.Word */ 54 | .highlight .w { color: #151515 } /* Text.Whitespace */ 55 | .highlight .mf { color: #d28445 } /* Literal.Number.Float */ 56 | .highlight .mh { color: #d28445 } /* Literal.Number.Hex */ 57 | .highlight .mi { color: #d28445 } /* Literal.Number.Integer */ 58 | .highlight .mo { color: #d28445 } /* Literal.Number.Oct */ 59 | .highlight .sb { color: #90a959 } /* Literal.String.Backtick */ 60 | .highlight .sc { color: #151515 } /* Literal.String.Char */ 61 | .highlight .sd { color: #b0b0b0 } /* Literal.String.Doc */ 62 | .highlight .s2 { color: #90a959 } /* Literal.String.Double */ 63 | .highlight .se { color: #d28445 } /* Literal.String.Escape */ 64 | .highlight .sh { color: #90a959 } /* Literal.String.Heredoc */ 65 | .highlight .si { color: #d28445 } /* Literal.String.Interpol */ 66 | .highlight .sx { color: #90a959 } /* Literal.String.Other */ 67 | .highlight .sr { color: #90a959 } /* Literal.String.Regex */ 68 | .highlight .s1 { color: #90a959 } /* Literal.String.Single */ 69 | .highlight .ss { color: #90a959 } /* Literal.String.Symbol */ 70 | .highlight .bp { color: #151515 } /* Name.Builtin.Pseudo */ 71 | .highlight .vc { color: #ac4142 } /* Name.Variable.Class */ 72 | .highlight .vg { color: #ac4142 } /* Name.Variable.Global */ 73 | .highlight .vi { color: #ac4142 } /* Name.Variable.Instance */ 74 | .highlight .il { color: #d28445 } /* Literal.Number.Integer.Long */ 75 | -------------------------------------------------------------------------------- /docs/guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | layout: page 3 | title: Using elfi 4 | --- 5 | 6 | # Using elfi 7 | 8 | This guide details the basic concepts of _elfi_ and its usage. 9 | 10 | For information about how to use _elfi_ in a React environment, please take a 11 | look at the [`elfi/react` docs](./react.md) 12 | 13 | ## Basic concepts 14 | 15 | _elfi_ allows you to create a _store_ which holds the whole _state_ of your 16 | application. The state is updated by dispatching functions that return a new 17 | state based on the previous ones. Such functions are called _changes_, and they 18 | should be pure functions (_i.e._ having no side effects). 19 | 20 | If you are familiar with [Flux][flux] or [Redux][redux] this might sound 21 | familiar to you, but it strives to remain simple by eliminating most of the 22 | boilerplate that you would expect to find with them. There are no dispatchers, 23 | no reducers, no actions and no action creators in _elfi_, only simple functions. 24 | 25 | Finally, the store can accept _subscribers_ which are also functions and which 26 | are called when a state change occurs. 27 | 28 | ## Creating a store 29 | 30 | Creating your _elfi_ store is done by importing `createStore` and calling it 31 | with an initial state. 32 | 33 | ```js 34 | import { createStore } from "elfi" 35 | 36 | const store = createStore(1) 37 | ``` 38 | 39 | In the example above, the state of our application is a number. This is 40 | perfectly valid and _elfi_ enforces no specific type for the internal state of 41 | the store. 42 | 43 | You can query for the current state of the store using `getState`: 44 | 45 | ```js 46 | store.getState() // => 1 47 | ``` 48 | 49 | ## Dispatching changes 50 | 51 | As mentioned previously, a _change_ is a function that returns a new state based 52 | on the current state of the store. 53 | 54 | Continuing our integer store example, we can write an `increment` change like 55 | this: 56 | 57 | ```js 58 | function increment(state) { 59 | return state + 1 60 | } 61 | ``` 62 | 63 | Such a change can be dispatched using `store.dispatch`: 64 | 65 | ```js 66 | store.dispatch(increment) 67 | store.getState() // => 2 68 | ``` 69 | 70 | Any extraneous arguments passed to dispatched will be passed to the change as 71 | well. This allows us to write and `add` change like this: 72 | 73 | ```js 74 | function add(state, n) { 75 | return state + n 76 | } 77 | 78 | store.dispatch(add, 40) 79 | store.getState() // => 42 80 | ``` 81 | 82 | ## Listening for changes 83 | 84 | You can add a subscriber by using `store.subscribe`. A subscriber is a function 85 | that takes two arguments: the old state and the new state. All subscribers of 86 | the store are called when a change occurs, and only if this change actually 87 | modifies the internal state of the store. 88 | 89 | ```js 90 | store.subscribe((oldState, newState) => console.log(newState)) 91 | store.dispatch(increment) // logs 43 92 | store.dispatch(x => x) // does not log anything since state is unchanged 93 | ``` 94 | 95 | `store.subscribe` returns a function that can be used to stop listening for 96 | changes: 97 | 98 | ```js 99 | const unsubscribe = store.subscribe(mySubscriber) 100 | // do things 101 | unsubscribe() 102 | ``` 103 | 104 | ## Middleware 105 | 106 | Middleware is a thin layer that allows you to customize the behavior of the 107 | store by hooking into the dispatching process. 108 | 109 | A middleware is a function (again!) that takes at least 3 arguments: 110 | 111 | - The next middleware function to call, 112 | - The current store state, 113 | - The change function that is being dispatched, 114 | - And any extra arguments to pass to the change. 115 | 116 | Here's an example of a simple logging middleware: 117 | 118 | ```js 119 | function loggerMiddleware(next, oldState, change, ...args) { 120 | const newState = next(oldState, change, ...args) 121 | console.log(change.name, oldState, newState) 122 | return newState 123 | } 124 | ``` 125 | 126 | You define what middleware you want to use at store creation time. `createStore` 127 | takes a second argument which is an array of middleware functions to use: 128 | 129 | ```js 130 | const store = createStore(1, [loggerMiddleware]) 131 | ``` 132 | 133 | Calling `next` chains to the next middleware piece, or to the internal 134 | dispatching mechanism. You should always return a valid state in your middleware 135 | or the internal state of your store will take the value of `undefined`. 136 | 137 | _elfi_ ships with some builtin middleware for common tasks, you can get more 138 | information about it in the [middleware documentation](./middleware.md). 139 | 140 | [flux]: https://github.com/facebook/flux 141 | [redux]: https://github.com/reactjs/redux 142 | -------------------------------------------------------------------------------- /docs/_site/react.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | elfi - React bindings 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | elfi 25 | 26 |

27 |
28 |
29 |

React bindings

30 | 31 |

elfi ships with some React bindings so you can quickly start working on an 32 | application using both of them. They are available in the elfi/react module.

33 | 34 |

storeShape

35 | 36 |

The storeShape object allows you to specify that a property or a context 37 | element of a component should behave like a store, which means it has the 38 | dispatch, getState and subscribe methods available.

39 | 40 |
import {storeShape} from "elfi/react"
 41 | 
 42 | function Counter(props, context) {
 43 |   const store = props.store
 44 | 
 45 |   return <div>{store.getState()}</div>
 46 | }
 47 | 
 48 | MyComponent.propTypes = {
 49 |   store: storeShape.isRequired,
 50 | }
 51 | 
52 |
53 | 54 |

Provider

55 | 56 |

The Provider is used as a container component that will trigger renders of its 57 | children every time the store is updated. It also passes the store to its 58 | children through context.

59 | 60 |
// Root.js
 61 | function Root(props) {
 62 |   return (
 63 |     <Provider store={props.store}>
 64 |       <App />
 65 |     </Provider>
 66 |   )
 67 | }
 68 | 
 69 | // App.js
 70 | function App(props, context) {
 71 |   // App has access to the store through context
 72 |   const store = context.store
 73 | 
 74 |   function increment(s) {
 75 |     return s + 1
 76 |   }
 77 | 
 78 |   function onClick() {
 79 |     store.dispatch(increment)
 80 |   }
 81 | 
 82 |   return (
 83 |     <div>
 84 |       <span>{store.getState()}</span>
 85 |       <button onClick={onClick}>Increment</button>
 86 |     </div>
 87 |   )
 88 | }
 89 | 
 90 | App.contextTypes = {
 91 |   store: storeShape.isRequired
 92 | }
 93 | 
94 |
95 | 96 |
97 | 102 | 103 | Fork me on GitHub 104 | 105 | 106 | 107 | 108 | -------------------------------------------------------------------------------- /docs/_site/middleware.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | elfi - Built-in middleware 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | elfi 25 | 26 |

27 |
28 |
29 |

Built-in middleware

30 | 31 |

elfi provides some builtin middleware. You’ll find documentation about them in 32 | this document.

33 | 34 |

logger (source

35 | 36 |

This middleware is used to log each change to whatever logging system you are 37 | using (a simple console.log by default)

38 | 39 |
import {createStore} from "elfi"
 40 | import createLoggerMiddleware from "elfi/middleware/logger"
 41 | 
 42 | const logger = createLoggerMiddleware()
 43 | 
 44 | const store = createStore(1, [logger])
 45 | 
46 |
47 | 48 |

You can define a custom logger by passing a function to 49 | createLoggerMiddleware. This function accepts a single argument which is 50 | object with the following shape {oldState, newState, change}.

51 | 52 |
import {createStore} from "elfi"
 53 | import createLoggerMiddleware from "elfi/middleware/logger"
 54 | 
 55 | function logOldState({oldState}) {
 56 |   console.log(oldState)
 57 | }
 58 | 
 59 | const logger = createLoggerMiddleware(({oldState}) => console.log(oldState))
 60 | 
 61 | const store = createStore(1, [logger])
 62 | 
63 |
64 | 65 |

versioning (source

66 | 67 |

This middleware is used to add a version number to the state without triggering 68 | another state change.

69 | 70 |
import {createStore} from "elfi"
 71 | import createVersioningMiddleware from "elfi/middleware/versioning"
 72 | 
 73 | const versioning = createVersioningMiddleware()
 74 | 
 75 | const store = createStore(1, [versioning])
 76 | 
77 |
78 | 79 |

By default, it acts as if state is a simple object and sets the version field 80 | of that object. You will probably want to define how to set the version and for 81 | this you can pass a custom setter function to createVersioningMiddleware.

82 | 83 |

It will be called with the new state as the sole argument. Here’s an example 84 | with Immutable.js:

85 | 86 |
import Immutable from "immutable"
 87 | import {createStore} from "elfi"
 88 | import createVersioningMiddleware from "elfi/middleware/versioning"
 89 | 
 90 | const versioning = createVersioningMiddleware(state => (
 91 |   state.set((state.get("version") || 0) + 1)
 92 | ))
 93 | const store = createStore(new Immutable.Map(), [versioning])
 94 | 
95 |
96 | 97 | 98 |
99 | 104 | 105 | Fork me on GitHub 106 | 107 | 108 | 109 | 110 | -------------------------------------------------------------------------------- /docs/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 33 | 34 | 37 | 41 | 45 | 46 | 49 | 53 | 57 | 58 | 67 | 76 | 85 | 86 | 109 | 111 | 112 | 114 | image/svg+xml 115 | 117 | 118 | 119 | 120 | 121 | 126 | 130 | 135 | 140 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /docs/_site/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 22 | 25 | 29 | 33 | 34 | 37 | 41 | 45 | 46 | 49 | 53 | 57 | 58 | 67 | 76 | 85 | 86 | 109 | 111 | 112 | 114 | image/svg+xml 115 | 117 | 118 | 119 | 120 | 121 | 126 | 130 | 135 | 140 | 145 | 146 | 147 | 148 | -------------------------------------------------------------------------------- /docs/_site/guide.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | elfi - Using elfi 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 |

23 | 24 | elfi 25 | 26 |

27 |
28 |
29 |

Using elfi

30 | 31 |

This guide details the basic concepts of elfi and it’s usage.

32 | 33 |

Basic concepts

34 | 35 |

elfi allows you to create a store which holds the whole state of your 36 | application. The state is updated by dispatching functions that return a new 37 | state based on the previous ones. Such functions are called changes, and they 38 | should be pure functions (i.e. having no side effects).

39 | 40 |

If you are familiar with Flux or Redux this might sound 41 | familiar to you, but it strives to remain simple by eliminating most of the 42 | boilerplate that you would expect to find with them. There are no dispatchers, 43 | no reducers, no actions and no action creators in elfi, only simple functions.

44 | 45 |

Finally, the store can accept subscribers which are also functions and which 46 | are called when a state change occurs.

47 | 48 |

Creating a store

49 | 50 |

Creating your elfi store is done by importing createStore and calling it 51 | with an initial state.

52 | 53 |
import {createStore} from "elfi"
 54 | 
 55 | const store = createStore(1)
 56 | 
57 |
58 | 59 |

In the example above, the state of our application is a number. This is 60 | perfectly valid and elfi enforces no specific type for the internal state of 61 | the store.

62 | 63 |

You can query for the current state of the store using getState:

64 | 65 |
store.getState() // => 1
 66 | 
67 |
68 | 69 |

Dispatching changes

70 | 71 |

As mentioned previously, a change is a function that returns a new state based 72 | on the current state of the store.

73 | 74 |

Continuing our integer store example, we can write an increment change like 75 | this:

76 | 77 |
function increment(state) {
 78 |   return state + 1
 79 | }
 80 | 
81 |
82 | 83 |

Such a change can be dispatched using store.dispatch:

84 | 85 |
store.dispatch(increment)
 86 | store.getState() // => 2
 87 | 
88 |
89 | 90 |

Any extraneous arguments passed to dispatched will be passed to the change as 91 | well. This allows us to write and add change like this:

92 | 93 |
function add(state, n) {
 94 |   return state + n
 95 | }
 96 | 
 97 | store.dispatch(add, 40)
 98 | store.getState() // => 42
 99 | 
100 |
101 | 102 |

Listening for changes

103 | 104 |

You can add a subscriber by using store.subscribe. A subscriber is a function 105 | that takes two arguments: the old state and the new state. All subscribers of 106 | the store are called when a change occurs, and only if this change actually 107 | modifies the internal state of the store.

108 | 109 |
store.subscribe((oldState, newState) => console.log(newState))
110 | store.dispatch(increment) // logs 43
111 | store.dispatch(x => x) // does not log anything since state is unchanged
112 | 
113 |
114 | 115 |

store.subscribe returns a function that can be used to stop listening for 116 | changes:

117 | 118 |
const unsubscribe = store.subscribe(mySubscriber)
119 | // do things
120 | unsubscribe()
121 | 
122 |
123 | 124 |

Middleware

125 | 126 |

Middleware is a thin layer that allows you to customize the behavior of the 127 | store by hooking into the dispatching process.

128 | 129 |

A middleware is a function (again!) that takes at least 3 arguments:

130 | 131 | 137 | 138 |

Here’s an example of a simple logging middleware:

139 | 140 |
function loggerMiddleware(next, oldState, change, ...args) {
141 |   const newState = next(oldState, change, ...args)
142 |   console.log(change.name, oldState, newState)
143 |   return newState
144 | }
145 | 
146 |
147 | 148 |

You define what middleware you want to use at store creation time. createStore 149 | takes a second argument which is an array of middleware functions to use:

150 | 151 |
const store = createStore(1, [loggerMiddleware])
152 | 
153 |
154 | 155 |

Calling next chains to the next middleware piece, or to the internal 156 | dispatching mechanism. You should always return a valid state in your middleware 157 | or the internal state of your store will take the value of undefined.

158 | 159 |

elfi ships with some builtin middleware for common tasks, you can get more 160 | information about it in the middleware documentation.

161 | 162 | 163 |
164 | 169 | 170 | Fork me on GitHub 171 | 172 | 173 | 174 | 175 | --------------------------------------------------------------------------------