Skip to content

Commit

Permalink
Add support wrappers
Browse files Browse the repository at this point in the history
  • Loading branch information
axules committed Jul 19, 2020
1 parent b678e9a commit c8bb2dc
Show file tree
Hide file tree
Showing 6 changed files with 324 additions and 24 deletions.
107 changes: 98 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
# redux-mill

1. [What is it?](#what-is-it)
1. [Reasons to use it](#you-should-try-it-if)
2. [Installation](#installation)
3. [Using](#Using)
2. [Reasons to use it](#you-should-try-it-if)
3. [Installation](#installation)
4. [Using](#Using)
- [config.reducerWrapper](#use-with-reducerWrapper)
- [config.actionWrapper](#use-with-actionWrapper)
- [As simple object](#reducer-is-simple-object)
- [As instance of function](#reducer-is-instance-of-function)
- [As instance of class](#reducer-is-instance-of-class)
4. [Without and with comparison](#without-and-with-comparison)
5. [Without and with comparison](#without-and-with-comparison)
6. [Tests](./src/tests)
- [redux-mill](./src/tests/index.test.js)
- [config.reducerWrapper](./src/tests/reducerWrapper.test.js)
- [config.actionWrapper](./src/tests/actionWrapper.test.js)


## What is it?
Expand Down Expand Up @@ -38,13 +44,14 @@ It is easy to understand but there are some details.

const reducer = {
EDIT_TITLE: (state, title) => ({ ...state, title }),
// ACTION_TYPE: (state, payload, action) => ({ ...state, ...}),
SAVE: {
// it is for SAVE action
0: state => ({ state, saving: true, error: "" }),
// it is for SAVE__END action
END: (state, payload) => ({ ...state, saving: false }),
END: (state, payload, action) => ({ ...state, saving: false }),
// it is for SAVE__FAIL action
FAIL: (state, payload) => ({ ...state, saving: false, error: payload.error })
FAIL: (state, payload, action) => ({ ...state, saving: false, error: payload.error })
}
// ...
};
Expand Down Expand Up @@ -131,6 +138,7 @@ It will be object with the same structure, but each value will be action creator

```javascript
import reduxMill from 'redux-mill';

const reducer = {
EDIT_TITLE: (state, title) => ({ ...state, title }),
SAVE: {
Expand All @@ -143,10 +151,10 @@ It will be object with the same structure, but each value will be action creator
initialState,
reducer,
'myStoreName',
{ divider: "__" }
{ divider: "__" },
);

// what will be in reducer object after reduxMill calling
// what will be in `reducer` object after reduxMill calling
{
EDIT_TITLE: function(payload) {
this._ = 'EDIT_TITLE';
Expand Down Expand Up @@ -221,19 +229,100 @@ reduxMill(`initialState`, `reducer`, `storeName`, `options`): function(selector:
| `stateDebug` | Boolean | false | enable debug state changes console logging |
| `divider` | String | '_' | it is divider between parent key and children subkey |
| `mainKey` | String | 0 | it is key for main handler for parent action name |
| `reducerWrapper` | Function(initState, Function) | | It is function wrapper for your reducer (e.g. immer) |
| `actionWrapper` | Function(Function) | | It is function wrapper for each action |

#### It returns function that is factory of selectors for REDUX[storeName]

```javascript
const store = reduxMill(initialState, reducer, 'storeName');
const selectItem = store((state, id) => state.items[id]);
// it return function(state[, ...args])
// it returns function(state, id)
const selectItem2 = (state, id) => state['storeName'].items[id];
// selectItem the same that selectItem2
// state['storeName'] - is our current store
// ...
```

### Use with `reducerWrapper`
If you want to wrap your reducer, then you should use `reducerWrapper`. When we make reducer function we will wrap it into `reducerWrapper(initialState, reducerFunction)`.
`reducerWrapper` should return `Function(state, action)`.

```javascript
import reduxMill from 'redux-mill';

function wrapReducer(baseState, callback) {
// calback - it is reducer function(state, action), returns state
console.log('your reudcer was wrapped', baseState, callback);
return (state = baseState, action) => {
console.log('reducer called with action: ', action);
console.log('we will generate new state each time!');
// don't repeat this trick with JSON.parse(JSON.stringfy(...))
// it will be applied for any result of reducer
// and for current state if this reducer doesn't contain required action
return JSON.parse(JSON.stringfy(callback(state, action)));
}
}

const initialState = {
title: '',
items: [
{ note: '11111' },
]
}

const reducer = {
setTitle: (draft, payload) => { draft.title = payload }
setNote: (draft, payload) => { draft.items[0].note = payload }
}

const store = reduxMill(initialState, reducer, 'storeName', { reducerWrapper: wrapReducer });
```

[Test cases for `reducerWrapper`](./src/tests/reducerWrapper.test.js)


### Use with `actionWrapper`
If you want to wrap each action handler separately then you should use `actionWrapper`. Each action handler will be wrapped into `actionWrapper(actionHandler)`. We expect that `actionHandler` is Function(state, action). `actionWrapper` should return `Function(state, [payload, action])`.

```javascript
import reduxMill from 'redux-mill';

function wrapAction(callback) {
// calback - it is action function(state, payload, action), returns state
console.log('your action handler was wrapped', callback);
return (state, payload, action) => {
console.log('Action was called: ', action.type);
console.log('we will generate new state');
// I don't recomend to use this trick with JSON.parse(JSON.stringfy(...))
// But you can, because it will be called only for actions, not for default
return JSON.parse(JSON.stringfy(callback(state, payload, action)));
}
}

const initialState = {
title: '',
items: [
{ note: '11111' },
]
}

const reducer = {
setTitle: (draft, payload) => { draft.title = payload },
setNote: (draft, payload) => { draft.items[0].note = payload }
}

const store = reduxMill(initialState, reducer, 'storeName', { actionWrapper: wrapAction });

// if you wont to use actionWrapper, then you should wrap each handler by your self, it is the same
// const reducer = {
// setTitle: produceAction((draft, { payload }) => { draft.title = payload }),
// setNote: produceAction((draft, { payload }) => { draft.items[0].note = payload })
// }
```

[Test cases for `actionWrapper`](./src/tests/actionWrapper.test.js)

## `reducer` is simple object

```js
Expand Down
52 changes: 44 additions & 8 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,14 @@ function consoleDebugLog() {
* @param {Object} initialState - default state
* @param {Object} rules - Your reducer. Object or instance of class
* @param {String} name - it is name for redux store
* @param {Object} config - additional options { debug, stateDebug, divider, mainKey }
* @param {Object} config - additional options {
* debug: Bool, default is false,
* stateDebug: Bool, default is false,
* divider: String, default is '_',
* mainKey: String or Number, default is 0,
* reducerWrapper: Function(initState, Function(state, action)):Function(state, action),
* actionWrapper: Function(Function(state, payload, action)):Function(state, payload, action)
* }
* @returns {Object} - function(selector: function(store, ...args))
*/

Expand All @@ -85,23 +92,42 @@ function _default(initialState, rules, name, config) {
debug = _defaultConfig$config.debug,
divider = _defaultConfig$config.divider,
stateDebug = _defaultConfig$config.stateDebug,
mainKey = _defaultConfig$config.mainKey;
mainKey = _defaultConfig$config.mainKey,
reducerWrapper = _defaultConfig$config.reducerWrapper,
actionWrapper = _defaultConfig$config.actionWrapper;

var debugLog = debug ? consoleDebugLog : function () {
return null;
}; // ----------------------------------------------------------
};
if (reducerWrapper && !isFunction(reducerWrapper)) throw new Error('reducerWrapper should be Function');
if (actionWrapper && !isFunction(actionWrapper)) throw new Error('actionWrapper should be Function');

function prepareAction(action) {
var wrapped = actionWrapper ? actionWrapper(action.bind(void 0)) : action.bind(void 0);

if (actionWrapper && !isFunction(wrapped)) {
throw new Error('actionWrapper should return Function(state, action)');
}

return wrapped;
} // ----------------------------------------------------------
// returns all of cases for rules-reducer

function getCases(rules, path) {
var _ref;

function getCases(rules, path) {
if (path === void 0) {
path = '';
}

checkAllowedType(rules);
if (!rules) return {};
if (isFunction(rules)) return _ref = {}, _ref[path] = rules.bind(void 0), _ref;

if (isFunction(rules)) {
var _ref;

return _ref = {}, _ref[path] = prepareAction(rules), _ref;
}

return Object.entries(rules).reduce(function (R, _ref2) {
var key = _ref2[0],
value = _ref2[1];
Expand All @@ -111,7 +137,7 @@ function _default(initialState, rules, name, config) {
throw new Error("[" + path + "." + key + "] should be function. Because it is [" + path + "] reducer's base handler");
}

R[path] = value.bind(void 0);
R[path] = prepareAction(value);
} else {
Object.assign(R, getCases(value, makeType(path, divider, key)));
}
Expand Down Expand Up @@ -193,6 +219,16 @@ function _default(initialState, rules, name, config) {
return state;
};

if (reducerWrapper) {
debugLog('Reducer will be wrapped into reducerWrapper', cases);
}

var wrappedReducer = reducerWrapper ? reducerWrapper(initialState, reducer) : reducer;

if (!isFunction(wrappedReducer)) {
throw new Error('reducerWrapper should return Function(state, action)');
}

var customSelector = function customSelector(selector) {
return function (state) {
for (var _len2 = arguments.length, args = new Array(_len2 > 1 ? _len2 - 1 : 0), _key2 = 1; _key2 < _len2; _key2++) {
Expand All @@ -203,6 +239,6 @@ function _default(initialState, rules, name, config) {
};
};

customSelector[name] = reducer;
customSelector[name] = wrappedReducer;
return customSelector;
}
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "redux-mill",
"version": "1.0.3",
"version": "1.1.1",
"author": {
"name": "Shelest Denis",
"email": "[email protected]"
Expand All @@ -16,7 +16,7 @@
],
"main": "./dist/index.js",
"deprecated": false,
"description": "The mutate method returns new object with changes",
"description": "It is the easy way to use redux by one object",
"keywords": [
"react redux",
"redux",
Expand Down
44 changes: 39 additions & 5 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,22 +47,46 @@ export function consoleDebugLog(...args) {
* @param {Object} initialState - default state
* @param {Object} rules - Your reducer. Object or instance of class
* @param {String} name - it is name for redux store
* @param {Object} config - additional options { debug, stateDebug, divider, mainKey }
* @param {Object} config - additional options {
* debug: Bool, default is false,
* stateDebug: Bool, default is false,
* divider: String, default is '_',
* mainKey: String or Number, default is 0,
* reducerWrapper: Function(initState, Function(state, action)):Function(state, action),
* actionWrapper: Function(Function(state, payload, action)):Function(state, payload, action)
* }
* @returns {Object} - function(selector: function(store, ...args))
*/
export default function(initialState, rules, name, config = {}) {
if (typeof rules !== 'object') throw new Error('Reducer should be Object');
const { debug, divider, stateDebug, mainKey } = {
const { debug, divider, stateDebug, mainKey, reducerWrapper, actionWrapper } = {
...defaultConfig,
...config
};
const debugLog = debug ? consoleDebugLog : () => null;
if (reducerWrapper && !isFunction(reducerWrapper)) throw new Error('reducerWrapper should be Function');
if (actionWrapper && !isFunction(actionWrapper)) throw new Error('actionWrapper should be Function');

function prepareAction(action) {
const wrapped = actionWrapper
? actionWrapper(action.bind(void 0))
: action.bind(void 0);
if (actionWrapper && !isFunction(wrapped)) {
throw new Error('actionWrapper should return Function(state, action)');
}
return wrapped;
}

// ----------------------------------------------------------
// returns all of cases for rules-reducer
function getCases(rules, path = '') {
checkAllowedType(rules);
if (!rules) return {};
if (isFunction(rules)) return { [path]: rules.bind(void 0) };
if (isFunction(rules)) {
return {
[path]: prepareAction(rules)
};
}

return Object.entries(rules).reduce((R, [key, value]) => {
if (key == mainKey) {
Expand All @@ -71,7 +95,7 @@ export default function(initialState, rules, name, config = {}) {
`[${path}.${key}] should be function. Because it is [${path}] reducer's base handler`
);
}
R[path] = value.bind(void 0);
R[path] = prepareAction(value);
} else {
Object.assign(R, getCases(value, makeType(path, divider, key)));
}
Expand Down Expand Up @@ -129,13 +153,23 @@ export default function(initialState, rules, name, config = {}) {
return state;
};

if (reducerWrapper) {
debugLog('Reducer will be wrapped into reducerWrapper', cases);
}
const wrappedReducer = reducerWrapper
? reducerWrapper(initialState, reducer)
: reducer;
if (!isFunction(wrappedReducer)) {
throw new Error('reducerWrapper should return Function(state, action)');
}

const customSelector = function(selector) {
return function(state, ...args) {
return selector(state[name], ...args);
};
};

customSelector[name] = reducer;
customSelector[name] = wrappedReducer;

return customSelector;
}
Loading

0 comments on commit c8bb2dc

Please sign in to comment.