Initial draft of array reducer
This commit is contained in:
parent
76c577ce6c
commit
0f7bd6efe4
|
@ -25,7 +25,6 @@ module.exports = {
|
|||
'react/prop-types': ['off'],
|
||||
'react/display-name': ['off'],
|
||||
'no-console': ['off'],
|
||||
//'arrow-parens': ['warn', 'as-needed'],
|
||||
},
|
||||
env: {
|
||||
browser: true,
|
||||
|
@ -33,12 +32,6 @@ module.exports = {
|
|||
es6: true,
|
||||
},
|
||||
globals: {
|
||||
_: true,
|
||||
_fmt: true,
|
||||
_misc: true,
|
||||
_arr: true,
|
||||
_cfg: true,
|
||||
Promise: true,
|
||||
RE_CHILD_INDEX_JS: true,
|
||||
}
|
||||
};
|
|
@ -1 +1,2 @@
|
|||
[ignore]
|
||||
[ignore]
|
||||
.*/__tests__/.*
|
|
@ -7,6 +7,7 @@ const exampleReducer = {
|
|||
form2: Types.reducer(Types.shape({
|
||||
lowerLevel: Types.number(5),
|
||||
lowerLevel2: Types.string('Blargle'),
|
||||
lowerLevelArray: Types.arrayOf(Types.string()),
|
||||
})),
|
||||
form3: Types.reducer({
|
||||
example2: Types.reducer(Types.shape({
|
||||
|
@ -23,6 +24,9 @@ const exampleReducer = {
|
|||
})
|
||||
})
|
||||
}),
|
||||
arrayTest: Types.reducer(Types.arrayOf(
|
||||
Types.number()
|
||||
))
|
||||
})
|
||||
};
|
||||
|
||||
|
@ -39,4 +43,8 @@ store.dispatch(test.actionsObject.example.form2.replace({ toast: 'nommyNom' }));
|
|||
store.dispatch(test.actionsObject.example.form2.reset());
|
||||
|
||||
console.log(111, test.selectorsObject.example.form2(store.getState()));
|
||||
console.log(222, test.actionsObject);
|
||||
|
||||
store.dispatch(test.actionsObject.example.arrayTest.replace([1,2,3]));
|
||||
store.dispatch(test.actionsObject.example.arrayTest.updateAtIndex(5, 0));
|
||||
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
//@flow
|
||||
import { Types } from '../structure';
|
||||
import { calculateDefaults } from '../reducers';
|
||||
|
||||
describe('reducers', () => {
|
||||
|
||||
describe('defaultValues', () => {
|
||||
it('Should provide correct default values for a given primitive type', () => {
|
||||
expect(calculateDefaults(Types.string('toast'))).toBe('toast');
|
||||
expect(calculateDefaults(Types.number(3))).toBe(3);
|
||||
expect(calculateDefaults(Types.string())).toBe('');
|
||||
expect(calculateDefaults(Types.number())).toBe(0);
|
||||
});
|
||||
|
||||
it('Should provide correct default values for an object', () => {
|
||||
const objectStructure = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number(),
|
||||
});
|
||||
expect(calculateDefaults(objectStructure)).toEqual({ test1: '', test2: 0 });
|
||||
});
|
||||
|
||||
it('Should provide correct default values for nested object', () => {
|
||||
const objectStructure = Types.shape({
|
||||
test1: Types.string(),
|
||||
test2: Types.number(),
|
||||
test3: Types.shape({
|
||||
test4: Types.string('foo'),
|
||||
})
|
||||
});
|
||||
expect(calculateDefaults(objectStructure)).toEqual({
|
||||
test1: '',
|
||||
test2: 0,
|
||||
test3: {
|
||||
test4: 'foo'
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
|
@ -3,7 +3,7 @@
|
|||
// Flow imports
|
||||
//==============================
|
||||
import type { StructureType, PrimitiveType } from './structure';
|
||||
import type { PartialReducer, Selectors } from './reducers';
|
||||
import type { PartialReducer } from './reducers';
|
||||
|
||||
import { combineReducers } from 'redux';
|
||||
import { reduce, find } from 'lodash';
|
||||
|
@ -14,7 +14,7 @@ export function buildReducers(name: string, structure: any, {
|
|||
baseSelector = state => state,
|
||||
locationString = '',
|
||||
}: {
|
||||
baseSelector: Selectors,
|
||||
baseSelector: any,
|
||||
locationString: string,
|
||||
} = {}): PartialReducer {
|
||||
|
||||
|
@ -48,10 +48,10 @@ export function buildReducers(name: string, structure: any, {
|
|||
let childReducer = containsReducers
|
||||
? buildReducers(propName, propStructure, {
|
||||
locationString: locationString ? `${locationString}.${propName}` : propName,
|
||||
baseSelector: state => baseSelector(state)[propName],
|
||||
baseSelector: (state: any) => baseSelector(state)[propName],
|
||||
})
|
||||
: createReducer(propValue, {
|
||||
locationString,
|
||||
locationString: `${locationString}.${propName}`,
|
||||
});
|
||||
|
||||
//As the object is built up, we want to assign the reducers/actions created
|
||||
|
|
|
@ -2,13 +2,6 @@
|
|||
//==============================
|
||||
// Flow imports
|
||||
//==============================
|
||||
import type {
|
||||
ObjectReducer,
|
||||
ObjectAction,
|
||||
ObjectSelector,
|
||||
ObjectReducerBehaviorsConfig,
|
||||
ObjectReducerBehaviors,
|
||||
} from './reducers/objectReducer';
|
||||
import type {
|
||||
StructureType,
|
||||
PrimitiveType,
|
||||
|
@ -17,16 +10,11 @@ import type {
|
|||
//==============================
|
||||
// Flow types
|
||||
//==============================
|
||||
export type Selectors = ObjectSelector;
|
||||
export type Actions = ObjectAction;
|
||||
export type Reducers = ObjectReducer;
|
||||
export type PartialReducer = {
|
||||
reducers: { [key: string]: Reducers},
|
||||
actionsObject: { [key: string]: Actions },
|
||||
selectorsObject?: { [key: string]: Selectors },
|
||||
reducers: { [key: string]: any },
|
||||
actionsObject: { [key: string]: any },
|
||||
selectorsObject?: { [key: string]: any },
|
||||
};
|
||||
type ReducerBehaviorsConfig = ObjectReducerBehaviorsConfig;
|
||||
type ReducerBehaviors = ObjectReducerBehaviors;
|
||||
|
||||
import {
|
||||
PROP_TYPES,
|
||||
|
@ -34,18 +22,24 @@ import {
|
|||
import { compose } from 'ramda';
|
||||
import { reduce } from 'lodash';
|
||||
import { createObjectReducer } from './reducers/objectReducer';
|
||||
import { createArrayReducer } from './reducers/arrayReducer';
|
||||
|
||||
|
||||
function determineReducerType(reducerDescriptor, {
|
||||
locationString,
|
||||
}) {
|
||||
const REDUCERS = {
|
||||
[PROP_TYPES._shape]: createObjectReducer,
|
||||
[PROP_TYPES._array]: createArrayReducer,
|
||||
[PROP_TYPES._boolean]: () => {},
|
||||
[PROP_TYPES._string]: () => {},
|
||||
[PROP_TYPES._number]: () => {},
|
||||
};
|
||||
const { structure } = reducerDescriptor();
|
||||
const { type } = structure();
|
||||
|
||||
let reducerFn = null;
|
||||
if (type === PROP_TYPES._shape) reducerFn = createObjectReducer;
|
||||
return {
|
||||
reducerFn,
|
||||
reducerFn: REDUCERS[type],
|
||||
reducerStructureDescriptor: structure,
|
||||
locationString,
|
||||
};
|
||||
|
@ -59,7 +53,7 @@ function callReducer({ reducerFn, reducerStructureDescriptor, locationString } =
|
|||
|
||||
export const createReducer = compose(callReducer, determineReducerType);
|
||||
|
||||
export function createReducerBehaviors(behaviorsConfig: ReducerBehaviorsConfig, locationString: string): ReducerBehaviors {
|
||||
export function createReducerBehaviors(behaviorsConfig: any, locationString: string): any {
|
||||
//Take a reducer behavior config object, and create the reducer behaviors using the location string
|
||||
return reduce(behaviorsConfig, (memo, behavior, name) => ({
|
||||
...memo,
|
||||
|
@ -68,10 +62,13 @@ export function createReducerBehaviors(behaviorsConfig: ReducerBehaviorsConfig,
|
|||
}
|
||||
|
||||
export function calculateDefaults(typeDescription: StructureType | PrimitiveType) {
|
||||
const { type, structure = {}} = typeDescription;
|
||||
if ([PROP_TYPES.array, PROP_TYPES.shape].find(type))
|
||||
return reduce(reducerStructure, (memo, propValue, propName) => ({
|
||||
const { type, structure = {}, defaultValue = '' } = typeDescription();
|
||||
const complex = [PROP_TYPES._array, PROP_TYPES._shape].indexOf(type) > -1;
|
||||
|
||||
if (!complex) return defaultValue;
|
||||
|
||||
return reduce(structure, (memo, propValue, propName) => ({
|
||||
...memo,
|
||||
[propName]: propValue().defaultValue,
|
||||
[propName]: calculateDefaults(propValue),
|
||||
}), {});
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
//==============================
|
||||
// Flow imports
|
||||
//==============================
|
||||
import type { ShapeStructure, StructureType } from '../structure';
|
||||
import type { StructureType } from '../structure';
|
||||
|
||||
//==============================
|
||||
// Flow types
|
||||
|
@ -10,6 +10,7 @@ import type { ShapeStructure, StructureType } from '../structure';
|
|||
export type ArrayReducerAction = {
|
||||
type: string,
|
||||
payload: any,
|
||||
index: number,
|
||||
};
|
||||
export type ArrayReducer = (state: Array<any>, action: ArrayReducerAction) => Array<any>;
|
||||
export type ArrayReducerBehavior = (state: Array<any>, payload: any, initialState: Array<any>, index: number | void) => Array<any>;
|
||||
|
@ -35,13 +36,13 @@ export type ArraySelector = (state: Object) => Array<any>;
|
|||
//==============================
|
||||
// JS imports
|
||||
//==============================
|
||||
import { reduce, isArray, isObject, isNull } from 'lodash';
|
||||
import { validateArray } from '../validatePayload';
|
||||
import { reduce, isArray, isNumber, isObject } from 'lodash';
|
||||
//import { validateArray } from '../validatePayload';
|
||||
import { createReducerBehaviors } from '../reducers';
|
||||
import { updateAtIndex, removeAtIndex } from '../utils/arrayUtils';
|
||||
|
||||
function checkIndex(state: Object, payload: any, behaviorName: string): boolean {
|
||||
if (!isNumber(index)) {
|
||||
function checkIndex(index: ?number, payload: any, behaviorName: string): boolean {
|
||||
if (!isNumber(index) || index === -1) {
|
||||
throw new Error(`Index not passed to ${behaviorName} for payload ${payload}.`);
|
||||
}
|
||||
return true;
|
||||
|
@ -49,34 +50,34 @@ function checkIndex(state: Object, payload: any, behaviorName: string): boolean
|
|||
|
||||
const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = {
|
||||
updateAtIndex: {
|
||||
action(value) { return value },
|
||||
reducer(state, payload, initialState, index) {
|
||||
checkIndex(index, payload, 'updateOne');
|
||||
reducer(state, payload, initialState, index = -1) {
|
||||
checkIndex(index, payload, 'updateAtIndex');
|
||||
if (isArray(payload) || isObject(payload)) return updateAtIndex(state, { ...state[index], ...payload }, index);
|
||||
return updateAtIndex(state, payload, index);
|
||||
}
|
||||
},
|
||||
resetAtIndex: {
|
||||
reducer(state, payload, initialState, index) {
|
||||
checkIndex(index, payload, 'updateOne');
|
||||
checkIndex(index, payload, 'resetAtIndex');
|
||||
return updateAtIndex(state, initialState, index);
|
||||
}
|
||||
},
|
||||
removeAtIndex: {
|
||||
reducer(state, payload, initialState, index) {
|
||||
checkIndex(index, payload, 'updateOne');
|
||||
checkIndex(index, payload, 'removeAtIndex');
|
||||
return removeAtIndex(state, index);
|
||||
}
|
||||
},
|
||||
replaceAtIndex: {
|
||||
reducer(state, payload, initialState, index) {
|
||||
checkIndex(index, payload, 'updateOne');
|
||||
checkIndex(index, payload, 'replaceAtIndex');
|
||||
return updateAtIndex(state, payload, index);
|
||||
}
|
||||
},
|
||||
replace: {
|
||||
action(value) {
|
||||
if(!isArray(value)) throw new Error('An array must be provided when replacing an array');
|
||||
return value;
|
||||
},
|
||||
reducer(state, payload) {
|
||||
return payload;
|
||||
|
@ -89,8 +90,6 @@ const DEFAULT_ARRAY_BEHAVIORS: ArrayReducerBehaviorsConfig = {
|
|||
},
|
||||
};
|
||||
|
||||
//TODO: All the array functionality!
|
||||
|
||||
export function createArrayReducer(reducerShape: StructureType, {
|
||||
locationString
|
||||
}: ArrayReducerOptions = {}) {
|
||||
|
@ -100,30 +99,25 @@ export function createArrayReducer(reducerShape: StructureType, {
|
|||
};
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
function createReducer(arrayTypeDescription: StructureType, behaviors: ArrayReducerBehaviors): ArrayReducer {
|
||||
const initialState = calculateDefaults(arrayTypeDescription);
|
||||
return (state = initialState, { type, payload }: ArrayReducerAction) => {
|
||||
return (state: Array<any> = [], { type, payload, index }: ArrayReducerAction) => {
|
||||
//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, validateArray(arrayTypeDescription, payload), initialState);
|
||||
return behaviors[type](state, payload, [], index);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function createActions(behaviorsConfig: ArrayReducerBehaviorsConfig, locationString: string, defaultPayload: any): ArrayActions {
|
||||
function createActions(behaviorsConfig: ArrayReducerBehaviorsConfig, locationString: string): ArrayActions {
|
||||
//Take a reducer behavior config object, and create actions using the location string
|
||||
return reduce(behaviorsConfig, (memo, behavior, name) => ({
|
||||
...memo,
|
||||
[name]: (value: Object) => ({
|
||||
[name]: (value: Array<any>, index: ?number) => ({
|
||||
type: `${locationString}.${name}`,
|
||||
payload: (behavior.action || (() => defaultPayload))(value) || {}
|
||||
payload: (behavior.action || (() => value))(value) || [],
|
||||
index,
|
||||
})
|
||||
}), {});
|
||||
}
|
||||
|
|
|
@ -8,7 +8,8 @@ export type ShapeStructure = {
|
|||
}
|
||||
export type StructureType = () => {
|
||||
type: string,
|
||||
structure: ShapeStructure | StructureType | PrimitiveType
|
||||
structure: ShapeStructure | StructureType | PrimitiveType,
|
||||
defaultValue?: any,
|
||||
};
|
||||
export type PrimitiveType = () => {
|
||||
type: $Keys<typeof PROP_TYPES>,
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
//@flow
|
||||
export function updateAtIndex(array: Array<any>, value: any, index: number): Array<any> {
|
||||
export function updateAtIndex(array: Array<any>, value: any, index: ?number): Array<any> {
|
||||
if (index === undefined || index === null) throw new Error('Must provide an index to updateAtIndex');
|
||||
return [
|
||||
...array.slice(0,index),
|
||||
value,
|
||||
|
@ -7,7 +8,8 @@ export function updateAtIndex(array: Array<any>, value: any, index: number): Arr
|
|||
];
|
||||
}
|
||||
|
||||
export function removeAtIndex(array: Array<any>, index: number): Array<any> {
|
||||
export function removeAtIndex(array: Array<any>, index: ?number): Array<any> {
|
||||
if (index === undefined || index === null) throw new Error('Must provide an index to removeAtIndex');
|
||||
return [
|
||||
...array.slice(0, index),
|
||||
...array.slice(index + 1),
|
||||
|
|
|
@ -52,6 +52,7 @@ export function validatePrimitive(primitive: any, value: mixed): mixed {
|
|||
export function validateArray(arrayStructure: any, value: Array<mixed>): Array<mixed> {
|
||||
//Validate arrays by performing either of the other validation types to each element of the array,
|
||||
//based on the provided reducer structure.
|
||||
if (!Array.isArray(value)) return [];
|
||||
const elementStructure = arrayStructure().structure;
|
||||
const elementType = elementStructure().type;
|
||||
return value.map(element => getTypeValidation(elementType)(elementStructure, element));
|
||||
|
|
Loading…
Reference in New Issue