stoar
v0.2.5
Published
Flux architecture
Downloads
53
Readme
Stoar
A set of tools for the Flux architecture. It provides you with:
- Dispatcher - An object that mediates data flow to and from your stores. This is the central hub of your Flux app.
- Store - A container for application state.
- Commander - The entry point where data flows into the dispatcher.
- Notifier - The exit point where data flows out of the stores.
% npm install stoar
var Stoar = require('stoar');
Building a simple Stoar Flux app
First, define your objects. These will be modules which can be required from the rest of your code.
- Create a dispatcher. Do this first; everything else comes from it.
- Create one or more stores from the dispatcher and provide action callbacks for each.
- Create a commander from the dispatcher, with optional custom methods.
- Create a notifier from the dispatcher.
Next, compose these modules into an app.
- Listen for change events on the notifier, re-rendering your top-level React component(s) for each change. Do not listen for change events from anywhere but the top level components, as changes will propagate down via your render functions.
- Send actions to the commander in response to various events in your app, e.g. user-initiated, server-push, app initialize, window resize, polling/fetching, etc. Actions always have the signature
(action, payload)
whereaction
is a string andpayload
is any value whatsoever. - At any point in the app it's okay to read data from the stores. However, only in a given store's action callback is it okay to mutate that store.
Here's the same thing but laid out as files.
// ------------------------------------
// dispatcher.js
var Stoar = require('stoar');
module.exports = Stoar.dispatcher();
// ------------------------------------
// ui-store.js
var dispatcher = require('./dispatcher');
var uiStore = module.exports = dispatcher.store({
// define data here
}, function(action, payload){
uiStore.set(...) // mutate the store here
});
// ------------------------------------
// data-store.js
var dispatcher = require('./dispatcher');
var uiStore = require('./ui-store');
var dataStore = module.exports = dispatcher.store({
// define data here
}, function(action, payload){
this.waitFor(uiStore);
// respond to the action while being able
// to see the latest data in uiStore
});
// ------------------------------------
// commander.js
var dispatcher = require('./dispatcher');
module.exports = dispatcher.commander({
doCustomThing: function(){}
});
// ------------------------------------
// notifier.js
var dispatcher = require('./dispatcher');
module.exports = dispatcher.notifier();
// ------------------------------------
// main-controller.js
var notifier = require('./notifier');
notifier.on('change', function(){
topLevelComponent.setProps(...)
});
// ------------------------------------
// some-component.jsx
var commander = require('./commander');
...
commander.send(action, payload); // send an action to dispatcher
commander.doCustomThing(); // custom method might have side effects
...
Stores
var store = Stoar.store({
isFloating: {
type: 'item',
value: false
},
foods: {
type: 'list',
value: ['pizza','salad','eggs']
},
hasFood: function(food){
var foods = this.getAll('foods');
return foods.indexOf(food) !== -1;
}
});
var isFloating = store.get('isFloating')
var firstFood = store.get('foods', 0)
store.forEach('foods', function(food){
// ...
});
Two data items and one custom method are defined in this store.
As you might have guessed, the data items are isFloating
and foods
and the method is hasFood
.
Each data item consists of a definition, which is an object with type
, value
and validate
properties.
- type - Possible values include:
item
,map
, andlist
. Optional; defaults toitem
. Thetype
determines what kinds of operations are available on this store property. - value - The initial content of this property in the store. If
type === 'map
, this must be a plain object. Iftype === 'list'
, this must be an array. This is optional, with a default value depending ontype
. - validate - An optional function that runs against every value set on this store property. For
map
andlist
types, this runs against every item in that map or list. It should throw for bad values. The return value is discarded. - loadable - Indicates that this property is associated with a remote data source. Setting this to
true
merely causes some extra properties to be added. For example if the property is calledposts
, then properties calledposts:loading
,posts:status
,posts:code
, andposts:timestamp
will be created too, each with a correspondingtype
. It's up to you whether and how to use these extra properties.
Immutability and Cloning
JS objects aren't immutable, but treating them as such makes many things easier.
Thus, store objects are read-only.
The only way to get data into a store is via the commander > dispatcher flow.
Also, stoar rejects resetting a mutable property to itself, even through the dispatcher, and provides a clone()
method that you can use instead of get()
in order to treat objects as immutable.
var store = new Stoar({
user: {
value: { name: 'jorge' }
}
});
var user = store.get('user');
user.name = 'Jorge';
store.set('user', user); // error!
var user = store.clone('user');
user.name = 'Jorge';
store.set('user', user); // success
Clones are shallow by default. You can optionally deep clone by passing true
:
var deepClone = store.clone('stuff', true);
API
Top-level API
var disp = Stoar.dispatcher()
- Create a dispatcher.
Dispatcher API
var store = dispatcher.store(defs, actionCallback)
- Create a store.defs
is an object describing the store's data.actionCallback
is a callback that receives a signature(action, payload)
for whenever the dispatcher receives a command, or an object keyed by action names and whose values are functions receiving an(action, payload)
signature.var commander = dispatcher.commander(methods)
- Create a commander.methods
is an object containing any custom method you'd like to have on the created commander.var notifier = dispatcher.notifier()
- Create a notifier.dispatcher.waitFor(store)
- Call this synchronously from within a store's action callback. Causes another store to be updated first.
Store
store.absorb(payload)
- Convenience method to apply amutate
action directly. Can only be called synchronously from within an action callback. (See commander API below).
Item accessors
store.get(prop)
- Returns the item.store.getLoadable(prop)
- Returns a composite object representing loadable info:{value:*,loading:*,timestamp:*,status:*}
store.clone(prop, [isDeep])
- Returns a clone of the item.
Item mutators
store.set(prop, val)
- Updates the item to the given value.store.unset(prop)
- Sets the item to undefined.store.toggle(prop)
- Inverts the value in place using!
.
Map accessors
store.get(prop, key)
- Returns the value at the given key.store.getLoadable(prop, key)
- Returns a composite object representing loadable info:{value:*,loading:*,timestamp:*,status:*}
store.clone(prop, isDeep)
- Clone the value at the given key.store.has(prop, key)
- Returns true if the map has the given key as an own property.store.getAll(prop)
- Returns a copy of the map. Modifying the copy will not change the map.store.getAllLoadables(prop)
- Returns a keyed map of composite objects (seegetLoadable()
).store.keys(prop)
- Returns an array of map keys.store.values(prop)
- Returns an array of map values.store.forEach(prop, cb, [ctx])
- Iterate the map.cb
is passed signature(value, name)
.store.isIdentical(prop, otherMap)
- Test whether the top-level contents of a different object are identical using===
.
Map mutators
store.set(prop, key, val)
- Updates the value at the given key.store.unset(prop, key)
- Deletes the value at the given key.store.setAll(prop, newMap)
- Merges in the new values.store.resetAll(prop, newMap)
- Overwrites the map with the new values.store.setExistingValuesTo(prop, val)
- Sets each existing map value toval
. Leaves keys unchanged.store.clear(prop)
- Empties out the map.store.toggle(prop, key)
- Inverts the value in place using!
.
List accessors
store.get(prop, idx)
- Returns the value at the given index.store.getLoadable(prop, idx)
- Returns a composite object representing loadable info:{value:*,loading:*,timestamp:*,status:*}
store.clone(prop, idx, isDeep)
- Clone the value at the given index.store.length(prop)
- Returns the length.store.getAll(prop)
- Returns a copy of the list. Modifying the copy will not change the list.store.getAllLoadables(prop)
- Returns a list of composite objects (seegetLoadable()
).store.isIdentical(prop, otherList)
- Test whether the top-level contents of a different list are identical using===
.
All of these are strictly accessors and call directly into the Array.prototype
method of the same name, but with prop
shifted off the args.
In old browsers you may need to polyfill Array.prototype
in order for these to work.
store.filter(prop, ...)
store.map(prop, ...)
store.some(prop, ...)
store.every(prop, ...)
store.forEach(prop, ...)
store.reduce(prop, ...)
store.reduceRight(prop, ...)
store.join(prop, sep)
store.slice(prop, ...)
store.concat(prop, ...)
store.indexOf(prop, ...)
store.lastIndexOf(prop, ...)
Lists mutators
store.set(prop, idx, val)
- Updates the value at the given index.store.resetAll(prop, newList)
- Overwrites the list with the new values.store.populate(prop, val, [len])
- Populate list withval
. Iflen
is given, list length is changed.store.clear(prop)
- Empties out the list.store.push(prop, val)
- Appends a value.store.unshift(prop, val)
- Prepends a value.store.pop(prop)
- Removes and returns the last value.store.shift(prop)
- Removes and returns the first value.store.truncateLength(prop, length)
- Limits length of list tolength
. Iflength
is bigger, becomes a no-op.store.toggle(prop, idx)
- Inverts the value in place using!
.store.splice(prop, ...)
- Splice the list in-place. Internally, args are passed directly toArray.prototype.splice
, withprop
shifted off the front.
Commander API
commander.send(action, payload)
- Send an action directly to the dispatcher.commander.myCustomMethod(any, args)
- Call a custom method that you provided when creating the commander.commander[mutatorMethod](store, ...args
) - Proxy for any store mutator method. Stores can't be mutated outside of their action callbacks. This therefore translates into an action calledmutate
which is passed to the given store. The payload contains the arguments passed to the commander, except withstore
shifted off the list. For convenience, in the action callback, you can doif (action === 'mutate') { this.absorb(payload) }
in order to apply mutations directly.
Notifier API
notifier.on('change', callback)
- Listen for changes to any of the stores that have been registered with the dispatcher.callback
is passed a signature(store, property, change)
, wherechange
is an object like{ oldVal: something, newVal: something }
. If the changed property was a list or map,change
will also containindex
orkey
, respectively.