├── .gitignore ├── .travis.yml ├── API.md ├── HISTORY.md ├── LICENSE.md ├── README.md ├── dist └── index.js ├── lib ├── example.js └── index.js ├── package.json └── test ├── index ├── basic_test.js └── react_test.js ├── mocha.opts ├── setup.js └── standard_test.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | /coverage 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | script: npm run coverage 3 | node_js: 4 | - iojs 5 | -------------------------------------------------------------------------------- /API.md: -------------------------------------------------------------------------------- 1 | ## Dispatcher 2 | 3 | > `new Dispatcher()` 4 | 5 | An event emitter used to dispatch application events. 6 | 7 | app = new Dispatcher() 8 | 9 | app.on('build:finish', function (duration) { 10 | console.log('build finished, took ' + duration + 'ms') 11 | }) 12 | 13 | app.emit('build:finish', 384) 14 | 15 | ### on 16 | 17 | > `on(event: string, callback())` 18 | 19 | Listens to an event. 20 | See [EventEmitter.on](http://devdocs.io/iojs/events#events_emitter_on_event_listener). 21 | 22 | ### off 23 | 24 | > `off(event: string, callback)` 25 | 26 | Unbinds an event listener. 27 | See [EventEmitter.removeListener](http://devdocs.io/iojs/events#events_emitter_removelistener_event_listener). 28 | 29 | ### emit 30 | 31 | > `emit(event: string, [...args])` 32 | 33 | Fires an event. 34 | See [EventEmitter.emit](http://devdocs.io/iojs/events#events_emitter_emit_event_listener). 35 | 36 | ### emitAfter 37 | 38 | > `emitAfter(event: string, [...args])` 39 | 40 | Emits an event after the current event stack has finished. If ran outside 41 | an event handler, the event will be triggered immediately instead. 42 | 43 | dispatcher.on('tweets:load', function () { 44 | dispatcher.emitAfter('tweets:refresh') 45 | // 1 46 | }) 47 | 48 | dispatcher.on('tweets:refresh', function () { 49 | // 2 50 | }) 51 | 52 | // in this case, `2` will run before `1` 53 | 54 | ### defer 55 | 56 | > `defer([key: string], callback())` 57 | 58 | Runs something after emitting. If `key` is specified, it will ensure that 59 | there will only be one function for that key to be called. 60 | 61 | store.defer(function () { 62 | // this will be called after emissions are complete 63 | }) 64 | 65 | ### isEmitting 66 | 67 | > `isEmitting()` 68 | 69 | Returns `true` if the event emitter is in the middle of emitting an event. 70 | 71 | ## Store 72 | 73 | > `new Store(dispatcher: Dispatcher, state, actions: Object)` 74 | 75 | A store is an object that keeps a state and listens to dispatcher events. 76 | 77 | Each action handler is a pure function that takes in the `state` and returns the new 78 | state—no mutation should be done here. 79 | 80 | let store = new Store(dispatcher, { 81 | }, { 82 | 'item:fetch': (state) => { 83 | getItem() 84 | .then((data) => { this.dispatcher.emit('item:fetch:load', { state: 'data', data: data }) }) 85 | .catch((err) => { this.dispatcher.emit('item:fetch:load', { state: 'error', error: err }) }) 86 | dispatcher.emit('item:fetch:load', { state: 'pending' }) 87 | 88 | promisify(getItem(), this.dispatcher, 'item:fetch:load') 89 | }, 90 | 91 | 'item:fetch:load': (state, result) => { 92 | return { ...state, ...result } 93 | } 94 | }) 95 | 96 | ### id 97 | 98 | > `id: String` 99 | 100 | A unique string ID for the instance. 101 | 102 | store.id //=> 's43' 103 | 104 | ### dispatcher 105 | 106 | > `dispatcher: String` 107 | 108 | A reference to the dispatcher. 109 | 110 | { 111 | 'list:add': (state) => { 112 | this.dispatcher.emit('list:add:error', 'Not allowed') 113 | } 114 | } 115 | 116 | ### getState 117 | 118 | > `getState()` 119 | 120 | Returns the current state of the store. 121 | 122 | store.getState() 123 | 124 | ### listen 125 | 126 | > `listen(callback(state), [{ immediate }])` 127 | 128 | Listens for changes, firing the function `callback` when it happens. The 129 | current state is passed onto the callback as the argument `state`. 130 | 131 | store.listen(function (state) { 132 | console.log('State changed:', state) 133 | }) 134 | 135 | The callback will be immediately invoked when you call `listen()`. To 136 | disable this behavior, pass `{ immediate: false }`. 137 | 138 | store.listen(function (state) { 139 | // ... 140 | }, { immediate: false }) 141 | 142 | ### unlisten 143 | 144 | > `unlisten(fn)` 145 | 146 | Unbinds a given change handler. 147 | 148 | function onChange () { ... } 149 | 150 | store.listen(onChange) 151 | store.unlisten(onChange) 152 | 153 | ### observe 154 | 155 | > `observe(actions: Object, [options: Object])` 156 | 157 | Listens to events in the dispatcher. 158 | 159 | store.observe({ 160 | 'list:add': function (state) { ... }, 161 | 'list:remove': function (state) { ... } 162 | }) 163 | 164 | ### extend 165 | 166 | > `extend(proto: Object)` 167 | 168 | Adds methods to the store object. 169 | 170 | let store = new Store(...) 171 | store.extend({ 172 | helperMethod () { 173 | return true 174 | } 175 | }) 176 | 177 | store.helperMethod() //=> true 178 | 179 | ### dup 180 | 181 | > `dup(dispatcher: Dispatcher)` 182 | 183 | Duplicates the store, listening to a new dispatcher. Great for unit 184 | testing. 185 | 186 | let store = new Store(...) 187 | 188 | let dispatcher = new Dispatcher() 189 | let newStore = store.dup(dispatcher) 190 | 191 | dispatch.emit('event') 192 | // ...will only be received by newStore 193 | 194 | ### resetState 195 | 196 | > `resetState(state: Object)` 197 | 198 | Resets the state to the new given `state`. This should never be used 199 | except perhaps in unit tests. 200 | 201 | store.resetState({ count: 0 }) 202 | 203 | ## Utilities 204 | 205 | Some helper functions. 206 | 207 | ### connectToStores 208 | 209 | > `connectToStores(Spec, Component = Spec)` 210 | 211 | Connects a React Component to a store. It makes the store's state available as properties. 212 | 213 | It takes the static function `getStores()` and connects to those stores. You 214 | may also provide a `getPropsFromStores()` method. 215 | 216 | let Component = React.createClass({ 217 | statics: { 218 | getStores () { return [store] }, 219 | getPropsFromStores (stores) { return stores[0].data } 220 | } 221 | }) 222 | 223 | Component = connectToStores(Component) 224 | 225 | Based on the [alt implementation](https://github.com/goatslacker/alt/blob/master/src/utils/connectToStores.js). 226 | 227 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## [v0.8.0] 2 | > Aug 4, 2015 3 | 4 | * Fix unmounting 5 | * Fix `Dispatcher#off()` 6 | * Fix `Store#unlisten()` 7 | 8 | [v0.8.0]: https://github.com/rstacruz/uflux/compare/v0.7.0...v0.8.0 9 | 10 | ## [v0.7.0] 11 | > Aug 4, 2015 12 | 13 | * `Store#listen()` is now fired immediately. 14 | 15 | [v0.7.0]: https://github.com/rstacruz/uflux/compare/v0.6.0...v0.7.0 16 | 17 | ## [v0.6.0] 18 | > Aug 4, 2015 19 | 20 | Rework the API. Major breaking change. 21 | 22 | * Deprecated: `Dispatcher#wait()` was removed. 23 | * Deprecated: Emitting dispatcher events in stores will now execute them immediately instead of waiting for changes. 24 | * Implement `Dispatcher#emitAfter()` to make emits that are at the end of the event stack. 25 | * Rename afterEmit() to `Dispatcher#defer()` 26 | * `Dispatcher#defer()` now doesn't take a key argument. 27 | 28 | [v0.6.0]: https://github.com/rstacruz/uflux/compare/v0.5.0...v0.6.0 29 | 30 | ## [v0.5.0] 31 | > Aug 3, 2015 32 | 33 | * Add `Store#id` as a unique identifier for stores. 34 | 35 | [v0.5.0]: https://github.com/rstacruz/uflux/compare/v0.4.0...v0.5.0 36 | 37 | ## [v0.4.0] 38 | > Aug 3, 2015 39 | 40 | * Store `change` events are now debounced. If a chain of dispatcher events will modify the store through many steps (by dispatching more events), the `change` event will now only happen once instead of multiple times. 41 | * Implemented `Dispatcher#isEmitting()` to check if the dispatcher is currently in the middle of an event handler. 42 | * Implemented `Dispatcher#emitDepth` to check how deep the stack is of event handlers. 43 | * Implemented `Dispatcher#afterEmit()` to execute something after event handlers have finished processing. 44 | 45 | [v0.4.0]: https://github.com/rstacruz/uflux/compare/v0.3.0...v0.4.0 46 | 47 | ## [v0.3.0] 48 | > Jul 31, 2015 49 | 50 | * Allow usage in the browser without precompilation (`dist/index.js`). 51 | 52 | [v0.3.0]: https://github.com/rstacruz/uflux/compare/v0.2.0...v0.3.0 53 | 54 | ## [v0.2.0] 55 | > Jul 31, 2015 56 | 57 | * Implement `Store.dup()` to allow testing stores. 58 | * Refactors `Store` internals. 59 | 60 | [v0.2.0]: https://github.com/rstacruz/uflux/compare/v0.1.0...v0.2.0 61 | 62 | ## v0.1.0 63 | > Jul 31, 2015 64 | 65 | * Initial release. 66 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Rico Sta. Cruz 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | > **Deprecated** — use [redux](https://www.npmjs.com/package/redux) instead; it works pretty much just like uflux and is better supported. 2 | 3 | ---- 4 | 5 | # μflux 6 | 7 | **uflux** - Another implementation for the Flux architecture for React apps that pushes minimalism far. 8 | 9 | * Store works with immutable objects 10 | * Unidirectional flow 11 | 12 | But also: 13 | 14 | * Reduced verbosity. no action constants, no action methods. To fire a method, just emit a signal from the disptacher. 15 | 16 | See [API.md](API.md) for full API documentation. 17 | 18 | [![Status](https://travis-ci.org/rstacruz/uflux.svg?branch=master)](https://travis-ci.org/rstacruz/uflux "See test builds") 19 | 20 | ## Usage 21 | 22 | When used via npm/bower/browserify/webpack/etc: 23 | 24 | ```js 25 | import { Dispatcher, Store, connectToStores } from 'uflux' 26 | ``` 27 | 28 | ### Composition 29 | 30 | Your application will be composed of: 31 | 32 | * One and only one Dispatcher singleton. 33 | * Many stores (singletons), each listening to the one Dispatcher. 34 | * Many React components, with some listening to changes to one or more stores. 35 | 36 | ### Dispatcher 37 | 38 | A disptacher is simply an [EventEmitter]. 39 | 40 | ```js 41 | const App = new Dispatcher() 42 | 43 | App.on('eventname', function () { ... }) 44 | App.emit('eventname') 45 | App.emit('eventname', arg1, arg2) 46 | ``` 47 | 48 | [EventEmitter]: http://devdocs.io/iojs/events#events_class_events_eventemitter 49 | 50 | ### Store 51 | 52 | A store is an object that keeps a state and listens to dispatcher events. 53 | Create a new store using `new Store(dispatcher, initialState, handlers)`. 54 | 55 | It listens to events from the dispatcher and responds by updating the store's state. 56 | 57 | Each handler is a pure function that takes in the `state` and returns the new 58 | state—no mutation should be done here. 59 | 60 | ```js 61 | const ListStore = new Store(App, { 62 | items: [] 63 | }, { 64 | 'list:add': function (state, item) { 65 | return { 66 | ...state, 67 | items: state.items.concat([ item ]) 68 | } 69 | } 70 | }) 71 | 72 | App.emit('list:add', '2') 73 | ListStore.getState() /* { items: [2] } */ 74 | ``` 75 | 76 | ### Actions 77 | 78 | To fire an action, just emit directly on your main dispatcher. No need for action methods. 79 | 80 | ```js 81 | App.emit('list:add') 82 | ``` 83 | 84 | If you're firing within an event listener (such as in a store), you can use `emitAfter()` to make the event trigger after all other events have triggered. 85 | 86 | ```js 87 | const DiceStore = new Store(App, { }, { 88 | 'dice:roll': function (state) { 89 | App.emitAfter('dice:refresh') 90 | return { number: Math.floor(Math.random() * 6) + 1 } 91 | } 92 | }) 93 | ``` 94 | 95 | ### React 96 | 97 | You can connect a react Component to a store using `connectToStores()`. The 98 | state of the store will be available as properties (`this.props`). 99 | 100 | ```js 101 | const ListView = React.createClass({ 102 | statics: { 103 | getStores () { 104 | return [ ListStore ] 105 | }, 106 | 107 | getPropsFromStores() { 108 | return ListStore.getState() 109 | } 110 | }, 111 | render () { 112 | return
hi, {this.props.name}
113 | } 114 | 115 | }) 116 | 117 | ListView = connectToStores(ListView) 118 | ``` 119 | 120 | ### Chaining events 121 | 122 | You can emit events inside handlers. They will be fired after committing the new state to the store. 123 | 124 | ```js 125 | const ListStore = new Store(App, { 126 | items: [] 127 | }, { 128 | 'list:add': function (state, item) { 129 | if (state.locked) { 130 | const err = new Error('List is locked') 131 | App.emit('list:error', err) 132 | return { ...state, error: err } 133 | } 134 | } 135 | }) 136 | 137 | App.on('list:error', function (err) { 138 | console.log(err.message) //=> "List is locked" 139 | console.log(ListStore.getState().error.message) //=> "List is locked" 140 | }) 141 | ``` 142 | 143 | ### Testing stores 144 | 145 | Create unit tests for stores by duplicating it and assigning it to a new dispatcher via `.dup()`. 146 | 147 | ```js 148 | const ListStore = new Store(...) 149 | 150 | const App = new Dispatcher() 151 | const TestListStore = ListStore.dup(App) 152 | 153 | App.emit('list:clear') 154 | // ...will only be received by TestListStore, not ListStore. 155 | ``` 156 | 157 |
158 | 159 | ## API 160 | 161 | See [API.md](API.md) for full API documentation. 162 | 163 | Unresolved API questions: 164 | 165 | * should we allow naming stores? this'd be a great way to make a global "save state" for your entire app 166 | * atomicity - is it possible? 167 | * can/should components listen to dispatch events? 168 | * is there a better function signature for new Store()? 169 | * what about stores that need to interact with each other (say AuthenticationStore)? 170 | * it should be possible to debounce store change events (eg, a chain of dispatch events that modify stores). but how? 171 | * ...post yours in [issues/](issues/) 172 | 173 |
174 | 175 | ## Extra notes 176 | 177 | ### Regular usage 178 | 179 | > [](#version) `` 180 | 181 | ```js 182 | var Store = window.uflux.Store 183 | var Dispatcher = window.uflux.Dispatcher 184 | var connectToStores = window.uflux.connectToStores 185 | ``` 186 | 187 | ### Babel 188 | 189 | Using [Babel] is recommended for JSX parsing and enabling ES2015 features. 190 | `--stage 0` is recommended, too, for rest spreading support (`{ ...state, active: 191 | true }`)—a feature very useful for Stores. 192 | 193 | [Babel]: https://babeljs.io 194 | 195 |
196 | 197 | ## Disclaimer 198 | 199 | This is built as a proof-of-concept and has not been battle-tested in a production setup. 200 | 201 |
202 | 203 | ## Thanks 204 | 205 | **uflux** © 2015+, Rico Sta. Cruz. Released under the [MIT] License.
206 | Authored and maintained by Rico Sta. Cruz with help from contributors ([list][contributors]). 207 | 208 | > [ricostacruz.com](http://ricostacruz.com)  ·  209 | > GitHub [@rstacruz](https://github.com/rstacruz)  ·  210 | > Twitter [@rstacruz](https://twitter.com/rstacruz) 211 | 212 | [MIT]: http://mit-license.org/ 213 | [contributors]: http://github.com/rstacruz/uflux/contributors 214 | -------------------------------------------------------------------------------- /dist/index.js: -------------------------------------------------------------------------------- 1 | (function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.uflux = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o 1 ? _len - 1 : 0), _key = 1; _key < _len; _key++) { 69 | args[_key - 1] = arguments[_key]; 70 | } 71 | 72 | this.removeListener.apply(this, [event].concat(args)); 73 | }, 74 | 75 | /** 76 | * emit : emit(event: string, [...args]) 77 | * Fires an event. 78 | * See [EventEmitter.emit](http://devdocs.io/iojs/events#events_emitter_emit_event_listener). 79 | */ 80 | 81 | emit: function emit(event) { 82 | try { 83 | var _EventEmitter$prototype$emit; 84 | 85 | this.emitDepth++; 86 | 87 | for (var _len2 = arguments.length, args = Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) { 88 | args[_key2 - 1] = arguments[_key2]; 89 | } 90 | 91 | return (_EventEmitter$prototype$emit = _events.EventEmitter.prototype.emit).call.apply(_EventEmitter$prototype$emit, [this, event].concat(args)); 92 | } finally { 93 | this.emitDepth--; 94 | if (this.emitDepth === 0) this.runDeferred(); 95 | } 96 | }, 97 | 98 | /** 99 | * emitAfter : emitAfter(event: string, [...args]) 100 | * Emits an event after the current event stack has finished. If ran outside 101 | * an event handler, the event will be triggered immediately instead. 102 | * 103 | * dispatcher.on('tweets:load', function () { 104 | * dispatcher.emitAfter('tweets:refresh') 105 | * // 1 106 | * }) 107 | * 108 | * dispatcher.on('tweets:refresh', function () { 109 | * // 2 110 | * }) 111 | * 112 | * // in this case, `2` will run before `1` 113 | */ 114 | 115 | emitAfter: function emitAfter(event) { 116 | var _this = this; 117 | 118 | for (var _len3 = arguments.length, args = Array(_len3 > 1 ? _len3 - 1 : 0), _key3 = 1; _key3 < _len3; _key3++) { 119 | args[_key3 - 1] = arguments[_key3]; 120 | } 121 | 122 | if (this.isEmitting()) { 123 | return this.defer(function () { 124 | return _this.emit.apply(_this, [event].concat(args)); 125 | }); 126 | } else { 127 | return this.emit.apply(this, [event].concat(args)); 128 | } 129 | }, 130 | 131 | /** 132 | * defer : defer([key: string], callback()) 133 | * Runs something after emitting. If `key` is specified, it will ensure that 134 | * there will only be one function for that key to be called. 135 | * 136 | * store.defer(function () { 137 | * // this will be called after emissions are complete 138 | * }) 139 | */ 140 | 141 | defer: function defer(callback) { 142 | if (this.isEmitting()) { 143 | if (!this._defer) this._defer = []; 144 | return this._defer.push(callback); 145 | } else { 146 | return callback.call(this); 147 | } 148 | }, 149 | 150 | /* 151 | * Private: runs the defer hooks. Done after an emit() 152 | */ 153 | 154 | runDeferred: function runDeferred() { 155 | var _this2 = this; 156 | 157 | var list = this._defer; 158 | if (!list) return; 159 | delete this._defer; 160 | list.forEach(function (callback) { 161 | callback.call(_this2); 162 | }); 163 | }, 164 | 165 | /** 166 | * isEmitting : isEmitting() 167 | * Returns `true` if the event emitter is in the middle of emitting an event. 168 | */ 169 | 170 | isEmitting: function isEmitting() { 171 | return this.emitDepth > 0; 172 | } 173 | }); 174 | 175 | /** 176 | * Store : new Store(dispatcher: Dispatcher, state, actions: Object) 177 | * (Class) A store is an object that keeps a state and listens to dispatcher events. 178 | * 179 | * Each action handler is a pure function that takes in the `state` and returns the new 180 | * state—no mutation should be done here. 181 | * 182 | * let store = new Store(dispatcher, { 183 | * }, { 184 | * 'item:fetch': (state) => { 185 | * getItem() 186 | * .then((data) => { this.dispatcher.emit('item:fetch:load', { state: 'data', data: data }) }) 187 | * .catch((err) => { this.dispatcher.emit('item:fetch:load', { state: 'error', error: err }) }) 188 | * dispatcher.emit('item:fetch:load', { state: 'pending' }) 189 | * 190 | * promisify(getItem(), this.dispatcher, 'item:fetch:load') 191 | * }, 192 | * 193 | * 'item:fetch:load': (state, result) => { 194 | * return { ...state, ...result } 195 | * } 196 | * }) 197 | */ 198 | 199 | function Store(dispatcher, state, actions) { 200 | // Subclass `Store` and instanciate it. 201 | var NewStore = (function (_Store) { 202 | _inherits(NewStore, _Store); 203 | 204 | function NewStore(dispatcher) { 205 | _classCallCheck(this, NewStore); 206 | 207 | _get(Object.getPrototypeOf(NewStore.prototype), 'constructor', this).call(this); 208 | this.dispatcher = dispatcher; 209 | this.id = 'store' + ids.storeInstance++; 210 | this.bindActions(); 211 | } 212 | 213 | return NewStore; 214 | })(Store); 215 | NewStore.prototype.state = state; 216 | NewStore.prototype.actionsList = [actions]; 217 | return new NewStore(dispatcher); 218 | } 219 | 220 | Store.prototype = _extends({}, _events.EventEmitter.prototype, { 221 | 222 | /** 223 | * id : id: String 224 | * A unique string ID for the instance. 225 | * 226 | * store.id //=> 's43' 227 | */ 228 | 229 | /* 230 | * Private: unpacks the old observed things. 231 | */ 232 | 233 | bindActions: function bindActions() { 234 | var _this3 = this; 235 | 236 | this.actionsList.forEach(function (actions) { 237 | _this3.observe(actions, { record: false }); 238 | }); 239 | }, 240 | 241 | /** 242 | * dispatcher : dispatcher: String 243 | * A reference to the dispatcher. 244 | * 245 | * { 246 | * 'list:add': (state) => { 247 | * this.dispatcher.emit('list:add:error', 'Not allowed') 248 | * } 249 | * } 250 | */ 251 | 252 | /** 253 | * Returns the current state of the store. 254 | * 255 | * store.getState() 256 | */ 257 | 258 | getState: function getState() { 259 | return this.state; 260 | }, 261 | 262 | /** 263 | * listen : listen(callback(state), [{ immediate }]) 264 | * Listens for changes, firing the function `callback` when it happens. The 265 | * current state is passed onto the callback as the argument `state`. 266 | * 267 | * store.listen(function (state) { 268 | * console.log('State changed:', state) 269 | * }) 270 | * 271 | * The callback will be immediately invoked when you call `listen()`. To 272 | * disable this behavior, pass `{ immediate: false }`. 273 | * 274 | * store.listen(function (state) { 275 | * // ... 276 | * }, { immediate: false }) 277 | */ 278 | 279 | listen: function listen(fn, options) { 280 | this.on('change', fn); 281 | if (!options || options.immediate) fn(this.getState()); 282 | }, 283 | 284 | /** 285 | * Unbinds a given change handler. 286 | * 287 | * function onChange () { ... } 288 | * 289 | * store.listen(onChange) 290 | * store.unlisten(onChange) 291 | */ 292 | 293 | unlisten: function unlisten(fn) { 294 | return this.removeListener('change', fn); 295 | }, 296 | 297 | /** 298 | * observe : observe(actions: Object, [options: Object]) 299 | * Listens to events in the dispatcher. 300 | * 301 | * store.observe({ 302 | * 'list:add': function (state) { ... }, 303 | * 'list:remove': function (state) { ... } 304 | * }) 305 | */ 306 | 307 | observe: function observe(actions, options) { 308 | this.bindToDispatcher(actions, this.dispatcher); 309 | 310 | // Add the observers to actionsList 311 | if (!options || options.record) { 312 | this.actionsList.push(actions); 313 | } 314 | }, 315 | 316 | /* 317 | * Private: binds actions object `actions` to a `dispatcher`. 318 | */ 319 | 320 | bindToDispatcher: function bindToDispatcher(actions, dispatcher) { 321 | var _this4 = this; 322 | 323 | Object.keys(actions).forEach(function (key) { 324 | var fn = actions[key]; 325 | dispatcher.on(key, function () { 326 | for (var _len4 = arguments.length, args = Array(_len4), _key4 = 0; _key4 < _len4; _key4++) { 327 | args[_key4] = arguments[_key4]; 328 | } 329 | 330 | var result = fn.apply(_this4, [_this4.getState()].concat(args)); 331 | if (result) _this4.resetState(result); 332 | }); 333 | }); 334 | }, 335 | 336 | /** 337 | * Adds methods to the store object. 338 | * 339 | * let store = new Store(...) 340 | * store.extend({ 341 | * helperMethod () { 342 | * return true 343 | * } 344 | * }) 345 | * 346 | * store.helperMethod() //=> true 347 | */ 348 | 349 | extend: function extend(proto) { 350 | var _this5 = this; 351 | 352 | Object.keys(proto).forEach(function (key) { 353 | _this5.constructor.prototype[key] = proto[key]; 354 | }); 355 | return this; 356 | }, 357 | 358 | /** 359 | * Duplicates the store, listening to a new dispatcher. Great for unit 360 | * testing. 361 | * 362 | * let store = new Store(...) 363 | * 364 | * let dispatcher = new Dispatcher() 365 | * let newStore = store.dup(dispatcher) 366 | * 367 | * dispatch.emit('event') 368 | * // ...will only be received by newStore 369 | */ 370 | 371 | dup: function dup(dispatcher) { 372 | var NewStore = this.constructor; 373 | return new NewStore(dispatcher); 374 | }, 375 | 376 | /** 377 | * Resets the state to the new given `state`. This should never be used 378 | * except perhaps in unit tests. 379 | * 380 | * store.resetState({ count: 0 }) 381 | */ 382 | 383 | resetState: function resetState(state) { 384 | var _this6 = this; 385 | 386 | this.state = state; 387 | this.dirty = true; 388 | 389 | // Use defer twice to make sure it's at the end of all stacks 390 | this.dispatcher.defer(function () { 391 | _this6.dispatcher.defer(function () { 392 | if (_this6.dirty) { 393 | delete _this6.dirty; 394 | _this6.emit('change', state); 395 | } 396 | }); 397 | }); 398 | } 399 | }); 400 | 401 | /** 402 | * Utilities: 403 | * (Module) Some helper functions. 404 | */ 405 | 406 | /** 407 | * Connects a React Component to a store. It makes the store's state available as properties. 408 | * 409 | * It takes the static function `getStores()` and connects to those stores. You 410 | * may also provide a `getPropsFromStores()` method. 411 | * 412 | * let Component = React.createClass({ 413 | * statics: { 414 | * getStores () { return [store] }, 415 | * getPropsFromStores (stores) { return stores[0].data } 416 | * } 417 | * }) 418 | * 419 | * Component = connectToStores(Component) 420 | * 421 | * Based on the [alt implementation](https://github.com/goatslacker/alt/blob/master/src/utils/connectToStores.js). 422 | */ 423 | 424 | function connectToStores(Spec) { 425 | var Component = arguments.length <= 1 || arguments[1] === undefined ? Spec : arguments[1]; 426 | return (function () { 427 | if (!Spec.getStores) { 428 | throw new Error('connectToStores(): ' + 'expected getStores() static function'); 429 | } 430 | 431 | if (!Spec.getPropsFromStores) { 432 | Spec.getPropsFromStores = function () { 433 | return Spec.getStores().reduce(function (output, store) { 434 | return _extends({}, output, store.getState()); 435 | }, {}); 436 | }; 437 | } 438 | 439 | var StoreConnection = React.createClass({ 440 | displayName: 'StoreConnection', 441 | 442 | getInitialState: function getInitialState() { 443 | return Spec.getPropsFromStores(this.props, this.context); 444 | }, 445 | 446 | componentWillReceiveProps: function componentWillReceiveProps(nextProps) { 447 | this.setState(Spec.getPropsFromStores(nextProps, this.context)); 448 | }, 449 | 450 | componentDidMount: function componentDidMount() { 451 | var _this7 = this; 452 | 453 | var stores = Spec.getStores(this.props, this.context); 454 | this.storeListeners = stores.map(function (store) { 455 | return store.listen(_this7.onChange); 456 | }); 457 | if (Spec.componentDidConnect) { 458 | Spec.componentDidConnect(this.props, this.context); 459 | } 460 | }, 461 | 462 | componentWillUnmount: function componentWillUnmount() { 463 | var _this8 = this; 464 | 465 | var stores = Spec.getStores(this.props, this.context); 466 | stores.forEach(function (store) { 467 | store.unlisten(_this8.onChange); 468 | }); 469 | }, 470 | 471 | onChange: function onChange() { 472 | this.setState(Spec.getPropsFromStores(this.props, this.context)); 473 | }, 474 | 475 | render: function render() { 476 | return React.createElement(Component, _extends({}, this.props, this.state)); 477 | } 478 | }); 479 | 480 | return StoreConnection; 481 | })(); 482 | } 483 | 484 | }).call(this,typeof global !== "undefined" ? global : typeof self !== "undefined" ? self : typeof window !== "undefined" ? window : {}) 485 | },{"events":2,"react":undefined}],2:[function(require,module,exports){ 486 | // Copyright Joyent, Inc. and other Node contributors. 487 | // 488 | // Permission is hereby granted, free of charge, to any person obtaining a 489 | // copy of this software and associated documentation files (the 490 | // "Software"), to deal in the Software without restriction, including 491 | // without limitation the rights to use, copy, modify, merge, publish, 492 | // distribute, sublicense, and/or sell copies of the Software, and to permit 493 | // persons to whom the Software is furnished to do so, subject to the 494 | // following conditions: 495 | // 496 | // The above copyright notice and this permission notice shall be included 497 | // in all copies or substantial portions of the Software. 498 | // 499 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 500 | // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 501 | // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 502 | // NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, 503 | // DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR 504 | // OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE 505 | // USE OR OTHER DEALINGS IN THE SOFTWARE. 506 | 507 | function EventEmitter() { 508 | this._events = this._events || {}; 509 | this._maxListeners = this._maxListeners || undefined; 510 | } 511 | module.exports = EventEmitter; 512 | 513 | // Backwards-compat with node 0.10.x 514 | EventEmitter.EventEmitter = EventEmitter; 515 | 516 | EventEmitter.prototype._events = undefined; 517 | EventEmitter.prototype._maxListeners = undefined; 518 | 519 | // By default EventEmitters will print a warning if more than 10 listeners are 520 | // added to it. This is a useful default which helps finding memory leaks. 521 | EventEmitter.defaultMaxListeners = 10; 522 | 523 | // Obviously not all Emitters should be limited to 10. This function allows 524 | // that to be increased. Set to zero for unlimited. 525 | EventEmitter.prototype.setMaxListeners = function(n) { 526 | if (!isNumber(n) || n < 0 || isNaN(n)) 527 | throw TypeError('n must be a positive number'); 528 | this._maxListeners = n; 529 | return this; 530 | }; 531 | 532 | EventEmitter.prototype.emit = function(type) { 533 | var er, handler, len, args, i, listeners; 534 | 535 | if (!this._events) 536 | this._events = {}; 537 | 538 | // If there is no 'error' event listener then throw. 539 | if (type === 'error') { 540 | if (!this._events.error || 541 | (isObject(this._events.error) && !this._events.error.length)) { 542 | er = arguments[1]; 543 | if (er instanceof Error) { 544 | throw er; // Unhandled 'error' event 545 | } 546 | throw TypeError('Uncaught, unspecified "error" event.'); 547 | } 548 | } 549 | 550 | handler = this._events[type]; 551 | 552 | if (isUndefined(handler)) 553 | return false; 554 | 555 | if (isFunction(handler)) { 556 | switch (arguments.length) { 557 | // fast cases 558 | case 1: 559 | handler.call(this); 560 | break; 561 | case 2: 562 | handler.call(this, arguments[1]); 563 | break; 564 | case 3: 565 | handler.call(this, arguments[1], arguments[2]); 566 | break; 567 | // slower 568 | default: 569 | len = arguments.length; 570 | args = new Array(len - 1); 571 | for (i = 1; i < len; i++) 572 | args[i - 1] = arguments[i]; 573 | handler.apply(this, args); 574 | } 575 | } else if (isObject(handler)) { 576 | len = arguments.length; 577 | args = new Array(len - 1); 578 | for (i = 1; i < len; i++) 579 | args[i - 1] = arguments[i]; 580 | 581 | listeners = handler.slice(); 582 | len = listeners.length; 583 | for (i = 0; i < len; i++) 584 | listeners[i].apply(this, args); 585 | } 586 | 587 | return true; 588 | }; 589 | 590 | EventEmitter.prototype.addListener = function(type, listener) { 591 | var m; 592 | 593 | if (!isFunction(listener)) 594 | throw TypeError('listener must be a function'); 595 | 596 | if (!this._events) 597 | this._events = {}; 598 | 599 | // To avoid recursion in the case that type === "newListener"! Before 600 | // adding it to the listeners, first emit "newListener". 601 | if (this._events.newListener) 602 | this.emit('newListener', type, 603 | isFunction(listener.listener) ? 604 | listener.listener : listener); 605 | 606 | if (!this._events[type]) 607 | // Optimize the case of one listener. Don't need the extra array object. 608 | this._events[type] = listener; 609 | else if (isObject(this._events[type])) 610 | // If we've already got an array, just append. 611 | this._events[type].push(listener); 612 | else 613 | // Adding the second element, need to change to array. 614 | this._events[type] = [this._events[type], listener]; 615 | 616 | // Check for listener leak 617 | if (isObject(this._events[type]) && !this._events[type].warned) { 618 | var m; 619 | if (!isUndefined(this._maxListeners)) { 620 | m = this._maxListeners; 621 | } else { 622 | m = EventEmitter.defaultMaxListeners; 623 | } 624 | 625 | if (m && m > 0 && this._events[type].length > m) { 626 | this._events[type].warned = true; 627 | console.error('(node) warning: possible EventEmitter memory ' + 628 | 'leak detected. %d listeners added. ' + 629 | 'Use emitter.setMaxListeners() to increase limit.', 630 | this._events[type].length); 631 | if (typeof console.trace === 'function') { 632 | // not supported in IE 10 633 | console.trace(); 634 | } 635 | } 636 | } 637 | 638 | return this; 639 | }; 640 | 641 | EventEmitter.prototype.on = EventEmitter.prototype.addListener; 642 | 643 | EventEmitter.prototype.once = function(type, listener) { 644 | if (!isFunction(listener)) 645 | throw TypeError('listener must be a function'); 646 | 647 | var fired = false; 648 | 649 | function g() { 650 | this.removeListener(type, g); 651 | 652 | if (!fired) { 653 | fired = true; 654 | listener.apply(this, arguments); 655 | } 656 | } 657 | 658 | g.listener = listener; 659 | this.on(type, g); 660 | 661 | return this; 662 | }; 663 | 664 | // emits a 'removeListener' event iff the listener was removed 665 | EventEmitter.prototype.removeListener = function(type, listener) { 666 | var list, position, length, i; 667 | 668 | if (!isFunction(listener)) 669 | throw TypeError('listener must be a function'); 670 | 671 | if (!this._events || !this._events[type]) 672 | return this; 673 | 674 | list = this._events[type]; 675 | length = list.length; 676 | position = -1; 677 | 678 | if (list === listener || 679 | (isFunction(list.listener) && list.listener === listener)) { 680 | delete this._events[type]; 681 | if (this._events.removeListener) 682 | this.emit('removeListener', type, listener); 683 | 684 | } else if (isObject(list)) { 685 | for (i = length; i-- > 0;) { 686 | if (list[i] === listener || 687 | (list[i].listener && list[i].listener === listener)) { 688 | position = i; 689 | break; 690 | } 691 | } 692 | 693 | if (position < 0) 694 | return this; 695 | 696 | if (list.length === 1) { 697 | list.length = 0; 698 | delete this._events[type]; 699 | } else { 700 | list.splice(position, 1); 701 | } 702 | 703 | if (this._events.removeListener) 704 | this.emit('removeListener', type, listener); 705 | } 706 | 707 | return this; 708 | }; 709 | 710 | EventEmitter.prototype.removeAllListeners = function(type) { 711 | var key, listeners; 712 | 713 | if (!this._events) 714 | return this; 715 | 716 | // not listening for removeListener, no need to emit 717 | if (!this._events.removeListener) { 718 | if (arguments.length === 0) 719 | this._events = {}; 720 | else if (this._events[type]) 721 | delete this._events[type]; 722 | return this; 723 | } 724 | 725 | // emit removeListener for all listeners on all events 726 | if (arguments.length === 0) { 727 | for (key in this._events) { 728 | if (key === 'removeListener') continue; 729 | this.removeAllListeners(key); 730 | } 731 | this.removeAllListeners('removeListener'); 732 | this._events = {}; 733 | return this; 734 | } 735 | 736 | listeners = this._events[type]; 737 | 738 | if (isFunction(listeners)) { 739 | this.removeListener(type, listeners); 740 | } else { 741 | // LIFO order 742 | while (listeners.length) 743 | this.removeListener(type, listeners[listeners.length - 1]); 744 | } 745 | delete this._events[type]; 746 | 747 | return this; 748 | }; 749 | 750 | EventEmitter.prototype.listeners = function(type) { 751 | var ret; 752 | if (!this._events || !this._events[type]) 753 | ret = []; 754 | else if (isFunction(this._events[type])) 755 | ret = [this._events[type]]; 756 | else 757 | ret = this._events[type].slice(); 758 | return ret; 759 | }; 760 | 761 | EventEmitter.listenerCount = function(emitter, type) { 762 | var ret; 763 | if (!emitter._events || !emitter._events[type]) 764 | ret = 0; 765 | else if (isFunction(emitter._events[type])) 766 | ret = 1; 767 | else 768 | ret = emitter._events[type].length; 769 | return ret; 770 | }; 771 | 772 | function isFunction(arg) { 773 | return typeof arg === 'function'; 774 | } 775 | 776 | function isNumber(arg) { 777 | return typeof arg === 'number'; 778 | } 779 | 780 | function isObject(arg) { 781 | return typeof arg === 'object' && arg !== null; 782 | } 783 | 784 | function isUndefined(arg) { 785 | return arg === void 0; 786 | } 787 | 788 | },{}]},{},[1])(1) 789 | }); -------------------------------------------------------------------------------- /lib/example.js: -------------------------------------------------------------------------------- 1 | var uflux = window.uflux 2 | var Store = uflux.Store 3 | var Dispatcher = uflux.Dispatcher 4 | var connectToStores = uflux.connectToStores 5 | 6 | var App = new Dispatcher() 7 | var ListStore = new Store(App, {}, { 8 | 'list:fetch': function (state) { 9 | this.storePromise(getList(), 'list:fetch:result') 10 | }, 11 | 12 | 'list:fetch:result': function (state, result) { 13 | return result 14 | } 15 | }) 16 | 17 | ListStore.extend({ 18 | storePromise: function (promise, event) { 19 | promise 20 | .then((data) => { 21 | this.dispatcher.emit(event, { status: 'success', data: data }) 22 | }) 23 | .catch((err) => { 24 | this.dispatcher.emit(event, { status: 'error', error: err }) 25 | }) 26 | this.dispatcher.emit(event, { status: 'pending' }) 27 | } 28 | }) 29 | 30 | var ListView = React.createClass({ 31 | statics: { 32 | getStores () { return [ListStore] } 33 | }, 34 | 35 | refresh () { 36 | App.emit('list:fetch') 37 | }, 38 | 39 | render () { 40 | return
41 | 42 | { this.props.status === 'pending' ? ( 43 |
Loading...
44 | ) : 45 | this.props.status === 'success' ? ( 46 |
Result: {JSON.stringify(this.props.data)}
47 | ) : 48 | this.props.status === 'error' ? ( 49 |
Err: {JSON.stringify(this.props.error)}
50 | ) : null } 51 |
52 | } 53 | 54 | }) 55 | 56 | ListView = connectToStores(ListView) 57 | 58 | React.render(, document.body) 59 | 60 | function getList () { 61 | return new Promise(function (ok, fail) { 62 | setTimeout(function () { 63 | ok([ 64 | { id: 1, name: 'Apple' }, 65 | { id: 2, name: 'Banana' }, 66 | { id: 3, name: 'Cherry' } 67 | ]) 68 | }, 500) 69 | }) 70 | } 71 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from 'events' 2 | let React 3 | 4 | if (global.React) { 5 | React = global.React 6 | } else { 7 | React = require('react') 8 | } 9 | 10 | let ids = { 11 | storeInstance: 0, 12 | callback: 0 13 | } 14 | 15 | /** 16 | * Dispatcher : new Dispatcher() 17 | * (Class) An event emitter used to dispatch application events. 18 | * 19 | * app = new Dispatcher() 20 | * 21 | * app.on('build:finish', function (duration) { 22 | * console.log('build finished, took ' + duration + 'ms') 23 | * }) 24 | * 25 | * app.emit('build:finish', 384) 26 | */ 27 | 28 | export function Dispatcher () { 29 | this.emitDepth = 0 30 | } 31 | 32 | Dispatcher.prototype = { 33 | ...EventEmitter.prototype, 34 | 35 | /** 36 | * on : on(event: string, callback()) 37 | * Listens to an event. 38 | * See [EventEmitter.on](http://devdocs.io/iojs/events#events_emitter_on_event_listener). 39 | */ 40 | 41 | /** 42 | * off : off(event: string, callback) 43 | * Unbinds an event listener. 44 | * See [EventEmitter.removeListener](http://devdocs.io/iojs/events#events_emitter_removelistener_event_listener). 45 | */ 46 | 47 | off (event, ...args) { 48 | this.removeListener(event, ...args) 49 | }, 50 | 51 | /** 52 | * emit : emit(event: string, [...args]) 53 | * Fires an event. 54 | * See [EventEmitter.emit](http://devdocs.io/iojs/events#events_emitter_emit_event_listener). 55 | */ 56 | 57 | emit (event: String, ...args) { 58 | try { 59 | this.emitDepth++ 60 | return EventEmitter.prototype.emit.call(this, event, ...args) 61 | } finally { 62 | this.emitDepth-- 63 | if (this.emitDepth === 0) this.runDeferred() 64 | } 65 | }, 66 | 67 | /** 68 | * emitAfter : emitAfter(event: string, [...args]) 69 | * Emits an event after the current event stack has finished. If ran outside 70 | * an event handler, the event will be triggered immediately instead. 71 | * 72 | * dispatcher.on('tweets:load', function () { 73 | * dispatcher.emitAfter('tweets:refresh') 74 | * // 1 75 | * }) 76 | * 77 | * dispatcher.on('tweets:refresh', function () { 78 | * // 2 79 | * }) 80 | * 81 | * // in this case, `2` will run before `1` 82 | */ 83 | 84 | emitAfter (event, ...args) { 85 | if (this.isEmitting()) { 86 | return this.defer(() => this.emit(event, ...args)) 87 | } else { 88 | return this.emit(event, ...args) 89 | } 90 | }, 91 | 92 | /** 93 | * defer : defer([key: string], callback()) 94 | * Runs something after emitting. If `key` is specified, it will ensure that 95 | * there will only be one function for that key to be called. 96 | * 97 | * store.defer(function () { 98 | * // this will be called after emissions are complete 99 | * }) 100 | */ 101 | 102 | defer (callback) { 103 | if (this.isEmitting()) { 104 | if (!this._defer) this._defer = [] 105 | return this._defer.push(callback) 106 | } else { 107 | return callback.call(this) 108 | } 109 | }, 110 | 111 | /* 112 | * Private: runs the defer hooks. Done after an emit() 113 | */ 114 | 115 | runDeferred () { 116 | var list = this._defer 117 | if (!list) return 118 | delete this._defer 119 | list.forEach((callback) => { callback.call(this) }) 120 | }, 121 | 122 | /** 123 | * isEmitting : isEmitting() 124 | * Returns `true` if the event emitter is in the middle of emitting an event. 125 | */ 126 | 127 | isEmitting () { 128 | return this.emitDepth > 0 129 | } 130 | } 131 | 132 | /** 133 | * Store : new Store(dispatcher: Dispatcher, state, actions: Object) 134 | * (Class) A store is an object that keeps a state and listens to dispatcher events. 135 | * 136 | * Each action handler is a pure function that takes in the `state` and returns the new 137 | * state—no mutation should be done here. 138 | * 139 | * let store = new Store(dispatcher, { 140 | * }, { 141 | * 'item:fetch': (state) => { 142 | * getItem() 143 | * .then((data) => { this.dispatcher.emit('item:fetch:load', { state: 'data', data: data }) }) 144 | * .catch((err) => { this.dispatcher.emit('item:fetch:load', { state: 'error', error: err }) }) 145 | * dispatcher.emit('item:fetch:load', { state: 'pending' }) 146 | * 147 | * promisify(getItem(), this.dispatcher, 'item:fetch:load') 148 | * }, 149 | * 150 | * 'item:fetch:load': (state, result) => { 151 | * return { ...state, ...result } 152 | * } 153 | * }) 154 | */ 155 | 156 | export function Store (dispatcher, state, actions) { 157 | // Subclass `Store` and instanciate it. 158 | const NewStore = class NewStore extends Store { 159 | constructor (dispatcher) { 160 | super() 161 | this.dispatcher = dispatcher 162 | this.id = 'store' + ids.storeInstance++ 163 | this.bindActions() 164 | } 165 | } 166 | NewStore.prototype.state = state 167 | NewStore.prototype.actionsList = [actions] 168 | return new NewStore(dispatcher) 169 | } 170 | 171 | Store.prototype = { 172 | ...EventEmitter.prototype, 173 | 174 | /** 175 | * id : id: String 176 | * A unique string ID for the instance. 177 | * 178 | * store.id //=> 's43' 179 | */ 180 | 181 | /* 182 | * Private: unpacks the old observed things. 183 | */ 184 | 185 | bindActions () { 186 | this.actionsList.forEach((actions) => { 187 | this.observe(actions, { record: false }) 188 | }) 189 | }, 190 | 191 | /** 192 | * dispatcher : dispatcher: String 193 | * A reference to the dispatcher. 194 | * 195 | * { 196 | * 'list:add': (state) => { 197 | * this.dispatcher.emit('list:add:error', 'Not allowed') 198 | * } 199 | * } 200 | */ 201 | 202 | /** 203 | * Returns the current state of the store. 204 | * 205 | * store.getState() 206 | */ 207 | 208 | getState () { 209 | return this.state 210 | }, 211 | 212 | /** 213 | * listen : listen(callback(state), [{ immediate }]) 214 | * Listens for changes, firing the function `callback` when it happens. The 215 | * current state is passed onto the callback as the argument `state`. 216 | * 217 | * store.listen(function (state) { 218 | * console.log('State changed:', state) 219 | * }) 220 | * 221 | * The callback will be immediately invoked when you call `listen()`. To 222 | * disable this behavior, pass `{ immediate: false }`. 223 | * 224 | * store.listen(function (state) { 225 | * // ... 226 | * }, { immediate: false }) 227 | */ 228 | 229 | listen (fn, options) { 230 | this.on('change', fn) 231 | if (!options || options.immediate) fn(this.getState()) 232 | }, 233 | 234 | /** 235 | * Unbinds a given change handler. 236 | * 237 | * function onChange () { ... } 238 | * 239 | * store.listen(onChange) 240 | * store.unlisten(onChange) 241 | */ 242 | 243 | unlisten (fn) { 244 | return this.removeListener('change', fn) 245 | }, 246 | 247 | /** 248 | * observe : observe(actions: Object, [options: Object]) 249 | * Listens to events in the dispatcher. 250 | * 251 | * store.observe({ 252 | * 'list:add': function (state) { ... }, 253 | * 'list:remove': function (state) { ... } 254 | * }) 255 | */ 256 | 257 | observe (actions, options) { 258 | this.bindToDispatcher(actions, this.dispatcher) 259 | 260 | // Add the observers to actionsList 261 | if (!options || options.record) { 262 | this.actionsList.push(actions) 263 | } 264 | }, 265 | 266 | /* 267 | * Private: binds actions object `actions` to a `dispatcher`. 268 | */ 269 | 270 | bindToDispatcher (actions, dispatcher) { 271 | Object.keys(actions).forEach((key) => { 272 | const fn = actions[key] 273 | dispatcher.on(key, (...args) => { 274 | const result = fn.apply(this, [ this.getState(), ...args ]) 275 | if (result) this.resetState(result) 276 | }) 277 | }) 278 | }, 279 | 280 | /** 281 | * Adds methods to the store object. 282 | * 283 | * let store = new Store(...) 284 | * store.extend({ 285 | * helperMethod () { 286 | * return true 287 | * } 288 | * }) 289 | * 290 | * store.helperMethod() //=> true 291 | */ 292 | 293 | extend (proto: Object) { 294 | Object.keys(proto).forEach((key) => { 295 | this.constructor.prototype[key] = proto[key] 296 | }) 297 | return this 298 | }, 299 | 300 | /** 301 | * Duplicates the store, listening to a new dispatcher. Great for unit 302 | * testing. 303 | * 304 | * let store = new Store(...) 305 | * 306 | * let dispatcher = new Dispatcher() 307 | * let newStore = store.dup(dispatcher) 308 | * 309 | * dispatch.emit('event') 310 | * // ...will only be received by newStore 311 | */ 312 | 313 | dup (dispatcher: Dispatcher) { 314 | const NewStore = this.constructor 315 | return new NewStore(dispatcher) 316 | }, 317 | 318 | /** 319 | * Resets the state to the new given `state`. This should never be used 320 | * except perhaps in unit tests. 321 | * 322 | * store.resetState({ count: 0 }) 323 | */ 324 | 325 | resetState (state: Object) { 326 | this.state = state 327 | this.dirty = true 328 | 329 | // Use defer twice to make sure it's at the end of all stacks 330 | this.dispatcher.defer(() => { 331 | this.dispatcher.defer(() => { 332 | if (this.dirty) { 333 | delete this.dirty 334 | this.emit('change', state) 335 | } 336 | }) 337 | }) 338 | } 339 | } 340 | 341 | /** 342 | * Utilities: 343 | * (Module) Some helper functions. 344 | */ 345 | 346 | /** 347 | * Connects a React Component to a store. It makes the store's state available as properties. 348 | * 349 | * It takes the static function `getStores()` and connects to those stores. You 350 | * may also provide a `getPropsFromStores()` method. 351 | * 352 | * let Component = React.createClass({ 353 | * statics: { 354 | * getStores () { return [store] }, 355 | * getPropsFromStores (stores) { return stores[0].data } 356 | * } 357 | * }) 358 | * 359 | * Component = connectToStores(Component) 360 | * 361 | * Based on the [alt implementation](https://github.com/goatslacker/alt/blob/master/src/utils/connectToStores.js). 362 | */ 363 | 364 | export function connectToStores (Spec, Component = Spec) { 365 | if (!Spec.getStores) { 366 | throw new Error('connectToStores(): ' + 367 | 'expected getStores() static function') 368 | } 369 | 370 | if (!Spec.getPropsFromStores) { 371 | Spec.getPropsFromStores = function () { 372 | return Spec.getStores().reduce((output, store) => { 373 | return { ...output, ...store.getState() } 374 | }, {}) 375 | } 376 | } 377 | 378 | const StoreConnection = React.createClass({ 379 | getInitialState () { 380 | return Spec.getPropsFromStores(this.props, this.context) 381 | }, 382 | 383 | componentWillReceiveProps (nextProps) { 384 | this.setState(Spec.getPropsFromStores(nextProps, this.context)) 385 | }, 386 | 387 | componentDidMount () { 388 | const stores = Spec.getStores(this.props, this.context) 389 | this.storeListeners = stores.map((store) => { 390 | return store.listen(this.onChange) 391 | }) 392 | if (Spec.componentDidConnect) { 393 | Spec.componentDidConnect(this.props, this.context) 394 | } 395 | }, 396 | 397 | componentWillUnmount () { 398 | const stores = Spec.getStores(this.props, this.context) 399 | stores.forEach((store) => { 400 | store.unlisten(this.onChange) 401 | }) 402 | }, 403 | 404 | onChange () { 405 | this.setState(Spec.getPropsFromStores(this.props, this.context)) 406 | }, 407 | 408 | render () { 409 | return React.createElement( 410 | Component, 411 | { ...this.props, ...this.state } 412 | ) 413 | } 414 | }) 415 | 416 | return StoreConnection 417 | } 418 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "uflux", 3 | "description": "Minimal flux implementation", 4 | "version": "0.8.0", 5 | "author": "Rico Sta. Cruz ", 6 | "bugs": { 7 | "url": "https://github.com/rstacruz/uflux/issues" 8 | }, 9 | "devDependencies": { 10 | "babel": "^5.8.12", 11 | "babel-eslint": "^4.0.5", 12 | "babelify": "^6.1.3", 13 | "browserify": "^11.0.0", 14 | "expect": "^1.8.0", 15 | "istanbul": "^0.3.17", 16 | "jsdom": "^5.6.1", 17 | "mdx": "github:rstacruz/mdx", 18 | "mocha": "^2.2.5", 19 | "mocha-jsdom": "^1.0.0", 20 | "mocha-standard": "^1.0.0", 21 | "react": "^0.13.3", 22 | "standard": "^4.5.4" 23 | }, 24 | "directories": { 25 | "test": "test" 26 | }, 27 | "homepage": "https://github.com/rstacruz/uflux#readme", 28 | "keywords": [ 29 | "dispatcher", 30 | "flux", 31 | "react", 32 | "store" 33 | ], 34 | "license": "MIT", 35 | "main": "dist/index.js", 36 | "repository": { 37 | "type": "git", 38 | "url": "git+https://github.com/rstacruz/uflux.git" 39 | }, 40 | "scripts": { 41 | "build": "npm run build-dist && npm run build-docs", 42 | "build-dist": "browserify -t [ babelify --stage 0 ] -u react -s uflux lib/index.js -o dist/index.js", 43 | "build-docs": "mdx lib/index.js -x private -f markdown > API.md", 44 | "prepublish": "npm run build", 45 | "test": "mocha", 46 | "coverage": "istanbul cover _mocha -- -R spec test/index/*.js" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /test/index/basic_test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, expect, beforeEach, before */ 2 | let jsdom = require('mocha-jsdom') 3 | let Dispatcher, Store 4 | let d, s 5 | 6 | describe('Dispatcher', function () { 7 | before(function () { 8 | let uflux = jsdom.rerequire('../../lib') 9 | Dispatcher = uflux.Dispatcher 10 | Store = uflux.Store 11 | }) 12 | 13 | beforeEach(function () { 14 | d = new Dispatcher() 15 | }) 16 | 17 | it('works', function (next) { 18 | d.on('myevent', () => next()) 19 | d.emit('myevent') 20 | }) 21 | 22 | it('carries 1 argument', function (next) { 23 | d.on('myevent', function (name) { 24 | expect(name).toEqual('world') 25 | next() 26 | }) 27 | d.emit('myevent', 'world') 28 | }) 29 | 30 | it('carries multiple arguments', function (next) { 31 | d.on('greet', function (name, greeting) { 32 | expect(name).toEqual('world') 33 | expect(greeting).toEqual('salut') 34 | next() 35 | }) 36 | d.emit('greet', 'world', 'salut') 37 | }) 38 | }) 39 | 40 | describe('Dispatcher.emitDepth', function () { 41 | beforeEach(function () { 42 | d = new Dispatcher() 43 | }) 44 | 45 | it('emitDepth = 0', function () { 46 | d.on('one', function () { }) 47 | d.emit('one') 48 | expect(d.emitDepth).toEqual(0) 49 | }) 50 | 51 | it('emitDepth = 1', function (next) { 52 | d.on('one', function () { 53 | expect(d.emitDepth).toEqual(1) 54 | next() 55 | }) 56 | 57 | d.emit('one') 58 | }) 59 | 60 | it('emitDepth = 2', function (next) { 61 | d.on('one', () => { d.emit('two') }) 62 | 63 | d.on('two', function () { 64 | expect(d.emitDepth).toEqual(2) 65 | next() 66 | }) 67 | 68 | d.emit('one') 69 | }) 70 | 71 | it('defer()', function (next) { 72 | d.defer(function () { 73 | expect(d.emitDepth).toEqual(0) 74 | next() 75 | }) 76 | 77 | d.on('one', () => { d.emit('two') }) 78 | d.on('two', () => { }) 79 | 80 | d.emit('one') 81 | }) 82 | }) 83 | 84 | // describe('Dispatcher.wait()', function () { 85 | // beforeEach(function () { 86 | // d = new Dispatcher() 87 | // }) 88 | 89 | // it('.wait() yields', function () { 90 | // var emissions = [] 91 | // d.on('myevent', (msg) => { emissions.push(msg) }) 92 | // d.wait(() => { d.emit('myevent', 2) }) 93 | // expect(emissions).toEqual([2]) 94 | // }) 95 | 96 | // it('.wait() works', function () { 97 | // var emissions = [] 98 | // d.on('myevent', () => { 99 | // emissions.push(2) 100 | // }) 101 | // d.wait(() => { 102 | // d.emit('myevent') 103 | // emissions.push(1) 104 | // }) 105 | // expect(emissions).toEqual([1, 2]) 106 | // }) 107 | // }) 108 | 109 | describe('Store', function () { 110 | beforeEach(function () { 111 | d = new Dispatcher() 112 | }) 113 | 114 | beforeEach(function () { 115 | s = new Store(d, { name: 'store', ids: [] }, { 116 | 'list:push': function (state, id) { 117 | return { ...state, ids: state.ids.concat([ id ]) } 118 | } 119 | }) 120 | }) 121 | 122 | it('works', function () { 123 | d.emit('list:push', 1) 124 | d.emit('list:push', 2) 125 | 126 | expect(s.getState().name).toEqual('store') 127 | expect(s.getState().ids).toEqual([ 1, 2 ]) 128 | }) 129 | 130 | it('emits a change event', function (next) { 131 | s.listen(function (state) { 132 | expect(state).toEqual({ name: 'store', ids: [ 1 ]}) 133 | next() 134 | }, { immediate: false }) 135 | d.emit('list:push', 1) 136 | }) 137 | 138 | it('listen() files immediately', function (next) { 139 | s.listen(function (state) { 140 | expect(state).toBeA('object') 141 | next() 142 | }) 143 | }) 144 | 145 | it('change events are debounced (with 2)', function (next) { 146 | s.listen(function (state) { 147 | expect(state).toEqual(2) 148 | next() 149 | }, { immediate: false }) 150 | s.observe({ 151 | 'one': (state) => { d.emitAfter('two'); return 1 }, 152 | 'two': (state) => { return 2 } 153 | }) 154 | 155 | d.emit('one') 156 | }) 157 | 158 | it('change events are debounced (with 3)', function (next) { 159 | s.listen(function (state) { 160 | expect(state).toEqual(3) 161 | next() 162 | }, { immediate: false }) 163 | s.observe({ 164 | 'one': (state) => { d.emitAfter('two'); return 1 }, 165 | 'two': (state) => { d.emitAfter('tri'); return 2 }, 166 | 'tri': (state) => { return 3 } 167 | }) 168 | 169 | d.emit('one') 170 | }) 171 | 172 | it('waits', function () { 173 | s.observe({ 174 | '1st-event': function (state) { 175 | d.emitAfter('2nd-event') 176 | return { ...state, ids: state.ids.concat([ 1 ]) } 177 | }, 178 | 179 | '2nd-event': function (state) { 180 | return { ...state, ids: state.ids.concat([ 2 ]) } 181 | } 182 | }) 183 | 184 | d.emit('1st-event') 185 | expect(s.getState().ids).toEqual([ 1, 2 ]) 186 | }) 187 | 188 | describe('subclass', function () { 189 | it('has .constructor', function () { 190 | expect(s.constructor).toBeA('function') 191 | }) 192 | }) 193 | 194 | describe('.extend()', function () { 195 | it('works', function () { 196 | s.extend({ hi () { return 'hello' } }) 197 | expect(s.hi()).toEqual('hello') 198 | }) 199 | }) 200 | 201 | describe('.dup()', function () { 202 | it('works', function () { 203 | let dd = new Dispatcher() 204 | let ss = s.dup(dd) 205 | 206 | dd.emit('list:push', 2) 207 | 208 | expect(ss.getState().ids).toEqual([ 2 ]) 209 | expect(s.getState().ids).toEqual([]) 210 | }) 211 | }) 212 | }) 213 | -------------------------------------------------------------------------------- /test/index/react_test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it, expect, beforeEach, afterEach, before */ 2 | let jsdom = require('mocha-jsdom') 3 | let React, Dispatcher, Store, connectToStores 4 | let d, s, div, View, spies 5 | 6 | describe('React', function () { 7 | jsdom() 8 | 9 | before(function () { 10 | React = jsdom.rerequire('react') 11 | let uflux = jsdom.rerequire('../../lib') 12 | 13 | Store = uflux.Store 14 | Dispatcher = uflux.Dispatcher 15 | connectToStores = uflux.connectToStores 16 | }) 17 | 18 | beforeEach(function () { 19 | div = document.createElement('div') 20 | }) 21 | 22 | beforeEach(function () { 23 | d = new Dispatcher() 24 | }) 25 | 26 | beforeEach(function () { 27 | s = new Store(d, { 28 | name: 'John' 29 | }, { 30 | 'name:set': (state, name) => { 31 | return { ...state, name: name } 32 | } 33 | }) 34 | }) 35 | 36 | beforeEach(function () { 37 | View = React.createClass({ 38 | propTypes: { 39 | name: React.PropTypes.string 40 | }, 41 | statics: { 42 | getStores: () => [s] 43 | }, 44 | 45 | render () { 46 | return
hi {this.props.name}
47 | } 48 | }) 49 | 50 | View = connectToStores(View) 51 | }) 52 | 53 | it('works', function () { 54 | React.render(, div) 55 | expect(div.textContent).toInclude('hi John') 56 | }) 57 | 58 | it('responds to changes', function () { 59 | React.render(, div) 60 | expect(div.textContent).toInclude('hi John') 61 | d.emit('name:set', 'Jane') 62 | expect(div.textContent).toInclude('hi Jane') 63 | }) 64 | 65 | it('can be unmounted', function () { 66 | spies = [ 67 | expect.spyOn(View.prototype, 'componentDidMount') 68 | ] 69 | 70 | let NewView = React.createClass({ 71 | getInitialState () { 72 | return { visible: true } 73 | }, 74 | 75 | render () { 76 | return ( 77 |
{ this.state.visible ? : null }
78 | ) 79 | } 80 | }) 81 | 82 | let inst = React.render(, div) 83 | inst.setState({ visible: false }) 84 | expect(View.prototype.componentDidMount).toHaveBeenCalled() 85 | }) 86 | 87 | afterEach(function () { 88 | if (spies) { 89 | spies.forEach((spy) => { spy.restore() }) 90 | spies = null 91 | } 92 | }) 93 | }) 94 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require test/setup 2 | --recursive 3 | -------------------------------------------------------------------------------- /test/setup.js: -------------------------------------------------------------------------------- 1 | require('babel/register')({ stage: 0 }) 2 | global.expect = require('expect') 3 | -------------------------------------------------------------------------------- /test/standard_test.js: -------------------------------------------------------------------------------- 1 | /* global describe, it */ 2 | describe('coding style', function () { 3 | this.timeout(5000) 4 | it('conforms to standard', require('mocha-standard').files([ 5 | 'lib/index.js', 6 | 'test/**/*.js' 7 | ], { 8 | parser: 'babel-eslint' 9 | })) 10 | }) 11 | --------------------------------------------------------------------------------