├── .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://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 |
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 |
--------------------------------------------------------------------------------