From 16355896fec5710e851b70b0ae05d5c90d0805ba Mon Sep 17 00:00:00 2001 From: Josh Perez Date: Fri, 29 May 2015 12:22:55 -0700 Subject: [PATCH] Add FP goodies baked into alt --- src/alt/store/AltStore.js | 73 ++++++++++++------ src/alt/store/index.js | 21 ++++-- test/async-test.js | 11 ++- test/es3-module-pattern.js | 2 +- test/functional-test.js | 147 +++++++++++++++++++++++++++++++++++++ test/index.js | 2 +- 6 files changed, 218 insertions(+), 38 deletions(-) create mode 100644 test/functional-test.js diff --git a/src/alt/store/AltStore.js b/src/alt/store/AltStore.js index 939b92c1..6d56fd4a 100644 --- a/src/alt/store/AltStore.js +++ b/src/alt/store/AltStore.js @@ -13,41 +13,70 @@ class AltStore { this[Sym.LIFECYCLE] = model[Sym.LIFECYCLE] this[Sym.STATE_CONTAINER] = state || model + this.preventDefault = false this._storeName = model._storeName this.boundListeners = model[Sym.ALL_LISTENERS] this.StoreModel = StoreModel + const output = model.output || ((a, b) => b) + + this.emitChange = () => { + this[EE].emit( + 'change', + output(alt, this[Sym.STATE_CONTAINER]) + ) + } + + const handleDispatch = (f, payload) => { + try { + return f() + } catch (e) { + if (model[Sym.HANDLING_ERRORS]) { + this[Sym.LIFECYCLE].emit( + 'error', + e, + payload, + this[Sym.STATE_CONTAINER] + ) + return false + } else { + throw e + } + } + } + fn.assign(this, model[Sym.PUBLIC_METHODS]) // Register dispatcher this.dispatchToken = alt.dispatcher.register((payload) => { + this.preventDefault = false this[Sym.LIFECYCLE].emit( 'beforeEach', payload, this[Sym.STATE_CONTAINER] ) - if (model[Sym.LISTENERS][payload.action]) { - let result = false - - try { - result = model[Sym.LISTENERS][payload.action](payload.data) - } catch (e) { - if (model[Sym.HANDLING_ERRORS]) { - this[Sym.LIFECYCLE].emit( - 'error', - e, - payload, - this[Sym.STATE_CONTAINER] - ) - } else { - throw e - } - } + const actionHandler = model[Sym.LISTENERS][payload.action] || + model.otherwise - if (result !== false) { - this.emitChange() - } + if (actionHandler) { + const result = handleDispatch(() => { + return actionHandler.call(model, payload.data) + }, payload) + + if (result !== false && !this.preventDefault) this.emitChange() + } + + if (model.reduce) { + handleDispatch(() => { + model.setState(model.reduce( + alt, + this[Sym.STATE_CONTAINER], + payload.data + )) + }, payload) + + if (!this.preventDefault) this.emitChange() } this[Sym.LIFECYCLE].emit( @@ -64,10 +93,6 @@ class AltStore { return this[EE] } - emitChange() { - this[EE].emit('change', this[Sym.STATE_CONTAINER]) - } - listen(cb) { this[EE].on('change', cb) return () => this.unlisten(cb) diff --git a/src/alt/store/index.js b/src/alt/store/index.js index c731d4c8..d4048ee4 100644 --- a/src/alt/store/index.js +++ b/src/alt/store/index.js @@ -37,7 +37,10 @@ function createPrototype(proto, alt, key, extras) { return fn.assign(proto, StoreMixin, { _storeName: key, alt: alt, - dispatcher: alt.dispatcher + dispatcher: alt.dispatcher, + preventDefault() { + this.getInstance().preventDefault = true + } }, extras) } @@ -74,6 +77,13 @@ export function createStoreFromObject(alt, StoreModel, key) { StoreProto.bindListeners ) } + /* istanbul ignore else */ + if (StoreProto.observe) { + StoreMixin.bindListeners.call( + StoreProto, + StoreProto.observe(alt) + ) + } // bind the lifecycle events /* istanbul ignore else */ @@ -117,13 +127,8 @@ export function createStoreFromClass(alt, StoreModel, key, ...argsForClass) { const store = new Store(...argsForClass) - if (config.bindListeners) { - store.bindListeners(config.bindListeners) - } - - if (config.datasource) { - store.exportAsync(config.datasource) - } + if (config.bindListeners) store.bindListeners(config.bindListeners) + if (config.datasource) store.registerAsync(config.datasource) storeInstance = fn.assign( new AltStore( diff --git a/test/async-test.js b/test/async-test.js index 48663a13..8d1dab6b 100644 --- a/test/async-test.js +++ b/test/async-test.js @@ -74,7 +74,6 @@ const StargazerSource = { } @createStore(alt) -@datasource(StargazerSource) class StargazerStore { static config = { stateKey: 'state' @@ -89,6 +88,8 @@ class StargazerStore { isLoading: false } + this.registerAsync(StargazerSource) + this.bindListeners({ loading: StargazerActions.fetchingUsers, receivedUsers: StargazerActions.usersReceived, @@ -240,9 +241,12 @@ export default { 'as a function'() { const FauxSource = sinon.stub().returns({}) - @datasource(FauxSource) class FauxStore { static displayName = 'FauxStore' + + constructor() { + this.exportAsync(FauxSource) + } } const store = alt.createStore(FauxStore) @@ -261,12 +265,11 @@ export default { } } - @datasource(PojoSource) class MyStore { static displayName = 'MyStore' } - const store = alt.createStore(MyStore) + const store = alt.createStore(datasource(PojoSource)(MyStore)) assert.isFunction(store.justTesting) assert.isFunction(store.isLoading) diff --git a/test/es3-module-pattern.js b/test/es3-module-pattern.js index e7499fd0..38243908 100644 --- a/test/es3-module-pattern.js +++ b/test/es3-module-pattern.js @@ -45,7 +45,7 @@ export default { 'store method exists'() { const storePrototype = Object.getPrototypeOf(myStore) - const assertMethods = ['constructor', 'getEventEmitter', 'emitChange', 'listen', 'unlisten', 'getState'] + const assertMethods = ['constructor', 'getEventEmitter', 'listen', 'unlisten', 'getState'] assert.deepEqual(Object.getOwnPropertyNames(storePrototype), assertMethods, 'methods exist for store') assert.isUndefined(myStore.addListener, 'event emitter methods not present') assert.isUndefined(myStore.removeListener, 'event emitter methods not present') diff --git a/test/functional-test.js b/test/functional-test.js new file mode 100644 index 00000000..bad3d29f --- /dev/null +++ b/test/functional-test.js @@ -0,0 +1,147 @@ +import { assert } from 'chai' +import Alt from '../dist/alt-with-runtime' +import sinon from 'sinon' + +export default { + 'functional goodies for alt': { + 'observing for changes in a POJO so we get context passed in'() { + const alt = new Alt() + + const observe = sinon.stub().returns({}) + const displayName = 'store' + + alt.createStore({ displayName, observe }) + + assert.ok(observe.calledOnce) + assert(observe.args[0][0] === alt, 'first arg is alt') + }, + + 'when observing changes, they are observed'() { + const alt = new Alt() + const actions = alt.generateActions('fire') + + const displayName = 'store' + + const store = alt.createStore({ + displayName, + observe() { + return { fire: actions.fire } + }, + fire() { } + }) + + assert(store.boundListeners.length === 1, 'there is 1 action bound') + }, + + 'otherwise works like a haskell guard'() { + const alt = new Alt() + const actions = alt.generateActions('fire', 'test') + + const spy = sinon.spy() + + const store = alt.createStore({ + displayName: 'store', + state: { x: 0 }, + bindListeners: { + fire: actions.fire + }, + + fire() { + this.setState({ x: 1 }) + }, + + otherwise() { + this.setState({ x: 2 }) + } + }) + + const kill = store.listen(spy) + + actions.test() + assert(store.getState().x === 2, 'the otherwise clause was ran') + + actions.fire() + assert(store.getState().x === 1, 'just fire was ran') + + assert.ok(spy.calledTwice) + + kill() + }, + + 'preventDefault prevents a change event to be emitted'() { + const alt = new Alt() + const actions = alt.generateActions('fire') + + const spy = sinon.spy() + + const store = alt.createStore({ + displayName: 'store', + state: { x: 0 }, + bindListeners: { + fire: actions.fire + }, + + fire() { + this.setState({ x: 1 }) + this.preventDefault() + } + }) + + const kill = store.listen(spy) + + actions.fire() + assert(store.getState().x === 1, 'just fire was ran') + + assert(spy.callCount === 0, 'store listener was never called') + + kill() + }, + + 'reduce fires on every dispatch if defined'() { + const alt = new Alt() + const actions = alt.generateActions('fire') + + const store = alt.createStore({ + displayName: 'store', + + state: { x: 0 }, + + reduce(alt, state) { + return { x: state.x + 1 } + } + }) + + actions.fire() + actions.fire() + actions.fire() + + assert(store.getState().x === 3, 'counter was incremented') + }, + + 'reduce doesnt emit if preventDefault'() { + const alt = new Alt() + const actions = alt.generateActions('fire') + + const store = alt.createStore({ + displayName: 'store', + + state: { x: 0 }, + + reduce(alt, state) { + this.preventDefault() + return {} + } + }) + + const spy = sinon.spy() + + const unsub = store.listen(spy) + + actions.fire() + + assert(spy.callCount === 0) + + unsub() + }, + } +} diff --git a/test/index.js b/test/index.js index 54324730..e8362c54 100644 --- a/test/index.js +++ b/test/index.js @@ -389,7 +389,7 @@ const tests = { 'store methods'() { const storePrototype = Object.getPrototypeOf(myStore) - const assertMethods = ['constructor', 'getEventEmitter', 'emitChange', 'listen', 'unlisten', 'getState'] + const assertMethods = ['constructor', 'getEventEmitter', 'listen', 'unlisten', 'getState'] assert.deepEqual(Object.getOwnPropertyNames(storePrototype), assertMethods, 'methods exist for store') assert.isUndefined(myStore.addListener, 'event emitter methods not present') assert.isUndefined(myStore.removeListener, 'event emitter methods not present')