Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add FP goodies baked into alt #279

Merged
merged 1 commit into from
Jun 2, 2015
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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