Initial draft of array behaviors

This commit is contained in:
Kai Moseley 2016-12-04 14:42:51 +00:00
parent 8c19355486
commit 065126c761
4 changed files with 177 additions and 16 deletions

View File

@ -2,14 +2,17 @@
//============================== //==============================
// Flow imports // Flow imports
//============================== //==============================
import type { ObjectReducer, ObjectAction, ObjectSelector } from './reducers/objectReducer'; import type {
ObjectReducer,
import { ObjectAction,
PROP_TYPES, ObjectSelector,
} from './structure'; ObjectReducerBehaviorsConfig,
import { compose } from 'ramda'; ObjectReducerBehaviors,
import { createObjectReducer } from './reducers/objectReducer'; } from './reducers/objectReducer';
//==============================
// Flow types
//==============================
export type Selectors = ObjectSelector; export type Selectors = ObjectSelector;
export type Actions = ObjectAction; export type Actions = ObjectAction;
export type Reducers = ObjectReducer; export type Reducers = ObjectReducer;
@ -18,6 +21,16 @@ export type PartialReducer = {
actionsObject: { [key: string]: Actions }, actionsObject: { [key: string]: Actions },
selectorsObject?: { [key: string]: Selectors }, selectorsObject?: { [key: string]: Selectors },
}; };
type ReducerBehaviorsConfig = ObjectReducerBehaviorsConfig;
type ReducerBehaviors = ObjectReducerBehaviors;
import {
PROP_TYPES,
} from './structure';
import { compose } from 'ramda';
import { reduce } from 'lodash';
import { createObjectReducer } from './reducers/objectReducer';
function determineReducerType(reducerDescriptor, { function determineReducerType(reducerDescriptor, {
locationString, locationString,
@ -41,3 +54,11 @@ 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 {
//Take a reducer behavior config object, and create the reducer behaviors using the location string
return reduce(behaviorsConfig, (memo, behavior, name) => ({
...memo,
[`${locationString}.${name}`]: behavior.reducer,
}), {});
}

View File

@ -1 +1,134 @@
//@flow
//==============================
// Flow imports
//==============================
import type { ShapeStructure, StructureType } from '../structure';
//==============================
// Flow types
//==============================
export type ArrayReducerAction = {
type: string,
payload: 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 ArrayReducerBehaviorsConfig = {
[key: string]: {
action?: (value: any) => any,
reducer: ArrayReducerBehavior,
}
};
export type ArrayReducerBehaviors = {
[key: string]: ArrayReducerBehavior,
};
export type ArrayAction = (value: Array<any>) => { type: string, payload: any, index?: number };
export type ArrayActions = {
[key: string]: Array<any>
};
export type ArrayReducerOptions = {
behaviorsConfig: ArrayReducerBehaviorsConfig,
locationString: string,
};
export type ArraySelector = (state: Object) => Array<any>;
//==============================
// JS imports
//==============================
import { reduce, isArray, isObject, isNull } 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)) {
throw new Error(`Index not passed to ${behaviorName} for payload ${payload}.`);
}
return true;
}
const DEFAULT_OBJECT_BEHAVIORS: ArrayReducerBehaviorsConfig = {
updateAtIndex: {
action(value) { return value },
reducer(state, payload, initialState, index) {
checkIndex(index, payload, 'updateOne');
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');
return updateAtIndex(state, initialState, index);
}
},
removeAtIndex: {
reducer(state, payload, initialState, index) {
checkIndex(index, payload, 'updateOne');
return removeAtIndex(state, index);
}
},
replaceAtIndex: {
reducer(state, payload, initialState, index) {
checkIndex(index, payload, 'updateOne');
return updateAtIndex(state, payload, index);
}
},
replace: {
action(value) {
if(!isArray(value)) throw new Error('An array must be provided when replacing an array');
},
reducer(state, payload) {
return payload;
}
},
reset: {
reducer(state, payload, initialState) {
return initialState;
}
},
};
//TODO: All the array functionality!
export function createArrayReducer(reducerShape: StructureType, {
locationString
}: ArrayReducerOptions = {}) {
return {
reducers: createReducer(reducerShape, createReducerBehaviors(DEFAULT_OBJECT_BEHAVIORS, locationString)),
actionsObject: createActions(DEFAULT_OBJECT_BEHAVIORS, locationString, {}),
};
}
function calculateDefaults(reducerStructure) {
return reduce(reducerStructure, (memo, propValue, propName) => ({
...memo,
[propName]: propValue().defaultValue,
}), {});
}
function createReducer(arrayStructure: StructureType, behaviors: ArrayReducerBehaviors): ArrayReducer {
const initialState = calculateDefaults(arrayStructure().structure);
return (state = initialState, { type, payload }: 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(arrayStructure, payload), initialState);
}
}
function createActions(behaviorsConfig: ArrayReducerBehaviorsConfig, locationString: string, defaultPayload: any): ArrayActions {
//Take a reducer behavior config object, and create actions using the location string
return reduce(behaviorsConfig, (memo, behavior, name) => ({
...memo,
[name]: (value: Object) => ({
type: `${locationString}.${name}`,
payload: (behavior.action || (() => defaultPayload))(value) || {}
})
}), {});
}

View File

@ -38,6 +38,7 @@ export type ObjectSelector = (state: Object) => Object;
//============================== //==============================
import { reduce } from 'lodash'; import { reduce } from 'lodash';
import { validateObject } from '../validatePayload'; import { validateObject } from '../validatePayload';
import { createReducerBehaviors } from '../reducers';
const DEFAULT_OBJECT_BEHAVIORS: ObjectReducerBehaviorsConfig = { const DEFAULT_OBJECT_BEHAVIORS: ObjectReducerBehaviorsConfig = {
update: { update: {
@ -102,12 +103,3 @@ function createActions(behaviorsConfig: ObjectReducerBehaviorsConfig, locationSt
} }
function createReducerBehaviors(behaviorsConfig: ObjectReducerBehaviorsConfig, locationString: string): ObjectReducerBehaviors {
//Take a reducer behavior config object, and create the reducer behaviors using the location string
return reduce(behaviorsConfig, (memo, behavior, name) => ({
...memo,
[`${locationString}.${name}`]: behavior.reducer,
}), {});
}

View File

@ -0,0 +1,15 @@
//@flow
export function updateAtIndex(array: Array<any>, value: any, index: number): Array<any> {
return [
...array.slice(0,index),
value,
...array.slice(index + 1),
];
}
export function removeAtIndex(array: Array<any>, index: number): Array<any> {
return [
...array.slice(0, index),
...array.slice(index + 1),
];
}