├── .babelrc ├── .eslintrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── dist ├── geiger.js └── geiger.js.map ├── gulpfile.js ├── package.json ├── src └── geiger.js └── test ├── geiger └── geiger.js └── setup.js /.babelrc: -------------------------------------------------------------------------------- 1 | { "stage": 0 } -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "ecmaFeatures": { 3 | "modules": true 4 | }, 5 | "env": { 6 | "browser": true, 7 | "es6": true, 8 | "node": true 9 | }, 10 | "parser": "babel-eslint", 11 | "rules": { 12 | "space-after-keywords": "always", 13 | "quotes": "single", 14 | "no-unused-vars": false, 15 | "new-cap": false 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.12" 4 | - "0.11" 5 | - "0.10" 6 | - "iojs" 7 | - "iojs-v1.0.4" 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Net Gusto 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 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Geiger, the unfancy flux 2 | 3 | [![](https://api.travis-ci.org/netgusto/Geiger.svg?branch=master)](https://travis-ci.org/netgusto/Geiger?branch=master) 4 | 5 | Tiny (<100 SLOC), no-dependencies Flux implementation with store synchronization (waitFor) and Dependency Injection features. 6 | 7 | Leverages React's (0.13+) contexts for injecting dependencies automatically down the component tree. 8 | 9 | For a basic implementation reference, have a look at [Idiomatic React](https://github.com/netgusto/IdiomaticReact). 10 | 11 | For a full-scale implementation reference, making use of **multiple synchronized stores**, have a look at [Idiomatic React Chat](https://github.com/netgusto/IdiomaticReact/tree/chat). 12 | 13 | Victoreen CD-715 Model 1A 14 | 15 | ## About Geiger 16 | 17 | * **Dependency injection**: Services are injected in components, and so are relative to your application context, rather than being used as global services 18 | * **No dispatcher**: rather than a central/global dispatcher, Geiger uses events and promises to manage the action flow, and to ensure that interdependent stores handle actions cooperatively 19 | * **Readable source code**: the source code should be small enough to be readable, and so to serve as the primary source of documentation 20 | 21 | ## Installation 22 | 23 | ```bash 24 | $ npm install --save geiger 25 | ``` 26 | 27 | ## Usage 28 | 29 | ```javascript 30 | // main.js 31 | 'use strict'; 32 | 33 | import React from 'react/addons'; 34 | 35 | import { ContextFactory, Action, Store } from 'geiger'; 36 | import TodoList from './TodoList'; 37 | 38 | // The Context component (think "Dependency Injection Container") 39 | const Context = ContextFactory({ 40 | todostore: React.PropTypes.object.isRequired, 41 | todoactions: React.PropTypes.object.isRequired 42 | }); 43 | 44 | // Actions; just relay to the store, but can be much thicker 45 | const todoactions = new (class extends Action { 46 | add(...args) { this.emit('add', ...args); } 47 | remove(...args) { this.emit('remove', ...args); } 48 | })(); 49 | 50 | // Store (Mutable) 51 | const todostore = new (class extends Store { 52 | 53 | constructor(actions, todos = []) { 54 | super(); 55 | 56 | this.todos = todos; 57 | 58 | // action handlers 59 | this.listen(actions, 'add', (todo) => { this.todos.push(todo); this.changed(); }); 60 | } 61 | 62 | // Public API 63 | getAll() { return this.todos; } 64 | 65 | })(todoactions, ['Todo One', 'Todo Two', 'Todo three']); 66 | 67 | React.render( 68 | ( } />), 72 | document.body 73 | ); 74 | ``` 75 | 76 | ```javascript 77 | //TodoList.js 78 | 'use strict'; 79 | 80 | import React from 'react/addons'; 81 | 82 | export default class TodoList extends React.Component { 83 | 84 | // declaring dependencies to inject; can be a subset of all the context 85 | static contextTypes = { 86 | todostore: React.PropTypes.object.isRequired, 87 | todoactions: React.PropTypes.object.isRequired 88 | }; 89 | 90 | // watching store changes 91 | componentWillMount() { 92 | this.unwatch = [this.context.todostore.watch(this.forceUpdate.bind(this))]; 93 | } 94 | 95 | // unwatching store changes 96 | componentWillUnmount() { this.unwatch.map(cbk => cbk()); } 97 | 98 | render() { 99 | const { todostore, todoactions } = this.context; 100 | 101 | return ( 102 |
103 |

Todos

104 | 105 | 106 | 107 |
    108 | {todostore.getAll().map(todo =>
  • {todo}
  • )} 109 |
110 |
111 | ); 112 | } 113 | } 114 | 115 | ``` 116 | 117 | ## Store synchronization 118 | 119 | To synchronize store reaction to actions, use the `waitFor()` method of the store. 120 | 121 | ```javascript 122 | 'use strict'; 123 | 124 | import { Store } from 'geiger'; 125 | 126 | export default class StoreC extends Store { 127 | 128 | constructor({ actions, storea, storeb }) { 129 | super(); 130 | 131 | this.listen(actions, 'createTodo', (todo) => { 132 | return this.waitFor([storea, storeb]).then(() => { 133 | doSomething(todo); 134 | }); 135 | }); 136 | } 137 | } 138 | ``` 139 | 140 | In this example, `waitFor()` returns a promise that'll wait for all given stores to be idle, and that'll execute `then` when that happens. This promise has to be passed to Geiger (hence the `return`; this is asserted at runtime by Geiger, so no worries). 141 | 142 | If you need to, you can `waitFor()` for stores that also `waitFor()` for other stores to complete their action handling. 143 | 144 | ## Test 145 | 146 | ``` 147 | $ npm test 148 | ``` 149 | 150 | ## Licence 151 | 152 | MIT. 153 | 154 | ## Maintainer 155 | 156 | @netgusto 157 | -------------------------------------------------------------------------------- /dist/geiger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, '__esModule', { 4 | value: true 5 | }); 6 | 7 | var _createClass = (function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ('value' in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; })(); 8 | 9 | var _get = function get(_x, _x2, _x3) { var _again = true; _function: while (_again) { var object = _x, property = _x2, receiver = _x3; desc = parent = getter = undefined; _again = false; var desc = Object.getOwnPropertyDescriptor(object, property); if (desc === undefined) { var parent = Object.getPrototypeOf(object); if (parent === null) { return undefined; } else { _x = parent; _x2 = property; _x3 = receiver; _again = true; continue _function; } } else if ('value' in desc) { return desc.value; } else { var getter = desc.get; if (getter === undefined) { return undefined; } return getter.call(receiver); } } }; 10 | 11 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError('Cannot call a class as a function'); } } 12 | 13 | function _inherits(subClass, superClass) { if (typeof superClass !== 'function' && superClass !== null) { throw new TypeError('Super expression must either be null or a function, not ' + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) subClass.__proto__ = superClass; } 14 | 15 | var _events = require('events'); 16 | 17 | 'use strict'; 18 | 19 | var Action = (function (_EventEmitter) { 20 | function Action() { 21 | _classCallCheck(this, Action); 22 | 23 | if (_EventEmitter != null) { 24 | _EventEmitter.apply(this, arguments); 25 | } 26 | } 27 | 28 | _inherits(Action, _EventEmitter); 29 | 30 | return Action; 31 | })(_events.EventEmitter); 32 | 33 | exports.Action = Action; 34 | 35 | var Store = (function (_EventEmitter2) { 36 | function Store() { 37 | for (var _len = arguments.length, args = Array(_len), _key = 0; _key < _len; _key++) { 38 | args[_key] = arguments[_key]; 39 | } 40 | 41 | _classCallCheck(this, Store); 42 | 43 | _get(Object.getPrototypeOf(Store.prototype), 'constructor', this).apply(this, args); 44 | this.dispatching = []; 45 | this.waiting = []; 46 | } 47 | 48 | _inherits(Store, _EventEmitter2); 49 | 50 | _createClass(Store, [{ 51 | key: 'changed', 52 | value: function changed() { 53 | this.emit('change'); 54 | } 55 | }, { 56 | key: 'watch', 57 | value: function watch(cbk) { 58 | var _this = this; 59 | 60 | this.on('change', cbk); 61 | return function () { 62 | return _this.removeListener('change', cbk); 63 | }; 64 | } 65 | }, { 66 | key: 'isDispatching', 67 | value: function isDispatching() { 68 | return this.dispatching.length > 0; 69 | } 70 | }, { 71 | key: 'isWaiting', 72 | value: function isWaiting() { 73 | return this.waiting.length > 0; 74 | } 75 | }, { 76 | key: 'listen', 77 | value: function listen(actions, event, cbk) { 78 | var _this2 = this; 79 | 80 | if (typeof actions !== 'object' || typeof actions.on !== 'function') { 81 | throw new Error('Store ' + this.constructor.name + '.listen() method expects an EventEmitter-compatible object as a first parameter.'); 82 | } 83 | 84 | actions.on(event, function () { 85 | for (var _len2 = arguments.length, args = Array(_len2), _key2 = 0; _key2 < _len2; _key2++) { 86 | args[_key2] = arguments[_key2]; 87 | } 88 | 89 | // dispatching begins 90 | _this2.dispatching.push(event); 91 | 92 | var res = cbk.apply(undefined, args); 93 | var ispromise = typeof res === 'object' && typeof res.then === 'function'; 94 | 95 | if (_this2.isWaiting() && !ispromise) { 96 | throw new Error('Store ' + _this2.constructor.name + ' waiting; action has to return the waiting promise (the promise returned by waitFor).'); 97 | } 98 | 99 | var dispatchingEnd = function dispatchingEnd() { 100 | _this2.dispatching.pop(); 101 | _this2.emit('dispatching:end', event); 102 | // dispatching ends 103 | }; 104 | 105 | if (ispromise) { 106 | res.then(dispatchingEnd); 107 | } else { 108 | dispatchingEnd(); 109 | } 110 | 111 | return res; 112 | }); 113 | } 114 | }, { 115 | key: 'wait', 116 | value: function wait() { 117 | for (var _len3 = arguments.length, args = Array(_len3), _key3 = 0; _key3 < _len3; _key3++) { 118 | args[_key3] = arguments[_key3]; 119 | } 120 | 121 | console.log('Geiger: wait() is deprecated in favour of waitFor(). Please, update your codebase.'); 122 | return this.waitFor.apply(this, args); 123 | } 124 | }, { 125 | key: 'waitFor', 126 | value: function waitFor(stores) { 127 | var _this3 = this; 128 | 129 | this.waiting.push(true); 130 | 131 | var promises = []; 132 | 133 | (Array.isArray(stores) ? stores : [stores]).map(function (store) { 134 | if (store.isDispatching()) { 135 | promises.push(new Promise(function (resolve) { 136 | return store.once('dispatching:end', resolve); 137 | })); 138 | } else { 139 | promises.push(true); 140 | } 141 | }); 142 | 143 | return Promise.all(promises).then(function () { 144 | return _this3.waiting.pop(); 145 | }); 146 | } 147 | }]); 148 | 149 | return Store; 150 | })(_events.EventEmitter); 151 | 152 | exports.Store = Store; 153 | var ContextFactory = function ContextFactory(propTypes) { 154 | 155 | return (function () { 156 | function FactoriedContext() { 157 | _classCallCheck(this, FactoriedContext); 158 | } 159 | 160 | _createClass(FactoriedContext, [{ 161 | key: 'getChildContext', 162 | value: function getChildContext() { 163 | var res = {}; 164 | for (var propname in propTypes) { 165 | res[propname] = this.props[propname]; 166 | } 167 | return res; 168 | } 169 | }, { 170 | key: 'render', 171 | value: function render() { 172 | return this.props.render(); 173 | } 174 | }], [{ 175 | key: 'childContextTypes', 176 | value: propTypes, 177 | enumerable: true 178 | }, { 179 | key: 'propTypes', 180 | value: propTypes, 181 | enumerable: true 182 | }]); 183 | 184 | return FactoriedContext; 185 | })(); 186 | }; 187 | exports.ContextFactory = ContextFactory; 188 | //# sourceMappingURL=geiger.js.map -------------------------------------------------------------------------------- /dist/geiger.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["geiger.js"],"names":[],"mappings":"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","file":"geiger.js","sourcesContent":["'use strict';\n\nimport { EventEmitter } from 'events';\n\nexport class Action extends EventEmitter { }\n\nexport class Store extends EventEmitter {\n\n constructor(...args) {\n super(...args);\n this.dispatching = [];\n this.waiting = [];\n }\n\n changed() { this.emit('change'); }\n\n watch(cbk) {\n this.on('change', cbk);\n return () => this.removeListener('change', cbk);\n }\n\n isDispatching() { return this.dispatching.length > 0; }\n\n isWaiting() { return this.waiting.length > 0; }\n\n listen(actions, event, cbk) {\n\n if(typeof actions !== 'object' || typeof actions.on !== 'function') { throw new Error('Store ' + this.constructor.name + '.listen() method expects an EventEmitter-compatible object as a first parameter.'); }\n\n actions.on(event, (...args) => {\n\n // dispatching begins\n this.dispatching.push(event);\n\n const res = cbk(...args);\n const ispromise = (typeof res === 'object' && typeof res.then === 'function');\n\n if(this.isWaiting() && !ispromise) { throw new Error('Store ' + this.constructor.name + ' waiting; action has to return the waiting promise (the promise returned by waitFor).'); }\n\n const dispatchingEnd = () => {\n this.dispatching.pop();\n this.emit('dispatching:end', event);\n // dispatching ends\n };\n\n if(ispromise) {\n res.then(dispatchingEnd);\n } else {\n dispatchingEnd();\n }\n\n return res;\n });\n }\n\n wait(...args) {\n console.log('Geiger: wait() is deprecated in favour of waitFor(). Please, update your codebase.');\n return this.waitFor(...args);\n }\n\n waitFor(stores) {\n\n this.waiting.push(true);\n\n const promises = [];\n\n (Array.isArray(stores) ? stores : [stores]).map(store => {\n if(store.isDispatching()) {\n promises.push(new Promise(resolve => store.once('dispatching:end', resolve)));\n } else { promises.push(true); }\n });\n\n return Promise.all(promises).then(() => this.waiting.pop());\n }\n}\n\nexport const ContextFactory = (propTypes) => {\n\n return class FactoriedContext {\n\n static childContextTypes = propTypes;\n static propTypes = propTypes;\n\n getChildContext() {\n const res = {};\n for(let propname in propTypes) { res[propname] = this.props[propname]; }\n return res;\n }\n\n render() { return this.props.render(); }\n };\n\n};\n"],"sourceRoot":"/source/"} -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var sourcemaps = require('gulp-sourcemaps'); 3 | var babel = require('gulp-babel'); 4 | var eslint = require('gulp-eslint'); 5 | var print = require('gulp-print'); 6 | 7 | var src = ['src/**/*.js']; 8 | 9 | var build = function(done) { 10 | gulp.src(src) 11 | .pipe(print()) 12 | .pipe(eslint()) 13 | .pipe(eslint.formatEach()) 14 | .pipe(eslint.failAfterError()) 15 | .pipe(sourcemaps.init()) 16 | .pipe(babel()) 17 | .pipe(sourcemaps.write('.')) 18 | .pipe(gulp.dest('dist')) 19 | .on('end', done); 20 | } 21 | 22 | var lint = function(done) { 23 | gulp.src(src) 24 | .pipe(eslint()) 25 | .pipe(eslint.formatEach()) 26 | .pipe(eslint.failAfterError()) 27 | .on('end', done); 28 | } 29 | 30 | gulp.task('build', function(done) { 31 | build(done); 32 | }); 33 | 34 | gulp.task('lint', function(done) { 35 | lint(done); 36 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "geiger", 3 | "version": "1.0.4", 4 | "description": "Tiny (<100 SLOC), no-dependencies Flux implementation with store synchronization (waitFor) and Dependency Injection features", 5 | "main": "dist/geiger.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha --compilers js:babel/register --recursive" 8 | }, 9 | "author": "Jérôme Schneider, Net Gusto ", 10 | "license": "MIT", 11 | "dependencies": {}, 12 | "devDependencies": { 13 | "babel-core": "5.3.3", 14 | "babel-eslint": "3.1.1", 15 | "babel-runtime": "5.3.3", 16 | "eslint": "0.21.0", 17 | "gulp": "3.8.11", 18 | "gulp-util": "3.0.4", 19 | "gulp-sourcemaps": "1.5.2", 20 | "gulp-babel": "5.1.0", 21 | "gulp-eslint": "0.12.0", 22 | "gulp-print": "1.1.0", 23 | "babel": "^5.2.17", 24 | "chai": "^2.3.0", 25 | "jsdom": "^3.1.2", 26 | "mocha": "^2.2.4", 27 | "react-tools": "^0.13.2", 28 | "jquery": "^2.1.4", 29 | "react": "^0.13.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/geiger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { EventEmitter } from 'events'; 4 | 5 | export class Action extends EventEmitter { } 6 | 7 | export class Store extends EventEmitter { 8 | 9 | constructor(...args) { 10 | super(...args); 11 | this.dispatching = []; 12 | this.waiting = []; 13 | } 14 | 15 | changed() { this.emit('change'); } 16 | 17 | watch(cbk) { 18 | this.on('change', cbk); 19 | return () => this.removeListener('change', cbk); 20 | } 21 | 22 | isDispatching() { return this.dispatching.length > 0; } 23 | 24 | isWaiting() { return this.waiting.length > 0; } 25 | 26 | listen(actions, event, cbk) { 27 | 28 | if(typeof actions !== 'object' || typeof actions.on !== 'function') { throw new Error('Store ' + this.constructor.name + '.listen() method expects an EventEmitter-compatible object as a first parameter.'); } 29 | 30 | actions.on(event, (...args) => { 31 | 32 | // dispatching begins 33 | this.dispatching.push(event); 34 | 35 | const res = cbk(...args); 36 | const ispromise = (typeof res === 'object' && typeof res.then === 'function'); 37 | 38 | if(this.isWaiting() && !ispromise) { throw new Error('Store ' + this.constructor.name + ' waiting; action has to return the waiting promise (the promise returned by waitFor).'); } 39 | 40 | const dispatchingEnd = () => { 41 | this.dispatching.pop(); 42 | this.emit('dispatching:end', event); 43 | // dispatching ends 44 | }; 45 | 46 | if(ispromise) { 47 | res.then(dispatchingEnd); 48 | } else { 49 | dispatchingEnd(); 50 | } 51 | 52 | return res; 53 | }); 54 | } 55 | 56 | wait(...args) { 57 | console.log('Geiger: wait() is deprecated in favour of waitFor(). Please, update your codebase.'); 58 | return this.waitFor(...args); 59 | } 60 | 61 | waitFor(stores) { 62 | 63 | this.waiting.push(true); 64 | 65 | const promises = []; 66 | 67 | (Array.isArray(stores) ? stores : [stores]).map(store => { 68 | if(store.isDispatching()) { 69 | promises.push(new Promise(resolve => store.once('dispatching:end', resolve))); 70 | } else { promises.push(true); } 71 | }); 72 | 73 | return Promise.all(promises).then(() => this.waiting.pop()); 74 | } 75 | } 76 | 77 | export const ContextFactory = (propTypes) => { 78 | 79 | return class FactoriedContext { 80 | 81 | static childContextTypes = propTypes; 82 | static propTypes = propTypes; 83 | 84 | getChildContext() { 85 | const res = {}; 86 | for(let propname in propTypes) { res[propname] = this.props[propname]; } 87 | return res; 88 | } 89 | 90 | render() { return this.props.render(); } 91 | }; 92 | 93 | }; 94 | -------------------------------------------------------------------------------- /test/geiger/geiger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import { Action, Store, ContextFactory } from '../../src/geiger'; 4 | import { expect } from 'chai'; 5 | 6 | describe('Geiger', function() { 7 | 8 | let fooaction, foostore, unwatchcbkfoo, unwatchcbkbar; 9 | 10 | it('should provide an Action base class', function() { 11 | expect(Action).to.be.a('function'); 12 | }); 13 | 14 | it('should provide a Store base class', function() { 15 | expect(Store).to.be.a('function'); 16 | }); 17 | 18 | it('should provide a ContextFactory', function() { 19 | expect(Store).to.be.a('function'); 20 | }); 21 | 22 | describe('Actions', function() { 23 | it('should build an Action object', function() { 24 | fooaction = new (class extends Action { 25 | add(what) { this.emit('add', { what }); } 26 | remove(what) { this.emit('remove', { what }); } 27 | clear() { this.emit('clear'); } 28 | })(); 29 | 30 | expect(fooaction).to.be.an('object'); 31 | }); 32 | 33 | it('should trigger actions', function() { 34 | expect(() => fooaction.add('bar')).to.not.throw(); 35 | }); 36 | 37 | it('should not trigger non-existing actions', function() { 38 | expect(() => fooaction.hello('bar')).to.throw(); 39 | }); 40 | 41 | it('should subscribe to action and to trigger it', function(done) { 42 | let triggered = false; 43 | 44 | fooaction.on('add', ({ what }) => { 45 | if(triggered) { 46 | throw new Error(`I'm supposed to be triggered only once !`); 47 | } 48 | 49 | triggered = true; 50 | 51 | done(); 52 | }); 53 | 54 | expect(() => fooaction.add('bar')).to.not.throw(); 55 | //expect(() => fooaction.add('baz')).to.throw('done() called multiple times'); 56 | }); 57 | 58 | it('should unsubscribe to action', function() { 59 | expect(() => fooaction.removeAllListeners('add')).to.not.throw(); 60 | }); 61 | 62 | it('should be unsubscribed to action', function() { 63 | expect(() => fooaction.add('baz')).to.not.throw(); 64 | }); 65 | }); 66 | 67 | describe('Stores', function() { 68 | it('should build a foo Store object', function() { 69 | 70 | foostore = new (class extends Store { 71 | 72 | constructor({ actionfoo }) { 73 | super(); 74 | 75 | this.foos = {}; 76 | 77 | this.listen(actionfoo, 'add', ({ what }) => { 78 | this.foos[what] = 'foo(' + what + ')'; 79 | this.changed(); 80 | }); 81 | 82 | this.listen(actionfoo, 'clear', () => { 83 | this.foos = {}; 84 | this.changed(); 85 | }); 86 | } 87 | 88 | // Public API 89 | 90 | getAll() { return this.foos; } 91 | get(what) { return what in this.foos ? this.foos[what] : null; } 92 | 93 | })({ actionfoo: fooaction }); 94 | 95 | expect(foostore).to.be.an('object'); 96 | }); 97 | 98 | it('should have side effects on stores with actions', function(done) { 99 | 100 | fooaction.add('hello'); 101 | fooaction.add('world'); 102 | 103 | setTimeout(() => { 104 | const foos = foostore.getAll(); 105 | 106 | expect(foos).to.be.an('object'); 107 | 108 | expect(foos).to.include.keys('hello'); 109 | expect(foos).to.include.keys('world'); 110 | 111 | expect(foos['hello']).to.equal('foo(hello)'); 112 | expect(foos['world']).to.equal('foo(world)'); 113 | 114 | fooaction.clear(); 115 | 116 | setTimeout(() => { 117 | expect(foostore.getAll()).to.be.empty; 118 | done(); 119 | }, 10); 120 | 121 | }, 10); 122 | }); 123 | 124 | it('should subscribe and react to store change', function(done) { 125 | 126 | let triggered = false; 127 | 128 | const updatecbk = () => { 129 | 130 | if(triggered) { 131 | throw new Error(`I'm supposed to be triggered only once !`); 132 | } 133 | 134 | triggered = true; 135 | 136 | const foos = foostore.getAll(); 137 | 138 | expect(foos).to.be.an('object'); 139 | //expect(foos).to.have.length.of(1); 140 | expect(foos).to.include.keys('nice'); 141 | expect(foos['nice']).to.equal('foo(nice)'); 142 | 143 | done(); 144 | }; 145 | 146 | unwatchcbkfoo = foostore.watch(updatecbk); 147 | fooaction.add('nice'); 148 | }); 149 | 150 | it('should unsubscribe from store change', function(done) { 151 | 152 | expect(() => unwatchcbkfoo()).to.not.throw(); 153 | fooaction.add('nice'); 154 | fooaction.clear(); 155 | 156 | setTimeout(done, 20); // giving time to throw `I'm supposed to be triggered only once !` if not properly unwatched 157 | }); 158 | 159 | 160 | describe('Synchronized with waitFor()', function() { 161 | 162 | let barstore; 163 | 164 | it('should build a bar store object depending on foostore', function() { 165 | 166 | barstore = new (class extends Store { 167 | 168 | constructor({ actionfoo, storefoo }) { 169 | super(); 170 | 171 | this.bars = {}; 172 | 173 | this.listen(actionfoo, 'add', ({ what }) => { 174 | return this.waitFor([storefoo]).then(() => { 175 | this.bars[what] = 'bar(' + storefoo.get(what) + ')'; 176 | this.changed(); 177 | }); 178 | }); 179 | } 180 | 181 | // Public API 182 | 183 | getAll() { return this.bars; } 184 | get(what) { return what in this.bars ? this.bars[what] : null; } 185 | 186 | })({ actionfoo: fooaction, storefoo: foostore }); 187 | 188 | expect(barstore).to.be.an('object'); 189 | }); 190 | 191 | it('should waitFor() depended-upon store', function(done) { 192 | 193 | let footriggered = false; 194 | let bartriggered = false; 195 | 196 | const updatecbkfoo = () => { 197 | 198 | if(footriggered) { 199 | throw new Error(`FOO: I'm supposed to be triggered only once !`); 200 | } 201 | 202 | footriggered = true; 203 | }; 204 | 205 | const updatecbkbar = () => { 206 | 207 | if(bartriggered) { 208 | throw new Error(`BAR: I'm supposed to be triggered only once !`); 209 | } 210 | 211 | bartriggered = true; 212 | 213 | done(); 214 | }; 215 | 216 | unwatchcbkfoo = foostore.watch(updatecbkfoo); 217 | unwatchcbkbar = barstore.watch(updatecbkbar); 218 | 219 | fooaction.add('something'); 220 | }); 221 | 222 | it('should sequence store reaction in the correct order', function() { 223 | const foos = foostore.getAll(); 224 | 225 | expect(foos).to.be.an('object'); 226 | expect(foos).to.include.keys('something'); 227 | expect(foos['something']).to.equal('foo(something)'); 228 | 229 | const bars = barstore.getAll(); 230 | 231 | expect(bars).to.be.an('object'); 232 | expect(bars).to.include.keys('something'); 233 | expect(bars['something']).to.equal('bar(foo(something))'); 234 | }); 235 | }); 236 | }); 237 | }); 238 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | import jsdom from 'jsdom'; 2 | import fs from 'fs'; 3 | const jquery = fs.readFileSync(__dirname + "/../node_modules/jquery/dist/jquery.js", "utf-8"); 4 | 5 | global.document = jsdom.jsdom(''); 6 | global.window = global.document.parentWindow; 7 | 8 | let scriptEl = window.document.createElement("script"); 9 | scriptEl.setAttribute('type', 'text/javascript'); 10 | scriptEl.innerHTML = jquery; 11 | window.document.head.appendChild(scriptEl); 12 | global.$ = window.$; 13 | --------------------------------------------------------------------------------