Unverified Commit e160c23f authored by Hannah Stepanek's avatar Hannah Stepanek Committed by GitHub

Merge pull request #700 from hypothesis/extract-create-store

Extract `createStore` helper out of `store` service
parents fbfc20e0 1edd8b07
'use strict';
const redux = require('redux');
// `.default` is needed because 'redux-thunk' is built as an ES2015 module
const thunk = require('redux-thunk').default;
const { createReducer, bindSelectors } = require('./util');
/**
* Create a Redux store from a set of _modules_.
*
* Each module defines the logic related to a particular piece of the application
* state, including:
*
* - The initial value of that state
* - The _actions_ that can change that state
* - The _selectors_ for reading that state or computing things
* from that state.
*
* On top of the standard Redux store methods, the returned store also exposes
* each action and selector from the input modules as a method which operates on
* the store.
*
* @param {Object[]} modules
* @param {any[]} initArgs - Arguments to pass to each state module's `init` function
* @param [any[]] middleware - List of additional Redux middlewares to use.
*/
function createStore(modules, initArgs, middleware = []) {
// Create the initial state and state update function.
const initialState = Object.assign({}, ...modules.map(m => m.init(...initArgs)));
const reducer = createReducer(...modules.map(m => m.update));
// Create the store.
const defaultMiddleware = [
// The `thunk` middleware handles actions which are functions.
// This is used to implement actions which have side effects or are
// asynchronous (see https://github.com/gaearon/redux-thunk#motivation)
thunk,
];
const enhancer = redux.applyMiddleware(...defaultMiddleware, ...middleware);
const store = redux.createStore(reducer, initialState, enhancer);
// Add actions and selectors as methods to the store.
const actions = Object.assign({}, ...modules.map(m => m.actions));
const boundActions = redux.bindActionCreators(actions, store.dispatch);
const selectors = Object.assign({}, ...modules.map(m => m.selectors));
const boundSelectors = bindSelectors(selectors, store.getState);
Object.assign(store, boundActions, boundSelectors);
return store;
}
module.exports = createStore;
......@@ -31,21 +31,15 @@
* 3. Checking that the UI correctly presents a given state.
*/
var redux = require('redux');
// `.default` is needed because 'redux-thunk' is built as an ES2015 module
var thunk = require('redux-thunk').default;
var modules = require('./modules');
var annotationsModule = require('./modules/annotations');
var framesModule = require('./modules/frames');
var linksModule = require('./modules/links');
var selectionModule = require('./modules/selection');
var sessionModule = require('./modules/session');
var viewerModule = require('./modules/viewer');
var createStore = require('./create-store');
var debugMiddleware = require('./debug-middleware');
var util = require('./util');
var annotations = require('./modules/annotations');
var frames = require('./modules/frames');
var links = require('./modules/links');
var selection= require('./modules/selection');
var session = require('./modules/session');
var viewer = require('./modules/viewer');
/**
* Redux middleware which triggers an Angular change-detection cycle
......@@ -73,55 +67,29 @@ function angularDigestMiddleware($rootScope) {
}
/**
* Create the Redux store for the application.
* Factory which creates the sidebar app's state store.
*
* Returns a Redux store augmented with methods for each action and selector in
* the individual state modules. ie. `store.actionName(args)` dispatches an
* action through the store and `store.selectorName(args)` invokes a selector
* passing the current state of the store.
*/
// @ngInject
function store($rootScope, settings) {
var enhancer = redux.applyMiddleware(
// The `thunk` middleware handles actions which are functions.
// This is used to implement actions which have side effects or are
// asynchronous (see https://github.com/gaearon/redux-thunk#motivation)
thunk,
var middleware = [
debugMiddleware,
angularDigestMiddleware.bind(null, $rootScope)
);
var store = redux.createStore(modules.update, modules.init(settings),
enhancer);
// Expose helper functions that create actions as methods of the
// `store` service to make using them easier from app code. eg.
//
// Instead of:
// store.dispatch(annotations.actions.addAnnotations(annotations))
// You can use:
// store.addAnnotations(annotations)
//
var actionCreators = redux.bindActionCreators(Object.assign({},
annotationsModule.actions,
framesModule.actions,
linksModule.actions,
selectionModule.actions,
sessionModule.actions,
viewerModule.actions
), store.dispatch);
// Expose selectors as methods of the `store` to make using them easier
// from app code.
//
// eg. Instead of:
// selection.isAnnotationSelected(store.getState(), id)
// You can use:
// store.isAnnotationSelected(id)
var selectors = util.bindSelectors(Object.assign({},
annotationsModule.selectors,
framesModule.selectors,
linksModule.selectors,
selectionModule.selectors,
sessionModule.selectors,
viewerModule.selectors
), store.getState);
angularDigestMiddleware.bind(null, $rootScope),
];
return Object.assign(store, actionCreators, selectors);
var modules = [
annotations,
frames,
links,
selection,
session,
viewer,
];
return createStore(modules, [settings], middleware);
}
module.exports = store;
'use strict';
/**
* This module defines the main update function (or 'reducer' in Redux's
* terminology) that handles app state updates. For an overview of how state
* management in Redux works, see the comments at the top of `store/index.js`
*
* Each sub-module in this folder defines:
*
* - An `init` function that returns the initial state relating to some aspect
* of the application
* - An `update` object mapping action types to a state update function for
* that action
* - A set of action creators - functions that return actions that can then
* be passed to `store.dispatch()`
* - A set of selectors - Utility functions that calculate some derived data
* from the state
*/
var annotations = require('./annotations');
var frames = require('./frames');
var links = require('./links');
var selection = require('./selection');
var session = require('./session');
var viewer = require('./viewer');
var util = require('../util');
function init(settings) {
return Object.assign(
{},
annotations.init(),
frames.init(),
links.init(),
selection.init(settings),
session.init(),
viewer.init()
);
}
var update = util.createReducer(...[
annotations.update,
frames.update,
links.update,
selection.update,
session.update,
viewer.update,
]);
module.exports = {
init: init,
update: update,
};
'use strict';
const createStore = require('../create-store');
const counterModule = {
init(value = 0) {
return { count: value };
},
update: {
['INCREMENT_COUNTER'](state, action) {
return { count: state.count + action.amount };
},
},
actions: {
increment(amount) {
return { type: 'INCREMENT_COUNTER', amount };
},
},
selectors: {
getCount(state) {
return state.count;
},
},
};
function counterStore(initArgs = [], middleware = []) {
return createStore([counterModule], initArgs, middleware);
}
describe('sidebar.store.create-store', () => {
it('returns a working Redux store', () => {
const store = counterStore();
store.dispatch(counterModule.actions.increment(5));
assert.equal(store.getState().count, 5);
});
it('notifies subscribers when state changes', () => {
const store = counterStore();
const subscriber = sinon.spy(() => assert.equal(store.getCount(), 1));
store.subscribe(subscriber);
store.increment(1);
assert.calledWith(subscriber);
});
it('passes initial state args to `init` function', () => {
const store = counterStore([21]);
assert.equal(store.getState().count, 21);
});
it('adds actions as methods to the store', () => {
const store = counterStore();
store.increment(5);
assert.equal(store.getState().count, 5);
});
it('adds selectors as methods to the store', () => {
const store = counterStore();
store.dispatch(counterModule.actions.increment(5));
assert.equal(store.getCount(), 5);
});
it('applies `thunk` middleware by default', () => {
const store = counterStore();
const doubleAction = (dispatch, getState) => {
dispatch(counterModule.actions.increment(getState().count));
};
store.increment(5);
store.dispatch(doubleAction);
assert.equal(store.getCount(), 10);
});
it('applies additional middleware', () => {
const actions = [];
const middleware = () => {
return next => {
return action => {
actions.push(action);
return next(action);
};
};
};
const store = counterStore([], [middleware]);
store.increment(5);
assert.deepEqual(actions, [{ type: 'INCREMENT_COUNTER', amount: 5 }]);
});
});
......@@ -289,15 +289,6 @@ describe('store', function () {
]);
});
describe('#subscribe()', function () {
it('notifies subscribers when the UI state changes', function () {
var listener = sinon.stub();
store.subscribe(listener);
store.addAnnotations([annotationFixtures.defaultAnnotation()]);
assert.called(listener);
});
});
describe('#setForceVisible()', function () {
it('sets the visibility of the annotation', function () {
store.setForceVisible('id1', true);
......@@ -520,29 +511,4 @@ describe('store', function () {
assert.equal(store.getState().annotations[0].$orphan, true);
});
});
describe('selector functions', function () {
// The individual state management modules in reducers/*.js define various
// 'selector' functions for extracting data from the app state. These are
// then re-exported on the store module.
it('re-exports selectors from reducers', function () {
var selectors = [
// Selection
'hasSelectedAnnotations',
'isAnnotationSelected',
// Annotations
'annotationExists',
'findIDsForTags',
'savedAnnotations',
// App Status
'isSidebar',
];
selectors.forEach(function (fnName) {
assert.equal(typeof store[fnName], 'function', fnName + ' was exported');
});
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment