Merge branch 'setMutation' into rewrite

This commit is contained in:
Yanick Champoux 2022-08-28 12:54:27 -04:00
commit af6db3403c
8 changed files with 164 additions and 40 deletions

View File

@ -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
View File

@ -0,0 +1,4 @@
- setMutation
- check that the mutations mutate
- documentation generator (mkdocs + jsdoc-to-markdown)
- createStore

View File

@ -60,3 +60,15 @@ Once an action is defined, its creator is accessible via the `actions` accessor.
console.log( todosDux.actions.addTodo('write tutorial') );
// prints { type: 'addTodo', payload: 'write tutorial' }
```
### Adding a mutation
Mutations are the reducing functions associated to actions. They
are defined via the `setMutation` method:
```js
todosDux.setMutation( 'addTodo', description => ({next_id: id, todos}) => ({
next_id: 1 + id,
todos: [...todos, { description, id, done: false }]
}));
```

View File

@ -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) {

View File

@ -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);
});

View File

@ -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');
});

View File

@ -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
View 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);
};
}