tests are working!
This commit is contained in:
parent
a934133cc1
commit
f24464a590
233
README.md
233
README.md
@ -17,7 +17,238 @@ All that to say, I had some fun yesterday and hacked a proto-lovechild
|
||||
of `Rematch` and `Updeep`, with a dash of [VueX][] inspiration.
|
||||
I call it... `Updux`.
|
||||
|
||||
## Example
|
||||
# Synopsis
|
||||
```
|
||||
import updux from 'updux';
|
||||
|
||||
import otherUpdux from './otherUpdux';
|
||||
|
||||
const {
|
||||
initial,
|
||||
reducer,
|
||||
actions,
|
||||
middleware,
|
||||
createStore,
|
||||
} = updux({
|
||||
initial: {},
|
||||
subduxes: {
|
||||
otherUpdux,
|
||||
},
|
||||
mutations: {
|
||||
},
|
||||
effects: {
|
||||
},
|
||||
|
||||
})
|
||||
```
|
||||
|
||||
# Description
|
||||
|
||||
`Updux` exports one function, `updux`, both as a named export and as
|
||||
its default export.
|
||||
|
||||
## helpers = updux(config);
|
||||
|
||||
`updux` is a way to minimize and simplify the boilerplate associated with the
|
||||
creation of a `Redux` store. It takes a shorthand configuration
|
||||
object, and generates the appropriate reducer, actions, middleware, etc.
|
||||
In true `Redux`-like fashion, just like reducers can be composed
|
||||
of sub-reducers, upduxs can be made of sub-upduxs.
|
||||
|
||||
### config
|
||||
|
||||
The config object recognize following properties.
|
||||
|
||||
#### initial
|
||||
|
||||
The default initial state of the reducer. Can be anything your
|
||||
heart desires.
|
||||
|
||||
#### subduxes
|
||||
|
||||
Object mapping slices of the state to sub-upduxs.
|
||||
|
||||
For example, if in plain Redux you would do
|
||||
|
||||
```
|
||||
import { combineReducers } from 'redux';
|
||||
import todosReducer from './todos';
|
||||
import statisticsReducer from './statistics';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
todos: todosReducer,
|
||||
stats: statisticsReducer,
|
||||
});
|
||||
```
|
||||
|
||||
then with Updux you'd do
|
||||
|
||||
```
|
||||
import { updux } from 'updux';
|
||||
import todos from './todos';
|
||||
import statistics from './statistics';
|
||||
|
||||
const rootUpdux = updux({
|
||||
subduxes: {
|
||||
todos, statistics
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### mutations
|
||||
|
||||
Object mapping actions to the associated state mutation.
|
||||
|
||||
For example, in `Redux` you'd do
|
||||
|
||||
```
|
||||
function todosReducer(state=[],action) {
|
||||
|
||||
switch(action.type) {
|
||||
case 'ADD': return [ ...state, action.payload ];
|
||||
|
||||
case 'DONE': return state.map( todo => todo.id === action.payload
|
||||
? { ...todo, done: true } : todo )
|
||||
|
||||
default: return state;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With Updux:
|
||||
|
||||
```
|
||||
const todosUpdux = updux({
|
||||
mutations: {
|
||||
add: todo => state => [ ...state, todo ],
|
||||
done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) )
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
The signature of the mutations is `(payload,action) => state => newState`.
|
||||
It is designed to play well with `Updeep`. This way, instead of doing
|
||||
|
||||
```
|
||||
mutation: {
|
||||
renameTodo: newName => state => { ...state, name: newName }
|
||||
}
|
||||
```
|
||||
|
||||
we can do
|
||||
|
||||
```
|
||||
mutation: {
|
||||
renameTodo: newName => u({ name: newName })
|
||||
}
|
||||
```
|
||||
|
||||
Also, the special key `*` can be used to match any
|
||||
action not explicitly matched by other mutations.
|
||||
|
||||
```
|
||||
const todosUpdux = updux({
|
||||
mutations: {
|
||||
add: todo => state => [ ...state, todo ],
|
||||
done: done_id => u.map( u.if( ({id} => id === done_id), {done: true} ) ),
|
||||
'*' (payload,action) => state => {
|
||||
console.warn( "unexpected action ", action.type );
|
||||
return state;
|
||||
},
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
#### effects
|
||||
|
||||
Plain object defining asynchronous actions and side-effects triggered by actions.
|
||||
The effects themselves are Redux middleware, expect with the `dispatch`
|
||||
property of the first argument augmented with all the available actions.
|
||||
|
||||
```
|
||||
updux({
|
||||
effects: {
|
||||
fetch: ({dispatch}) => next => async (action) => {
|
||||
next(action);
|
||||
|
||||
let result = await fetch(action.payload.url).then( result => result.json() );
|
||||
dispatch.fetchSuccess(result);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
|
||||
## return value
|
||||
|
||||
`updux` returns an object with the following properties:
|
||||
|
||||
### initial
|
||||
|
||||
Default initial state of the reducer. If applicable, merge
|
||||
the initial states of `config` and `subduxes`, with
|
||||
`config` having precedence over `subduxes`.
|
||||
|
||||
If nothing was given, defaults to an empty object.
|
||||
|
||||
### reducer
|
||||
|
||||
A Redux reducer generated using the computed initial state and
|
||||
mutations.
|
||||
|
||||
### mutations
|
||||
|
||||
Merge of the config and subduxes mutations. If an action trigger
|
||||
mutations in both the main updux and its subduxes, the subduxes
|
||||
mutations will be performed first.
|
||||
|
||||
### actions
|
||||
|
||||
Action creators for all actions used in the mutations, effects and subdox
|
||||
of the updox config.
|
||||
|
||||
The action creators have the signature `(payload={},meta={}) => ({type,
|
||||
payload,meta})` (with the extra sugar that if `meta` or `payload` are not
|
||||
specified, the key is not present in the produced action).
|
||||
|
||||
### middleware
|
||||
|
||||
A middleware aggregating all the effects defined in the
|
||||
updox and its subduxes. Effects of the updox itself are
|
||||
done before the subdoxes effects.
|
||||
|
||||
### createStore
|
||||
|
||||
Same as doing
|
||||
|
||||
```
|
||||
import { createStore, applyMiddleware } from 'redux';
|
||||
|
||||
const { initial, reducer, middleware, actions } = updox(...);
|
||||
|
||||
const store = createStore( initial, reducer, applyMiddleware(middleware) );
|
||||
|
||||
for ( let type in actions ) {
|
||||
store.dispatch[type] = (...args) => {
|
||||
store.dispatch(actions[type](...args))
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
So that later on you can do
|
||||
|
||||
```
|
||||
store.dispatch.addTodo(...);
|
||||
|
||||
// still work
|
||||
store.dispatch( actions.addTodo(...) );
|
||||
```
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
# Example
|
||||
|
||||
#### battle.js
|
||||
|
||||
|
@ -1,4 +1,5 @@
|
||||
module.exports = {
|
||||
"plugins": [["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }]],
|
||||
presets: [
|
||||
[
|
||||
'@babel/preset-env',
|
||||
|
@ -11,5 +11,8 @@
|
||||
"lodash": "^4.17.15",
|
||||
"redux": "^4.0.4",
|
||||
"updeep": "^1.2.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/plugin-proposal-pipeline-operator": "^7.5.0"
|
||||
}
|
||||
}
|
||||
|
100
src/index.js
100
src/index.js
@ -9,12 +9,12 @@ function actionFor(type) {
|
||||
};
|
||||
}
|
||||
|
||||
function buildInitial({initial = {}, reducers = {}}) {
|
||||
function buildInitial({initial = {}, subduxes = {}}) {
|
||||
let state = initial;
|
||||
|
||||
if (fp.isPlainObject(initial)) {
|
||||
initial = fp.mergeAll([
|
||||
fp.mapValues(fp.getOr({}, 'initial'), reducers),
|
||||
fp.mapValues(fp.getOr({}, 'initial'), subduxes),
|
||||
initial,
|
||||
]);
|
||||
}
|
||||
@ -22,8 +22,8 @@ function buildInitial({initial = {}, reducers = {}}) {
|
||||
return initial;
|
||||
}
|
||||
|
||||
function buildActions({mutations = {}, reducers = {}}) {
|
||||
let actions = fp.mergeAll(fp.map(fp.getOr({}, 'actions'), reducers)) || {};
|
||||
function buildActions({mutations = {}, subduxes = {}}) {
|
||||
let actions = fp.mergeAll(fp.map(fp.getOr({}, 'actions'), subduxes)) || {};
|
||||
|
||||
Object.keys(mutations).forEach(type => {
|
||||
if (!actions[type]) {
|
||||
@ -34,41 +34,71 @@ function buildActions({mutations = {}, reducers = {}}) {
|
||||
return actions;
|
||||
}
|
||||
|
||||
function buildMutations({mutations = {}, reducers = {}}) {
|
||||
let subMut = {};
|
||||
const composeMutations = (m1,m2) =>
|
||||
(payload=null,action={}) => state => m2(payload,action)(
|
||||
m1(payload,action)(state) );
|
||||
|
||||
for (let slice in reducers) {
|
||||
for (let mutation in reducers[slice].mutations) {
|
||||
subMut = u(
|
||||
{
|
||||
[mutation]: {
|
||||
[slice]: u.constant(reducers[slice].mutations[mutation]),
|
||||
},
|
||||
},
|
||||
subMut,
|
||||
);
|
||||
}
|
||||
}
|
||||
function buildMutations({mutations = {}, subduxes = {}}) {
|
||||
// we have to differentiate the subduxes with '*' than those
|
||||
// without, as the root '*' is not the same as any sub-'*'
|
||||
|
||||
subMut = fp.mapValues(updates => action =>
|
||||
u(fp.mapValues(f => f(action))(updates)),
|
||||
)(subMut);
|
||||
const actions = fp.uniq( Object.keys(mutations).concat(
|
||||
...Object.values( subduxes ).map( ({mutations}) => Object.keys(mutations) )
|
||||
) );
|
||||
|
||||
for (let name in mutations) {
|
||||
if (subMut[name]) {
|
||||
const pre = subMut[name];
|
||||
// let's seed with noops
|
||||
let mergedMutations = {};
|
||||
|
||||
subMut[name] = action => state =>
|
||||
mutations[name](action)(pre(action)(state));
|
||||
} else {
|
||||
subMut[name] = mutations[name];
|
||||
}
|
||||
}
|
||||
actions.forEach( action => {
|
||||
mergedMutations[action] = () => state => state;
|
||||
});
|
||||
|
||||
|
||||
console.log(mergedMutations);
|
||||
|
||||
Object.entries( subduxes ).forEach( ([slice, {mutations,reducer}]) => {
|
||||
|
||||
if( mutations['*'] ) {
|
||||
const localized = (payload=null,action={}) => u.updateIn( slice, mutations['*'](payload,action) );
|
||||
console.log("b");
|
||||
mergedMutations = fp.mapValues(
|
||||
mutation => composeMutations(
|
||||
(dummy,action) => u.updateIn(slice,
|
||||
state => reducer(state,action)
|
||||
), mutation )
|
||||
)(mergedMutations);
|
||||
return;
|
||||
}
|
||||
|
||||
Object.entries(mutations).forEach(([type,mutation]) => {
|
||||
const localized = (payload=null,action={}) => u.updateIn( slice, mutation(payload,action) );
|
||||
|
||||
if( type !== '*' ) {
|
||||
console.log("a");
|
||||
|
||||
mergedMutations[type] = composeMutations(
|
||||
localized, mergedMutations[type]
|
||||
)
|
||||
}
|
||||
else {
|
||||
}
|
||||
})
|
||||
});
|
||||
console.log(mergedMutations);
|
||||
|
||||
Object.entries(mutations).forEach(([type,mutation]) => {
|
||||
console.log(type,":",mutation,":",mergedMutations[type]);
|
||||
|
||||
mergedMutations[type] = composeMutations(
|
||||
mergedMutations[type], mutation
|
||||
)
|
||||
});
|
||||
|
||||
return mergedMutations;
|
||||
|
||||
return subMut;
|
||||
}
|
||||
|
||||
function buildMiddleware({effects={},reducers={}},{actions}) {
|
||||
function buildMiddleware({effects={},subduxes={}},{actions}) {
|
||||
return api => {
|
||||
|
||||
for ( let type in actions ) {
|
||||
@ -79,14 +109,12 @@ function buildMiddleware({effects={},reducers={}},{actions}) {
|
||||
return [
|
||||
...fp.toPairs(effects).map(([type,effect])=> {
|
||||
return api => next => action => {
|
||||
console.log(action);
|
||||
|
||||
if( action.type !== type ) return next(action);
|
||||
|
||||
return effect(api)(next)(action);
|
||||
};
|
||||
}),
|
||||
...fp.map( 'middleware', reducers )
|
||||
...fp.map( 'middleware', subduxes )
|
||||
].filter(x=>x).reduceRight( (next,mw) => mw(api)(next), original_next )
|
||||
}}
|
||||
}
|
||||
@ -100,7 +128,7 @@ function updux(config) {
|
||||
|
||||
dux.mutations = buildMutations(config);
|
||||
|
||||
dux.upreducer = action => state => {
|
||||
dux.upreducer = (action={}) => state => {
|
||||
if (state === null) state = dux.initial;
|
||||
|
||||
const a =
|
||||
|
63
src/splat.test.js
Normal file
63
src/splat.test.js
Normal file
@ -0,0 +1,63 @@
|
||||
import updux from '.';
|
||||
import u from 'updeep';
|
||||
|
||||
const tracer = chr => u({ tracer: s => (s||'') + chr });
|
||||
|
||||
test( 'mutations, simple', () => {
|
||||
const dux = updux({
|
||||
mutations: {
|
||||
foo: () => tracer('a'),
|
||||
'*': (p,a) => { console.log(a); return tracer('b') },
|
||||
},
|
||||
});
|
||||
|
||||
const store = dux.createStore();
|
||||
|
||||
expect(store.getState()).toEqual({ tracer: 'b'});
|
||||
|
||||
store.dispatch.foo();
|
||||
|
||||
expect(store.getState()).toEqual({ tracer: 'ba', });
|
||||
|
||||
store.dispatch({ type: 'bar' });
|
||||
|
||||
expect(store.getState()).toEqual({ tracer: 'bab', });
|
||||
});
|
||||
|
||||
test( 'with subduxes', () => {
|
||||
const dux = updux({
|
||||
mutations: {
|
||||
foo: () => tracer('a'),
|
||||
'*': (dummy,a) => { console.log("got XXX " ,a); return tracer('b') },
|
||||
bar: () => ({bar}) => ({ bar, tracer: bar.tracer })
|
||||
},
|
||||
subduxes: {
|
||||
bar: updux({
|
||||
mutations: {
|
||||
foo: () => tracer('d'),
|
||||
'*': (dummy,a) => { console.log( "got a ", dummy, a ); return tracer('e') },
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const store = dux.createStore();
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
tracer: 'b',
|
||||
bar: { tracer: 'e' } });
|
||||
|
||||
store.dispatch.foo();
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
tracer: 'ba',
|
||||
bar: { tracer: 'ed' } });
|
||||
|
||||
store.dispatch({type: 'bar'});
|
||||
|
||||
expect(store.getState()).toEqual({
|
||||
tracer: 'ede',
|
||||
bar: { tracer: 'ede' } });
|
||||
|
||||
|
||||
});
|
13
src/test.js
13
src/test.js
@ -55,7 +55,7 @@ test( 'sub reducers', () => {
|
||||
});
|
||||
|
||||
const { initial, actions, reducer } = updux({
|
||||
reducers: {
|
||||
subduxes: {
|
||||
foo, bar
|
||||
}
|
||||
});
|
||||
@ -99,7 +99,7 @@ test('precedence between root and sub-reducers', () => {
|
||||
}
|
||||
}
|
||||
},
|
||||
reducers: {
|
||||
subduxes: {
|
||||
foo: updux({
|
||||
initial: {
|
||||
bar: 2,
|
||||
@ -135,26 +135,21 @@ test( 'middleware', async () => {
|
||||
mutations: {
|
||||
inc: (addition) => state => state + addition,
|
||||
doEeet: () => state => {
|
||||
console.log("up", state);
|
||||
|
||||
return state + 'Z';
|
||||
},
|
||||
},
|
||||
effects: {
|
||||
doEeet: api => next => async action => {
|
||||
console.log(api);
|
||||
api.dispatch.inc('a');
|
||||
next(action);
|
||||
await timeout(1000);
|
||||
api.dispatch.inc('c');
|
||||
}
|
||||
},
|
||||
reducers: {
|
||||
subduxes: {
|
||||
foo: updux({
|
||||
effects: {
|
||||
doEeet: (api) => next => action => {
|
||||
console.log("in foo",api);
|
||||
|
||||
api.dispatch({ type: 'inc', payload: 'b'});
|
||||
next(action);
|
||||
}
|
||||
@ -165,8 +160,6 @@ test( 'middleware', async () => {
|
||||
|
||||
const store = createStore();
|
||||
|
||||
console.log(store);
|
||||
|
||||
store.dispatch.doEeet();
|
||||
|
||||
expect(store.getState()).toEqual( 'abZ' );
|
||||
|
Loading…
Reference in New Issue
Block a user