diff --git a/README.md b/README.md index 74a8628..cb97236 100644 --- a/README.md +++ b/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 diff --git a/babel.config.js b/babel.config.js index f037a1a..83a7a08 100644 --- a/babel.config.js +++ b/babel.config.js @@ -1,4 +1,5 @@ module.exports = { +"plugins": [["@babel/plugin-proposal-pipeline-operator", { "proposal": "minimal" }]], presets: [ [ '@babel/preset-env', diff --git a/package.json b/package.json index de6431b..0166e0b 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/src/index.js b/src/index.js index c02266f..c262377 100644 --- a/src/index.js +++ b/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 = diff --git a/src/splat.test.js b/src/splat.test.js new file mode 100644 index 0000000..113a235 --- /dev/null +++ b/src/splat.test.js @@ -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' } }); + + +}); diff --git a/src/test.js b/src/test.js index a0c3552..32fb945 100644 --- a/src/test.js +++ b/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' );