pass mutations in constructor as a map

typescript
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
NEXT
- Mutations passed to the constructor can be arrays of arrays.
1.2.0 2019-11-06
- The middleware's 'getState' returns the local state of its updux,
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 = {
sum: number
}
sum: number;
};
test( 'added mutation is present', () => {
const updux = new Updux<MyState>({
initial: { sum: 0 },
});
test("added mutation is present", () => {
const updux = new Updux<MyState>({
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.mutations;
const store = updux.createStore();
store.dispatch.add(3);
updux.addMutation(add, ({ n }, action) => ({ sum }) => ({ sum: sum + n }));
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 { Action, ActionCreator, ActionPayloadGenerator, Dictionary } from '../types';
import fp from "lodash/fp";
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>( type: T, transform: never ): ActionCreator<T,undefined>
export function actionCreator<T extends string>( type: T, transform: null ): ActionCreator<T,null>
export function actionCreator(type:any, transform:any ) {
export function actionCreator<T extends string, P extends any>(
type: T,
transform: (...args: any[]) => P
): 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 ) {
return Object.assign(
(...args: any[]) => ({ type, payload: transform(...args) }),
{ type } )
}
if (transform === null) {
return Object.assign(() => ({ type }), { type });
}
if( transform === null ) {
return Object.assign( () => ({ type }), { type } )
}
return Object.assign( (payload: unknown) => ({type, payload}) );
return Object.assign((payload: unknown) => ({ type, payload }), { type });
}
function actionFor(type:string): ActionCreator {
const f = ( (payload = undefined, meta = undefined) =>
fp.pickBy(v => v !== undefined)({type, payload, meta}) as Action
);
export function actionFor(type: string): ActionCreator {
const f = (payload = undefined, meta = undefined) =>
fp.pickBy(v => v !== undefined)({ type, payload, meta }) as Action;
return Object.assign(f, {
_genericAction: true,
type
_genericAction: true,
type
});
}
type ActionPair = [ string, ActionCreator ];
type ActionPair = [string, ActionCreator];
function buildActions(
generators : Dictionary<ActionPayloadGenerator> = {},
generators: Dictionary<ActionPayloadGenerator> = {},
actionNames: string[] = [],
subActions : ActionPair[] = [],
):Dictionary<ActionCreator> {
subActions: ActionPair[] = []
): 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(
([type,f]) => !f._genericAction
)( subActions );
const actions: any = [
...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) })
])
];
const actions : any = [
...(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);
return fp.fromPairs(actions);
}
export default buildActions;

View File

@ -1,72 +1,68 @@
import fp from 'lodash/fp';
import u from 'updeep';
import {Mutation, Action, Dictionary} from '../types';
import fp from "lodash/fp";
import u from "updeep";
import { Mutation, Action, Dictionary, MutationEntry } from "../types";
const composeMutations = (mutations: Mutation[]) =>
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 = {
[ slice: string ]: Dictionary<Mutation>
}
[slice: string]: Dictionary<Mutation>;
};
function buildMutations(
mutations :Dictionary<Mutation|([Mutation,boolean|undefined])> = {},
subduxes = {}
mutations: Dictionary<Mutation | [Mutation, boolean | undefined]> = {},
subduxes = {}
) {
// we have to differentiate the subduxes with '*' than those
// without, as the root '*' is not the same as any sub-'*'
const actions = fp.uniq(
Object.keys(mutations).concat(
...Object.values(subduxes).map(({mutations = {}}:any) =>
Object.keys(mutations),
),
),
...Object.values(subduxes).map(({ mutations = {} }: any) =>
Object.keys(mutations)
)
)
);
let mergedMutations :Dictionary<Mutation[]> = {};
let mergedMutations: Dictionary<Mutation[]> = {};
let [globby, nonGlobby] = fp.partition(
([_, {mutations = {}}]:any) => mutations['*'],
Object.entries(subduxes),
([_, { mutations = {} }]: any) => mutations["*"],
Object.entries(subduxes)
);
globby = fp.flow([
fp.fromPairs,
fp.mapValues(({reducer}) => (_:any, action :Action) => ( state: any ) =>
reducer(state, action),
),
fp.mapValues(({ reducer }) => (_: any, action: Action) => (state: any) =>
reducer(state, action)
)
])(globby);
const globbyMutation = (payload:any, action:Action) =>
u(fp.mapValues((mut:any) => mut(payload, action))(globby));
const globbyMutation = (payload: any, action: Action) =>
u(fp.mapValues((mut: any) => mut(payload, action))(globby));
actions.forEach(action => {
mergedMutations[action] = [globbyMutation];
});
nonGlobby.forEach(([slice, {mutations = {}, reducer = {}}]:any[]) => {
nonGlobby.forEach(([slice, { mutations = {}, reducer = {} }]: any[]) => {
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));
}
};
mergedMutations[type].push(localized);
});
});
Object.entries(mutations).forEach(([type, mutation]) => {
if ( Array.isArray(mutation) ) {
if( mutation[1] ) {
mergedMutations[type] = [
mutation[0]
]
}
else mergedMutations[type].push( mutation[0] );
}
else mergedMutations[type].push(mutation);
if (Array.isArray(mutation)) {
if (mutation[1]) {
mergedMutations[type] = [mutation[0]];
} else mergedMutations[type].push(mutation[0]);
} else mergedMutations[type].push(mutation);
});
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 {
UpduxConfig
} from './types';
export { default as Updux } from "./updux";
export { UpduxConfig } from "./types";
export { actionCreator } from "./buildActions";
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
? {
payload: P;
}
: {payload?: P};
: { payload?: P };
export type Action<T extends string = string, P = any> = {
type: T;
} & 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> = (
payload: A['payload'],
action: A,
payload: A["payload"],
action: A
) => (state: S) => S;
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> = {
type: T;
_genericAction?: boolean;
@ -26,12 +32,11 @@ export type ActionCreator<T extends string = string, P = any> = {
export type UpduxDispatch = Dispatch & Dictionary<Function>;
/**
* Configuration object given to Updux's constructor.
* @typeparam S Type of the Updux's state. Defaults to `any`.
*/
export type UpduxConfig<S=any> = {
* Configuration object given to Updux's constructor.
* @typeparam S Type of the Updux's state. Defaults to `any`.
*/
export type UpduxConfig<S = any> = {
/**
* The default initial state of the reducer. Can be anything your
* 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>;
@ -184,10 +189,10 @@ export type UpduxConfig<S=any> = {
export type Upreducer<S = any> = (action: Action) => (state: S) => S;
export interface UpduxMiddlewareAPI<S> {
dispatch: UpduxDispatch,
getState(): any,
getRootState(): S
dispatch: UpduxDispatch;
getState(): any;
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 u from 'updeep';
import {observable, computed, toJS} from 'mobx';
import fp from "lodash/fp";
import u from "updeep";
import { observable, computed, toJS } from "mobx";
import buildActions from './buildActions';
import buildInitial from './buildInitial';
import buildMutations from './buildMutations';
import buildActions, { actionFor } from "./buildActions";
import buildInitial from "./buildInitial";
import buildMutations from "./buildMutations";
import buildCreateStore from './buildCreateStore';
import buildMiddleware from './buildMiddleware';
import buildUpreducer from './buildUpreducer';
import buildCreateStore from "./buildCreateStore";
import buildMiddleware from "./buildMiddleware";
import buildUpreducer from "./buildUpreducer";
import {
UpduxConfig,
Dictionary,
@ -17,29 +17,30 @@ import {
Mutation,
Upreducer,
UpduxDispatch,
UpduxMiddleware
} from './types';
UpduxMiddleware,
MutationEntry
} from "./types";
import {Middleware, Store} from 'redux';
export {actionCreator} from './buildActions';
import { Middleware, Store } from "redux";
export { actionCreator } from "./buildActions";
type StoreWithDispatchActions<
S = any,
Actions = {[action: string]: (...args: any) => Action}
Actions = { [action: string]: (...args: any) => Action }
> = Store<S> & {
dispatch: {[type in keyof Actions]: (...args: any) => void};
dispatch: { [type in keyof Actions]: (...args: any) => void };
};
export type Dux<S> = Pick<
Updux<S>,
| 'subduxes'
| 'actions'
| 'initial'
| 'mutations'
| 'reducer'
| 'middleware'
| 'createStore'
| 'upreducer'
| "subduxes"
| "actions"
| "initial"
| "mutations"
| "reducer"
| "middleware"
| "createStore"
| "upreducer"
>;
/**
@ -98,35 +99,36 @@ export class Updux<S = any> {
*/
groomMutations: (mutation: Mutation<S>) => Mutation<S>;
@observable private localEffects: Dictionary<
UpduxMiddleware<S>
>;
@observable private localEffects: Dictionary<UpduxMiddleware<S>>;
@observable private localActions: Dictionary<ActionCreator>;
@observable private localMutations: Dictionary<
Mutation<S> | [Mutation<S>, boolean | undefined]
>;
> = {};
constructor(config: UpduxConfig = {}) {
this.groomMutations = config.groomMutations || ((x: Mutation<S>) => x);
this.subduxes = fp.mapValues((value: UpduxConfig | Updux) =>
fp.isPlainObject(value) ? new Updux(value) : value,
)(fp.getOr({}, 'subduxes', config)) as Dictionary<Updux>;
fp.isPlainObject(value) ? new Updux(value) : value
)(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>(
config.initial,
fp.mapValues(({initial}) => initial)(this.subduxes),
fp.mapValues(({ initial }) => initial)(this.subduxes)
);
this.localMutations = fp.mapValues((m: Mutation<S>) =>
this.groomMutations(m),
)(fp.getOr({}, 'mutations', config));
let mutations = fp.getOr([], "mutations", config);
if (!Array.isArray(mutations)) {
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.
*/
@computed get middleware(): UpduxMiddleware<S> {
return buildMiddleware(
this.localEffects,
this.actions,
this.subduxes,
);
return buildMiddleware(this.localEffects, this.actions, this.subduxes);
}
/**
@ -164,10 +162,10 @@ export class Updux<S = any> {
this.localActions,
[...Object.keys(this.localMutations), ...Object.keys(this.localEffects)],
fp.flatten(
Object.values(this.subduxes).map(({actions}: Updux) =>
Object.entries(actions),
),
),
Object.values(this.subduxes).map(({ actions }: Updux) =>
Object.entries(actions)
)
)
);
}
@ -245,7 +243,7 @@ export class Updux<S = any> {
this.reducer,
this.initial,
this.middleware as Middleware,
this.actions,
this.actions
) as () => StoreWithDispatchActions<S, typeof actions>;
}
@ -264,7 +262,7 @@ export class Updux<S = any> {
actions: this.actions,
reducer: this.reducer,
mutations: this.mutations,
initial: this.initial,
initial: this.initial
};
}
@ -283,12 +281,15 @@ export class Updux<S = any> {
addMutation<A extends ActionCreator>(
creator: A,
mutation: Mutation<S, A extends (...args: any[]) => infer R ? R : never>,
isSink?: boolean,
isSink?: boolean
) {
this.localActions[creator.type] = creator;
this.localMutations[creator.type] = [
let c = fp.isFunction(creator) ? creator : actionFor(creator);
this.localActions[c.type] = c;
this.localMutations[c.type] = [
this.groomMutations(mutation as any) as Mutation<S>,
isSink,
isSink
];
}
}