Merge branch 'selectors'
This commit is contained in:
commit
4665ee39f7
1
.gitignore
vendored
1
.gitignore
vendored
@ -1,2 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
tsconfig.tsbuildinfo
|
tsconfig.tsbuildinfo
|
||||||
|
**/*.orig
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
<!-- docs/_sidebar.md -->
|
<!-- docs/_sidebar.md -->
|
||||||
|
|
||||||
* [Home](/)
|
* [Home](/)
|
||||||
|
* [Concepts](concepts.md)
|
||||||
* API Reference
|
* API Reference
|
||||||
* [Updux](updux.md)
|
* [Updux](updux.md)
|
||||||
|
30
docs/concepts.md
Normal file
30
docs/concepts.md
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
# Updux concepts
|
||||||
|
|
||||||
|
## effects
|
||||||
|
|
||||||
|
Updux effects are a superset of redux middleware. I kept that format, and the
|
||||||
|
use of `next` mostly because I wanted to give myself a way to alter
|
||||||
|
actions before they hit the reducer, something that `redux-saga` and
|
||||||
|
`rematch` don't allow.
|
||||||
|
|
||||||
|
An effect has the signature
|
||||||
|
|
||||||
|
```js
|
||||||
|
const effect = ({ getState, dispatch, getRootState, selectors})
|
||||||
|
=> next => action => { ... }
|
||||||
|
```
|
||||||
|
|
||||||
|
The first argument is like the usual redux middlewareApi, except
|
||||||
|
for the availability of selectors and of the root updux's state.
|
||||||
|
|
||||||
|
Also, the function `dispatch` is augmented to be able to be called
|
||||||
|
with the allowed actions as props. For example, assuming that the action
|
||||||
|
`complete_todo` has been declared somewhere, then it's possible to do:
|
||||||
|
|
||||||
|
```js
|
||||||
|
updux.addEffect( 'todo_bankrupcy',
|
||||||
|
({ getState, dispatch }) => next => action => {
|
||||||
|
getState.forEach( todo => dispatch.complete_todo( todo.id ) );
|
||||||
|
}
|
||||||
|
)
|
||||||
|
```
|
@ -36,6 +36,23 @@ const { actions } = updux({
|
|||||||
|
|
||||||
actions.foo({ x: 1, y: 2 }); // => { type: foo, payload: { x:1, y:2 } }
|
actions.foo({ x: 1, y: 2 }); // => { type: foo, payload: { x:1, y:2 } }
|
||||||
actions.bar(1,2); // => { type: bar, payload: { x:1, y:2 } }
|
actions.bar(1,2); // => { type: bar, payload: { x:1, y:2 } }
|
||||||
|
|
||||||
|
|
||||||
|
#### selectors
|
||||||
|
|
||||||
|
Dictionary of selectors for the current updux. The updux also
|
||||||
|
inherit its dubduxes' selectors.
|
||||||
|
|
||||||
|
The selectors are available via the class' getter and, for
|
||||||
|
middlewares, the middlewareApi.
|
||||||
|
|
||||||
|
```js
|
||||||
|
const todoUpdux = new Updux({
|
||||||
|
selectors: {
|
||||||
|
done: state => state.filter( ({done}) => done ),
|
||||||
|
byId: state => targetId => state.find( ({id}) => id === targetId ),
|
||||||
|
}
|
||||||
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
#### mutations
|
#### mutations
|
||||||
@ -332,3 +349,11 @@ baz(); // => { type: 'baz', payload: undefined }
|
|||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### selectors
|
||||||
|
|
||||||
|
Returns a dictionary of the
|
||||||
|
updux's selectors. Subduxes' selectors
|
||||||
|
are included as well (with the mapping to the sub-state already
|
||||||
|
taken care of you).
|
||||||
|
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import {
|
|||||||
UpduxMiddlewareAPI,
|
UpduxMiddlewareAPI,
|
||||||
EffectEntry
|
EffectEntry
|
||||||
} from "../types";
|
} from "../types";
|
||||||
|
import Updux from "..";
|
||||||
|
|
||||||
const MiddlewareFor = (
|
const MiddlewareFor = (
|
||||||
type: any,
|
type: any,
|
||||||
@ -23,12 +24,13 @@ const MiddlewareFor = (
|
|||||||
|
|
||||||
type Next = (action: Action) => any;
|
type Next = (action: Action) => any;
|
||||||
|
|
||||||
function sliceMw(slice: string, mw: Middleware): Middleware {
|
function sliceMw(slice: string, mw: Middleware, updux: Updux): Middleware {
|
||||||
return api => {
|
return api => {
|
||||||
const getSliceState =
|
const getSliceState =
|
||||||
slice.length > 0 ? () => fp.get(slice, api.getState()) : api.getState;
|
slice.length > 0 ? () => fp.get(slice, api.getState()) : api.getState;
|
||||||
const getRootState = (api as any).getRootState || api.getState;
|
const getRootState = (api as any).getRootState || api.getState;
|
||||||
return mw({ ...api, getState: getSliceState, getRootState } as any);
|
return mw({ ...api, getState: getSliceState, getRootState,
|
||||||
|
selectors: updux.selectors } as any);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,11 +39,11 @@ function buildMiddleware<S = any>(
|
|||||||
actions: Dictionary<ActionCreator> = {}
|
actions: Dictionary<ActionCreator> = {}
|
||||||
): UpduxMiddleware<S> {
|
): UpduxMiddleware<S> {
|
||||||
let mws = middlewareEntries
|
let mws = middlewareEntries
|
||||||
.map(([slice, actionType, mw, isGen]: any) =>
|
.map(([updux, slice, actionType, mw, isGen]: any) =>
|
||||||
isGen ? [slice, actionType, mw()] : [slice, actionType, mw]
|
isGen ? [updux, slice, actionType, mw()] : [updux, slice, actionType, mw]
|
||||||
)
|
)
|
||||||
.map(([slice, actionType, mw]) =>
|
.map(([updux, slice, actionType, mw]) =>
|
||||||
MiddlewareFor(actionType, sliceMw(slice, mw))
|
MiddlewareFor(actionType, sliceMw(slice, mw, updux))
|
||||||
);
|
);
|
||||||
|
|
||||||
return (api: UpduxMiddlewareAPI<S>) => {
|
return (api: UpduxMiddlewareAPI<S>) => {
|
||||||
|
26
src/buildSelectors/index.ts
Normal file
26
src/buildSelectors/index.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import fp from 'lodash/fp';
|
||||||
|
import Updux from '..';
|
||||||
|
import { Dictionary, Selector } from '../types';
|
||||||
|
|
||||||
|
function subSelectors([slice, subdux]: [string, Updux]): [string, Selector][] {
|
||||||
|
const selectors = subdux.selectors;
|
||||||
|
if (!selectors) return [];
|
||||||
|
|
||||||
|
return Object.entries(
|
||||||
|
fp.mapValues(selector => (state: any) =>
|
||||||
|
(selector as any)(state[slice])
|
||||||
|
)(selectors)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function buildSelectors(
|
||||||
|
localSelectors: Dictionary<Selector> = {},
|
||||||
|
subduxes: Dictionary<Updux> = {}
|
||||||
|
) {
|
||||||
|
return Object.fromEntries(
|
||||||
|
[
|
||||||
|
Object.entries(subduxes).flatMap(subSelectors),
|
||||||
|
Object.entries(localSelectors),
|
||||||
|
].flat()
|
||||||
|
);
|
||||||
|
}
|
@ -1,228 +1,277 @@
|
|||||||
import Updux, { actionCreator } from ".";
|
import Updux, { actionCreator } from '.';
|
||||||
import u from "updeep";
|
import u from 'updeep';
|
||||||
|
import mwUpdux from './middleware_aux';
|
||||||
|
|
||||||
test("simple effect", () => {
|
test('simple effect', () => {
|
||||||
const tracer = jest.fn();
|
const tracer = jest.fn();
|
||||||
|
|
||||||
const store = new Updux({
|
const store = new Updux({
|
||||||
effects: {
|
|
||||||
foo: (api: any) => (next: any) => (action: any) => {
|
|
||||||
tracer();
|
|
||||||
next(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).createStore();
|
|
||||||
|
|
||||||
expect(tracer).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
store.dispatch({ type: "bar" });
|
|
||||||
|
|
||||||
expect(tracer).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
store.dispatch.foo();
|
|
||||||
|
|
||||||
expect(tracer).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("effect and sub-effect", () => {
|
|
||||||
const tracer = jest.fn();
|
|
||||||
|
|
||||||
const tracerEffect = (signature: string) => (api: any) => (next: any) => (
|
|
||||||
action: any
|
|
||||||
) => {
|
|
||||||
tracer(signature);
|
|
||||||
next(action);
|
|
||||||
};
|
|
||||||
|
|
||||||
const store = new Updux({
|
|
||||||
effects: {
|
|
||||||
foo: tracerEffect("root")
|
|
||||||
},
|
|
||||||
subduxes: {
|
|
||||||
zzz: {
|
|
||||||
effects: {
|
effects: {
|
||||||
foo: tracerEffect("child")
|
foo: (api: any) => (next: any) => (action: any) => {
|
||||||
}
|
tracer();
|
||||||
}
|
next(action);
|
||||||
}
|
},
|
||||||
}).createStore();
|
|
||||||
|
|
||||||
expect(tracer).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
store.dispatch({ type: "bar" });
|
|
||||||
|
|
||||||
expect(tracer).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
store.dispatch.foo();
|
|
||||||
|
|
||||||
expect(tracer).toHaveBeenNthCalledWith(1, "root");
|
|
||||||
expect(tracer).toHaveBeenNthCalledWith(2, "child");
|
|
||||||
});
|
|
||||||
|
|
||||||
test('"*" effect', () => {
|
|
||||||
const tracer = jest.fn();
|
|
||||||
|
|
||||||
const store = new Updux({
|
|
||||||
effects: {
|
|
||||||
"*": api => next => action => {
|
|
||||||
tracer();
|
|
||||||
next(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).createStore();
|
|
||||||
|
|
||||||
expect(tracer).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
store.dispatch({ type: "bar" });
|
|
||||||
|
|
||||||
expect(tracer).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("async effect", async () => {
|
|
||||||
function timeout(ms: number) {
|
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
|
||||||
}
|
|
||||||
|
|
||||||
const tracer = jest.fn();
|
|
||||||
|
|
||||||
const store = new Updux({
|
|
||||||
effects: {
|
|
||||||
foo: api => next => async action => {
|
|
||||||
next(action);
|
|
||||||
await timeout(1000);
|
|
||||||
tracer();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}).createStore();
|
|
||||||
|
|
||||||
expect(tracer).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
store.dispatch.foo();
|
|
||||||
|
|
||||||
expect(tracer).not.toHaveBeenCalled();
|
|
||||||
|
|
||||||
await timeout(1000);
|
|
||||||
|
|
||||||
expect(tracer).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("getState is local", () => {
|
|
||||||
let childState;
|
|
||||||
let rootState;
|
|
||||||
let rootFromChild;
|
|
||||||
|
|
||||||
const child = new Updux({
|
|
||||||
initial: { alpha: 12 },
|
|
||||||
effects: {
|
|
||||||
doIt: ({ getState, getRootState }) => next => action => {
|
|
||||||
childState = getState();
|
|
||||||
rootFromChild = getRootState();
|
|
||||||
next(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const root = new Updux({
|
|
||||||
initial: { beta: 24 },
|
|
||||||
subduxes: { child },
|
|
||||||
effects: {
|
|
||||||
doIt: ({ getState }) => next => action => {
|
|
||||||
rootState = getState();
|
|
||||||
next(action);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = root.createStore();
|
|
||||||
store.dispatch.doIt();
|
|
||||||
|
|
||||||
expect(rootState).toEqual({ beta: 24, child: { alpha: 12 } });
|
|
||||||
expect(rootFromChild).toEqual({ beta: 24, child: { alpha: 12 } });
|
|
||||||
expect(childState).toEqual({ alpha: 12 });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("middleware as map", () => {
|
|
||||||
let childState;
|
|
||||||
let rootState;
|
|
||||||
let rootFromChild;
|
|
||||||
|
|
||||||
const doIt = actionCreator("doIt");
|
|
||||||
|
|
||||||
const child = new Updux({
|
|
||||||
initial: "",
|
|
||||||
effects: [
|
|
||||||
[
|
|
||||||
doIt,
|
|
||||||
() => next => action => {
|
|
||||||
next(u({ payload: (p: string) => p + "Child" }, action) as any);
|
|
||||||
}
|
|
||||||
]
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const root = new Updux({
|
|
||||||
initial: { message: "" },
|
|
||||||
subduxes: { child },
|
|
||||||
effects: [
|
|
||||||
[
|
|
||||||
"^",
|
|
||||||
() => next => action => {
|
|
||||||
next(u({ payload: (p: string) => p + "Pre" }, action) as any);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
doIt,
|
|
||||||
() => next => action => {
|
|
||||||
next(u({ payload: (p: string) => p + "Root" }, action) as any);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"*",
|
|
||||||
() => next => action => {
|
|
||||||
next(u({ payload: (p: string) => p + "After" }, action) as any);
|
|
||||||
}
|
|
||||||
],
|
|
||||||
[
|
|
||||||
"$",
|
|
||||||
() => next => action => {
|
|
||||||
next(u({ payload: (p: string) => p + "End" }, action) as any);
|
|
||||||
}
|
|
||||||
]
|
|
||||||
],
|
|
||||||
mutations: [[doIt, (message: any) => () => ({ message })]]
|
|
||||||
});
|
|
||||||
|
|
||||||
const store = root.createStore();
|
|
||||||
store.dispatch.doIt("");
|
|
||||||
|
|
||||||
expect(store.getState()).toEqual({ message: "PreRootAfterChildEnd" });
|
|
||||||
});
|
|
||||||
|
|
||||||
test("generator", () => {
|
|
||||||
const updux = new Updux({
|
|
||||||
initial: 0,
|
|
||||||
mutations: [["doIt", payload => () => payload]],
|
|
||||||
effects: [
|
|
||||||
[
|
|
||||||
"doIt",
|
|
||||||
() => {
|
|
||||||
let i = 0;
|
|
||||||
return () => (next: any) => (action: any) =>
|
|
||||||
next({ ...action, payload: ++i });
|
|
||||||
},
|
},
|
||||||
true
|
}).createStore();
|
||||||
]
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
const store1 = updux.createStore();
|
expect(tracer).not.toHaveBeenCalled();
|
||||||
store1.dispatch.doIt();
|
|
||||||
expect(store1.getState()).toEqual(1);
|
|
||||||
store1.dispatch.doIt();
|
|
||||||
expect(store1.getState()).toEqual(2);
|
|
||||||
updux.actions;
|
|
||||||
|
|
||||||
const store2 = updux.createStore();
|
store.dispatch({ type: 'bar' });
|
||||||
store2.dispatch.doIt();
|
|
||||||
expect(store2.getState()).toEqual(1);
|
expect(tracer).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
store.dispatch.foo();
|
||||||
|
|
||||||
|
expect(tracer).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('effect and sub-effect', () => {
|
||||||
|
const tracer = jest.fn();
|
||||||
|
|
||||||
|
const tracerEffect = (signature: string) => (api: any) => (next: any) => (
|
||||||
|
action: any
|
||||||
|
) => {
|
||||||
|
tracer(signature);
|
||||||
|
next(action);
|
||||||
|
};
|
||||||
|
|
||||||
|
const store = new Updux({
|
||||||
|
effects: {
|
||||||
|
foo: tracerEffect('root'),
|
||||||
|
},
|
||||||
|
subduxes: {
|
||||||
|
zzz: {
|
||||||
|
effects: {
|
||||||
|
foo: tracerEffect('child'),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).createStore();
|
||||||
|
|
||||||
|
expect(tracer).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
store.dispatch({ type: 'bar' });
|
||||||
|
|
||||||
|
expect(tracer).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
store.dispatch.foo();
|
||||||
|
|
||||||
|
expect(tracer).toHaveBeenNthCalledWith(1, 'root');
|
||||||
|
expect(tracer).toHaveBeenNthCalledWith(2, 'child');
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('"*" effect', () => {
|
||||||
|
test('from the constructor', () => {
|
||||||
|
const tracer = jest.fn();
|
||||||
|
|
||||||
|
const store = new Updux({
|
||||||
|
effects: {
|
||||||
|
'*': api => next => action => {
|
||||||
|
tracer();
|
||||||
|
next(action);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).createStore();
|
||||||
|
|
||||||
|
expect(tracer).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
store.dispatch({ type: 'bar' });
|
||||||
|
|
||||||
|
expect(tracer).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('from addEffect', () => {
|
||||||
|
const tracer = jest.fn();
|
||||||
|
|
||||||
|
const updux = new Updux({});
|
||||||
|
|
||||||
|
updux.addEffect('*', api => next => action => {
|
||||||
|
tracer();
|
||||||
|
next(action);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(tracer).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
updux.createStore().dispatch({ type: 'bar' });
|
||||||
|
|
||||||
|
expect(tracer).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('action can be modified', () => {
|
||||||
|
|
||||||
|
const mw = mwUpdux.middleware;
|
||||||
|
|
||||||
|
const next = jest.fn();
|
||||||
|
|
||||||
|
mw({dispatch:{}} as any)(next as any)({type: 'bar'});
|
||||||
|
|
||||||
|
expect(next).toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(next.mock.calls[0][0]).toMatchObject({meta: 'gotcha'});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('async effect', async () => {
|
||||||
|
function timeout(ms: number) {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
|
||||||
|
const tracer = jest.fn();
|
||||||
|
|
||||||
|
const store = new Updux({
|
||||||
|
effects: {
|
||||||
|
foo: api => next => async action => {
|
||||||
|
next(action);
|
||||||
|
await timeout(1000);
|
||||||
|
tracer();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}).createStore();
|
||||||
|
|
||||||
|
expect(tracer).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
store.dispatch.foo();
|
||||||
|
|
||||||
|
expect(tracer).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await timeout(1000);
|
||||||
|
|
||||||
|
expect(tracer).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('getState is local', () => {
|
||||||
|
let childState;
|
||||||
|
let rootState;
|
||||||
|
let rootFromChild;
|
||||||
|
|
||||||
|
const child = new Updux({
|
||||||
|
initial: { alpha: 12 },
|
||||||
|
effects: {
|
||||||
|
doIt: ({ getState, getRootState }) => next => action => {
|
||||||
|
childState = getState();
|
||||||
|
rootFromChild = getRootState();
|
||||||
|
next(action);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const root = new Updux({
|
||||||
|
initial: { beta: 24 },
|
||||||
|
subduxes: { child },
|
||||||
|
effects: {
|
||||||
|
doIt: ({ getState }) => next => action => {
|
||||||
|
rootState = getState();
|
||||||
|
next(action);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = root.createStore();
|
||||||
|
store.dispatch.doIt();
|
||||||
|
|
||||||
|
expect(rootState).toEqual({ beta: 24, child: { alpha: 12 } });
|
||||||
|
expect(rootFromChild).toEqual({ beta: 24, child: { alpha: 12 } });
|
||||||
|
expect(childState).toEqual({ alpha: 12 });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('middleware as map', () => {
|
||||||
|
let childState;
|
||||||
|
let rootState;
|
||||||
|
let rootFromChild;
|
||||||
|
|
||||||
|
const doIt = actionCreator('doIt');
|
||||||
|
|
||||||
|
const child = new Updux({
|
||||||
|
initial: '',
|
||||||
|
effects: [
|
||||||
|
[
|
||||||
|
doIt,
|
||||||
|
() => next => action => {
|
||||||
|
next(
|
||||||
|
u(
|
||||||
|
{ payload: (p: string) => p + 'Child' },
|
||||||
|
action
|
||||||
|
) as any
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const root = new Updux({
|
||||||
|
initial: { message: '' },
|
||||||
|
subduxes: { child },
|
||||||
|
effects: [
|
||||||
|
[
|
||||||
|
'^',
|
||||||
|
() => next => action => {
|
||||||
|
next(
|
||||||
|
u({ payload: (p: string) => p + 'Pre' }, action) as any
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
doIt,
|
||||||
|
() => next => action => {
|
||||||
|
next(
|
||||||
|
u({ payload: (p: string) => p + 'Root' }, action) as any
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'*',
|
||||||
|
() => next => action => {
|
||||||
|
next(
|
||||||
|
u(
|
||||||
|
{ payload: (p: string) => p + 'After' },
|
||||||
|
action
|
||||||
|
) as any
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'$',
|
||||||
|
() => next => action => {
|
||||||
|
next(
|
||||||
|
u({ payload: (p: string) => p + 'End' }, action) as any
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
|
],
|
||||||
|
mutations: [[doIt, (message: any) => () => ({ message })]],
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = root.createStore();
|
||||||
|
store.dispatch.doIt('');
|
||||||
|
|
||||||
|
expect(store.getState()).toEqual({ message: 'PreRootAfterChildEnd' });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('generator', () => {
|
||||||
|
const updux = new Updux({
|
||||||
|
initial: 0,
|
||||||
|
mutations: [['doIt', payload => () => payload]],
|
||||||
|
effects: [
|
||||||
|
[
|
||||||
|
'doIt',
|
||||||
|
() => {
|
||||||
|
let i = 0;
|
||||||
|
return () => (next: any) => (action: any) =>
|
||||||
|
next({ ...action, payload: ++i });
|
||||||
|
},
|
||||||
|
true,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const store1 = updux.createStore();
|
||||||
|
store1.dispatch.doIt();
|
||||||
|
expect(store1.getState()).toEqual(1);
|
||||||
|
store1.dispatch.doIt();
|
||||||
|
expect(store1.getState()).toEqual(2);
|
||||||
|
updux.actions;
|
||||||
|
|
||||||
|
const store2 = updux.createStore();
|
||||||
|
store2.dispatch.doIt();
|
||||||
|
expect(store2.getState()).toEqual(1);
|
||||||
});
|
});
|
||||||
|
13
src/middleware_aux.ts
Normal file
13
src/middleware_aux.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import Updux from '.';
|
||||||
|
|
||||||
|
const updux = new Updux({
|
||||||
|
subduxes: {
|
||||||
|
foo: { initial: "banana" }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
updux.addEffect('*', api => next => action => {
|
||||||
|
next({...action, meta: "gotcha" });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default updux;
|
55
src/selectors.test.ts
Normal file
55
src/selectors.test.ts
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
import Updux from '.';
|
||||||
|
|
||||||
|
test('basic selectors', () => {
|
||||||
|
const updux = new Updux({
|
||||||
|
subduxes: {
|
||||||
|
bogeys: {
|
||||||
|
selectors: {
|
||||||
|
bogey: (bogeys: any) => (id: string) => bogeys[id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
selectors: {
|
||||||
|
bogeys: ({ bogeys }: any) => bogeys,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
bogeys: {
|
||||||
|
foo: 1,
|
||||||
|
bar: 2,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(updux.selectors.bogeys(state)).toEqual({ foo: 1, bar: 2 });
|
||||||
|
expect((updux.selectors.bogey(state) as any)('foo')).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('available in the middleware', () => {
|
||||||
|
const updux = new Updux({
|
||||||
|
subduxes: {
|
||||||
|
bogeys: {
|
||||||
|
initial: { enkidu: 'foo' },
|
||||||
|
selectors: {
|
||||||
|
bogey: (bogeys: any) => (id: string) => bogeys[id],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
effects: {
|
||||||
|
doIt: ({ selectors: { bogey }, getState }) => next => action => {
|
||||||
|
next({
|
||||||
|
...action,
|
||||||
|
payload: bogey(getState())('enkidu'),
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
mutations: {
|
||||||
|
doIt: payload => state => ({ ...state, payload }),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = updux.createStore();
|
||||||
|
store.dispatch.doIt();
|
||||||
|
|
||||||
|
expect(store.getState()).toMatchObject({ payload: 'foo' });
|
||||||
|
});
|
@ -98,6 +98,8 @@ export type UpduxConfig<S = any> = {
|
|||||||
[type: string]: ActionCreator;
|
[type: string]: ActionCreator;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
selectors?: Dictionary<Selector>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Object mapping actions to the associated state mutation.
|
* Object mapping actions to the associated state mutation.
|
||||||
*
|
*
|
||||||
@ -198,7 +200,10 @@ export interface UpduxMiddlewareAPI<S> {
|
|||||||
dispatch: UpduxDispatch;
|
dispatch: UpduxDispatch;
|
||||||
getState(): any;
|
getState(): any;
|
||||||
getRootState(): S;
|
getRootState(): S;
|
||||||
|
selectors: Dictionary<Selector>;
|
||||||
}
|
}
|
||||||
export type UpduxMiddleware<S = any> = (
|
export type UpduxMiddleware<S = any> = (
|
||||||
api: UpduxMiddlewareAPI<S>
|
api: UpduxMiddlewareAPI<S>
|
||||||
) => (next: UpduxDispatch) => (action: Action) => any;
|
) => (next: UpduxDispatch) => (action: Action) => any;
|
||||||
|
|
||||||
|
export type Selector<S = any> = (state:S) => any;
|
||||||
|
46
src/updux.ts
46
src/updux.ts
@ -18,10 +18,12 @@ import {
|
|||||||
UpduxDispatch,
|
UpduxDispatch,
|
||||||
UpduxMiddleware,
|
UpduxMiddleware,
|
||||||
MutationEntry,
|
MutationEntry,
|
||||||
EffectEntry
|
EffectEntry,
|
||||||
|
Selector
|
||||||
} from "./types";
|
} from "./types";
|
||||||
|
|
||||||
import { Middleware, Store, PreloadedState } from "redux";
|
import { Middleware, Store, PreloadedState } from "redux";
|
||||||
|
import buildSelectors from "./buildSelectors";
|
||||||
export { actionCreator } from "./buildActions";
|
export { actionCreator } from "./buildActions";
|
||||||
|
|
||||||
type StoreWithDispatchActions<
|
type StoreWithDispatchActions<
|
||||||
@ -44,26 +46,34 @@ export type Dux<S> = Pick<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export class Updux<S = any> {
|
export class Updux<S = any> {
|
||||||
subduxes: Dictionary<Updux>;
|
subduxes: Dictionary<Updux> = {};
|
||||||
|
|
||||||
|
private local_selectors: Dictionary<Selector<S>> = {};
|
||||||
|
|
||||||
initial: S;
|
initial: S;
|
||||||
|
|
||||||
groomMutations: (mutation: Mutation<S>) => Mutation<S>;
|
groomMutations: (mutation: Mutation<S>) => Mutation<S>;
|
||||||
|
|
||||||
private localEffects: EffectEntry<S>[] = [];
|
|
||||||
|
|
||||||
private localActions: Dictionary<ActionCreator> = {};
|
private localEffects: EffectEntry<S>[] = [];
|
||||||
|
|
||||||
private localMutations: Dictionary<
|
private localActions: Dictionary<ActionCreator> = {};
|
||||||
|
|
||||||
|
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) =>
|
const selectors = fp.getOr( {}, 'selectors', config ) as Dictionary<Selector>;
|
||||||
fp.isPlainObject(value) ? new Updux(value) : value
|
Object.entries(selectors).forEach( ([name,sel]: [string,Function]) => this.addSelector(name,sel as Selector) );
|
||||||
)(fp.getOr({}, "subduxes", config)) as Dictionary<Updux>;
|
|
||||||
|
Object.entries( fp.mapValues((value: UpduxConfig | Updux) =>
|
||||||
|
fp.isPlainObject(value) ? new Updux(value as any) : value
|
||||||
|
)(fp.getOr({}, "subduxes", config))).forEach(
|
||||||
|
([slice,sub]) => this.subduxes[slice] = sub as any
|
||||||
|
);
|
||||||
|
|
||||||
const actions = fp.getOr({}, "actions", config);
|
const actions = fp.getOr({}, "actions", config);
|
||||||
Object.entries(actions).forEach(([type, payload]: [string, any]): any =>
|
Object.entries(actions).forEach(([type, payload]: [string, any]): any =>
|
||||||
@ -96,7 +106,6 @@ export class Updux<S = any> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get actions(): Dictionary<ActionCreator> {
|
get actions(): Dictionary<ActionCreator> {
|
||||||
|
|
||||||
return buildActions([
|
return buildActions([
|
||||||
...(Object.entries(this.localActions) as any),
|
...(Object.entries(this.localActions) as any),
|
||||||
...(fp.flatten(
|
...(fp.flatten(
|
||||||
@ -108,7 +117,7 @@ export class Updux<S = any> {
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
get upreducer(): Upreducer<S> {
|
get upreducer(): Upreducer<S> {
|
||||||
return buildUpreducer(this.initial, this.mutations);
|
return buildUpreducer(this.initial, this.mutations);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -191,22 +200,21 @@ export class Updux<S = any> {
|
|||||||
get _middlewareEntries() {
|
get _middlewareEntries() {
|
||||||
const groupByOrder = (mws: any) =>
|
const groupByOrder = (mws: any) =>
|
||||||
fp.groupBy(
|
fp.groupBy(
|
||||||
([_, actionType]: any) =>
|
([a,b, actionType]: any) =>
|
||||||
["^", "$"].includes(actionType) ? actionType : "middle",
|
["^", "$"].includes(actionType) ? actionType : "middle",
|
||||||
mws
|
mws
|
||||||
);
|
);
|
||||||
|
|
||||||
let subs = fp.flow([
|
let subs = fp.flow([
|
||||||
fp.mapValues("_middlewareEntries"),
|
|
||||||
fp.toPairs,
|
fp.toPairs,
|
||||||
fp.map(([slice, entries]) =>
|
fp.map(([slice, updux]) =>
|
||||||
entries.map(([ps, ...args]: any) => [[slice, ...ps], ...args])
|
updux._middlewareEntries.map(([u, ps, ...args]: any) => [u,[slice, ...ps], ...args])
|
||||||
),
|
),
|
||||||
fp.flatten,
|
fp.flatten,
|
||||||
groupByOrder
|
groupByOrder
|
||||||
])(this.subduxes);
|
])(this.subduxes);
|
||||||
|
|
||||||
let local = groupByOrder(this.localEffects.map(x => [[], ...x]));
|
let local = groupByOrder(this.localEffects.map(x => [this,[], ...x]));
|
||||||
|
|
||||||
return fp.flatten(
|
return fp.flatten(
|
||||||
[
|
[
|
||||||
@ -219,6 +227,14 @@ export class Updux<S = any> {
|
|||||||
].filter(x => x)
|
].filter(x => x)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addSelector( name: string, selector: Selector) {
|
||||||
|
this.local_selectors[name] = selector;
|
||||||
|
}
|
||||||
|
|
||||||
|
get selectors() {
|
||||||
|
return buildSelectors(this.local_selectors, this.subduxes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default Updux;
|
export default Updux;
|
||||||
|
Loading…
Reference in New Issue
Block a user