generic actions
This commit is contained in:
parent
27958a6d14
commit
4e4fa13d90
16
src/Updux.d.ts
vendored
16
src/Updux.d.ts
vendored
@ -1,19 +1,17 @@
|
||||
type UpduxConfig<TState> = Partial<{
|
||||
initial: TState;
|
||||
subduxes: Record<string,any>;
|
||||
subduxes: Record<string, any>;
|
||||
actions: Record<string, any>;
|
||||
selectors: Record<string, Function>;
|
||||
mutations: Record<string, Function>;
|
||||
mappedSelectors: Record<string,Function>;
|
||||
effects: Record<string,Function>;
|
||||
reactions: Record<string,Function>;
|
||||
mappedSelectors: Record<string, Function>;
|
||||
effects: Record<string, Function>;
|
||||
reactions: Record<string, Function>;
|
||||
mappedReaction: Function;
|
||||
}>
|
||||
}>;
|
||||
|
||||
|
||||
export class Updux<TState=unknown> {
|
||||
|
||||
constructor( config: UpduxConfig<TState> );
|
||||
export class Updux<TState = unknown> {
|
||||
constructor(config: UpduxConfig<TState>);
|
||||
|
||||
get initial(): TState;
|
||||
get selectors(): unknown;
|
||||
|
40
src/Updux.ts
40
src/Updux.ts
@ -9,12 +9,9 @@ import { buildActions } from './buildActions';
|
||||
import { buildSelectors } from './buildSelectors';
|
||||
import { action } from './actions';
|
||||
import { buildUpreducer } from './buildUpreducer';
|
||||
import {
|
||||
buildMiddleware,
|
||||
augmentMiddlewareApi,
|
||||
} from './buildMiddleware';
|
||||
import { buildMiddleware, augmentMiddlewareApi } from './buildMiddleware';
|
||||
|
||||
import { AggregateDuxState, Dict } from './types';
|
||||
import { AggregateDuxActions, AggregateDuxState, Dict } from './types';
|
||||
|
||||
/**
|
||||
* Configuration object typically passed to the constructor of the class Updux.
|
||||
@ -23,7 +20,7 @@ export interface UpduxConfig<
|
||||
TState = any,
|
||||
TActions = {},
|
||||
TSelectors = {},
|
||||
TSubduxes = {},
|
||||
TSubduxes = {}
|
||||
> {
|
||||
/**
|
||||
* Local initial state.
|
||||
@ -39,7 +36,7 @@ export interface UpduxConfig<
|
||||
/**
|
||||
* Local actions.
|
||||
*/
|
||||
actions?: Record<string, any>;
|
||||
actions?: TActions;
|
||||
|
||||
/**
|
||||
* Local selectors.
|
||||
@ -77,9 +74,9 @@ export interface UpduxConfig<
|
||||
|
||||
export class Updux<
|
||||
TState extends any = {},
|
||||
TActions = {},
|
||||
TActions extends object = {},
|
||||
TSelectors = {},
|
||||
TSubduxes extends object = {},
|
||||
TSubduxes extends object = {}
|
||||
> {
|
||||
/** @type { unknown } */
|
||||
#initial = {};
|
||||
@ -94,7 +91,7 @@ export class Updux<
|
||||
#mappedSelectors = undefined;
|
||||
#mappedReaction = undefined;
|
||||
|
||||
constructor(config: UpduxConfig<TState,TActions,TSelectors,TSubduxes>) {
|
||||
constructor(config: UpduxConfig<TState, TActions, TSelectors, TSubduxes>) {
|
||||
this.#initial = config.initial ?? {};
|
||||
this.#subduxes = config.subduxes ?? {};
|
||||
|
||||
@ -145,7 +142,7 @@ export class Updux<
|
||||
this.#mappedSelectors = {
|
||||
...this.#mappedSelectors,
|
||||
[name]: f,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
get middleware() {
|
||||
@ -158,12 +155,12 @@ export class Updux<
|
||||
}
|
||||
|
||||
/** @member { unknown } */
|
||||
get initial() : AggregateDuxState<TState,TSubduxes> {
|
||||
get initial(): AggregateDuxState<TState, TSubduxes> {
|
||||
return this.#memoInitial(this.#initial, this.#subduxes);
|
||||
}
|
||||
|
||||
get actions(): Record<string, Function> {
|
||||
return this.#memoActions(this.#actions, this.#subduxes);
|
||||
get actions(): AggregateDuxActions<TActions, TSubduxes> {
|
||||
return this.#memoActions(this.#actions, this.#subduxes) as any;
|
||||
}
|
||||
|
||||
get selectors() {
|
||||
@ -333,14 +330,15 @@ export class Updux<
|
||||
}
|
||||
|
||||
createStore(initial?: unknown, enhancerGenerator?: Function) {
|
||||
const enhancer = (enhancerGenerator ?? applyMiddleware)(
|
||||
this.middleware
|
||||
);
|
||||
|
||||
const enhancer = (enhancerGenerator ?? applyMiddleware)(this.middleware);
|
||||
|
||||
const store : {
|
||||
getState: Function & Record<string,Function>,
|
||||
dispatch: Function & Record<string,Function>,
|
||||
selectors: Record<string,Function>,
|
||||
actions: Record<string,Function>,
|
||||
const store: {
|
||||
getState: Function & Record<string, Function>;
|
||||
dispatch: Function & Record<string, Function>;
|
||||
selectors: Record<string, Function>;
|
||||
actions: AggregateDuxActions<TActions, TSubduxes>;
|
||||
} = reduxCreateStore(
|
||||
this.reducer,
|
||||
initial ?? this.initial,
|
||||
|
@ -3,11 +3,11 @@ import { action } from './actions';
|
||||
test('action generators', () => {
|
||||
const foo = action('foo');
|
||||
|
||||
expect(foo.type).toEqual( 'foo');
|
||||
expect(foo()).toMatchObject( { type: 'foo' });
|
||||
expect(foo.type).toEqual('foo');
|
||||
expect(foo()).toMatchObject({ type: 'foo' });
|
||||
|
||||
const bar = action('bar');
|
||||
|
||||
expect(bar.type).toEqual( 'bar');
|
||||
expect(bar()).toMatchObject( { type: 'bar' });
|
||||
expect(bar.type).toEqual('bar');
|
||||
expect(bar()).toMatchObject({ type: 'bar' });
|
||||
});
|
||||
|
@ -1,11 +1,16 @@
|
||||
export type Action<T extends string=string,TPayload=unknown> = {
|
||||
type: T; meta?: Record<string,unknown>; } & (
|
||||
{ payload?: TPayload }
|
||||
)
|
||||
export type Action<T extends string = string, TPayload = unknown> = {
|
||||
type: T;
|
||||
meta?: Record<string, unknown>;
|
||||
} & {
|
||||
payload?: TPayload;
|
||||
};
|
||||
|
||||
export type ActionGenerator<TType extends string = string, TPayloadGen = undefined> = {
|
||||
export type ActionGenerator<
|
||||
TType extends string = string,
|
||||
TPayloadGen = undefined
|
||||
> = {
|
||||
type: TType;
|
||||
} & (TPayloadGen extends (...args:any) => any
|
||||
} & (TPayloadGen extends (...args: any) => any
|
||||
? (...args: Parameters<TPayloadGen>) => {
|
||||
type: TType;
|
||||
payload: ReturnType<TPayloadGen>;
|
||||
@ -22,7 +27,7 @@ export type ActionGenerator<TType extends string = string, TPayloadGen = undefin
|
||||
|
||||
export function action(type, payloadFunction = null) {
|
||||
const generator = function (...payloadArg) {
|
||||
const result :Action = { type };
|
||||
const result: Action = { type };
|
||||
|
||||
if (payloadFunction) {
|
||||
result.payload = payloadFunction(...payloadArg);
|
||||
|
@ -1,9 +1,7 @@
|
||||
import { buildInitial } from '.';
|
||||
|
||||
test('basic', () => {
|
||||
expect(
|
||||
buildInitial({ a: 1 }, { b: { initial: { c: 2 } } })
|
||||
).toMatchObject({
|
||||
expect(buildInitial({ a: 1 }, { b: { initial: { c: 2 } } })).toMatchObject({
|
||||
a: 1,
|
||||
b: { c: 2 },
|
||||
});
|
||||
|
@ -1,16 +1,23 @@
|
||||
import { map, mapValues, merge } from 'lodash';
|
||||
|
||||
export function buildSelectors(localSelectors, splatSelector = {}, subduxes ={}) {
|
||||
export function buildSelectors(
|
||||
localSelectors,
|
||||
splatSelector = {},
|
||||
subduxes = {}
|
||||
) {
|
||||
const subSelectors = map(subduxes, ({ selectors }, slice) => {
|
||||
if (!selectors) return {};
|
||||
if (slice === '*') return {};
|
||||
|
||||
return mapValues(selectors, (func: Function) => (state) => func(state[slice]));
|
||||
return mapValues(
|
||||
selectors,
|
||||
(func: Function) => (state) => func(state[slice])
|
||||
);
|
||||
});
|
||||
|
||||
let splat = {};
|
||||
|
||||
for ( const name in splatSelector ) {
|
||||
for (const name in splatSelector) {
|
||||
splat[name] =
|
||||
(state) =>
|
||||
(...args) => {
|
||||
@ -25,7 +32,7 @@ export function buildSelectors(localSelectors, splatSelector = {}, subduxes ={})
|
||||
)
|
||||
);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return merge({}, ...subSelectors, localSelectors, splat);
|
||||
}
|
||||
|
@ -8,7 +8,8 @@ export function buildUpreducer(initial, mutations, subduxes = {}) {
|
||||
: null;
|
||||
|
||||
return (action) => (state) => {
|
||||
if( !action?.type ) throw new Error("upreducer called with a bad action");
|
||||
if (!action?.type)
|
||||
throw new Error('upreducer called with a bad action');
|
||||
|
||||
let newState = state ?? initial;
|
||||
|
||||
|
@ -32,7 +32,7 @@ test('README.md', () => {
|
||||
});
|
||||
|
||||
test('tutorial', () => {
|
||||
const todosDux = new Updux({
|
||||
const todosDux = new Updux<any, any>({
|
||||
initial: {
|
||||
next_id: 1,
|
||||
todos: [],
|
||||
|
@ -14,9 +14,11 @@ test('basic', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(dux.reducer(undefined, dux.actions.doIt())).toEqual( 'bingo');
|
||||
expect(dux.reducer(undefined, dux.actions.doIt())).toEqual('bingo');
|
||||
|
||||
expect(dux.reducer(undefined, dux.actions.thisToo())).toEqual( 'straight type');
|
||||
expect(dux.reducer(undefined, dux.actions.thisToo())).toEqual(
|
||||
'straight type'
|
||||
);
|
||||
});
|
||||
|
||||
test('override', () => {
|
||||
@ -48,8 +50,7 @@ test('override', () => {
|
||||
undefined
|
||||
);
|
||||
|
||||
expect(state).toMatchObject(
|
||||
{
|
||||
expect(state).toMatchObject({
|
||||
alpha: ['foo', 'bar'],
|
||||
subbie: 1,
|
||||
});
|
||||
@ -77,8 +78,9 @@ test('order of processing', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(dux.reducer(undefined, foo()))
|
||||
.toMatchObject({ x: ['subdux', 'main'] });
|
||||
expect(dux.reducer(undefined, foo())).toMatchObject({
|
||||
x: ['subdux', 'main'],
|
||||
});
|
||||
});
|
||||
|
||||
test('setMutation', () => {
|
||||
@ -89,21 +91,20 @@ test('setMutation', () => {
|
||||
});
|
||||
|
||||
// noop
|
||||
expect(dux.reducer(undefined, foo())).toEqual( '');
|
||||
expect(dux.reducer(undefined, foo())).toEqual('');
|
||||
|
||||
dux.setMutation('foo', () => () => 'foo');
|
||||
|
||||
expect(dux.reducer(undefined, foo())).toEqual( 'foo');
|
||||
|
||||
expect(dux.reducer(undefined, foo())).toEqual('foo');
|
||||
});
|
||||
|
||||
test('setMutation, name as function', () => {
|
||||
const bar = action('bar');
|
||||
const bar = action('bar');
|
||||
|
||||
const dux = new Updux({
|
||||
initial: '',
|
||||
});
|
||||
dux.setMutation(bar, () => () => 'bar');
|
||||
|
||||
expect(dux.reducer(undefined, bar())).toEqual( 'bar');
|
||||
const dux = new Updux({
|
||||
initial: '',
|
||||
});
|
||||
dux.setMutation(bar, () => () => 'bar');
|
||||
|
||||
expect(dux.reducer(undefined, bar())).toEqual('bar');
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ test('basic reducer', () => {
|
||||
|
||||
expect(typeof dux.reducer).toBe('function');
|
||||
|
||||
expect(dux.reducer({ a: 1 }, { type: 'foo' })).toMatchObject({a:1}); // noop
|
||||
expect(dux.reducer({ a: 1 }, { type: 'foo' })).toMatchObject({ a: 1 }); // noop
|
||||
});
|
||||
|
||||
test('basic upreducer', () => {
|
||||
@ -15,7 +15,7 @@ test('basic upreducer', () => {
|
||||
|
||||
expect(typeof dux.upreducer).toBe('function');
|
||||
|
||||
expect(dux.upreducer({type:'foo'})({ a: 1 })).toMatchObject({a:1}); // noop
|
||||
expect(dux.upreducer({ type: 'foo' })({ a: 1 })).toMatchObject({ a: 1 }); // noop
|
||||
});
|
||||
|
||||
test('reducer with action', () => {
|
||||
@ -28,5 +28,5 @@ test('reducer with action', () => {
|
||||
},
|
||||
});
|
||||
|
||||
expect(dux.reducer({ a: 1 }, { type: 'inc' })).toMatchObject({a:2});
|
||||
expect(dux.reducer({ a: 1 }, { type: 'inc' })).toMatchObject({ a: 2 });
|
||||
});
|
||||
|
@ -135,8 +135,7 @@ test('splat subscriptions, more', () => {
|
||||
initial: { a: 1 },
|
||||
actions: { foo: null, incAll: null },
|
||||
mutations: {
|
||||
foo: (id) => (state) =>
|
||||
state.a === id ? { ...state, b: 1 } : state,
|
||||
foo: (id) => (state) => state.a === id ? { ...state, b: 1 } : state,
|
||||
incAll: () => (state) => ({ ...state, a: state.a + 1 }),
|
||||
},
|
||||
reactions: [() => snitch],
|
||||
@ -163,16 +162,8 @@ test('splat subscriptions, more', () => {
|
||||
|
||||
expect(snitch).toHaveBeenCalledTimes(2);
|
||||
|
||||
expect(snitch).toHaveBeenCalledWith(
|
||||
{ a: 1 },
|
||||
undefined,
|
||||
expect.anything()
|
||||
);
|
||||
expect(snitch).toHaveBeenCalledWith(
|
||||
{ a: 2 },
|
||||
undefined,
|
||||
expect.anything()
|
||||
);
|
||||
expect(snitch).toHaveBeenCalledWith({ a: 1 }, undefined, expect.anything());
|
||||
expect(snitch).toHaveBeenCalledWith({ a: 2 }, undefined, expect.anything());
|
||||
|
||||
snitch.mockReset();
|
||||
|
||||
@ -194,11 +185,7 @@ test('splat subscriptions, more', () => {
|
||||
|
||||
expect(snitch).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(snitch).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
{ a: 1 },
|
||||
expect.anything()
|
||||
);
|
||||
expect(snitch).toHaveBeenCalledWith(undefined, { a: 1 }, expect.anything());
|
||||
|
||||
// only one subscriber left
|
||||
snitch.mockReset();
|
||||
@ -246,9 +233,7 @@ test('many levels down', () => {
|
||||
mutations: {
|
||||
add: () => (x) => x + 1,
|
||||
},
|
||||
reactions: [
|
||||
(store) => (state) => snitch(state, store),
|
||||
],
|
||||
reactions: [(store) => (state) => snitch(state, store)],
|
||||
},
|
||||
},
|
||||
},
|
||||
|
@ -109,8 +109,7 @@ test('subscription within subduxes', () => {
|
||||
let innerState = jest.fn(() => null);
|
||||
let outerState = jest.fn(() => null);
|
||||
|
||||
const resetMocks = () =>
|
||||
[innerState, outerState].map((f) => f.mockReset());
|
||||
const resetMocks = () => [innerState, outerState].map((f) => f.mockReset());
|
||||
|
||||
const inner = new Updux({
|
||||
initial: 1,
|
||||
|
25
src/types.ts
25
src/types.ts
@ -1,20 +1,35 @@
|
||||
|
||||
export type Dict<T> = Record<string, T>;
|
||||
|
||||
export type UnionToIntersection<U> = (
|
||||
U extends any ? (k: U) => void : never
|
||||
) extends (k: infer I) => void
|
||||
? I
|
||||
: never;
|
||||
|
||||
type Subdux<TState = any> = {
|
||||
initial: TState
|
||||
initial: TState;
|
||||
};
|
||||
|
||||
type StateOf<D> = D extends { initial: infer I } ? I : unknown;
|
||||
type ActionsOf<C> = C extends { actions: infer A } ? {
|
||||
[K in keyof A]: Function
|
||||
} : {};
|
||||
|
||||
type Subduxes = Record<string,Subdux>;
|
||||
type Subduxes = Record<string, Subdux>;
|
||||
|
||||
export type DuxStateSubduxes<C> = C extends { '*': infer I }
|
||||
? {
|
||||
[key: string]: StateOf<I>;
|
||||
[index: number]: StateOf<I>;
|
||||
}
|
||||
: { [K in keyof C]: StateOf<C[K]> };
|
||||
: { [K in keyof C]: StateOf<C[K]> };
|
||||
|
||||
export type AggregateDuxState<TState, TSubduxes> = TState & DuxStateSubduxes<TSubduxes>;
|
||||
export type AggregateDuxState<TState, TSubduxes> = TState &
|
||||
DuxStateSubduxes<TSubduxes>;
|
||||
|
||||
type DuxActionsSubduxes<C> = C extends object ? ActionsOf<C[keyof C]> : unknown;
|
||||
|
||||
type ItemsOf<C> = C extends object ? C[keyof C] : unknown;
|
||||
|
||||
export type AggregateDuxActions<TActions, TSubduxes> = TActions &
|
||||
UnionToIntersection<ActionsOf<ItemsOf<TSubduxes>>>;
|
||||
|
Loading…
Reference in New Issue
Block a user