add mutations
This commit is contained in:
parent
b4a96b67ea
commit
9255548326
@ -8,9 +8,9 @@ module.exports = {
|
|||||||
browser: true,
|
browser: true,
|
||||||
},
|
},
|
||||||
plugins: ['todo-plz', 'no-only-tests'],
|
plugins: ['todo-plz', 'no-only-tests'],
|
||||||
overrides: [
|
overrides: [],
|
||||||
],
|
|
||||||
rules: {
|
rules: {
|
||||||
|
'no-console': ['error'],
|
||||||
'todo-plz/ticket-ref': ['error', { pattern: 'GT[0-9]+' }],
|
'todo-plz/ticket-ref': ['error', { pattern: 'GT[0-9]+' }],
|
||||||
'no-only-tests/no-only-tests': [
|
'no-only-tests/no-only-tests': [
|
||||||
'error',
|
'error',
|
||||||
|
4
TODO
Normal file
4
TODO
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
- setMutation
|
||||||
|
- check that the mutations mutate
|
||||||
|
- documentation generator (mkdocs + jsdoc-to-markdown)
|
||||||
|
- createStore
|
52
src/Updux.js
52
src/Updux.js
@ -1,7 +1,9 @@
|
|||||||
import R from 'remeda';
|
import R from 'remeda';
|
||||||
|
import u from 'updeep';
|
||||||
import { createStore as reduxCreateStore } from 'redux';
|
import { createStore as reduxCreateStore } from 'redux';
|
||||||
|
|
||||||
import { buildSelectors } from './selectors.js';
|
import { buildSelectors } from './selectors.js';
|
||||||
|
import { buildUpreducer } from './upreducer.js';
|
||||||
import { action } from './actions.js';
|
import { action } from './actions.js';
|
||||||
|
|
||||||
function isActionGen(action) {
|
function isActionGen(action) {
|
||||||
@ -34,7 +36,11 @@ export class Updux {
|
|||||||
this.#addSubduxActions(slice, sub),
|
this.#addSubduxActions(slice, sub),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.#selectors = buildSelectors( config.selectors, config.splatSelectors, this.#subduxes );
|
this.#selectors = buildSelectors(
|
||||||
|
config.selectors,
|
||||||
|
config.splatSelectors,
|
||||||
|
this.#subduxes,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#addSubduxActions(_slice, subdux) {
|
#addSubduxActions(_slice, subdux) {
|
||||||
@ -73,19 +79,43 @@ export class Updux {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get upreducer() {
|
get upreducer() {
|
||||||
return (action) => (state) => {
|
return buildUpreducer(this.#mutations, this.#subduxes);
|
||||||
const mutation = this.#mutations[action.type];
|
|
||||||
|
|
||||||
if (mutation) {
|
|
||||||
state = mutation(action.payload, action)(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
return state;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string | Function} action - Action triggering the mutation. If
|
||||||
|
* the action is a string, it has to have been previously declared for this
|
||||||
|
* updux, but if it's a function generator, it'll be automatically added to the
|
||||||
|
* updux if not already present (the idea being that making a typo on a string
|
||||||
|
* is easy, but passing a wrong function very more unlikely).
|
||||||
|
* @param {Function} mutation - Mutating function.
|
||||||
|
* @return {void}
|
||||||
|
*/
|
||||||
setMutation(action, mutation) {
|
setMutation(action, mutation) {
|
||||||
this.#mutations[action.type] = mutation;
|
// TODO option strict: false to make it okay to auto-create
|
||||||
|
// the actions as strings?
|
||||||
|
if (action.type) {
|
||||||
|
if (!this.#actions[action.type]) {
|
||||||
|
this.#actions[action.type] = action;
|
||||||
|
} else if (this.#actions[action.type] !== action) {
|
||||||
|
throw new Error(
|
||||||
|
`action '${action.type}' not defined for this updux or definition is different`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
action = action.type;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.#actions[action]) {
|
||||||
|
throw new Error(`action '${action}' is not defined`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.#mutations[action] = mutation;
|
||||||
|
}
|
||||||
|
|
||||||
|
get mutations() {
|
||||||
|
return this.#mutations;
|
||||||
}
|
}
|
||||||
|
|
||||||
createStore(initial = undefined, enhancerGenerator = undefined) {
|
createStore(initial = undefined, enhancerGenerator = undefined) {
|
||||||
|
@ -2,25 +2,23 @@ import { test, expect } from 'vitest';
|
|||||||
|
|
||||||
import { dux } from './Updux.js';
|
import { dux } from './Updux.js';
|
||||||
|
|
||||||
test( "basic selectors", () => {
|
test('basic selectors', () => {
|
||||||
|
|
||||||
const foo = dux({
|
const foo = dux({
|
||||||
initial: {
|
initial: {
|
||||||
x: 1,
|
x: 1,
|
||||||
},
|
},
|
||||||
selectors: {
|
selectors: {
|
||||||
getX: ({x}) => x,
|
getX: ({ x }) => x,
|
||||||
},
|
},
|
||||||
subduxes: {
|
subduxes: {
|
||||||
bar: {
|
bar: {
|
||||||
initial: { y: 2 },
|
initial: { y: 2 },
|
||||||
selectors: {
|
selectors: {
|
||||||
getY: ({y}) => y
|
getY: ({ y }) => y,
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
expect( foo.selectors.getY({bar:{y:3}} ) ).toBe(3);
|
expect(foo.selectors.getY({ bar: { y: 3 } })).toBe(3);
|
||||||
|
});
|
||||||
} );
|
|
||||||
|
@ -4,7 +4,7 @@ import u from 'updeep';
|
|||||||
|
|
||||||
import { action } from './actions.js';
|
import { action } from './actions.js';
|
||||||
|
|
||||||
import { Updux } from './Updux.js';
|
import { Updux, dux } from './Updux.js';
|
||||||
|
|
||||||
test('set a mutation', () => {
|
test('set a mutation', () => {
|
||||||
const dux = new Updux({
|
const dux = new Updux({
|
||||||
@ -17,14 +17,57 @@ test('set a mutation', () => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
dux.setMutation(dux.actions.foo, (payload, action) =>
|
dux.setMutation(dux.actions.foo, (payload, action) => {
|
||||||
u({
|
expect(payload).toEqual({ x: 'hello ' });
|
||||||
|
expect(action).toEqual(dux.actions.foo('hello '));
|
||||||
|
return u({
|
||||||
x: payload.x + action.type,
|
x: payload.x + action.type,
|
||||||
}),
|
});
|
||||||
);
|
});
|
||||||
|
|
||||||
const result = dux.reducer(undefined, dux.actions.foo('hello '));
|
const result = dux.reducer(undefined, dux.actions.foo('hello '));
|
||||||
expect(result).toEqual({
|
expect(result).toEqual({
|
||||||
x: 'hello foo',
|
x: 'hello foo',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('mutation of a subdux', async () => {
|
||||||
|
const bar = dux({
|
||||||
|
actions: {
|
||||||
|
baz: null,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
bar.setMutation('baz', () => (state) => ({ ...state, x: 1 }));
|
||||||
|
|
||||||
|
const foo = dux({
|
||||||
|
subduxes: { bar },
|
||||||
|
});
|
||||||
|
|
||||||
|
const store = foo.createStore();
|
||||||
|
store.dispatch.baz();
|
||||||
|
expect(store.getState()).toMatchObject({ bar: { x: 1 } });
|
||||||
|
});
|
||||||
|
|
||||||
|
test('strings and generators', async () => {
|
||||||
|
const actionA = action('a');
|
||||||
|
|
||||||
|
const foo = dux({
|
||||||
|
actions: {
|
||||||
|
b: null,
|
||||||
|
a: actionA,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// as a string and defined
|
||||||
|
expect(() => foo.setMutation('a', () => {})).not.toThrow();
|
||||||
|
|
||||||
|
// as a generator and defined
|
||||||
|
expect(() => foo.setMutation(actionA, () => {})).not.toThrow();
|
||||||
|
|
||||||
|
// as a string, not defined
|
||||||
|
expect(() => foo.setMutation('c', () => {})).toThrow();
|
||||||
|
|
||||||
|
foo.setMutation(action('d'), () => {});
|
||||||
|
|
||||||
|
expect(foo.actions.d).toBeTypeOf('function');
|
||||||
|
});
|
||||||
|
@ -3,17 +3,19 @@ import R from 'remeda';
|
|||||||
export function buildSelectors(
|
export function buildSelectors(
|
||||||
localSelectors,
|
localSelectors,
|
||||||
splatSelector = {},
|
splatSelector = {},
|
||||||
subduxes = {}
|
subduxes = {},
|
||||||
) {
|
) {
|
||||||
const subSelectors = Object.entries(subduxes).map(([slice,{ selectors }]) => {
|
const subSelectors = Object.entries(subduxes).map(
|
||||||
|
([slice, { selectors }]) => {
|
||||||
if (!selectors) return {};
|
if (!selectors) return {};
|
||||||
if (slice === '*') return {};
|
if (slice === '*') return {};
|
||||||
|
|
||||||
return R.mapValues(
|
return R.mapValues(
|
||||||
selectors,
|
selectors,
|
||||||
(func) => (state) => func(state[slice])
|
(func) => (state) => func(state[slice]),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
});
|
|
||||||
|
|
||||||
let splat = {};
|
let splat = {};
|
||||||
|
|
||||||
@ -28,10 +30,10 @@ export function buildSelectors(
|
|||||||
res,
|
res,
|
||||||
mapValues(
|
mapValues(
|
||||||
subduxes['*'].selectors,
|
subduxes['*'].selectors,
|
||||||
(selector) => () => selector(value)
|
(selector) => () => selector(value),
|
||||||
)
|
),
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return R.mergeAll([ ...subSelectors, localSelectors, splat ]);
|
return R.mergeAll([...subSelectors, localSelectors, splat]);
|
||||||
}
|
}
|
||||||
|
35
src/upreducer.js
Normal file
35
src/upreducer.js
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
import R from 'remeda';
|
||||||
|
import u from 'updeep';
|
||||||
|
|
||||||
|
const localMutation = (mutations) => (action) => (state) => {
|
||||||
|
const mutation = mutations[action.type];
|
||||||
|
|
||||||
|
if (!mutation) return state;
|
||||||
|
|
||||||
|
return mutation(action.payload, action)(state);
|
||||||
|
};
|
||||||
|
|
||||||
|
const subMutations = (subduxes) => (action) => (state) => {
|
||||||
|
const subReducers =
|
||||||
|
Object.keys(subduxes).length > 0
|
||||||
|
? R.mapValues(subduxes, R.prop('upreducer'))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
if (!subReducers) return state;
|
||||||
|
|
||||||
|
if (subReducers['*']) {
|
||||||
|
return u.updateIn('*', subReducers['*'](action), state);
|
||||||
|
}
|
||||||
|
|
||||||
|
const update = R.mapValues(subReducers, (upReducer) => upReducer(action));
|
||||||
|
|
||||||
|
return u(update, state);
|
||||||
|
};
|
||||||
|
|
||||||
|
export function buildUpreducer(mutations, subduxes) {
|
||||||
|
return (action) => (state) => {
|
||||||
|
state = subMutations(subduxes)(action)(state);
|
||||||
|
|
||||||
|
return localMutation(mutations)(action)(state);
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user