add mutations
This commit is contained in:
parent
b4a96b67ea
commit
9255548326
@ -8,9 +8,9 @@ module.exports = {
|
||||
browser: true,
|
||||
},
|
||||
plugins: ['todo-plz', 'no-only-tests'],
|
||||
overrides: [
|
||||
],
|
||||
overrides: [],
|
||||
rules: {
|
||||
'no-console': ['error'],
|
||||
'todo-plz/ticket-ref': ['error', { pattern: 'GT[0-9]+' }],
|
||||
'no-only-tests/no-only-tests': [
|
||||
'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 u from 'updeep';
|
||||
import { createStore as reduxCreateStore } from 'redux';
|
||||
|
||||
import { buildSelectors } from './selectors.js';
|
||||
import { buildUpreducer } from './upreducer.js';
|
||||
import { action } from './actions.js';
|
||||
|
||||
function isActionGen(action) {
|
||||
@ -34,7 +36,11 @@ export class Updux {
|
||||
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) {
|
||||
@ -73,19 +79,43 @@ export class Updux {
|
||||
}
|
||||
|
||||
get upreducer() {
|
||||
return (action) => (state) => {
|
||||
const mutation = this.#mutations[action.type];
|
||||
|
||||
if (mutation) {
|
||||
state = mutation(action.payload, action)(state);
|
||||
}
|
||||
|
||||
return state;
|
||||
};
|
||||
return buildUpreducer(this.#mutations, this.#subduxes);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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) {
|
||||
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) {
|
||||
|
@ -2,25 +2,23 @@ import { test, expect } from 'vitest';
|
||||
|
||||
import { dux } from './Updux.js';
|
||||
|
||||
test( "basic selectors", () => {
|
||||
|
||||
test('basic selectors', () => {
|
||||
const foo = dux({
|
||||
initial: {
|
||||
x: 1,
|
||||
},
|
||||
selectors: {
|
||||
getX: ({x}) => x,
|
||||
getX: ({ x }) => x,
|
||||
},
|
||||
subduxes: {
|
||||
bar: {
|
||||
initial: { y: 2 },
|
||||
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 { Updux } from './Updux.js';
|
||||
import { Updux, dux } from './Updux.js';
|
||||
|
||||
test('set a mutation', () => {
|
||||
const dux = new Updux({
|
||||
@ -17,14 +17,57 @@ test('set a mutation', () => {
|
||||
},
|
||||
});
|
||||
|
||||
dux.setMutation(dux.actions.foo, (payload, action) =>
|
||||
u({
|
||||
dux.setMutation(dux.actions.foo, (payload, action) => {
|
||||
expect(payload).toEqual({ x: 'hello ' });
|
||||
expect(action).toEqual(dux.actions.foo('hello '));
|
||||
return u({
|
||||
x: payload.x + action.type,
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
const result = dux.reducer(undefined, dux.actions.foo('hello '));
|
||||
expect(result).toEqual({
|
||||
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(
|
||||
localSelectors,
|
||||
splatSelector = {},
|
||||
subduxes = {}
|
||||
subduxes = {},
|
||||
) {
|
||||
const subSelectors = Object.entries(subduxes).map(([slice,{ selectors }]) => {
|
||||
const subSelectors = Object.entries(subduxes).map(
|
||||
([slice, { selectors }]) => {
|
||||
if (!selectors) return {};
|
||||
if (slice === '*') return {};
|
||||
|
||||
return R.mapValues(
|
||||
selectors,
|
||||
(func) => (state) => func(state[slice])
|
||||
(func) => (state) => func(state[slice]),
|
||||
);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
let splat = {};
|
||||
|
||||
@ -28,10 +30,10 @@ export function buildSelectors(
|
||||
res,
|
||||
mapValues(
|
||||
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