diff --git a/README.md b/README.md index c2b56a4..7f4c403 100644 --- a/README.md +++ b/README.md @@ -5,13 +5,15 @@ [![Downloads][downloads-image]][downloads-url] Action dispatcher for unidirectional data flows. Creates tiny models of data -that can be accessed through actions with an API of only 5 functions +that can be accessed through actions through a small API. ## Usage ````js const barracks = require('barracks') -const store = barracks({ +const store = barracks() + +store.use({ onError: (err, state, createSend) => { console.error(`error: ${err}`) }, @@ -41,16 +43,21 @@ document.addEventListener('DOMContentLoaded', () => { ```` ## API -### store = barracks(handlers) -Initialize a new `barracks` instance. Takes an optional object of handlers. -Handlers can be: +### store = barracks(hooks?) +Initialize a new `barracks` instance. Takes an optional object of hooks which +is passed to `.use()`. + +### store.use(hooks) +Register new hooks on the store. Hooks are little plugins that can extend +behavior or perform actions at specific points in the lifecycle. The following +hooks are possible: - __onError(err, state, createSend):__ called when an `effect` or - `subscription` emit an error. If no handler is passed, the default handler - will `throw` on each error. -- __onAction(data, state, name, caller, createSend):__ called when an - `action` is fired. -- __onStateChange(data, state, prev, caller, createSend):__ called after a reducer - changes the `state`. + `subscription` emit an error. If no hook is passed, the default hook will + `throw` on each error. +- __onAction(data, state, name, caller, createSend):__ called when an `action` + is fired. +- __onStateChange(data, state, prev, caller, createSend):__ called after a + reducer changes the `state`. `createSend()` is a special function that allows the creation of a new named `send()` function. The first argument should be a string which is the name, the @@ -58,10 +65,10 @@ second argument is a boolean `callOnError` which can be set to `true` to call the `onError` hook istead of a provided callback. It then returns a `send(actionName, data?)` function. -Handlers should be used with care, as they're the most powerful interface into +Hooks should be used with care, as they're the most powerful interface into the state. For application level code it's generally recommended to delegate to actions inside models using the `send()` call, and only shape the actions -inside the handlers. +inside the hooks. ### store.model() Register a new model on the store. Models are optionally namespaced objects diff --git a/index.js b/index.js index b935119..800dd3a 100644 --- a/index.js +++ b/index.js @@ -2,21 +2,21 @@ const mutate = require('xtend/mutable') const assert = require('assert') const xtend = require('xtend') +const applyHook = require('./apply-hook') + module.exports = dispatcher // initialize a new barracks instance // obj -> obj -function dispatcher (handlers) { - handlers = handlers || {} - assert.equal(typeof handlers, 'object', 'barracks: handlers should be undefined or an object') +function dispatcher (hooks) { + hooks = hooks || {} + assert.equal(typeof hooks, 'object', 'barracks: hooks should be undefined or an object') - const onError = wrapOnError(handlers.onError || defaultOnError) - const onAction = handlers.onAction - const onStateChange = handlers.onStateChange + const onStateChangeHooks = [] + const onActionHooks = [] + const onErrorHooks = [] - assert.ok(!handlers.onError || typeof handlers.onError === 'function', 'barracks: onError should be undefined or a function') - assert.ok(!onAction || typeof onAction === 'function', 'barracks: onAction should be undefined or a function') - assert.ok(!onStateChange || typeof onStateChange === 'function', 'barracks: onStateChange should be undefined or a function') + useHooks(hooks) var reducersCalled = false var effectsCalled = false @@ -32,8 +32,22 @@ function dispatcher (handlers) { start.model = setModel start.state = getState start.start = start + start.use = useHooks return start + // push an object of hooks onto an array + // obj -> null + function useHooks (hooks) { + assert.equal(typeof hooks, 'object', 'barracks.use: hooks should be an object') + assert.ok(!hooks.onError || typeof hooks.onError === 'function', 'barracks.use: onError should be undefined or a function') + assert.ok(!hooks.onAction || typeof hooks.onAction === 'function', 'barracks.use: onAction should be undefined or a function') + assert.ok(!hooks.onStateChange || typeof hooks.onStateChange === 'function', 'barracks.use: onStateChange should be undefined or a function') + + if (hooks.onError) onErrorHooks.push(wrapOnError(hooks.onError)) + if (hooks.onAction) onActionHooks.push(hooks.onAction) + if (hooks.onStateChange) onStateChangeHooks.push(hooks.onStateChange) + } + // push a model to be initiated // obj -> null function setModel (model) { @@ -68,7 +82,7 @@ function dispatcher (handlers) { } } - // initialize the store handlers, get the send() function + // initialize the store hooks, get the send() function // obj? -> fn function start (opts) { opts = opts || {} @@ -87,7 +101,9 @@ function dispatcher (handlers) { apply(ns, model.effects, effects) } if (!subsCalled && model.subscriptions && opts.subscriptions !== false) { - apply(ns, model.subscriptions, subscriptions, createSend, onError) + apply(ns, model.subscriptions, subscriptions, createSend, function (err) { + applyHook(onErrorHooks, err) + }) } }) @@ -96,6 +112,8 @@ function dispatcher (handlers) { if (!opts.noEffects) effectsCalled = true if (!opts.noSubscriptions) subsCalled = true + if (!onErrorHooks.length) onErrorHooks.push(wrapOnError(defaultOnError)) + return createSend // call an action from a view @@ -120,7 +138,7 @@ function dispatcher (handlers) { function onErrorCallback (err) { err = err || null if (err) { - onError(err, _state, function createSend (selfName) { + applyHook(onErrorHooks, err, _state, function createSend (selfName) { return function send (name, data) { assert.equal(typeof name, 'string', 'barracks.store.start.send: name should be a string') data = (typeof data === 'undefined' ? null : data) @@ -144,7 +162,9 @@ function dispatcher (handlers) { var effectsCalled = false const newState = xtend(_state) - if (onAction) onAction(data, _state, name, caller, createSend) + if (onActionHooks.length) { + applyHook(onActionHooks, data, _state, name, caller, createSend) + } // validate if a namespace exists. Namespaces are delimited by ':'. var actionName = name @@ -163,8 +183,8 @@ function dispatcher (handlers) { mutate(newState, reducers[actionName](data, _state)) } reducersCalled = true - if (onStateChange) { - onStateChange(data, newState, _state, actionName, createSend) + if (onStateChangeHooks.length) { + applyHook(onStateChangeHooks, data, newState, _state, actionName, createSend) } _state = newState cb() diff --git a/test.js b/test.js index 20f2ffc..155a431 100644 --- a/test.js +++ b/test.js @@ -27,6 +27,44 @@ tape('api: store.model()', (t) => { }) }) +tape('api: store.use()', (t) => { + t.test('should call multiples', (t) => { + t.plan(1) + const store = barracks() + const called = { first: false, second: false } + + store.use({ + onAction: (data, state, name, caller, createSend) => { + called.first = true + } + }) + + store.use({ + onAction: (data, state, name, caller, createSend) => { + called.second = true + } + }) + + store.model({ + state: { + count: 0 + }, + reducers: { + foo: (data, state) => ({ count: state.count + 1 }) + } + }) + + const createSend = store.start() + const send = createSend('test', true) + send('foo', { count: 3 }) + + setTimeout(function () { + const expected = { first: true, second: true } + t.deepEqual(called, expected, 'all hooks were called') + }, 100) + }) +}) + tape('api: createSend = store.start(opts)', (t) => { t.test('should validate input types', (t) => { t.plan(3)