microcosm
Advanced tools
Comparing version 1.1.0 to 1.2.0
# Changelog | ||
### 1.2.0 | ||
- All stores can implement a `serialize` method which allows them to | ||
shape how app state is serialized to JSON. | ||
### 1.1.0 | ||
@@ -4,0 +9,0 @@ |
@@ -1,2 +0,2 @@ | ||
module.exports=function(t){function n(e){if(r[e])return r[e].exports;var o=r[e]={exports:{},id:e,loaded:!1};return t[e].call(o.exports,o,o.exports,n),o.loaded=!0,o.exports}var r={};return n.m=t,n.c=r,n.p="",n(0)}([function(t,n,r){"use strict";n.__esModule=!0;var e=r(3);n.tag=e,n["default"]=r(2)},function(t){"use strict";var n=function(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")},r=function(){function t(){n(this,t),this._callbacks=[]}return t.prototype.ignore=function(t){this._callbacks=this._callbacks.filter(function(n){return n!==t})},t.prototype.listen=function(t){this._callbacks=this._callbacks.concat(t)},t.prototype.pump=function(){for(var t=0;t<this._callbacks.length;t++)this._callbacks[t]()},t}();t.exports=r},function(t,n,r){"use strict";var e=function(t){return t&&t.__esModule?t["default"]:t},o=Object.assign||function(t){for(var n=1;n<arguments.length;n++){var r=arguments[n];for(var e in r)Object.prototype.hasOwnProperty.call(r,e)&&(t[e]=r[e])}return t},i=function(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);t.prototype=Object.create(n&&n.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),n&&(t.__proto__=n)},s=function(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")},u=e(r(1)),a=e(r(5)),c=function(t){function n(){s(this,n),t.call(this),this._stores=[],this._state=this.getInitialState()}return i(n,t),n.prototype.shouldUpdate=function(t,n){return 0==a(t,n)},n.prototype.getInitialState=function(){return{}},n.prototype.seed=function(t){var n=this._stores.filter(function(n){return t[n]});n.forEach(function(n){this.set(n,n.getInitialState(t[n]))},this)},n.prototype.set=function(t,n){this._state=o({},this._state,function(){var r={};return r[t]=n,r}())},n.prototype.has=function(t){return this._stores.indexOf(t)>-1},n.prototype.get=function(t,n){return this._state[t]||t.getInitialState(n)},n.prototype.send=function(t){for(var n=this,r=arguments.length,e=Array(r>1?r-1:0),o=1;r>o;o++)e[o-1]=arguments[o];if(e.length<t.length)return this.send.bind(this,t);var i=t.apply(void 0,e);return i instanceof Promise?i.then(function(r){return n.dispatch(t,r)}):this.dispatch(t,i)},n.prototype.dispatch=function(t,n){var r=this,e=this._stores.reduce(function(e,o){return t in o&&(e[o]=o[t](r.get(o),n)),e},o({},this._state));return this.shouldUpdate(this._state,e)&&(this._state=e,this.pump()),n},n.prototype.addStore=function(){for(var t=arguments.length,n=Array(t),r=0;t>r;r++)n[r]=arguments[r];this._stores=this._stores.concat(n)},n.prototype.toJSON=function(){return this.serialize()},n.prototype.serialize=function(){var t=this;return this._stores.reduce(function(n,r){return n[r]=t.get(r),n},{})},n}(u);t.exports=c},function(t,n,r){"use strict";var e=function(t){return t&&t.__esModule?t["default"]:t};n.__esModule=!0;var o=e(r(4)),i=0,s=function(t){var n=t.bind(null),r="_microcosm-"+i++;return n.toString=function(){return r},n};n.infuse=s,n["default"]=function(t){return o(t,s)}},function(t){"use strict";t.exports=function(t,n){var r=void 0===arguments[0]?{}:arguments[0],e=void 0===arguments[2]?{}:arguments[2],o=Object.keys(r);return o.reduce(function(t,e){return t[e]=n(r[e],e),t},e)}},function(t,n,r){/*! | ||
module.exports=function(t){function n(e){if(r[e])return r[e].exports;var o=r[e]={exports:{},id:e,loaded:!1};return t[e].call(o.exports,o,o.exports,n),o.loaded=!0,o.exports}var r={};return n.m=t,n.c=r,n.p="",n(0)}([function(t,n,r){"use strict";n.__esModule=!0;var e=r(3);n.tag=e,n["default"]=r(2)},function(t){"use strict";var n=function(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")},r=function(){function t(){n(this,t),this._callbacks=[]}return t.prototype.ignore=function(t){this._callbacks=this._callbacks.filter(function(n){return n!==t})},t.prototype.listen=function(t){this._callbacks=this._callbacks.concat(t)},t.prototype.pump=function(){for(var t=0;t<this._callbacks.length;t++)this._callbacks[t]()},t}();t.exports=r},function(t,n,r){"use strict";var e=function(t){return t&&t.__esModule?t["default"]:t},o=Object.assign||function(t){for(var n=1;n<arguments.length;n++){var r=arguments[n];for(var e in r)Object.prototype.hasOwnProperty.call(r,e)&&(t[e]=r[e])}return t},i=function(t,n){if("function"!=typeof n&&null!==n)throw new TypeError("Super expression must either be null or a function, not "+typeof n);t.prototype=Object.create(n&&n.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),n&&(t.__proto__=n)},s=function(t,n){if(!(t instanceof n))throw new TypeError("Cannot call a class as a function")},u=e(r(1)),a=e(r(5)),c=function(t){function n(){s(this,n),t.call(this),this._stores=[],this._state=this.getInitialState()}return i(n,t),n.prototype.shouldUpdate=function(t,n){return 0==a(t,n)},n.prototype.getInitialState=function(){return{}},n.prototype.seed=function(t){var n=this._stores.filter(function(n){return t[n]});n.forEach(function(n){this.set(n,n.getInitialState(t[n]))},this)},n.prototype.set=function(t,n){this._state=o({},this._state,function(){var r={};return r[t]=n,r}())},n.prototype.has=function(t){return this._stores.indexOf(t)>-1},n.prototype.get=function(t,n){return this._state[t]||t.getInitialState(n)},n.prototype.send=function(t){for(var n=this,r=arguments.length,e=Array(r>1?r-1:0),o=1;r>o;o++)e[o-1]=arguments[o];if(e.length<t.length)return this.send.bind(this,t);var i=t.apply(void 0,e);return i instanceof Promise?i.then(function(r){return n.dispatch(t,r)}):this.dispatch(t,i)},n.prototype.dispatch=function(t,n){var r=this,e=this._stores.reduce(function(e,o){return t in o&&(e[o]=o[t](r.get(o),n)),e},o({},this._state));return this.shouldUpdate(this._state,e)&&(this._state=e,this.pump()),n},n.prototype.addStore=function(){for(var t=arguments.length,n=Array(t),r=0;t>r;r++)n[r]=arguments[r];this._stores=this._stores.concat(n)},n.prototype.serialize=function(){var t=this;return this._stores.reduce(function(n,r){var e=t.get(r);return"serialize"in r&&(e=r.serialize(e)),n[r]=e,n},{})},n.prototype.toJSON=function(){return this.serialize(this.state)},n}(u);t.exports=c},function(t,n,r){"use strict";var e=function(t){return t&&t.__esModule?t["default"]:t};n.__esModule=!0;var o=e(r(4)),i=0,s=function(t){var n=t.bind(null),r="_microcosm-"+i++;return n.toString=function(){return r},n};n.infuse=s,n["default"]=function(t){return o(t,s)}},function(t){"use strict";t.exports=function(t,n){var r=void 0===arguments[2]?{}:arguments[2],e=Object.keys(t);return e.reduce(function(r,e){return r[e]=n(t[e],e),r},r)}},function(t,n,r){/*! | ||
* is-equal-shallow <https://github.com/jonschlinkert/is-equal-shallow> | ||
@@ -3,0 +3,0 @@ * |
@@ -1,4 +0,4 @@ | ||
import Route from 'stores/Route' | ||
import Items from 'stores/Items' | ||
import Lists from 'stores/Lists' | ||
import Route from './stores/Route' | ||
import Items from './stores/Items' | ||
import Lists from './stores/Lists' | ||
import Microcosm from 'Microcosm' | ||
@@ -5,0 +5,0 @@ |
@@ -10,14 +10,20 @@ /** | ||
let routes = { | ||
'/' : require('../components/layouts/Home'), | ||
'/list/:id' : require('../components/layouts/Show') | ||
} | ||
let routes = [ | ||
{ path: '/', handler: require('../components/layouts/Home') }, | ||
{ path: '/list/:id', handler: require('../components/layouts/Show') } | ||
] | ||
export default { | ||
install(flux) { | ||
Object.keys(routes).forEach(route => { | ||
page(route, function({ params }) { | ||
flux.send(Route.set, { handler: routes[route], params }) | ||
}) | ||
install(app) { | ||
let action = app.send(Route.set) | ||
// Create a callback for each route that pushes the event | ||
// into the app's dispatcher | ||
// | ||
// TODO: Ideally, we'd detect that the route changed and | ||
// reduce down to a route/handler within the associated | ||
// store or action | ||
routes.forEach(({ path, handler }) => { | ||
page(path, ({ params }) => action({ handler, params })) | ||
}) | ||
@@ -24,0 +30,0 @@ |
@@ -1,3 +0,3 @@ | ||
import Lists from 'actions/lists' | ||
import contrast from 'contrast' | ||
import Lists from '../actions/lists' | ||
import contrast from '../../lib/contrast' | ||
import uid from 'uid' | ||
@@ -4,0 +4,0 @@ |
@@ -18,7 +18,9 @@ var Webpack = require('webpack') | ||
files: [ | ||
'src/**/__tests__/*.js*' | ||
'src/**/__tests__/*.js*', | ||
'example/src/**/__tests__/*.js*' | ||
], | ||
preprocessors: { | ||
'src/**/__tests__/*.js*': [ 'webpack', 'sourcemap' ] | ||
'src/**/__tests__/*.js*' : [ 'webpack', 'sourcemap' ], | ||
'example/src/**/__tests__/*.js*' : [ 'webpack', 'sourcemap' ] | ||
}, | ||
@@ -25,0 +27,0 @@ |
{ | ||
"name": "microcosm", | ||
"version": "1.1.0", | ||
"version": "1.2.0", | ||
"description": "An experimental flux implimentation", | ||
"main": "dist/Microcosm.js", | ||
"scripts": { | ||
"coveralls": "CONTINUOUS_INTEGRATION=true npm test && coveralls < coverage/report-lcov/lcov.info", | ||
"prepublish": "NODE_ENV=production webpack -p", | ||
"test": "NODE_ENV=test karma start", | ||
"coveralls": "CONTINUOUS_INTEGRATION=true npm test && coveralls < coverage/report-lcov/lcov.info", | ||
"example": "node example/server" | ||
"start": "node example/server", | ||
"test": "NODE_ENV=test karma start" | ||
}, | ||
@@ -12,0 +12,0 @@ "repository": { |
212
README.md
@@ -28,3 +28,3 @@ Important! This is largely an exploratory repo, used to vet some ideas | ||
Microcosm injects a couple of opinions regarding the Flux | ||
Microcosm injects a a couple of opinions regarding the Flux | ||
architecture: | ||
@@ -38,11 +38,137 @@ | ||
updated by returning a new value. | ||
3. All Actions that return promises will wait to resolve before | ||
3. Stores do not contain data, they _shape_ it. See the section on | ||
stores below. | ||
4. All Actions that return promises will wait to resolve before | ||
dispatching. | ||
4. It should be easily to embed in libraries. Additional features | ||
5. It should be easily to embed in libraries. Additional features such | ||
should be able to layer on top. | ||
6. It should utilize language features over implementation details as | ||
much as possible. | ||
## Design | ||
Without getting too lofty, this is roughly the ideal scenario for a | ||
Microcosm: | ||
``` | ||
|--> [Store] ---| | ||
[app.send] ------> [Action] ------> [Dispatcher] ---+--> [Store] ---+--> [app.shouldUpdate?] | ||
^ |--> [Store] ---| | | ||
| | | ||
| v | ||
[External Services] <--------------------------------------------------------- [YES] | ||
|- User Interface | ||
|- Router | ||
|- Firebase sync | ||
``` | ||
## Writing a Microcosm | ||
A new app starts out as an extension of the `Microcosm` class: | ||
```javascript | ||
import Microcosm from 'microcosm' | ||
class MyApp extends Microcosm { | ||
// Great things await | ||
} | ||
``` | ||
A microcosm is solely responsible for managing a global state | ||
object. This value is assigned initially through `getInitialState`, | ||
much like a react component: | ||
```javascript | ||
import Microcosm from 'microcosm' | ||
class MyApp extends Microcosm { | ||
// This is actually the default implementation | ||
getInitialState() { | ||
return {} | ||
} | ||
} | ||
``` | ||
Although a microcosm is exclusively responsible for managing its own | ||
state, `stores` shape how that data is changed: | ||
```javascript | ||
import Microcosm from 'microcosm' | ||
let Messages = { | ||
getInitialState() { | ||
return [] | ||
}, | ||
toString() { | ||
return 'messages' | ||
} | ||
} | ||
class MyApp extends Microcosm { | ||
constructor() { | ||
super() | ||
this.addStore(Messages) | ||
} | ||
getInitialState() { | ||
return {} | ||
} | ||
} | ||
``` | ||
Now the `Messages` store will be responsible for shaping the data kept | ||
within the `messages` key of this app's state. | ||
Requests to change this data can be handled with `Actions`. An action | ||
is simply a function that has been tagged with a unique | ||
identifier. The `tag` module included with `Microcosm` can do just | ||
that: | ||
```javascript | ||
import Microcosm, { tag } from 'microcosm' | ||
let Actions = { | ||
createMessage(options) { | ||
// Here, we are simply returning options. However this | ||
// gives you an opportunity to modify parameters before they | ||
// are sent to stores | ||
return options | ||
} | ||
} | ||
let Messages = { | ||
getInitialState() { | ||
return [] | ||
}, | ||
[Actions.createMessage](oldState, parameters) { | ||
return oldState.concat(parameters) | ||
}, | ||
toString() { | ||
return 'messages' | ||
} | ||
} | ||
class MyApp extends Microcosm { | ||
constructor() { | ||
super() | ||
this.addStore(Messages) | ||
} | ||
getInitialState() { | ||
return {} | ||
} | ||
} | ||
``` | ||
`MyApp` is now setup to accept actions, filtering them through the | ||
`Messages` store before saving them. More information on how to | ||
trigger actions and retrieve state follows. | ||
## How Actions work | ||
Actions are simple objects that have been tagged with unique identifiers: | ||
Actions are simply functions. They must implement a `toString` method | ||
so that stores can know when to respond to them. For those familiar | ||
with traditional flux, this `toString` method replaces the need to | ||
maintain constants for each action type. | ||
Fortunately, the `tag` function makes this quite mangeable: | ||
``` javascript | ||
@@ -52,7 +178,5 @@ import tag from 'microcosm/tag' | ||
let Messages = tag({ | ||
create(message) { | ||
return { message, time: new Date() } | ||
} | ||
}) | ||
@@ -66,4 +190,7 @@ ``` | ||
You can fire them like: | ||
Microcosms implement a `send` method. This will run execute a given | ||
action with an arbitrary number of arguments (following the first). | ||
This works like: | ||
```javascript | ||
@@ -73,6 +200,23 @@ app.send(Messages.create, 'This property will be passed to the dispatcher') | ||
### Currying actions | ||
`send` automatically curries invocations that do not include the | ||
expected number of arguments. To repeat the previous example with currying: | ||
```javascript | ||
let create = app.send(Messages.create) | ||
create('This property will be passed to the dispatcher') | ||
``` | ||
Technically, this is even possible (but you didn't hear it from me): | ||
```javascript | ||
let sum = (a, b) => a + b | ||
app.send(sum)(2, 3) // => 5 | ||
``` | ||
## How Stores work | ||
Stores are plain objects. They must implement a `getInitialState` and | ||
`toString()` method. They listen to actions by providing methods at | ||
`toString` method. They listen to actions by providing methods at | ||
the unique signature of an Action, like: | ||
@@ -98,10 +242,14 @@ | ||
Each Store instance manages a subset of a global state object owned by an | ||
individual Microcosm instance. By returning a new state object within responses | ||
to actions, they modify state. | ||
Each store manages a subset of a global state object owned by an | ||
individual Microcosm instance. By returning a new state object within | ||
responses to actions, they modify state. | ||
Microcosm will use `getInitialState` to produce the initial value for the subset | ||
a store manages. | ||
Microcosm will use `getInitialState` to produce the initial value for | ||
the subset a store manages. | ||
Unlike actions, stores must be registered with the system. There are two reason for this. First: to tell Microcosm what Stores should be responsible for managing state. Second: to dictate the priority of dispatcher multicasting (similar to `waitFor` in the standard Flux dispatcher) | ||
Unlike actions, stores must be registered with the system. There are | ||
two reason for this. First: to tell Microcosm what Stores should be | ||
responsible for managing state. Second: to dictate the priority of | ||
dispatcher multicasting (similar to `waitFor` in the standard Flux | ||
dispatcher) | ||
@@ -113,3 +261,7 @@ ```javascript | ||
// Called first: | ||
this.addStore(Messages) | ||
// Called second: | ||
this.addStore(OtherStoreThatDependsOnMessages) | ||
} | ||
@@ -119,2 +271,34 @@ } | ||
### Getting the value out of a store | ||
Similar to a `Map`, microcosms implement a `get` and `set` | ||
method. `set` should never be called directly, however it is exposed | ||
should you wish to define your own method of assignment. As for `get`: | ||
```javascript | ||
app.get(Store) | ||
``` | ||
This works because the app accesses the internal state object | ||
(returned initially from `app.getInitialState`) using `Store` as a | ||
key. Since the store implements a `toString` method, it coerces into | ||
the proper key and returns the expected value. | ||
## Listening to changes | ||
All Microcosm instances are event emitters. They emit a single change event that you can subscribe to like: | ||
```javascript | ||
let app = new Microcosm() | ||
// Add a callback | ||
app.listen(callback) | ||
// Remove a callback | ||
app.ignore(callback) | ||
// Force an emission | ||
app.pump() | ||
``` | ||
## Additional Notes | ||
@@ -121,0 +305,0 @@ |
@@ -23,3 +23,3 @@ import Action from './fixtures/Action' | ||
it ('can serialize', function() { | ||
it ('can serialize to JSON', function() { | ||
let m = new Microcosm() | ||
@@ -31,2 +31,21 @@ | ||
it ('runs through serialize methods on stores', function() { | ||
let m = new Microcosm() | ||
m.addStore({ | ||
getInitialState() { | ||
return 'this will not display' | ||
}, | ||
serialize(state) { | ||
state.should.equal(this.getInitialState()) | ||
return 'this is a test' | ||
}, | ||
toString() { | ||
return 'serialize-test' | ||
} | ||
}) | ||
m.toJSON().should.have.property('serialize-test', 'this is a test') | ||
}) | ||
it ('passes seed data to stores', function() { | ||
@@ -33,0 +52,0 @@ let seed = { fiz: 'buz' } |
@@ -78,16 +78,23 @@ /** | ||
addStore(...store) { | ||
this._stores = this._stores.concat(store) | ||
addStore(...stores) { | ||
this._stores = this._stores.concat(stores) | ||
} | ||
toJSON() { | ||
return this.serialize() | ||
} | ||
serialize(state) { | ||
return this._stores.reduce((memo, store) => { | ||
let state = this.get(store) | ||
serialize() { | ||
return this._stores.reduce((memo, store) => { | ||
memo[store] = this.get(store) | ||
if ('serialize' in store) { | ||
state = store.serialize(state) | ||
} | ||
memo[store] = state | ||
return memo | ||
}, {}) | ||
} | ||
toJSON() { | ||
return this.serialize(this.state) | ||
} | ||
} |
@@ -11,3 +11,3 @@ /** | ||
export default (entity={}, fn, initial={}) => { | ||
export default (entity, fn, initial={}) => { | ||
let keys = Object.keys(entity) | ||
@@ -14,0 +14,0 @@ |
Sorry, the diff of this file is not supported yet
75304
73
1077
301