flux-commons-store
Advanced tools
Comparing version 0.0.6 to 0.0.7
@@ -11,5 +11,4 @@ jest.autoMockOff() | ||
var dispatcher = new Dispatcher(); | ||
it('registers with the dispatcher', () => { | ||
var dispatcher = new Dispatcher(); | ||
var store = new Store(dispatcher); | ||
@@ -22,22 +21,80 @@ expect(dispatcher.register).toBeCalled(); | ||
describe('with a real dispatcher', () => { | ||
var dispatcher = new ActualDispatcher(); | ||
var store = new Store(dispatcher); | ||
var action = {key: 'action1'}; | ||
var handler = jest.genMockFn(); | ||
var dispatcher, store, handler; | ||
it('gets a valid token', () => { | ||
beforeEach(() => { | ||
dispatcher = new ActualDispatcher(); | ||
store = new Store(dispatcher); | ||
handler = jest.genMockFn(); | ||
}); | ||
it('#constructor sets a valid token', () => { | ||
store = new Store(dispatcher); | ||
expect(store.dispatcherToken()).toBeDefined(); | ||
}); | ||
it('matches an action', () => { | ||
it('#listenToAction matches an action using strings', () => { | ||
var action = 'Action 1'; | ||
store.listenToAction(action, handler); | ||
dispatcher.dispatch(action); | ||
expect(handler.mock.calls[0][0]).toBe('Action 1'); | ||
}); | ||
it('#listenToAction does not match different string actions', () => { | ||
store.listenToAction('action 1', handler); | ||
dispatcher.dispatch('action 2'); | ||
expect(handler).not.toBeCalled(); | ||
}); | ||
it('#listenToAction matches object actions by reference', () => { | ||
var action = {}; | ||
var payload = {action: action}; | ||
store.listenToAction(action, handler); | ||
dispatcher.dispatch(payload); | ||
expect(handler.mock.calls[0][0]).toBe(payload); | ||
}); | ||
expect(handler).toBeCalledWith(payload); | ||
it('#listenToAction does not match different object references', () => { | ||
var action1 = {key: 'value'}; | ||
var action2 = {key: 'value'}; | ||
store.listenToAction(action1, handler); | ||
var payload = {action: action2}; | ||
dispatcher.dispatch(payload); | ||
expect(handler).not.toBeCalled(); | ||
}); | ||
it('matches an action with a custom matcher', () => { | ||
it('#listenToAction doesnt match when the payload.action is not set', () => { | ||
var action = {}; | ||
var payload = {actionType: action}; | ||
store.listenToAction(action, handler); | ||
dispatcher.dispatch(payload); | ||
expect(handler).not.toBeCalled(); | ||
}); | ||
it('#listenTo matches using a custom key in the payload', () => { | ||
var actionType = {}; | ||
var payload = {actionType: actionType}; | ||
store.listenTo('actionType', actionType, handler); | ||
dispatcher.dispatch(payload); | ||
expect(handler.mock.calls[0][0]).toBe(payload); | ||
}); | ||
it('#listenToMatchingAction matches an action with a custom matcher', () => { | ||
var payload = {action: {someTag: 'tag'}}; | ||
var matcher = (action, params) => action.someTag === 'tag'; | ||
var matcher = (payload) => payload.action.someTag === 'tag'; | ||
store.listenToMatchingAction(matcher, handler); | ||
@@ -47,10 +104,32 @@ | ||
expect(handler).toBeCalledWith(payload); | ||
expect(handler.mock.calls[0][0]).toBe(payload); | ||
}); | ||
it('fails when trying to listen to an undefined action', () => { | ||
it('#listenToAction emits a change event when matching an action', () => { | ||
var changeListener = jest.genMockFn(); | ||
store.listenToAction('action', handler); | ||
store.addChangeListener(changeListener); | ||
dispatcher.dispatch('action'); | ||
expect(changeListener).toBeCalled(); | ||
}) | ||
it('#handler does not emit a change event when setSilent is called', () => { | ||
var changeListener = jest.genMockFn(); | ||
store.addChangeListener(changeListener); | ||
handler = (payload, setSilent) => setSilent(); | ||
store.listenToAction('action', handler); | ||
dispatcher.dispatch('action'); | ||
expect(changeListener).not.toBeCalled(); | ||
}) | ||
it('#listenToAction fails when trying to listen to an undefined action', () => { | ||
expect(() => store.listenToAction(null, {})).toThrow(); | ||
}); | ||
it('fails when trying to listen to an action with a undefined handler', () => { | ||
it('#listenToAction fails when trying to listen to an action with a undefined handler', () => { | ||
expect(() => store.listenToAction({}, null)).toThrow(); | ||
@@ -57,0 +136,0 @@ }); |
107
lib/store.js
@@ -5,5 +5,12 @@ var EventEmitter = require('events').EventEmitter; | ||
// Base class for Flux Stores. | ||
// Provide out of the box auto-registering with Flux Dispatchers and | ||
// a simple API to register actions/handlers. | ||
class Store extends EventEmitter { | ||
constructor(dispatcher) { | ||
if (dispatcher && !dispatcher.register) { | ||
throw new Error('The dispatcher provided does not have a register method'); | ||
} | ||
if (dispatcher && dispatcher.register) { | ||
@@ -13,15 +20,45 @@ this._dispatcherToken = dispatcher.register(makePayloadHandler().bind(this)); | ||
this._actions = []; | ||
this._actionListeners = []; | ||
} | ||
listenToAction (action, handler, deferExecution) { | ||
var action = {action, handler, deferExecution}; | ||
validateActions([action]); | ||
this._actions.push(action); | ||
// Register a listener that will be used to analize each action dispatched | ||
// by asking for the 'action' property inside the payload and comparing it | ||
// with the action provided here. If the payload itself is the action it will | ||
// also match properly. | ||
// | ||
// See listenTo for docs on the other arguments. | ||
// | ||
listenToAction(action, handler, deferExecution) { | ||
this.listenTo('action', action, handler, deferExecution); | ||
} | ||
// Register a listener and specify where the 'action' lives in the payload. | ||
// i.e: if you dispatch a payload that has the action inside a property named | ||
// 'actionType' then you should register the listener here by | ||
// setting the 'actionKey' to 'actionType'. | ||
// | ||
// - deferExecution: if true the emit change on the store will be emitted | ||
// on the next tick (do not use this if you are not sure of why you need it) | ||
// | ||
// - handler: the handler that will be called when a listener matches the | ||
// dispatched action. The handler receives the payload as first argument | ||
// and a function setSilent. Call the setSilent fn if you want to avoid | ||
// this particular handler from emitting a change on the store. | ||
// | ||
listenTo(actionKey, action, handler, deferExecution) { | ||
var actionListener = {actionKey, action, handler, deferExecution}; | ||
validateActionListener(actionListener); | ||
this._actionListeners.push(actionListener); | ||
} | ||
// Register a listener by providing a custom matcher. | ||
// The matcher must be a function that will be called with the 'payload' as | ||
// the main argument, a match will be identified when the matcher returns true. | ||
listenToMatchingAction (matcher, handler, deferExecution) { | ||
this._actions.push({matcher, handler, deferExecution}); | ||
this._actionListeners.push({matcher, handler, deferExecution}); | ||
} | ||
// The store will automatically emit a change event and call the registered | ||
// callbacks every time a handler from the list of actionListeners gets | ||
// executed (unless the handler calls the setSilent helper). | ||
emitChange() { | ||
@@ -45,14 +82,16 @@ this.emit(CHANGE_EVENT); | ||
// Show a developer warning if the action does not have a descriptor or handler. | ||
function validateActions(actions) { | ||
actions.forEach(function(action) { | ||
if (!action.matcher && (!action.action || !action.handler)) { | ||
throw new Error('The action: [' + action.name + ']' + | ||
' is not valid, either lacks the action to match or a handler.'); | ||
} | ||
}); | ||
// Show a very visible error for the developer if the listener is malformed. | ||
function validateActionListener(action) { | ||
var invalid = !action.action || !action.handler; | ||
if (invalid) { | ||
throw new Error(`The action listener provided is not valid, the action | ||
or the handler are missing.`); | ||
} | ||
} | ||
// Factory for building the main function requried by | ||
// the Flux/Dispatcher from facebook. | ||
function makePayloadHandler() { | ||
function makePayloadHandler() { | ||
return function(payload) { | ||
@@ -62,32 +101,38 @@ | ||
// use the last from the list. | ||
var matchedAction; | ||
var matchedActionListener; | ||
this._actions.forEach(action => { | ||
// Analyze the payload against an actionListener | ||
// Either by strict equality or a custom matcher. | ||
function isAMatch(actionListener, payload) { | ||
var matcher = actionListener.matcher; | ||
if (action.matcher) { | ||
if (action.matcher(payload.action, payload.params)) { | ||
matchedAction = action; | ||
} | ||
if (matcher && matcher(payload)) { | ||
return true; | ||
} else { | ||
if (action.action && (action.action === payload.action)) { | ||
matchedAction = action; | ||
} | ||
return actionListener.action === payload || | ||
actionListener.action === payload[actionListener.actionKey]; | ||
} | ||
} | ||
this._actionListeners.forEach(actionListener => { | ||
if ( isAMatch(actionListener, payload) ) matchedActionListener = actionListener; | ||
}); | ||
// Execute the action that matched (the last one if multiple matches found) | ||
if (matchedAction) { | ||
if (!matchedAction.handler) { | ||
throw new Error('Action provided without a handler'); | ||
// Execute the handler associated to the action that matched | ||
if (matchedActionListener) { | ||
if (!matchedActionListener.handler) { | ||
throw new Error('Handler not found on the action listener provided.'); | ||
} | ||
matchedAction.handler(payload); | ||
var silent = false; | ||
var setSilent = () => silent = true; | ||
matchedActionListener.handler(payload, setSilent); | ||
if (matchedAction.deferExecution) { | ||
if (matchedActionListener.deferExecution && !silent) { | ||
setTimeout(function() { | ||
this.emitChange(); | ||
}.bind(this), 0); | ||
} else { | ||
} else if (!silent) { | ||
this.emitChange(); | ||
@@ -94,0 +139,0 @@ } |
{ | ||
"name": "flux-commons-store", | ||
"version": "0.0.6", | ||
"version": "0.0.7", | ||
"description": "Base Store class to use with Flux/Dispatcher", | ||
@@ -5,0 +5,0 @@ "main": "dist/store.js", |
@@ -29,3 +29,3 @@ # Flux Commons Store | ||
// Generic matchers | ||
var unauthorizedMatcher = (action, params) => params.response.status === 401; | ||
var unauthorizedMatcher = (payload) => payload.params.response.status === 401; | ||
myStore.listenToMatchingAction(unauthorizedMatcher, handleUnauthorized); | ||
@@ -40,23 +40,29 @@ ``` | ||
* Matching actions. The Store will automatically register itself with the Dispatcher provided. In order for the Store to know how to match actions, it will expect that the dispatched 'Payloads' have at least a property named `action` that will contain the `action` that we want to match (being an obj or a literal). On the other hand if you want to implement custom matchers based on the params of the payload then the Store will expect a property `params` inside the payload. | ||
* Actions. Actions can be anything, the store provides a default impl where it checks for a property 'action' inside the payload to perform the | ||
matching, by doing strict equality. However you can just send a 'String' to the dispatch and it will work too. | ||
## Store API | ||
### `.listenToAction(action, handler)` | ||
(Check the store.js docs for full explanation and complete API) | ||
`.listenToAction(action, handler)` | ||
* `action` any js objcet that will be compared with the `payload.action` object by strict equality. | ||
* `handler` the function to execute when there is a 'match', `handler(payload)` | ||
* `handler` the function to execute when there is a 'match', `handler(payload, setSilent)` | ||
### `.listenToMatchingAction(matcher, handler)` | ||
* `matcher` a function on the way `matcher(action, params)`. The store will check each dispatched payload with this matcher and execute the handler only when the matcher returns true. The `action` and `params` will be read from the payload. | ||
`.listenToMatchingAction(matcher, handler)` | ||
* `matcher` a function on the way `matcher(payload)`. The store will check each dispatched payload with this matcher and execute the handler only when the matcher returns true. | ||
### `.dispatcherToken()` | ||
`.dispatcherToken()` | ||
* returns the token created by the Dispatcher when registering the callback that listens to all the actions flowing through it. | ||
### `.addChangeListener(cb) .removeChangeListener(cb) .emitChange()` | ||
`.addChangeListener(cb) .removeChangeListener(cb) .emitChange()` | ||
* Expose a event like interface to listen to any change event happening on the store. By default every time a handler is executed in the Store a change event will be emitted. | ||
## Gist with more code and examples | ||
https://gist.github.com/rafaelchiti/915c680b4713c459026d | ||
## More docs to come. | ||
For more details please check the tests and the `store.js` the code is very simple and self explanatory. | ||
For more details please check the tests and the `store.js` the code is very simple and self explanatory (hopefully). |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
17215
317
67