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