pass mutations in constructor as a map

This commit is contained in:
Yanick Champoux 2020-01-02 13:53:24 -05:00
parent 057226dfd1
commit e78568b3cb
8 changed files with 203 additions and 160 deletions

View File

@ -1,5 +1,8 @@
# Revision history for Updux # Revision history for Updux
NEXT
- Mutations passed to the constructor can be arrays of arrays.
1.2.0 2019-11-06 1.2.0 2019-11-06
- The middleware's 'getState' returns the local state of its updux, - The middleware's 'getState' returns the local state of its updux,
instead of the root state. Plus we add `getRootState` to get instead of the root state. Plus we add `getRootState` to get

View File

@ -1,23 +1,20 @@
import Updux, { actionCreator } from './updux'; import Updux, { actionCreator } from "./updux";
type MyState = { type MyState = {
sum: number sum: number;
} };
test( 'added mutation is present', () => { test("added mutation is present", () => {
const updux = new Updux<MyState>({ const updux = new Updux<MyState>({
initial: { sum: 0 }, initial: { sum: 0 }
}); });
const add = actionCreator('add', (n : number) => ({n}) ) const add = actionCreator("add", (n: number) => ({ n }));
// must add 'add' in the actions 9.9 updux.addMutation(add, ({ n }, action) => ({ sum }) => ({ sum: sum + n }));
updux.addMutation(
add, ({n},action) => ({sum}) => ({sum: sum + n})
);
updux.mutations;
const store = updux.createStore();
store.dispatch.add(3);
expect(store.getState()).toEqual({ sum: 3 }); const store = updux.createStore();
store.dispatch.add(3);
expect(store.getState()).toEqual({ sum: 3 });
}); });

View File

@ -1,59 +1,75 @@
import fp from 'lodash/fp'; import fp from "lodash/fp";
import { Action, ActionCreator, ActionPayloadGenerator, Dictionary } from '../types'; import {
Action,
ActionCreator,
ActionPayloadGenerator,
Dictionary
} from "../types";
export function actionCreator<T extends string,P extends any>( type: T, transform: (...args: any[]) => P ): ActionCreator<T,P> export function actionCreator<T extends string, P extends any>(
export function actionCreator<T extends string>( type: T, transform: never ): ActionCreator<T,undefined> type: T,
export function actionCreator<T extends string>( type: T, transform: null ): ActionCreator<T,null> transform: (...args: any[]) => P
export function actionCreator(type:any, transform:any ) { ): ActionCreator<T, P>;
export function actionCreator<T extends string>(
type: T,
transform: null
): ActionCreator<T, null>;
export function actionCreator<T extends string>(
type: T
): ActionCreator<T, undefined>;
export function actionCreator(type: any, transform?: any) {
if (transform) {
return Object.assign(
(...args: any[]) => ({ type, payload: transform(...args) }),
{ type }
);
}
if( transform ) { if (transform === null) {
return Object.assign( return Object.assign(() => ({ type }), { type });
(...args: any[]) => ({ type, payload: transform(...args) }), }
{ type } )
}
if( transform === null ) { return Object.assign((payload: unknown) => ({ type, payload }), { type });
return Object.assign( () => ({ type }), { type } )
}
return Object.assign( (payload: unknown) => ({type, payload}) );
} }
function actionFor(type:string): ActionCreator { export function actionFor(type: string): ActionCreator {
const f = ( (payload = undefined, meta = undefined) => const f = (payload = undefined, meta = undefined) =>
fp.pickBy(v => v !== undefined)({type, payload, meta}) as Action fp.pickBy(v => v !== undefined)({ type, payload, meta }) as Action;
);
return Object.assign(f, { return Object.assign(f, {
_genericAction: true, _genericAction: true,
type type
}); });
} }
type ActionPair = [ string, ActionCreator ]; type ActionPair = [string, ActionCreator];
function buildActions( function buildActions(
generators : Dictionary<ActionPayloadGenerator> = {}, generators: Dictionary<ActionPayloadGenerator> = {},
actionNames: string[] = [], actionNames: string[] = [],
subActions : ActionPair[] = [], subActions: ActionPair[] = []
):Dictionary<ActionCreator> { ): Dictionary<ActionCreator> {
// priority => generics => generic subs => craft subs => creators
// priority => generics => generic subs => craft subs => creators const [crafted, generic] = fp.partition(([type, f]) => !f._genericAction)(
subActions
);
const [ crafted, generic ] = fp.partition( const actions: any = [
([type,f]) => !f._genericAction ...actionNames.map(type => [type, actionFor(type)]),
)( subActions ); ...generic,
...crafted,
...Object.entries(
generators
).map(([type, payload]: [string, Function]): any => [
type,
(payload as any).type
? payload
: (...args: any) => ({ type, payload: payload(...args) })
])
];
const actions : any = [ return fp.fromPairs(actions);
...(actionNames.map( type => [ type, actionFor(type) ] )),
...generic,
...crafted,
...Object.entries(generators).map(
([type, payload]: [ string, Function ]): any => [type, (payload as any).type ? payload : (...args: any) => ({ type, payload: payload(...args) })]
),
];
return fp.fromPairs(actions);
} }
export default buildActions; export default buildActions;

View File

@ -1,72 +1,68 @@
import fp from 'lodash/fp'; import fp from "lodash/fp";
import u from 'updeep'; import u from "updeep";
import {Mutation, Action, Dictionary} from '../types'; import { Mutation, Action, Dictionary, MutationEntry } from "../types";
const composeMutations = (mutations: Mutation[]) => const composeMutations = (mutations: Mutation[]) =>
mutations.reduce((m1, m2) => (payload: any = null, action: Action) => state => mutations.reduce((m1, m2) => (payload: any = null, action: Action) => state =>
m2(payload, action)(m1(payload, action)(state)), m2(payload, action)(m1(payload, action)(state))
); );
type SubMutations = { type SubMutations = {
[ slice: string ]: Dictionary<Mutation> [slice: string]: Dictionary<Mutation>;
} };
function buildMutations( function buildMutations(
mutations :Dictionary<Mutation|([Mutation,boolean|undefined])> = {}, mutations: Dictionary<Mutation | [Mutation, boolean | undefined]> = {},
subduxes = {} subduxes = {}
) { ) {
// we have to differentiate the subduxes with '*' than those // we have to differentiate the subduxes with '*' than those
// without, as the root '*' is not the same as any sub-'*' // without, as the root '*' is not the same as any sub-'*'
const actions = fp.uniq( const actions = fp.uniq(
Object.keys(mutations).concat( Object.keys(mutations).concat(
...Object.values(subduxes).map(({mutations = {}}:any) => ...Object.values(subduxes).map(({ mutations = {} }: any) =>
Object.keys(mutations), Object.keys(mutations)
), )
), )
); );
let mergedMutations :Dictionary<Mutation[]> = {}; let mergedMutations: Dictionary<Mutation[]> = {};
let [globby, nonGlobby] = fp.partition( let [globby, nonGlobby] = fp.partition(
([_, {mutations = {}}]:any) => mutations['*'], ([_, { mutations = {} }]: any) => mutations["*"],
Object.entries(subduxes), Object.entries(subduxes)
); );
globby = fp.flow([ globby = fp.flow([
fp.fromPairs, fp.fromPairs,
fp.mapValues(({reducer}) => (_:any, action :Action) => ( state: any ) => fp.mapValues(({ reducer }) => (_: any, action: Action) => (state: any) =>
reducer(state, action), reducer(state, action)
), )
])(globby); ])(globby);
const globbyMutation = (payload:any, action:Action) => const globbyMutation = (payload: any, action: Action) =>
u(fp.mapValues((mut:any) => mut(payload, action))(globby)); u(fp.mapValues((mut: any) => mut(payload, action))(globby));
actions.forEach(action => { actions.forEach(action => {
mergedMutations[action] = [globbyMutation]; mergedMutations[action] = [globbyMutation];
}); });
nonGlobby.forEach(([slice, {mutations = {}, reducer = {}}]:any[]) => { nonGlobby.forEach(([slice, { mutations = {}, reducer = {} }]: any[]) => {
Object.entries(mutations).forEach(([type, mutation]) => { Object.entries(mutations).forEach(([type, mutation]) => {
const localized = (payload = null, action :Action) => { const localized = (payload = null, action: Action) => {
return u.updateIn(slice)((mutation as Mutation)(payload, action)); return u.updateIn(slice)((mutation as Mutation)(payload, action));
} };
mergedMutations[type].push(localized); mergedMutations[type].push(localized);
}); });
}); });
Object.entries(mutations).forEach(([type, mutation]) => { Object.entries(mutations).forEach(([type, mutation]) => {
if ( Array.isArray(mutation) ) { if (Array.isArray(mutation)) {
if( mutation[1] ) { if (mutation[1]) {
mergedMutations[type] = [ mergedMutations[type] = [mutation[0]];
mutation[0] } else mergedMutations[type].push(mutation[0]);
] } else mergedMutations[type].push(mutation);
}
else mergedMutations[type].push( mutation[0] );
}
else mergedMutations[type].push(mutation);
}); });
return fp.mapValues(composeMutations)(mergedMutations); return fp.mapValues(composeMutations)(mergedMutations);

View File

@ -1,8 +1,8 @@
import Updux from './updux'; import Updux from "./updux";
export { default as Updux } from './updux'; export { default as Updux } from "./updux";
export { export { UpduxConfig } from "./types";
UpduxConfig
} from './types'; export { actionCreator } from "./buildActions";
export default Updux; export default Updux;

25
src/mutations.test.ts Normal file
View File

@ -0,0 +1,25 @@
import Updux, { actionCreator } from "./updux";
describe("as array of arrays", () => {
const doIt = actionCreator("doIt");
const updux = new Updux({
initial: "",
mutations: [
[doIt, () => () => "bingo"],
["thisToo", () => () => "straight type"]
]
});
const store = updux.createStore();
test("doIt", () => {
store.dispatch.doIt();
expect(store.getState()).toEqual("bingo");
});
test("straight type", () => {
store.dispatch.thisToo();
expect(store.getState()).toEqual("straight type");
});
});

View File

@ -1,24 +1,30 @@
import {Dispatch, Middleware} from 'redux'; import { Dispatch, Middleware } from "redux";
type MaybePayload<P> = P extends object | string | boolean | number type MaybePayload<P> = P extends object | string | boolean | number
? { ? {
payload: P; payload: P;
} }
: {payload?: P}; : { payload?: P };
export type Action<T extends string = string, P = any> = { export type Action<T extends string = string, P = any> = {
type: T; type: T;
} & MaybePayload<P>; } & MaybePayload<P>;
export type Dictionary<T> = {[key: string]: T}; export type Dictionary<T> = { [key: string]: T };
export type Mutation<S = any, A extends Action = Action> = ( export type Mutation<S = any, A extends Action = Action> = (
payload: A['payload'], payload: A["payload"],
action: A, action: A
) => (state: S) => S; ) => (state: S) => S;
export type ActionPayloadGenerator = (...args: any[]) => any; export type ActionPayloadGenerator = (...args: any[]) => any;
export type MutationEntry = [
ActionCreator | string,
Mutation<any, Action<string, any>>,
boolean?
];
export type ActionCreator<T extends string = string, P = any> = { export type ActionCreator<T extends string = string, P = any> = {
type: T; type: T;
_genericAction?: boolean; _genericAction?: boolean;
@ -26,12 +32,11 @@ export type ActionCreator<T extends string = string, P = any> = {
export type UpduxDispatch = Dispatch & Dictionary<Function>; export type UpduxDispatch = Dispatch & Dictionary<Function>;
/** /**
* Configuration object given to Updux's constructor. * Configuration object given to Updux's constructor.
* @typeparam S Type of the Updux's state. Defaults to `any`. * @typeparam S Type of the Updux's state. Defaults to `any`.
*/ */
export type UpduxConfig<S=any> = { export type UpduxConfig<S = any> = {
/** /**
* The default initial state of the reducer. Can be anything your * The default initial state of the reducer. Can be anything your
* heart desires. * heart desires.
@ -155,7 +160,7 @@ export type UpduxConfig<S=any> = {
* } * }
* }); * });
*/ */
mutations?: any; mutations?: { [actionType: string]: Mutation<S> } | MutationEntry[];
groomMutations?: (m: Mutation<S>) => Mutation<S>; groomMutations?: (m: Mutation<S>) => Mutation<S>;
@ -184,10 +189,10 @@ export type UpduxConfig<S=any> = {
export type Upreducer<S = any> = (action: Action) => (state: S) => S; export type Upreducer<S = any> = (action: Action) => (state: S) => S;
export interface UpduxMiddlewareAPI<S> { export interface UpduxMiddlewareAPI<S> {
dispatch: UpduxDispatch, dispatch: UpduxDispatch;
getState(): any, getState(): any;
getRootState(): S getRootState(): S;
} }
export type UpduxMiddleware<S=any> = (api: UpduxMiddlewareAPI<S> ) => ( next: UpduxDispatch ) => ( action: Action ) => any; export type UpduxMiddleware<S = any> = (
api: UpduxMiddlewareAPI<S>
) => (next: UpduxDispatch) => (action: Action) => any;

View File

@ -1,14 +1,14 @@
import fp from 'lodash/fp'; import fp from "lodash/fp";
import u from 'updeep'; import u from "updeep";
import {observable, computed, toJS} from 'mobx'; import { observable, computed, toJS } from "mobx";
import buildActions from './buildActions'; import buildActions, { actionFor } from "./buildActions";
import buildInitial from './buildInitial'; import buildInitial from "./buildInitial";
import buildMutations from './buildMutations'; import buildMutations from "./buildMutations";
import buildCreateStore from './buildCreateStore'; import buildCreateStore from "./buildCreateStore";
import buildMiddleware from './buildMiddleware'; import buildMiddleware from "./buildMiddleware";
import buildUpreducer from './buildUpreducer'; import buildUpreducer from "./buildUpreducer";
import { import {
UpduxConfig, UpduxConfig,
Dictionary, Dictionary,
@ -17,29 +17,30 @@ import {
Mutation, Mutation,
Upreducer, Upreducer,
UpduxDispatch, UpduxDispatch,
UpduxMiddleware UpduxMiddleware,
} from './types'; MutationEntry
} from "./types";
import {Middleware, Store} from 'redux'; import { Middleware, Store } from "redux";
export {actionCreator} from './buildActions'; export { actionCreator } from "./buildActions";
type StoreWithDispatchActions< type StoreWithDispatchActions<
S = any, S = any,
Actions = {[action: string]: (...args: any) => Action} Actions = { [action: string]: (...args: any) => Action }
> = Store<S> & { > = Store<S> & {
dispatch: {[type in keyof Actions]: (...args: any) => void}; dispatch: { [type in keyof Actions]: (...args: any) => void };
}; };
export type Dux<S> = Pick< export type Dux<S> = Pick<
Updux<S>, Updux<S>,
| 'subduxes' | "subduxes"
| 'actions' | "actions"
| 'initial' | "initial"
| 'mutations' | "mutations"
| 'reducer' | "reducer"
| 'middleware' | "middleware"
| 'createStore' | "createStore"
| 'upreducer' | "upreducer"
>; >;
/** /**
@ -98,35 +99,36 @@ export class Updux<S = any> {
*/ */
groomMutations: (mutation: Mutation<S>) => Mutation<S>; groomMutations: (mutation: Mutation<S>) => Mutation<S>;
@observable private localEffects: Dictionary< @observable private localEffects: Dictionary<UpduxMiddleware<S>>;
UpduxMiddleware<S>
>;
@observable private localActions: Dictionary<ActionCreator>; @observable private localActions: Dictionary<ActionCreator>;
@observable private localMutations: Dictionary< @observable private localMutations: Dictionary<
Mutation<S> | [Mutation<S>, boolean | undefined] Mutation<S> | [Mutation<S>, boolean | undefined]
>; > = {};
constructor(config: UpduxConfig = {}) { constructor(config: UpduxConfig = {}) {
this.groomMutations = config.groomMutations || ((x: Mutation<S>) => x); this.groomMutations = config.groomMutations || ((x: Mutation<S>) => x);
this.subduxes = fp.mapValues((value: UpduxConfig | Updux) => this.subduxes = fp.mapValues((value: UpduxConfig | Updux) =>
fp.isPlainObject(value) ? new Updux(value) : value, fp.isPlainObject(value) ? new Updux(value) : value
)(fp.getOr({}, 'subduxes', config)) as Dictionary<Updux>; )(fp.getOr({}, "subduxes", config)) as Dictionary<Updux>;
this.localActions = fp.getOr({}, 'actions', config); this.localActions = fp.getOr({}, "actions", config);
this.localEffects = fp.getOr({}, 'effects', config); this.localEffects = fp.getOr({}, "effects", config);
this.initial = buildInitial<any>( this.initial = buildInitial<any>(
config.initial, config.initial,
fp.mapValues(({initial}) => initial)(this.subduxes), fp.mapValues(({ initial }) => initial)(this.subduxes)
); );
this.localMutations = fp.mapValues((m: Mutation<S>) => let mutations = fp.getOr([], "mutations", config);
this.groomMutations(m), if (!Array.isArray(mutations)) {
)(fp.getOr({}, 'mutations', config)); mutations = fp.toPairs(mutations);
}
mutations.forEach(args => (this.addMutation as any)(...args));
} }
/** /**
@ -138,11 +140,7 @@ export class Updux<S = any> {
* alongside `getState` to get the root state. * alongside `getState` to get the root state.
*/ */
@computed get middleware(): UpduxMiddleware<S> { @computed get middleware(): UpduxMiddleware<S> {
return buildMiddleware( return buildMiddleware(this.localEffects, this.actions, this.subduxes);
this.localEffects,
this.actions,
this.subduxes,
);
} }
/** /**
@ -164,10 +162,10 @@ export class Updux<S = any> {
this.localActions, this.localActions,
[...Object.keys(this.localMutations), ...Object.keys(this.localEffects)], [...Object.keys(this.localMutations), ...Object.keys(this.localEffects)],
fp.flatten( fp.flatten(
Object.values(this.subduxes).map(({actions}: Updux) => Object.values(this.subduxes).map(({ actions }: Updux) =>
Object.entries(actions), Object.entries(actions)
), )
), )
); );
} }
@ -245,7 +243,7 @@ export class Updux<S = any> {
this.reducer, this.reducer,
this.initial, this.initial,
this.middleware as Middleware, this.middleware as Middleware,
this.actions, this.actions
) as () => StoreWithDispatchActions<S, typeof actions>; ) as () => StoreWithDispatchActions<S, typeof actions>;
} }
@ -264,7 +262,7 @@ export class Updux<S = any> {
actions: this.actions, actions: this.actions,
reducer: this.reducer, reducer: this.reducer,
mutations: this.mutations, mutations: this.mutations,
initial: this.initial, initial: this.initial
}; };
} }
@ -283,12 +281,15 @@ export class Updux<S = any> {
addMutation<A extends ActionCreator>( addMutation<A extends ActionCreator>(
creator: A, creator: A,
mutation: Mutation<S, A extends (...args: any[]) => infer R ? R : never>, mutation: Mutation<S, A extends (...args: any[]) => infer R ? R : never>,
isSink?: boolean, isSink?: boolean
) { ) {
this.localActions[creator.type] = creator; let c = fp.isFunction(creator) ? creator : actionFor(creator);
this.localMutations[creator.type] = [
this.localActions[c.type] = c;
this.localMutations[c.type] = [
this.groomMutations(mutation as any) as Mutation<S>, this.groomMutations(mutation as any) as Mutation<S>,
isSink, isSink
]; ];
} }
} }