Skip to content

Commit

Permalink
Merge pull request #279 from goatslacker/microflux-ideas
Browse files Browse the repository at this point in the history
Add FP goodies baked into alt
  • Loading branch information
goatslacker committed Jun 2, 2015
2 parents 3811915 + 1635589 commit a68c57d
Show file tree
Hide file tree
Showing 6 changed files with 218 additions and 38 deletions.
73 changes: 49 additions & 24 deletions src/alt/store/AltStore.js
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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)
Expand Down
21 changes: 13 additions & 8 deletions src/alt/store/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down Expand Up @@ -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 */
Expand Down Expand Up @@ -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(
Expand Down
11 changes: 7 additions & 4 deletions test/async-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ const StargazerSource = {
}

@createStore(alt)
@datasource(StargazerSource)
class StargazerStore {
static config = {
stateKey: 'state'
Expand All @@ -89,6 +88,8 @@ class StargazerStore {
isLoading: false
}

this.registerAsync(StargazerSource)

this.bindListeners({
loading: StargazerActions.fetchingUsers,
receivedUsers: StargazerActions.usersReceived,
Expand Down Expand Up @@ -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)
Expand All @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion test/es3-module-pattern.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down
147 changes: 147 additions & 0 deletions test/functional-test.js
Original file line number Diff line number Diff line change
@@ -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()
},
}
}
2 changes: 1 addition & 1 deletion test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down

0 comments on commit a68c57d

Please sign in to comment.