typescript
Yanick Champoux 2021-09-28 18:13:22 -04:00
parent 003a1309bd
commit fd064f5996
6 changed files with 237 additions and 0 deletions

View File

@ -4,6 +4,7 @@
"@yanick/updeep": "../updeep",
"lodash": "^4.17.15",
"lodash-es": "^4.17.21",
"moize": "^6.1.0",
"redux": "^4.0.5",
"ts-action": "^11.0.0",
"ts-node": "^8.6.2",

47
src/Updux.js Normal file
View File

@ -0,0 +1,47 @@
import moize from 'moize';
import u from '@yanick/updeep';
import { buildInitial } from './buildInitial/index.js';
import { buildActions } from './buildActions/index.js';
import { action } from './actions.js';
/**
* @public
* `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, upduxes can be made of sub-upduxes (`subduxes` for short) for different slices of the root state.
*/
export class Updux {
#initial = {};
#subduxes = {};
#actions = {};
constructor(config) {
this.#initial = config.initial ?? {};
this.#subduxes = config.subduxes ?? {};
this.#actions = config.actions ?? {};
}
#memoInitial = moize( buildInitial );
#memoActions = moize(buildActions);
get initial() {
return this.#memoInitial(this.#initial,this.#subduxes);
}
get actions() {
return this.#memoActions(this.#actions, this.#subduxes);
}
addAction(type, payloadFunc) {
const theAction = action(type,payloadFunc);
this.#actions = u( { [type]: theAction }, this.#actions );
return theAction;
}
}

132
src/Updux.test.js Normal file
View File

@ -0,0 +1,132 @@
import { test } from 'tap';
import { Updux } from './Updux.js';
import { action } from './actions.js';
test('basic state', async (t) => {
const alpha = new Updux({
initial: { sub: 1 },
});
const beta = new Updux({
initial: { foo: 1 },
});
const dux = new Updux({
initial: { a: 1, b: 'two' },
subduxes: { alpha, beta },
});
t.same(dux.initial,{
a: 1,
b: 'two',
alpha: { sub: 1 },
beta: { foo: 1 },
});
});
test('basic actions', async (t) => {
const alpha = new Updux({
actions: {
foo: action('foo'),
},
});
const beta = new Updux({});
const dux = new Updux({
subduxes: { alpha, beta },
actions: {
bar: action('bar' ),
},
});
t.same(Object.keys(dux.actions).sort(),['bar', 'foo']);
});
test('addAction', async(t) => {
const dux = new Updux({
actions: {
bar: action('bar'),
},
});
dux.addAction('foo');
t.same(Object.keys(dux.actions).sort(),['bar', 'foo']);
});
test('basic selectors', async(t) => {
const alpha = new Updux({
initial: { quux: 3 },
}).addSelector('getQuux', ({ quux }) => quux);
const dux = new Updux({
initial: {
foo: 1,
bar: 4,
},
subduxes: { alpha },
selectors: {
getBar: ({ bar }) => bar,
},
})
.addSelector('getFoo', (state) => state.foo)
.addSelector('getAdd', ({ foo }) => (add) => add + foo)
.addAction('stuff');
t.equal(dux.selectors.getBar({ bar: 3 }), 3);
t.equal(dux.selectors.getFoo({ foo: 3 }) , 3);
t.equal(alpha.selectors.getQuux({ quux: 1 }),1);
t.equal(dux.selectors.getQuux({ alpha: { quux: 1 } }) ,1);
const store = dux.createStore();
t.equal(store.selectors.getFoo(),1);
t.equal(store.selectors.getQuux(),3);
t.equal(store.selectors.getAdd(7),8);
});
test('mutations', async () => {
const alpha = new Updux({
initial: { quux: 3 },
})
.addAction( 'add' )
.addMutation( 'add', ( toAdd ) => (state) => ({ quux: state.quux + toAdd }) );
const dux = new Updux({
initial: {
foo: 1,
bar: 4,
},
subduxes: { alpha },
})
.addAction( 'subtract' )
.addMutation( 'add', toAdd => state => ({ ...state, foo: state.foo + toAdd }) )
.addMutation( 'subtract', toSubtract => state => ({ ...state, bar: state.bar - toSubtract }) );
const store = dux.createStore();
t.same(store.getState(),{
foo: 1, bar: 4, alpha: { quux: 3 },
})
store.dispatch.add(10);
t.same(store.getState(), {
foo: 11, bar: 4, alpha: { quux: 13 },
})
store.dispatch.subtract(20);
t.same(store.getState(), {
foo: 11, bar: -16, alpha: { quux: 13 },
})
});

19
src/actions.js Normal file
View File

@ -0,0 +1,19 @@
export function action( type, payloadFunction = null ) {
const generator = function(payloadArg) {
const result = { type };
if( payloadFunction )
result.payload = payloadFunction(payloadArg);
return result;
}
generator.type = type;
return generator;
}

17
src/actions.test.js Normal file
View File

@ -0,0 +1,17 @@
import { test } from 'tap';
import { action } from './actions.js';
test( 'action generators', async (t) => {
const foo = action('foo');
t.equal( foo.type, 'foo' );
t.same( foo(), { type: 'foo' } );
const bar = action('bar');
t.equal( bar.type, 'bar' );
t.same( bar(), { type: 'bar' } );
} )

21
src/buildActions/index.js Normal file
View File

@ -0,0 +1,21 @@
export function buildActions(actions={}, subduxes={}) {
// priority => generics => generic subs => craft subs => creators
const merged = { ...actions };
Object.values(subduxes).forEach(({ actions }) => {
if(!actions) return;
Object.entries(actions).forEach(([type, func]) => {
if (merged[type]) {
if (merged[type] === func) return;
throw new Error(
`trying to merge two different actions ${type}`
);
}
merged[type] = func;
});
});
return merged;
}