├── .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 | [](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 |
Reload
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 |
--------------------------------------------------------------------------------