diff --git a/src/index.js b/src/index.js index ed2b66f..be6a4df 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import { buildReducers } from './redux-arg/buildReducers'; -import { createStore, compose } from 'redux'; +import { createStore, compose, combineReducers } from 'redux'; import { Types } from './redux-arg/structure'; const exampleReducer = { @@ -30,26 +30,62 @@ const exampleReducer = { arrayTest: Types.reducer(Types.arrayOf( Types.number(), [1,3,4] - )) + )), + primitiveTest: Types.reducer(Types.number(4)), }) }; +const exampleReducer2 = { + screen: Types.reducer(Types.shape({ + handlerIsCBS: Types.boolean(true), + handlerIsENG: Types.boolean(true), + invoices: Types.arrayOf(Types.string()), + activeInvoice: Types.number(-1), + deleteInvoiceModalActive: Types.boolean(), + suppInvoiceModalActive: Types.boolean(), + paymentModalActive: Types.boolean(), + resetModalActive: Types.boolean(), + reinstateModalActive: Types.boolean(), + })), + delete: Types.reducer(Types.shape({ + invoiceSequence: Types.number(), + vehicleSequence: Types.number(), + deleteReason: Types.string(), + })), + supp: Types.reducer(Types.shape({ + invoiceSequence: Types.number(), + vehicleSequence: Types.number(), + mode: Types.string(), + amount: Types.string(), + })), +}; + const test = buildReducers('example', exampleReducer); +const test2 = buildReducers('invoices', exampleReducer2); const store = createStore( - test.reducers, + combineReducers({ + example: test.reducers, + invoices: test2.reducers, + }), compose(window.devToolsExtension ? window.devToolsExtension() : f => f) ); -store.dispatch(test.actionsObject.example.form2.update({ lowerLevel: 2, lowerLevel2: 'Rawrg', lowerLevelArray: [3, 'foo'] })); -store.dispatch(test.actionsObject.example.form2.reset()); -store.dispatch(test.actionsObject.example.form2.replace({ toast: 'nommyNom' })); -store.dispatch(test.actionsObject.example.form2.reset()); +store.dispatch(test.actions.example.form2.update({ lowerLevel: 2, lowerLevel2: 'Rawrg', lowerLevelArray: [3, 'foo'] })); +store.dispatch(test.actions.example.form2.reset()); +store.dispatch(test.actions.example.form2.replace({ toast: 'nommyNom' })); +store.dispatch(test.actions.example.form2.reset()); -console.log(111, test.selectorsObject.example.form2(store.getState())); -console.log(222, test.actionsObject); +console.log(111, test.selectors.example.form2(store.getState())); +console.log(222, test.actions); +console.log(333, test.selectors); +console.log(444, test.selectors.example.primitiveTest(store.getState())); +console.log(555, test2.selectors, test2.actions); +console.log(666, test2.selectors.screen(store.getState())); -store.dispatch(test.actionsObject.example.arrayTest.replace([1,2,3])); -store.dispatch(test.actionsObject.example.arrayTest.updateAtIndex(5, 0)); -store.dispatch(test.actionsObject.example.arrayTest.updateAtIndex('foo', 0)); +store.dispatch(test.actions.example.arrayTest.replace([1,2,3])); +store.dispatch(test.actions.example.arrayTest.updateAtIndex(5, 0)); +store.dispatch(test.actions.example.arrayTest.updateAtIndex('foo', 0)); +store.dispatch(test.actions.example.primitiveTest.update(5)); +store.dispatch(test.actions.example.primitiveTest.reset()); diff --git a/src/redux-arg/buildReducers.js b/src/redux-arg/buildReducers.js index 1aedb52..cd5bc3b 100644 --- a/src/redux-arg/buildReducers.js +++ b/src/redux-arg/buildReducers.js @@ -10,8 +10,9 @@ import { reduce, find } from 'lodash'; import { createReducer } from './reducers'; import { PROP_TYPES } from './structure'; + export function buildReducers(name: string, structure: any, { - baseSelector = state => state, + baseSelector = state => state[name], locationString = '', }: { baseSelector: any, @@ -24,8 +25,8 @@ export function buildReducers(name: string, structure: any, { //returned to the call site for use in the rest of the application. const temp = reduce(structure, processStructure, { reducers: {}, - actionsObject: {}, - selectorsObject: {}, + actions: {}, + selectors: {}, }); //The Redux 'combineReducers' helper function is used here to save a little bit of boilerplate. @@ -63,13 +64,13 @@ export function buildReducers(name: string, structure: any, { ...memo.reducers, [propName]: childReducer.reducers, }, - actionsObject: { - ...memo.actionsObject, - [propName]: childReducer.actionsObject, + actions: { + ...memo.actions, + [propName]: childReducer.actions, }, - selectorsObject: { - ...memo.selectorsObject, - [propName]: containsReducers ? childReducer.selectorsObject : state => baseSelector(state)[propName], + selectors: { + ...memo.selectors, + [propName]: containsReducers ? childReducer.selectors : state => baseSelector(state)[propName], }, }; } diff --git a/src/redux-arg/reducers.js b/src/redux-arg/reducers.js index f17d0b9..15032d8 100644 --- a/src/redux-arg/reducers.js +++ b/src/redux-arg/reducers.js @@ -12,10 +12,12 @@ import type { //============================== export type PartialReducer = { reducers: { [key: string]: any }, - actionsObject: { [key: string]: any }, - selectorsObject?: { [key: string]: any }, + actions: { [key: string]: any }, + selectors: { [key: string]: any }, }; +export type Selector = (state: Object) => any; + import { PROP_TYPES, } from './structure'; @@ -23,6 +25,7 @@ import { compose } from 'ramda'; import { reduce } from 'lodash'; import { createObjectReducer } from './reducers/objectReducer'; import { createArrayReducer } from './reducers/arrayReducer'; +import { createPrimitiveReducer } from './reducers/primitiveReducer'; function determineReducerType(reducerDescriptor, { @@ -31,9 +34,9 @@ function determineReducerType(reducerDescriptor, { const REDUCERS = { [PROP_TYPES._shape]: createObjectReducer, [PROP_TYPES._array]: createArrayReducer, - [PROP_TYPES._boolean]: () => {}, - [PROP_TYPES._string]: () => {}, - [PROP_TYPES._number]: () => {}, + [PROP_TYPES._boolean]: createPrimitiveReducer, + [PROP_TYPES._string]: createPrimitiveReducer, + [PROP_TYPES._number]: createPrimitiveReducer, }; const { structure } = reducerDescriptor(); const { type } = structure(); diff --git a/src/redux-arg/reducers/arrayReducer.js b/src/redux-arg/reducers/arrayReducer.js index 14801ad..0fd249c 100644 --- a/src/redux-arg/reducers/arrayReducer.js +++ b/src/redux-arg/reducers/arrayReducer.js @@ -2,7 +2,7 @@ //============================== // Flow imports //============================== -import type { StructureType, ArrayStructureType } from '../structure'; +import type { ArrayStructureType } from '../structure'; //============================== // Flow types @@ -100,7 +100,7 @@ export function createArrayReducer(arrayTypeDescription: ArrayStructureType, { }: ArrayReducerOptions = {}) { return { reducers: createReducer(arrayTypeDescription, createReducerBehaviors(DEFAULT_ARRAY_BEHAVIORS, locationString)), - actionsObject: createActions(DEFAULT_ARRAY_BEHAVIORS, locationString, {}), + actions: createActions(DEFAULT_ARRAY_BEHAVIORS, locationString, {}), }; } @@ -146,7 +146,7 @@ function createActions(behaviorsConfig: ArrayReducerBehaviorsConfig, locationStr ...memo, [name]: (value: Array, index: ?number) => ({ type: `${locationString}.${name}`, - payload: (behavior.action || (() => value))(value) || [], + payload: (behavior.action || (() => value))(value), index, }) }), {}); diff --git a/src/redux-arg/reducers/objectReducer.js b/src/redux-arg/reducers/objectReducer.js index 27b765d..d991086 100644 --- a/src/redux-arg/reducers/objectReducer.js +++ b/src/redux-arg/reducers/objectReducer.js @@ -2,7 +2,7 @@ //============================== // Flow imports //============================== -import type { ShapeStructure, StructureType } from '../structure'; +import type { StructureType } from '../structure'; //============================== // Flow types @@ -11,7 +11,6 @@ export type ObjectReducerAction = { type: string, payload: Object, }; -export type ObjectReducerFactory = (reducerStructure: ShapeStructure) => ObjectReducer; export type ObjectReducer = (state: Object, action: ObjectReducerAction) => Object; export type ObjectReducerBehavior = (state: Object, payload: Object | void, initialState: Object) => Object; export type ObjectReducerBehaviorsConfig = { @@ -31,7 +30,6 @@ export type ObjectReducerOptions = { behaviorsConfig: ObjectReducerBehaviorsConfig, locationString: string, }; -export type ObjectSelector = (state: Object) => Object; //============================== // JS imports @@ -66,7 +64,7 @@ export function createObjectReducer(reducerShape: StructureType, { }: ObjectReducerOptions = {}) { return { reducers: createReducer(reducerShape, createReducerBehaviors(DEFAULT_OBJECT_BEHAVIORS, locationString)), - actionsObject: createActions(DEFAULT_OBJECT_BEHAVIORS, locationString, {}), + actions: createActions(DEFAULT_OBJECT_BEHAVIORS, locationString, {}), }; } @@ -100,7 +98,7 @@ function createActions(behaviorsConfig: ObjectReducerBehaviorsConfig, locationSt ...memo, [name]: (value: Object) => ({ type: `${locationString}.${name}`, - payload: (behavior.action || (() => defaultPayload))(value) || {} + payload: (behavior.action || (() => defaultPayload))(value), }) }), {}); } diff --git a/src/redux-arg/reducers/primitiveReducer.js b/src/redux-arg/reducers/primitiveReducer.js index 8b13789..8535226 100644 --- a/src/redux-arg/reducers/primitiveReducer.js +++ b/src/redux-arg/reducers/primitiveReducer.js @@ -1 +1,92 @@ +//@flow +//============================== +// Flow imports +//============================== +import type { PrimitiveType } from '../structure'; + +//============================== +// Flow types +//============================== +export type PrimitiveReducerAction = { + type: string, + payload: mixed, +}; +export type PrimitiveReducer = (state: mixed, action: PrimitiveReducerAction) => mixed; +export type PrimitiveReducerBehavior = (state: mixed, payload: mixed | void, initialState: mixed) => mixed; +export type PrimitiveReducerBehaviorsConfig = { + [key: string]: { + action?: (value: mixed) => mixed, + reducer: PrimitiveReducerBehavior, + } +}; +export type PrimitiveReducerBehaviors = { + [key: string]: PrimitiveReducerBehavior, +}; +export type PrimitiveAction = (value: mixed) => { type: string, payload: mixed }; +export type PrimitiveActions = { + [key: string]: PrimitiveAction +}; +export type PrimitiveReducerOptions = { + behaviorsConfig: PrimitiveReducerBehaviorsConfig, + locationString: string, +}; + +//============================== +// JS imports +//============================== +import { reduce } from 'lodash'; +import { validatePrimitive } from '../validatePayload'; +import { createReducerBehaviors } from '../reducers'; + +const DEFAULT_PRIMITIVE_BEHAVIORS: PrimitiveReducerBehaviorsConfig = { + update: { + action(value) { return value }, + reducer(state, payload) { + if (payload === undefined) return state; + return payload; + } + }, + reset: { + reducer(state, payload, initialState) { + return initialState; + } + }, +}; + + +export function createPrimitiveReducer(primitiveType: PrimitiveType, { + locationString +}: PrimitiveReducerOptions = {}) { + return { + reducers: createReducer(primitiveType, createReducerBehaviors(DEFAULT_PRIMITIVE_BEHAVIORS, locationString)), + actions: createActions(DEFAULT_PRIMITIVE_BEHAVIORS, locationString, {}), + }; +} + + +function createReducer(primitiveType: PrimitiveType, behaviors: PrimitiveReducerBehaviors): PrimitiveReducer { + const initialState: mixed = validatePrimitive(primitiveType, primitiveType().defaultValue); + return (state = initialState, { type, payload }: PrimitiveReducerAction) => { + //If the action type does not match any of the specified behaviors, just return the current state. + if (!behaviors[type]) return state; + + //Sanitize the payload using the reducer shape, then apply the sanitized + //payload to the state using the behavior linked to this action type. + return behaviors[type](state, validatePrimitive(primitiveType, payload), initialState); + } +} + + +function createActions(behaviorsConfig: PrimitiveReducerBehaviorsConfig, locationString: string, defaultPayload: any): PrimitiveActions { + //Take a reducer behavior config object, and create actions using the location string + return reduce(behaviorsConfig, (memo, behavior, name) => ({ + ...memo, + [name]: (value: mixed) => ({ + type: `${locationString}.${name}`, + payload: (behavior.action || (() => defaultPayload))(value), + }) + }), {}); +} + + diff --git a/src/redux-arg/structure.js b/src/redux-arg/structure.js index 45ae2b5..11654cb 100644 --- a/src/redux-arg/structure.js +++ b/src/redux-arg/structure.js @@ -4,7 +4,7 @@ // Flow types //============================== export type ShapeStructure = { - [key: string]: StructureType | PrimitiveType, + [key: string]: StructureType | PrimitiveType | ArrayStructureType, } export type StructureType = () => { type: string,