From 745056abb5e70e5b30104959badc73b135055c6f Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sat, 25 Feb 2017 06:43:47 -0600 Subject: [PATCH 01/75] documentation refinement --- src/docs/guide/conceptConditional.md | 48 +++++++++++++++ src/docs/guide/conceptHash.md | 20 +++---- src/docs/guide/conceptJoin.md | 88 +++++++++++++-------------- src/docs/guide/fullExample.md | 90 +++++++++++++++------------- src/docs/guide/toc.json | 20 +++++-- src/docs/guide/why.md | 25 ++++++++ src/docs/home.md | 51 ++++++++++++---- src/reducer/conditionalReducer.js | 39 +++++++----- src/reducer/joinReducers.js | 7 ++- src/reducer/reducerHash.js | 7 ++- 10 files changed, 262 insertions(+), 133 deletions(-) create mode 100644 src/docs/guide/conceptConditional.md create mode 100644 src/docs/guide/why.md diff --git a/src/docs/guide/conceptConditional.md b/src/docs/guide/conceptConditional.md new file mode 100644 index 0000000..73921d3 --- /dev/null +++ b/src/docs/guide/conceptConditional.md @@ -0,0 +1,48 @@ +There are times where you may wish to conditionally apply a reduction. + +There can be many reasons for this. Take a simple example where you +wish to bypass a reduction process upon determination that an action +will not impact an entire branch of your state tree. In this example +the conditional aspect is purely an optimization. + +This can be accomplished through the {@link conditionalReducer} utility. + +```JavaScript +import * as Redux from 'redux'; +import * as AstxReduxUtil from 'astx-redux-util'; +import x from './myAppReducer.x'; +import y from './myAppReducer.y'; + +const reduceWidget = + AstxReduxUtil.conditionalReducer( + // conditionally apply when action.type begins with 'widget.edit' + (curState, action, originalReducerState) => action.type.startsWith('widget.edit'), + Redux.combineReducers({ + x, + y + })); + +export default function widget(widget={}, action) { + return reduceWidget(widget, action); +} +``` + +Here we only invoke the supplied reducer +([Redux.combineReducers](http://redux.js.org/docs/api/combineReducers.html)) +when the action.type begins with 'widget.edit'. In our contrived +example, our action types are organized with a federated namespace, so +it is easy to isolate which actions will impact various parts of our +state. + +**Please Note** that because we did not supply an "elseReducerFn" (the +third parameter to {@link conditionalReducer}), the default {@link +reducerPassThrough} is used for the else condition, in essence +retaining the same state for a falsy directive. + +**Also Note** that this example is merely intended to introduce you to +the concept of conditional reduction. It is somewhat "contrived", +allowing us to discuss the topic in isolation. In reality, this +example may be inappropriate because the optimization is minimal, and +it tends to make the code more brittle. With that said, however, keep +in mind that there are "more legitimate" reasons to apply conditional +reduction ... we will see this in subsequent discussions. diff --git a/src/docs/guide/conceptHash.md b/src/docs/guide/conceptHash.md index a47a548..0048a1a 100644 --- a/src/docs/guide/conceptHash.md +++ b/src/docs/guide/conceptHash.md @@ -1,26 +1,26 @@ -Reducers commonly reason about the action.type, driving conditional -logic through a switch statement: +Reducers frequently reason about the action.type, very often using a +switch statement to drive conditional logic: ```JavaScript export default function widget(widget=null, action) { switch (action.type) { - case ActionType.widget.edit: + case 'widget.edit': return action.widget; - case ActionType.widget.edit.close: + case 'widget.edit.close': return null; default: - return state; + return widget; } } ``` The {@link reducerHash} function *(the most common composition reducer)* provides a more elegant solution, eliminating the switch -statement altogether. It creates a higher-order reducer, by combining +statement altogether. It creates a higher-order reducer, by combining a set of sub-reducer functions that are indexed by the standard action.type. @@ -29,8 +29,8 @@ action.type. import { reducerHash } from 'astx-redux-util'; const reduceWidget = reducerHash({ - [ActionType.widget.edit] (widget, action) => action.widget, - [ActionType.widget.edit.close] (widget, action) => null, + ['widget.edit'] (widget, action) => action.widget, + ['widget.edit.close'] (widget, action) => null, }); export default function widget(widget=null, action) { @@ -42,11 +42,11 @@ Not only is the conditional logic better encapsulated, but the default pass-through logic is automatically applied ... passing the original state when no action.type is acted on. -**Please note** that because {@link reducerHash} is a higher-order +**Please Note** that because {@link reducerHash} is a higher-order creator function, it is invoked outside the scope of the widget() reducer. This is an optimization, so as to not incur the creation overhead on each reducer invocation. -**Also note** that because {@link reducerHash} is so central to the +**Also Note** that because {@link reducerHash} is so central to the rudimentary aspects of reduction, it is common to provide a value-added {@tutorial logExt}. diff --git a/src/docs/guide/conceptJoin.md b/src/docs/guide/conceptJoin.md index 004d51d..57e8fc2 100644 --- a/src/docs/guide/conceptJoin.md +++ b/src/docs/guide/conceptJoin.md @@ -1,10 +1,10 @@ Occasionally it is necessary for a state to have multiple reductions -applied to it. This typically occurs when the involved reducers +applied to it. Typically this occurs when the reducers involved represent fundamentally different operational types. -Let's review an example to clarify this concept. +An example should clarify this concept. -Say we have a widget state, that contains x/y properties: +Let's say we have a widget that contains x/y properties: ```JavaScript { @@ -15,13 +15,13 @@ Say we have a widget state, that contains x/y properties: } ``` -Typically this would be controlled through a standard -Redux.combineReducers: +The individual x/y properties are nicely managed by the standard +[Redux.combineReducers] function: ```JavaScript import * as Redux from 'redux'; -import x from './myAppReducer.x; -import y from './myAppReducer.y; +import x from './myAppReducer.x'; +import y from './myAppReducer.y'; const contentReducer = Redux.combineReducers({ @@ -34,20 +34,20 @@ export default function widget(widget={}, action) { } ``` -What happens, however, if the widget can take on a null value (say for -example, it only exists when it is being edited)? +However, **what happens if the widget can take on a null value** (say for +example, it only exists when it is being edited)? -In this case, there is more work to do because +In this case, **we have more work to do**: 1. we need to apply the editor open/close logic, and - 2. conditionally apply the Redux.combineReducer because it cannot - operate on null state. + 2. conditionally manage it's content *(because + [Redux.combineReducers] cannot operate on null state)*. -One way to accomplish this is through procedural logic, as follows: +One way to accomplish this is through the following procedural logic: ```JavaScript import * as Redux from 'redux'; -import x from './myAppReducer.x; -import y from './myAppReducer.y; +import x from './myAppReducer.x'; +import y from './myAppReducer.y'; const contentReducer = Redux.combineReducers({ @@ -61,11 +61,11 @@ export default function widget(widget=null, action) { let nextState = widget; switch (action.type) { - case 'editOpen': + case 'widget.edit': nextState = action.widget; break; - case 'editClose': + case 'widget.edit.close': nextState = null; break; @@ -83,37 +83,31 @@ export default function widget(widget=null, action) { } ``` -A more elegant solution can be accomplished using reducer composition, -eliminating the procedural code completely. - -**Enter two new higher-order reducers** *(in addition to {@link -reducerHash} which we have covered previously)*: - -1. {@link joinReducers} combines two or more reducers, logically - executing each in sequence. - -2. {@link conditionalReducer} conditionally executes a reducerFn when - the conditionalFn returns truthy. +A more elegant solution can be accomplished by employing reducer +composition, eliminating the procedural code completely. We have +already discussed {@link reducerHash} and {@link conditionalReducer}. +A third utility, the {@link joinReducers} function, combines two or +more reducers logically executing each in sequence. *The following snippet, is equivalent to the one above:* ```JavaScript import * as Redux from 'redux'; -import * as AstReduxUtil from 'astx-redux-util'; -import x from './myAppReducer.x; -import y from './myAppReducer.y; +import * as AstxReduxUtil from 'astx-redux-util'; +import x from './myAppReducer.x'; +import y from './myAppReducer.y'; const reduceWidget = - AstReduxUtil.joinReducers( - // first reducer: determines content shape (i.e. null or {}) - AstReduxUtil.reducerHash({ - editOpen (widget, action) => action.widget, - editClose(widget, action) => null + AstxReduxUtil.joinReducers( + // first: determine content shape (i.e. null or {}) + AstxReduxUtil.reducerHash({ + ['widget.edit'] (widget, action) => action.widget, + ['widget.edit.close'] (widget, action) => null }), - // second reducer: detailing individual x/y fields - // ... only executed when there is content - AstReduxUtil.conditionalReducer( - (curState, action, originalReducerState) => curState !== null, + // second: maintain individual x/y fields + AstxReduxUtil.conditionalReducer( + // ONLY when there is content + (widget, action, originalReducerState) => widget !== null, Redux.combineReducers({ x, y @@ -128,16 +122,20 @@ export default function widget(widget=null, action) { Here the joinReducers combines multiple reducers together as one. - The first reducer (reducerHash) interprets the - 'editOpen'/'editClose' action.type, providing content shape or not - (i.e. null). + `'widget.edit`/`'widget.edit.close'` action type, providing object + content or not (i.e. null). - The second reducer (conditionalReducer) conditionally invokes the - third reducer (combineReducers), only when the state has content + third reducer ([Redux.combineReducers]), only when the state has content (i.e. non-null). **Reducer composition provides a more elegant solution that is -functional in nature.** +purely functional in nature.** -**Please note** that the higher-order reducer functions are invoked +**Please Note** that the higher-order reducer functions are invoked outside the scope of the widget() reducer, as an optimization, so as to not incur the creation overhead on each reducer invocation. + + + +[Redux.combineReducers]: http://redux.js.org/docs/api/combineReducers.html diff --git a/src/docs/guide/fullExample.md b/src/docs/guide/fullExample.md index 0fbfde0..6d86918 100644 --- a/src/docs/guide/fullExample.md +++ b/src/docs/guide/fullExample.md @@ -1,7 +1,7 @@ If we take our widget example one step further (from our {@tutorial conceptJoin} discussion), let's say in addition to the x/y properties, -we now have a curHash - a determinate of whether application content -has changed. +we now introduce a curHash - which is a determinate of whether +application content has changed. ```JavaScript { @@ -18,46 +18,49 @@ has a unique vantage point for this task, because it is a central clearing house that has knowledge anytime the widget state changes. This is even independent of how many properties the widget has! Our immutable pattern dictates that if our state changes, a new instance -will be created. Therefore, we can safely change the curHash anytime +will be introduced. Therefore, we can safely change the curHash anytime the widget instance has changed. -Building on our last example (in {@tutorial conceptJoin}), we -can accomplish this new requirement by simply adding a third -sub-reducer to our reduceWidget function. +Building on our last example (in {@tutorial conceptJoin}), we can +accomplish this new requirement by simply adding yet another reducer +to our reduceWidget function. ```JavaScript import * as Redux from 'redux'; -import * as AstReduxUtil from 'astx-redux-util'; -import x from './myAppReducer.x; -import y from './myAppReducer.y; +import * as AstxReduxUtil from 'astx-redux-util'; +import x from './myAppReducer.x'; +import y from './myAppReducer.y'; +import Widget from './myWidgetUtil'; const reduceWidget = - AstReduxUtil.joinReducers( - // first reducer: determines content shape (i.e. null or {}) - AstReduxUtil.reducerHash({ - editOpen (widget, action) => action.widget, - editClose(widget, action) => null + AstxReduxUtil.joinReducers( + // first: determine content shape (i.e. null or {}) + AstxReduxUtil.reducerHash({ + ['widget.edit'] (widget, action) => action.widget, + ['widget.edit.close'] (widget, action) => null }), - // second reducer: detailing individucal x/y fields - // ... only executed when there is content - AstReduxUtil.conditionalReducer( - (curState, action, originalReducerState) => curState !== null, - Redux.combineReducers({ - x, - y, - curHash: AstReduxUtil.reducerPassThrough - })), - - // third reducer: maintaining the curHash (NEW from last example) - // ... only executed when widget has changed - AstReduxUtil.conditionalReducer( - (curState, action, originalReducerState) => curState !== null && - originalReducerState !== curState, - (curState, action) => { - curState.curHash = someHashOf(curState); // OK to mutate (different instance) - return curState; - }) + // next ... + AstxReduxUtil.conditionalReducer( + // ... when widget is being edited (i.e. has content) + (widget, action, originalReducerState) => widget !== null, + AstxReduxUtil.joinReducers( + // maintain individual x/y fields + Redux.combineReducers({ + x, + y + }), + // ... NEW from last example + AstxReduxUtil.conditionalReducer( + // ... when widget has changed + (widget, action, originalReducerState) => originalReducerState !== widget, + // maintain curHash + (widget, action) => { + widget.curHash = Widget.hash(widget); // NOTE: OK to mutate (different instance) + return widget; + }) + ) + ) ); export default function widget(widget=null, action) { @@ -70,13 +73,20 @@ Composition** can **simplify your life**! We have combined 3 sub-reducers into one, applying conditional logic as needed through functional decomposition! -**Please note** that we use the {@tutorial originalReducerState} to -determine if the widget has changed from ANY of the prior sub-reducers -(see the discussion of this topic in the provided link). +**Please NOTE**: + +1. The {@tutorial originalReducerState} is used to determine when the + widget has changed from ANY of the prior sub-reducers (see the + discussion of this topic in the provided link). + +2. The curHash should only be maintained when the widget has content + (i.e. non-null). This is accomplished through the **nesting** of + conditionalReducer (the outer one insures the widget is non-null). -**Also note** that contrary to any **red flags** that may have -been raised on your initial glance of the code, **it is OK** to mutate -the curState variable in our third reducer, because we know a new instance -has already been created (via one of the prior reducers). +3. Contrary to any **red flags** that may have been raised on your + initial glance of the code, **it is OK** to mutate the `widget` + state in the last reducer, because we know one of the prior + reducers has injected a new widget instance (via the + `originalReducerState !== widget` condition). **Life is GOOD!** diff --git a/src/docs/guide/toc.json b/src/docs/guide/toc.json index 0e84e1c..964aacf 100644 --- a/src/docs/guide/toc.json +++ b/src/docs/guide/toc.json @@ -7,24 +7,32 @@ "order": 2, "title": "Basics" }, - "conceptJoin": { + "conceptConditional": { "order": 3, - "title": "Composition" + "title": "Conditional Reduction" }, - "fullExample": { + "conceptJoin": { "order": 4, + "title": "Joining Reducers" + }, + "fullExample": { + "order": 5, "title": "A Most Excellent Example" }, + "why": { + "order": 6, + "title": "Why astx-redux-util?" + }, "logExt": { - "order": 5, + "order": 7, "title": "Logging Extension" }, "originalReducerState": { - "order": 6, + "order": 8, "title": "originalReducerState" }, "LICENSE": { - "order": 7, + "order": 9, "title": "MIT License" } } diff --git a/src/docs/guide/why.md b/src/docs/guide/why.md new file mode 100644 index 0000000..6b156d6 --- /dev/null +++ b/src/docs/guide/why.md @@ -0,0 +1,25 @@ +This section provides a brief insight into why astx-redux-util was +created, and how it compares to other similar utilities. + +Some astx-redux-util functions (i.e. {@link reducerHash}) are stock +features in other reducer libraries, such as such as [redux-actions +handleactions](https://www.npmjs.com/package/redux-actions#handleactionsreducermap-defaultstate). +With that said, astx-redux-util promotes other reducer compositions +that can be used in conjunction with one another. + +One underlying reason astx-redux-util was created was to provide my +initial exposure into npm publishing. Because I am new to both Node and +GitHub, and I wanted a small project, where I could focus on the setup +and tooling required for publishing a micro-library. This also +includes various aspects of project documentation (something that is +important to me). + +astx-redux-util was pulled directly out of a sandbox project that I +used to learn a number of technologies and frameworks. + +My hope is to eventually add more functionality from my sandbox +project. One aspect in particular is related to managing action +creation with a twist: consistently maintaining action types in +conjunction with action creators. + +</Kevin> diff --git a/src/docs/home.md b/src/docs/home.md index ca336d2..6230215 100644 --- a/src/docs/home.md +++ b/src/docs/home.md @@ -1,17 +1,48 @@ # astx-redux-util -The astx-redux-util library provides several redux reducer composition -utilities, of which the most prevalent is {@link reducerHash} which -allows you to displace the dreaded switch statement ... *but there is -much more!* +The astx-redux-util library promotes several redux reducer +composition utilities. + +Reducer composition is not new. Redux itself provides the innovative +[combineReducers](http://redux.js.org/docs/api/combineReducers.html) +utility which allows you to blend individul reducers together to build +up the overall shape of your appliction state. + +The most prevalent astx-redux-util utility is {@link reducerHash}, +which allows you to combine sub-reducers in such a way as to eliminate +the switch statement commonly used to delineate action type. + +Additionally, astx-redux-util promotes other reducer compositions that +can be used in conjunction with one another. ## At a Glance - {@tutorial start} ... installation and access -- Basic Concepts - - {@tutorial conceptHash} ... eliminate the reducer switch statement - - {@tutorial conceptJoin} ... team up multiple reducers -- {@tutorial fullExample} ... more complete example of astx-redux-util -- {@tutorial logExt} ... conceptual extension for reducer-based centralized logging -- SideBar: {@tutorial originalReducerState} ... sidebar discussion of originalReducerState + +- Concepts: + + - {@tutorial conceptHash} ... using {@link reducerHash}, eliminate + the switch statement commonly found in reducers *("look ma, no + switch")* + + - {@tutorial conceptConditional} ... using {@link + conditionalReducer}, invoke a reducer only when certain + constraints are met *("to reduce or NOT to reduce; that is the + question")* + + - {@tutorial conceptJoin} ... using {@link joinReducers}, team up + multiple reducers to promote higher order functionality *("working + together is success" - Henry Ford)* + +- {@tutorial fullExample} ... a more complete example employing many + of the astx-redux-util utility functions + +- {@tutorial why} ... why was astx-redux-util created, and how does it + compare to other utilities + +- {@tutorial logExt} ... conceptual extension for reducer-based + centralized logging + +- {@tutorial originalReducerState} ... a sidebar discussion of + originalReducerState diff --git a/src/reducer/conditionalReducer.js b/src/reducer/conditionalReducer.js index 9b45c7b..873192f 100644 --- a/src/reducer/conditionalReducer.js +++ b/src/reducer/conditionalReducer.js @@ -1,32 +1,39 @@ 'use strict'; -import {} from '../reduxAPI'; // TODO: placebo import required for JSDoc (ISSUE: JSDoc seems to require at least one import to expose these items) +import reducerPassThrough from './reducerPassThrough'; + /** - * Create a higher-order reducer that conditionally executes the - * supplied reducerFn, when the conditionalFn returns truthy. - * - * **Examples** of conditionalReducer can be found in {@tutorial - * conceptJoin} and {@tutorial fullExample}. + * Create a higher-order reducer that conditionally executes one of + * the supplied reducerFns, based on the conditionalFn() return + * directive. + * + * The **User Guide** discusses conditionalReducer() in more detail + * (see {@tutorial conceptConditional}), and additional examples can + * be found in {@tutorial conceptJoin} and {@tutorial fullExample}. * * @param {conditionalReducerCB} conditionalFn - a callback function - * which determines when the supplied reducerFn will be executed. + * whose return value determines which reducerFn is executed + * ... truthy: thenReducerFn(), falsy: elseReducerFn(). + * + * @param {reducerFn} thenReducerFn - the "wrapped" reducer invoked + * when conditionalFn returns truthy. * - * @param {reducerFn} reducerFn - the "wrapped" reducer function that - * is conditionally executed. + * @param {reducerFn} [elseReducerFn=reducerPassThrough] - the + * optional "wrapped" reducer invoked when conditionalFn returns + * falsy. * * @returns {reducerFn} a newly created reducer function (described above). */ -export default function conditionalReducer(conditionalFn, reducerFn) { +export default function conditionalReducer(conditionalFn, thenReducerFn, elseReducerFn=reducerPassThrough) { // TODO: consider validation of conditionalReducer() params // expose our new higher-order reducer - // ... which conditionally executes reducerFn(), when directed by conditionalFn() - // NOTE: For more info on he originalReducerState parameter, refer to the User Guide {@tutorial originalReducerState} + // NOTE: For more info on he originalReducerState parameter, refer to the User Guide {@tutorial originalReducerState} return (state, action, originalReducerState) => conditionalFn(state, action, originalReducerState) - ? reducerFn(state, action, originalReducerState) - : state; + ? thenReducerFn(state, action, originalReducerState) + : elseReducerFn(state, action, originalReducerState); } @@ -37,7 +44,7 @@ export default function conditionalReducer(conditionalFn, reducerFn) { /** * A callback function (used in {@link conditionalReducer}) which - * conditionally determines whether it's supplied reducerFn will be + * conditionally determines whether it's supplied ??reducerFn will be * executed. * * @callback conditionalReducerCB @@ -57,6 +64,6 @@ export default function conditionalReducer(conditionalFn, reducerFn) { * Further information can be found in the {@tutorial * originalReducerState} discussion of the User Guide. * - * @returns {truthy} A truthy value indicating whether the reducerFn + * @returns {truthy} A truthy value indicating whether the ??reducerFn * should be executed or not. */ diff --git a/src/reducer/joinReducers.js b/src/reducer/joinReducers.js index 290ff47..7efaf5e 100644 --- a/src/reducer/joinReducers.js +++ b/src/reducer/joinReducers.js @@ -7,9 +7,10 @@ import {} from '../reduxAPI'; // TODO: placebo import required for JSDoc (ISSUE: * logically executing each in sequence (in essence combining their * functionality into one). This is useful when combining various * reducer types into one logical construct. - * - * **Examples** of joinReducers can be found in {@tutorial - * conceptJoin} and {@tutorial fullExample}. + * + * The **User Guide** discusses joinReducers() in more detail + * (see {@tutorial conceptJoin}), and additional examples can + * be found in {@tutorial fullExample}. * * @param {...reducerFn} reducerFns two or more reducer functions to join * together. diff --git a/src/reducer/reducerHash.js b/src/reducer/reducerHash.js index 994ae6d..a8d3259 100644 --- a/src/reducer/reducerHash.js +++ b/src/reducer/reducerHash.js @@ -12,10 +12,11 @@ import reducerPassThrough from './reducerPassThrough'; * provides an alternative to the switch statement (commonly used to * provide this control mechanism). * - * **Examples** can be found in the {@tutorial conceptHash} - * discussion, which contains more information about reducerHash. + * The **User Guide** discusses reducerHash() in more detail (see + * {@tutorial conceptHash}), and additional examples can be found in + * {@tutorial conceptJoin} and {@tutorial fullExample}. * - * Because this function is so central to the rudimentary aspects of + * **NOTE**: Because this function is so central to the rudimentary aspects of * reduction, it is common to provide a value-added {@tutorial logExt}. * * @param {ActionReducerHash} actionHandlers - a hash of reducer functions, From ab6a831c3e222c5149113cdd42a7c258e9d62626 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sat, 25 Feb 2017 10:17:17 -0600 Subject: [PATCH 02/75] tweak ALL REFERENCE types to be hidden from our visual index (because they are NOT part of our util) --- src/reducer/reducerHash.js | 5 ++--- src/reduxAPI.js | 34 ++-------------------------------- 2 files changed, 4 insertions(+), 35 deletions(-) diff --git a/src/reducer/reducerHash.js b/src/reducer/reducerHash.js index a8d3259..d430dd0 100644 --- a/src/reducer/reducerHash.js +++ b/src/reducer/reducerHash.js @@ -41,12 +41,11 @@ export default function reducerHash(actionHandlers) { //*** /** + * @typedef {Object} ActionReducerHash + * * A hash of reducer functions, indexed by the standard redux * action.type. * - * @namespace ActionReducerHash - * @type Object - * * @property {reducerFn} actionType1 - The reducer function servicing: 'actionType1'. * @property {reducerFn} actionType2 - The reducer function servicing: 'actionType2'. * @property {reducerFn} ...more - ...etc. diff --git a/src/reduxAPI.js b/src/reduxAPI.js index d539dfe..fd9834d 100644 --- a/src/reduxAPI.js +++ b/src/reduxAPI.js @@ -10,10 +10,6 @@ //*** Specification: reducerFn //*** -// TODO: consider reducerFn: defined with @function instead of @callback -// - @callback is hidden [NOT indexed] in GLOBAL, and appears under Type definitions -// - while @function is promoted in the GLOBAL index, and appears with all other functions - /** * A standard [redux reducer function]{@link http://redux.js.org/docs/basics/Reducers.html} * that is responsible for state changes. @@ -32,38 +28,12 @@ //*** Specification: Action //*** -// TODO: consider Action: defined with @namespace/@property (promoted as NAMESPACES) -// - only bad thing is it insists on placing description at bottom -// (even when using @description) - /** + * @typedef {Object} Action + * * A standard [redux Action object]{@link http://redux.js.org/docs/basics/Actions.html} * that drives the reduction process. * - * @namespace Action - * @type Object - * * @property {string|Symbol} type - The action type. * @property {*} whatever - Additional app-specific payload (as needed). */ - - -// TODO: consider Action: defined with @interface (promoted as INTERFACES) - -/* - * A standard [redux Action object]{@link http://redux.js.org/docs/basics/Actions.html} - * that drives the reduction process. - * @interface Action - */ - -/* - * The action type. - * @name Action#type - * @type string|Symbol - */ - -/* - * Additional app-specific payload (as needed). - * @name Action#whatever - * @type * - */ From 9d4898907f5585d99f37cff4591132771b0d5a2e Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sat, 25 Feb 2017 10:18:15 -0600 Subject: [PATCH 03/75] correct conditionalReducerCB documentation --- src/reducer/conditionalReducer.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/reducer/conditionalReducer.js b/src/reducer/conditionalReducer.js index 873192f..be5f39e 100644 --- a/src/reducer/conditionalReducer.js +++ b/src/reducer/conditionalReducer.js @@ -43,9 +43,8 @@ export default function conditionalReducer(conditionalFn, thenReducerFn, elseRed //*** /** - * A callback function (used in {@link conditionalReducer}) which - * conditionally determines whether it's supplied ??reducerFn will be - * executed. + * A callback function (used in {@link conditionalReducer}) whose + * return value determines which reducerFn is executed. * * @callback conditionalReducerCB * @@ -64,6 +63,6 @@ export default function conditionalReducer(conditionalFn, thenReducerFn, elseRed * Further information can be found in the {@tutorial * originalReducerState} discussion of the User Guide. * - * @returns {truthy} A truthy value indicating whether the ??reducerFn - * should be executed or not. + * @returns {truthy} A truthy value indicating which reducerFn is + * executed ... truthy: thenReducerFn(), falsy: elseReducerFn(). */ From ce6440b4a1df59dda6787173ff815fec73574eb7 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sat, 25 Feb 2017 11:39:47 -0600 Subject: [PATCH 04/75] bump version back to initial release - 0.1.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 977e24a..cbf9644 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "astx-redux-util", - "version": "1.0.0", + "version": "0.1.0", "description": "Several redux reducer composition utilities.", "main": "src/index.js", "scripts": { From 151b43f8516c79fb8f61fef7834a5c95bb063eb0 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sat, 25 Feb 2017 13:45:10 -0600 Subject: [PATCH 05/75] reword README.md --- README.md | 43 ++++++++++++++++++++++++------------------- src/docs/home.md | 4 ++-- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 0680605..3b4f0cc 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,27 @@ # astx-redux-util -The [astx-redux-util] library provides several redux reducer composition -utilities, of which the most prevalent is **reducerHash()** which -allows you to displace the dreaded switch statement ... **but there is -much more!** +The [astx-redux-util] library promotes several redux reducer +composition utilities. +Reducer composition is not new. Redux itself provides the innovative +[combineReducers](http://redux.js.org/docs/api/combineReducers.html) +utility which allows you to blend individual reducers together to build +up the overall shape of your application state. -## Documentation +The most prevalent [astx-redux-util] utility is **reducerHash()**, +which allows you to combine sub-reducers in such a way as to eliminate +the switch statement commonly used to delineate action type. -Comprehensive documentation can be found at: [astx-redux-util]. +**Additionally**, [astx-redux-util] promotes other reducer compositions that +can be used in conjunction with one another. + + +## Comprehensive Documentation + +Docs can be found at +[astx-redux-util][https://astx-redux-util.js.org/], which includes +both **API** details, and a **User Guide** with complete and thorough +**examples**! ## Install @@ -21,25 +34,17 @@ npm install --save astx-redux-util ## Usage ```JavaScript - import {reducerHash} from 'astx-redux-util'; + import { reducerHash } from 'astx-redux-util'; - const myReducer = reducerHash({ - [ActionType.widget.edit] (widget, action) => action.widget, - [ActionType.widget.edit.close] (widget, action) => null, + const reduceWidget = reducerHash({ + ['widget.edit'] (widget, action) => action.widget, + ['widget.edit.close'] (widget, action) => null, }); export default function widget(widget=null, action) { - return myReducer(widget, action); + return reduceWidget(widget, action); } ``` - -## Don't Miss - -For a more complete and thorough example of how these utilities can be -used, don't miss the **Full Documentation** at [astx-redux-util] -which includes a **Comprehensive Example**. - - [astx-redux-util]: https://astx-redux-util.js.org/ diff --git a/src/docs/home.md b/src/docs/home.md index 6230215..717a946 100644 --- a/src/docs/home.md +++ b/src/docs/home.md @@ -5,8 +5,8 @@ composition utilities. Reducer composition is not new. Redux itself provides the innovative [combineReducers](http://redux.js.org/docs/api/combineReducers.html) -utility which allows you to blend individul reducers together to build -up the overall shape of your appliction state. +utility which allows you to blend individual reducers together to build +up the overall shape of your application state. The most prevalent astx-redux-util utility is {@link reducerHash}, which allows you to combine sub-reducers in such a way as to eliminate From cfb404b9aa7a4cde7becff0bb61d62fcb79ff931 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sat, 25 Feb 2017 14:08:53 -0600 Subject: [PATCH 06/75] updated why --- src/docs/guide/why.md | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/docs/guide/why.md b/src/docs/guide/why.md index 6b156d6..d151323 100644 --- a/src/docs/guide/why.md +++ b/src/docs/guide/why.md @@ -1,25 +1,27 @@ -This section provides a brief insight into why astx-redux-util was +This section provides some insight into why astx-redux-util was created, and how it compares to other similar utilities. -Some astx-redux-util functions (i.e. {@link reducerHash}) are stock -features in other reducer libraries, such as such as [redux-actions +One of the astx-redux-util functions ({@link reducerHash}) is a stock +feature in other reducer libraries, such as [redux-actions handleactions](https://www.npmjs.com/package/redux-actions#handleactionsreducermap-defaultstate). With that said, astx-redux-util promotes other reducer compositions that can be used in conjunction with one another. -One underlying reason astx-redux-util was created was to provide my +**SideBar**: One underlying reason astx-redux-util was created was to provide my initial exposure into npm publishing. Because I am new to both Node and GitHub, and I wanted a small project, where I could focus on the setup and tooling required for publishing a micro-library. This also includes various aspects of project documentation (something that is important to me). -astx-redux-util was pulled directly out of a sandbox project that I -used to learn a number of technologies and frameworks. +The astx-redux-util library was pulled directly out of a sandbox +project that I used to learn a number of technologies and frameworks. -My hope is to eventually add more functionality from my sandbox -project. One aspect in particular is related to managing action -creation with a twist: consistently maintaining action types in +My hope is to eventually add more astx-redux-util functionality from +my sandbox project. One aspect in particular is related to managing +action creators with a twist: consistently maintaining action types in conjunction with action creators. +I hope you enjoy this effort, and comments are welcome. + </Kevin> From dd15f1791b84fd40d7d4d4cbdda2ff62e063e142 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sat, 25 Feb 2017 14:11:12 -0600 Subject: [PATCH 07/75] update doc ref --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3b4f0cc..9cf2f7f 100644 --- a/README.md +++ b/README.md @@ -18,8 +18,7 @@ can be used in conjunction with one another. ## Comprehensive Documentation -Docs can be found at -[astx-redux-util][https://astx-redux-util.js.org/], which includes +Docs can be found at https://astx-redux-util.js.org/, which includes both **API** details, and a **User Guide** with complete and thorough **examples**! From e5a00d7369432ead504a820fbe13de96521d6d77 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sat, 25 Feb 2017 14:14:04 -0600 Subject: [PATCH 08/75] minor doc reference change --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9cf2f7f..dee640c 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,9 @@ can be used in conjunction with one another. ## Comprehensive Documentation -Docs can be found at https://astx-redux-util.js.org/, which includes -both **API** details, and a **User Guide** with complete and thorough -**examples**! +Complete documentation can be found at +https://astx-redux-util.js.org/, which includes both **API** details, +and a **User Guide** with full and thorough **examples**! ## Install From 3cd06317d8e897aa2c1526b8276d71ee6aa9b077 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sun, 26 Feb 2017 08:07:35 -0600 Subject: [PATCH 09/75] minor why rewording --- src/docs/guide/why.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/docs/guide/why.md b/src/docs/guide/why.md index d151323..ea95122 100644 --- a/src/docs/guide/why.md +++ b/src/docs/guide/why.md @@ -14,14 +14,15 @@ and tooling required for publishing a micro-library. This also includes various aspects of project documentation (something that is important to me). -The astx-redux-util library was pulled directly out of a sandbox -project that I used to learn a number of technologies and frameworks. +The astx-redux-util library was pulled from a sandbox project that I +used to study several technologies and frameworks. -My hope is to eventually add more astx-redux-util functionality from -my sandbox project. One aspect in particular is related to managing -action creators with a twist: consistently maintaining action types in -conjunction with action creators. +My hope is to eventually promote additional astx-redux-util +functionality from my sandbox project. One aspect in particular is +related to managing action creators with a twist: *consistently +maintaining action types in conjunction with their corresponding +action creators*. -I hope you enjoy this effort, and comments are welcome. +I hope you enjoy this effort, and comments are always welcome. </Kevin> From dca3e1cd2a0dbc0fb442ce26fe3d9ec06d12ddee Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sun, 26 Feb 2017 08:49:16 -0600 Subject: [PATCH 10/75] preliminary work in replacing reducerPassThrough with lodash identity --- src/docs/guide/conceptConditional.md | 6 +++--- src/docs/guide/conceptHash.md | 6 ++++-- src/index.js | 12 +++--------- src/reducer/conditionalReducer.js | 4 ++-- src/reducer/reducerHash.js | 3 ++- src/reducer/spec/reducerHash.spec.js | 4 ++-- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/src/docs/guide/conceptConditional.md b/src/docs/guide/conceptConditional.md index 73921d3..093069f 100644 --- a/src/docs/guide/conceptConditional.md +++ b/src/docs/guide/conceptConditional.md @@ -35,9 +35,9 @@ it is easy to isolate which actions will impact various parts of our state. **Please Note** that because we did not supply an "elseReducerFn" (the -third parameter to {@link conditionalReducer}), the default {@link -reducerPassThrough} is used for the else condition, in essence -retaining the same state for a falsy directive. +third parameter to {@link conditionalReducer}), the default [identity +function](https://lodash.com/docs#identity) is used for the else +condition, in essence retaining the same state for a falsy directive. **Also Note** that this example is merely intended to introduce you to the concept of conditional reduction. It is somewhat "contrived", diff --git a/src/docs/guide/conceptHash.md b/src/docs/guide/conceptHash.md index 0048a1a..a1e3762 100644 --- a/src/docs/guide/conceptHash.md +++ b/src/docs/guide/conceptHash.md @@ -39,8 +39,10 @@ action.type. ``` Not only is the conditional logic better encapsulated, but the default -pass-through logic is automatically applied ... passing the original -state when no action.type is acted on. +pass-through logic is automatically applied (using the [identity +function](https://lodash.com/docs#identity)), passing through the +original state when no action.type is acted on. + **Please Note** that because {@link reducerHash} is a higher-order creator function, it is invoked outside the scope of the widget() diff --git a/src/index.js b/src/index.js index 7b8495d..f1239d0 100644 --- a/src/index.js +++ b/src/index.js @@ -6,17 +6,13 @@ import reducerHash from './reducer/reducerHash'; import reducerPassThrough from './reducer/reducerPassThrough'; -// TODO: promote this doc through a JSDoc mechanism. - //*** //*** Promote all library utilities through a single module. //*** -// TODO: validate following examples are correct. - // NOTE: This non-default export supports ES6 imports. // Example: -// import { reducerPassThrough } from 'astx-redux-util'; +// import { reducerHash } from 'astx-redux-util'; // -or- // import * as AstxReduxUtil from 'astx-redux-util'; export { @@ -26,13 +22,11 @@ export { reducerPassThrough, }; -// NOTE: This default export supports CommonJS modules (because Babel does NOT promote them otherwise). +// NOTE: This default export supports CommonJS modules (otherwise Babel does NOT promote them). // Example: -// const { reducerPassThrough } = require('astx-redux-util'); +// const { reducerHash } = require('astx-redux-util'); // -or- // const AstxReduxUtil = require('astx-redux-util'); -// -or- -// import AstxReduxUtil from 'astx-redux-util'; export default { conditionalReducer, joinReducers, diff --git a/src/reducer/conditionalReducer.js b/src/reducer/conditionalReducer.js index be5f39e..b158edf 100644 --- a/src/reducer/conditionalReducer.js +++ b/src/reducer/conditionalReducer.js @@ -19,9 +19,9 @@ import reducerPassThrough from './reducerPassThrough'; * @param {reducerFn} thenReducerFn - the "wrapped" reducer invoked * when conditionalFn returns truthy. * - * @param {reducerFn} [elseReducerFn=reducerPassThrough] - the + * @param {reducerFn} [elseReducerFn=identity] - the * optional "wrapped" reducer invoked when conditionalFn returns - * falsy. + * falsy. DEFAULT: [identity function](https://lodash.com/docs#identity) * * @returns {reducerFn} a newly created reducer function (described above). */ diff --git a/src/reducer/reducerHash.js b/src/reducer/reducerHash.js index d430dd0..d003b36 100644 --- a/src/reducer/reducerHash.js +++ b/src/reducer/reducerHash.js @@ -6,7 +6,8 @@ import reducerPassThrough from './reducerPassThrough'; * Create a higher-order reducer by combining a set of sub-reducer * functions that are indexed by the standard action.type. When no * action.type is acted on, the original state is merely - * passed-through. + * passed-through (using the [identity + * function](https://lodash.com/docs#identity)). * * This is one of the more prevalent composition reducers, and * provides an alternative to the switch statement (commonly used to diff --git a/src/reducer/spec/reducerHash.spec.js b/src/reducer/spec/reducerHash.spec.js index 0d503ce..9ac31f5 100644 --- a/src/reducer/spec/reducerHash.spec.js +++ b/src/reducer/spec/reducerHash.spec.js @@ -22,8 +22,8 @@ function baseTest(actionType, expectedState) { } describe('reducerHash() tests', () => { - baseTest('widget.edit', actionWidget); + baseTest('widget.edit', actionWidget); baseTest('widget.edit.close', null); - baseTest('some.other.task', stateWidget); + baseTest('some.other.action', stateWidget); // TODO: test edge case: a) validating hash, and b) hash containing an undefined key }); From 379993514ed69d0d69e12dba90fcc76d0805ae9c Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sun, 26 Feb 2017 10:02:40 -0600 Subject: [PATCH 11/75] replace reducerPassThrough with lodash identity --- package.json | 3 +++ src/index.js | 3 --- src/reducer/conditionalReducer.js | 4 ++-- src/reducer/reducerHash.js | 6 ++--- src/reducer/reducerPassThrough.js | 15 ------------ src/reducer/spec/reducerPassThrough.spec.js | 26 --------------------- 6 files changed, 8 insertions(+), 49 deletions(-) delete mode 100644 src/reducer/reducerPassThrough.js delete mode 100644 src/reducer/spec/reducerPassThrough.spec.js diff --git a/package.json b/package.json index cbf9644..c7f213a 100644 --- a/package.json +++ b/package.json @@ -61,5 +61,8 @@ "npm-run-all": "^4.0.1", "rimraf": "^2.5.4", "webpack": "^2.2.1" + }, + "dependencies": { + "lodash.identity": "^3.0.0" } } diff --git a/src/index.js b/src/index.js index f1239d0..811c12f 100644 --- a/src/index.js +++ b/src/index.js @@ -3,7 +3,6 @@ import conditionalReducer from './reducer/conditionalReducer'; import joinReducers from './reducer/joinReducers'; import reducerHash from './reducer/reducerHash'; -import reducerPassThrough from './reducer/reducerPassThrough'; //*** @@ -19,7 +18,6 @@ export { conditionalReducer, joinReducers, reducerHash, - reducerPassThrough, }; // NOTE: This default export supports CommonJS modules (otherwise Babel does NOT promote them). @@ -31,5 +29,4 @@ export default { conditionalReducer, joinReducers, reducerHash, - reducerPassThrough, }; diff --git a/src/reducer/conditionalReducer.js b/src/reducer/conditionalReducer.js index b158edf..ffb9384 100644 --- a/src/reducer/conditionalReducer.js +++ b/src/reducer/conditionalReducer.js @@ -1,6 +1,6 @@ 'use strict'; -import reducerPassThrough from './reducerPassThrough'; +import identity from 'lodash.identity'; /** @@ -25,7 +25,7 @@ import reducerPassThrough from './reducerPassThrough'; * * @returns {reducerFn} a newly created reducer function (described above). */ -export default function conditionalReducer(conditionalFn, thenReducerFn, elseReducerFn=reducerPassThrough) { +export default function conditionalReducer(conditionalFn, thenReducerFn, elseReducerFn=identity) { // TODO: consider validation of conditionalReducer() params diff --git a/src/reducer/reducerHash.js b/src/reducer/reducerHash.js index d003b36..25f4ba4 100644 --- a/src/reducer/reducerHash.js +++ b/src/reducer/reducerHash.js @@ -1,6 +1,6 @@ 'use strict'; -import reducerPassThrough from './reducerPassThrough'; +import identity from 'lodash.identity'; /** * Create a higher-order reducer by combining a set of sub-reducer @@ -29,9 +29,9 @@ export default function reducerHash(actionHandlers) { // TODO: consider validation of actionHandlers param. - const locateHandler = (action) => actionHandlers[action.type] || reducerPassThrough; + const locateHandler = (action) => actionHandlers[action.type] || identity; - // expose the new reducer fn, which resolves according the the supplied hash + // expose the new reducer fn, which resolves according the the supplied actionHandlers return (state, action) => locateHandler(action)(state, action); } diff --git a/src/reducer/reducerPassThrough.js b/src/reducer/reducerPassThrough.js deleted file mode 100644 index fad400e..0000000 --- a/src/reducer/reducerPassThrough.js +++ /dev/null @@ -1,15 +0,0 @@ -'use strict'; - -import {} from '../reduxAPI'; // TODO: placebo import required for JSDoc (ISSUE: JSDoc seems to require at least one import to expose these items) - -/** - * A "placebo" reducer that passes through it's supplied state as-is. - * - * @param {*} state - The current immutable state that is the reduction target. - * @param {Action} action - The standard redux Action object that drives the reduction process. - * - * @returns {*} The resulting state after reduction *(in this case the supplied state as-is)*. - */ -export default function reducerPassThrough(state, action) { - return state; -} diff --git a/src/reducer/spec/reducerPassThrough.spec.js b/src/reducer/spec/reducerPassThrough.spec.js deleted file mode 100644 index f46f371..0000000 --- a/src/reducer/spec/reducerPassThrough.spec.js +++ /dev/null @@ -1,26 +0,0 @@ -'use strict'; - -import expect from 'expect'; -import {reducerPassThrough} from '../../index'; // module under test - -describe('reducerPassThrough() tests', () => { - - describe('insure state is passed through untouched', () => { - testIt('string', 'myStr'); - testIt('number', 123); - testIt('boolean', true); - testIt('object', {myObjState: 55.0}); - testIt('Symbol', Symbol('Wow a Symbol')); - testIt('undefined', undefined); - testIt('null', null); - }); - -}); - -const action = { type: 'type-not-referenced', payload: 'not-referenced' }; - -function testIt(stateType, state) { - it(`for ${stateType}`, () => { - expect(reducerPassThrough(state, action)).toBe(state); - }); -} From 2a6508f51663a181b88303da1e4d3ca54024d5aa Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sun, 26 Feb 2017 10:03:03 -0600 Subject: [PATCH 12/75] refined example comments --- src/docs/guide/conceptJoin.md | 11 ++++++----- src/docs/guide/fullExample.md | 32 ++++++++++++++++++-------------- 2 files changed, 24 insertions(+), 19 deletions(-) diff --git a/src/docs/guide/conceptJoin.md b/src/docs/guide/conceptJoin.md index 57e8fc2..82c6f77 100644 --- a/src/docs/guide/conceptJoin.md +++ b/src/docs/guide/conceptJoin.md @@ -57,7 +57,7 @@ const contentReducer = export default function widget(widget=null, action) { - // first: determine content shape (i.e. null or {}) + // FIRST: determine content shape (i.e. {} or null) let nextState = widget; switch (action.type) { @@ -73,7 +73,8 @@ export default function widget(widget=null, action) { nextState = widget; } - // second: maintain individual x/y fields ONLY when there is content + // SECOND: maintain individual x/y fields + // ONLY when widget has content (i.e. is being edited) if (nextState !== null) { nextState = contentReducer.reduce(nextState, action); } @@ -98,15 +99,15 @@ import y from './myAppReducer.y'; const reduceWidget = AstxReduxUtil.joinReducers( - // first: determine content shape (i.e. null or {}) + // FIRST: determine content shape (i.e. {} or null) AstxReduxUtil.reducerHash({ ['widget.edit'] (widget, action) => action.widget, ['widget.edit.close'] (widget, action) => null }), - // second: maintain individual x/y fields AstxReduxUtil.conditionalReducer( - // ONLY when there is content + // SECOND: maintain individual x/y fields + // ONLY when widget has content (i.e. is being edited) (widget, action, originalReducerState) => widget !== null, Redux.combineReducers({ x, diff --git a/src/docs/guide/fullExample.md b/src/docs/guide/fullExample.md index 6d86918..33f95b5 100644 --- a/src/docs/guide/fullExample.md +++ b/src/docs/guide/fullExample.md @@ -34,29 +34,27 @@ import Widget from './myWidgetUtil'; const reduceWidget = AstxReduxUtil.joinReducers( - // first: determine content shape (i.e. null or {}) + // FIRST: determine content shape (i.e. {} or null) AstxReduxUtil.reducerHash({ ['widget.edit'] (widget, action) => action.widget, ['widget.edit.close'] (widget, action) => null }), - // next ... AstxReduxUtil.conditionalReducer( - // ... when widget is being edited (i.e. has content) + // NEXT: maintain individual x/y fields + // ONLY when widget has content (i.e. is being edited) (widget, action, originalReducerState) => widget !== null, AstxReduxUtil.joinReducers( - // maintain individual x/y fields Redux.combineReducers({ x, y }), - // ... NEW from last example AstxReduxUtil.conditionalReducer( - // ... when widget has changed + // LAST: maintain curHash + // ONLY when widget has content (see condition above) -AND- has changed (widget, action, originalReducerState) => originalReducerState !== widget, - // maintain curHash (widget, action) => { - widget.curHash = Widget.hash(widget); // NOTE: OK to mutate (different instance) + widget.curHash = Widget.hash(widget); // OK to mutate (because of changed instance) return widget; }) ) @@ -75,13 +73,19 @@ functional decomposition! **Please NOTE**: -1. The {@tutorial originalReducerState} is used to determine when the - widget has changed from ANY of the prior sub-reducers (see the - discussion of this topic in the provided link). +1. The curHash should only be maintained when the widget **has + content** (i.e. non-null), -AND- **has changed** . -2. The curHash should only be maintained when the widget has content - (i.e. non-null). This is accomplished through the **nesting** of - conditionalReducer (the outer one insures the widget is non-null). + - The former condition is accomplished through conditionalReducer + **nesting**. In other words, the outer conditionalReducer insures + the widget is non-null. + + - The latter condition utilizes the {@tutorial + originalReducerState} parameter to determine when the widget has + changed from ANY of the prior sub-reducers. This parameter + provides visibility to the {@tutorial originalReducerState} when + multiple reducers are combined. Please refer to the {@tutorial + originalReducerState} discussion for more insight. 3. Contrary to any **red flags** that may have been raised on your initial glance of the code, **it is OK** to mutate the `widget` From cfdb61bb4425e31e3404aaae8c2f719c8d36e234 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sun, 26 Feb 2017 13:28:56 -0600 Subject: [PATCH 13/75] clarified docs related to originalReducerState and the accumulative nature of joinReducers --- src/docs/guide/originalReducerState.md | 66 ++++++++++++++++++-------- src/reducer/conditionalReducer.js | 8 ++-- src/reducer/joinReducers.js | 10 ++++ 3 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/docs/guide/originalReducerState.md b/src/docs/guide/originalReducerState.md index 4489c88..6bf83e0 100644 --- a/src/docs/guide/originalReducerState.md +++ b/src/docs/guide/originalReducerState.md @@ -1,28 +1,56 @@ -The {@link conditionalReducer} function exposes an -"originalReducerState" parameter to it's ({@link conditionalReducerCB}). +This sidebar discussion provides some insight into +**originalReducerState** (*mostly an internal implementation detail*). -The originalReducerState represents the immutable state at the time of -the start of the reduction process. This is useful in determining -that state has changed within a series of reductions (see {@link joinReducers}), +A fundamental problem in sequentially joining reducers is that each +reducer should be able to build on what has been accomplished by a +prior reducer. In essence it is an accumulative process. + +The {@link joinReducers} utility handles this by cumulatively passing the +state parameter that was returned from any prior reducer (in the chain +of reducers to execute). + +While this does NOT relax the immutable constraint of the reducer's +state parameter, it is possible for a down-stream reducer to receive a +state parameter that is a different instance from the start of the +reduction process (because an up-stream reducer needed to alter it in +some way). + +As it turns out, this is typically NOT a concern to a client, rather +merely an implementation detail. + +There are edge cases, however, where a client needs visibility to the +**originalReducerState**: *the immutable state at the start of the +reduction process*. One case in particular is determining that state +has changed within a series of reductions (i.e. {@link joinReducers}), because each individual reducer only has visibility of the state -within it's own reduction process. +within it's own reduction process. This case is higlighted in +{@tutorial fullExample}. + +As a result, the **originalReducerState** is **publicly exposed** as +the 3rd parameter to the {@link conditionalReducerCB} function (the +{@link conditionalReducer} callback parameter that makes this +determination). + +Internally, the way in which astx-redux-util manages the +originalReducerState is by passing it as a 3rd parameter to any +reducer it is in control of (i.e. invokes). While from a redux +perspective, this is non-standard, it doesn't really hurt, because +reducer functions are NOT reasoning about a 3rd parameter. The only +risk is if redux should (at some future point) start to employ +additional reducer parameters. -There is an example of this in the {@tutorial fullExample}. +By an overwhelming majority (99.9% of the time), **you should seldom have +to worry about how originalReducerState is maintained**, because +astx-redux-util does this for you. -Because this deals with how state changes over a series of reducers -(run in sequence), it is natural that the {@link joinReducers} utility -automatically maintains the originalReducerState. +The only time any of this concerns you is if your application reducer +is in a mid-stream execution chain which invokes a downstream {@link +conditionalReducer}. In this case (which is rare), your code is +responsible for passing originalReducerState (the 3rd reducer +parameter) to the downstream reducer. -By an overwhelming majority (99.9% of the time), you should never have -to worry about how this state is maintained, because {@link joinReducers} does -this for you. -With that said, the way in which originalReducerState is communicated -(internally), is by passing it as a 3rd parameter through the reducer -chain. While from a redux perspective, this is non-standard, it -doesn't really hurt, because reducer functions are NOT reasoning about -a 3rd parameter. The only risk is if redux should (at some -future point) start to employ additional reducer parameters. +??? OLD ... TRASH BELOW (once a confirmation that it is NOT NEEDED) Here are the significant take-away points of interest: diff --git a/src/reducer/conditionalReducer.js b/src/reducer/conditionalReducer.js index ffb9384..0382b05 100644 --- a/src/reducer/conditionalReducer.js +++ b/src/reducer/conditionalReducer.js @@ -48,7 +48,8 @@ export default function conditionalReducer(conditionalFn, thenReducerFn, elseRed * * @callback conditionalReducerCB * - * @param {*} state - The current immutable state that is the reduction target. + * @param {*} state - The current immutable state that is the + * reduction target. * * @param {Action} action - The standard redux Action object that * drives the reduction process. @@ -57,8 +58,9 @@ export default function conditionalReducer(conditionalFn, thenReducerFn, elseRed * of the start of the reduction process. * * This is useful in determining whether state has changed within a - * series of reductions ... because each individual reducer only has - * visibility of the state within it's own reduction process. + * series of reductions {@link joinReducers} ... because each + * individual reducer only has visibility of the state within it's own + * reduction process. * * Further information can be found in the {@tutorial * originalReducerState} discussion of the User Guide. diff --git a/src/reducer/joinReducers.js b/src/reducer/joinReducers.js index 7efaf5e..4990b89 100644 --- a/src/reducer/joinReducers.js +++ b/src/reducer/joinReducers.js @@ -8,6 +8,16 @@ import {} from '../reduxAPI'; // TODO: placebo import required for JSDoc (ISSUE: * functionality into one). This is useful when combining various * reducer types into one logical construct. * + * **Please Note:** Because each reducer is able to build on what has + * been accomplished by a prior reducer, joinReducers cumulatively + * passes the state parameter that was returned from any prior reducer + * (in the chain of reducers to execute). In essence this is an + * accumulative process. While this does NOT relax the immutable + * constraint of the reducer's state parameter, it is possible for a + * down-stream reducer to receive a state parameter that is a + * different instance from the start of the reduction process (because + * an up-stream reducer needed to alter it in some way). + * * The **User Guide** discusses joinReducers() in more detail * (see {@tutorial conceptJoin}), and additional examples can * be found in {@tutorial fullExample}. From 2cbe1b69be8fbf6b0eee22e646330d7c0209633c Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sun, 26 Feb 2017 16:17:35 -0600 Subject: [PATCH 14/75] minor re-wording --- src/docs/guide/conceptConditional.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/docs/guide/conceptConditional.md b/src/docs/guide/conceptConditional.md index 093069f..a17d6eb 100644 --- a/src/docs/guide/conceptConditional.md +++ b/src/docs/guide/conceptConditional.md @@ -34,15 +34,15 @@ example, our action types are organized with a federated namespace, so it is easy to isolate which actions will impact various parts of our state. -**Please Note** that because we did not supply an "elseReducerFn" (the -third parameter to {@link conditionalReducer}), the default [identity +**Note:** Because we did not supply "elseReducerFn" to {@link +conditionalReducer} (the third parameter), the default [identity function](https://lodash.com/docs#identity) is used for the else condition, in essence retaining the same state for a falsy directive. -**Also Note** that this example is merely intended to introduce you to +**More to Come:** This example is merely intended to introduce you to the concept of conditional reduction. It is somewhat "contrived", allowing us to discuss the topic in isolation. In reality, this example may be inappropriate because the optimization is minimal, and -it tends to make the code more brittle. With that said, however, keep -in mind that there are "more legitimate" reasons to apply conditional -reduction ... we will see this in subsequent discussions. +it tends to make the code more brittle. Keep in mind, however, +**there are more legitimate reasons to apply conditional reduction** +... we will see this in subsequent discussions. From 46c865aa57a40531b14f418b005c2c7cff62178d Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Sun, 26 Feb 2017 16:25:04 -0600 Subject: [PATCH 15/75] minor re-wording --- src/docs/guide/originalReducerState.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/docs/guide/originalReducerState.md b/src/docs/guide/originalReducerState.md index 6bf83e0..a00efb7 100644 --- a/src/docs/guide/originalReducerState.md +++ b/src/docs/guide/originalReducerState.md @@ -39,7 +39,7 @@ reducer functions are NOT reasoning about a 3rd parameter. The only risk is if redux should (at some future point) start to employ additional reducer parameters. -By an overwhelming majority (99.9% of the time), **you should seldom have +By an overwhelming majority of the time (99.9%), **you should seldom have to worry about how originalReducerState is maintained**, because astx-redux-util does this for you. From 53c02bde48fb9d0d692617177cd34dc67b9a2f9d Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Mon, 27 Feb 2017 10:11:58 -0600 Subject: [PATCH 16/75] minor comment clarification --- src/reducer/reducerHash.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/reducer/reducerHash.js b/src/reducer/reducerHash.js index 25f4ba4..bfb68a9 100644 --- a/src/reducer/reducerHash.js +++ b/src/reducer/reducerHash.js @@ -29,6 +29,8 @@ export default function reducerHash(actionHandlers) { // TODO: consider validation of actionHandlers param. + // internal function: locate handler from actionHandlers action.type hash lookup + // ... default: identity pass-through const locateHandler = (action) => actionHandlers[action.type] || identity; // expose the new reducer fn, which resolves according the the supplied actionHandlers From 27ea83dfb02923f5bad3479dc2ad65f93ae3214f Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Mon, 27 Feb 2017 10:12:22 -0600 Subject: [PATCH 17/75] refactor to remove widget reference --- src/reducer/spec/reducerHash.spec.js | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/src/reducer/spec/reducerHash.spec.js b/src/reducer/spec/reducerHash.spec.js index 9ac31f5..1f02e22 100644 --- a/src/reducer/spec/reducerHash.spec.js +++ b/src/reducer/spec/reducerHash.spec.js @@ -1,29 +1,27 @@ 'use strict'; import expect from 'expect'; -import AstxReduxUtil from '../../index'; // module under test (NOTE: we vary import techniques) +import AstxReduxUtil from '../../index'; // module under test (NOTE: we purposely vary import techniques) -const reduceWidget = AstxReduxUtil.reducerHash({ - 'widget.edit': (widget, action) => action.widget, - 'widget.edit.close': (widget, action) => null, +const reducerUnderTest = AstxReduxUtil.reducerHash({ + 'edit': (state, action) => action.payload, + 'edit.close': (state, action) => null, }); -function widget(widget=null, action) { - return reduceWidget(widget, action); -} - -const stateWidget = 'stateWidget'; -const actionWidget = 'actionWidget'; +const initialState = 'initialState'; +const actionPayload = 'actionPayload'; -function baseTest(actionType, expectedState) { +function performTest(actionType, expectedState) { it(`process: '${actionType}'`, () => { - expect(widget(stateWidget, {type: actionType, widget: actionWidget})).toBe(expectedState); + expect(reducerUnderTest(initialState, {type: actionType, payload: actionPayload})).toBe(expectedState); }); } describe('reducerHash() tests', () => { - baseTest('widget.edit', actionWidget); - baseTest('widget.edit.close', null); - baseTest('some.other.action', stateWidget); + performTest('edit', actionPayload); + performTest('edit.close', null); + performTest('other.action', initialState); + // TODO: test edge case: a) validating hash, and b) hash containing an undefined key + }); From 785b1860fd53db2843432966fd61bbf8fec7ea4c Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Mon, 27 Feb 2017 10:12:40 -0600 Subject: [PATCH 18/75] added conditionalReducer unit tests --- src/reducer/spec/conditionalReducer.spec.js | 53 +++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 src/reducer/spec/conditionalReducer.spec.js diff --git a/src/reducer/spec/conditionalReducer.spec.js b/src/reducer/spec/conditionalReducer.spec.js new file mode 100644 index 0000000..beb1071 --- /dev/null +++ b/src/reducer/spec/conditionalReducer.spec.js @@ -0,0 +1,53 @@ +'use strict'; + +import expect from 'expect'; +import {conditionalReducer} from '../../index'; // module under test (NOTE: we purposely vary import techniques) + +const initialState = 'initialState'; +const thenState = 'thenState'; +const elseState = 'elseState'; + +const thenAction = 'thenAction'; + +const elseCondition = 'elseContidion'; + +const thenReducer = (state, action) => thenState; +const elseReducer = (state, action) => elseState; + +function performTest(reducer, actionType, expectedState) { + it(`process: '${actionType}'`, () => { + expect(reducer(initialState, {type: actionType})).toBe(expectedState); + }); +} + +describe('conditionalReducer() tests', () => { + + + describe('conditionalReducer with if only', () => { + + const reducerUnderTest = conditionalReducer( + (state, action, originalReducerState) => action.type === thenAction, + thenReducer + ); + + performTest(reducerUnderTest, thenAction, thenState); + performTest(reducerUnderTest, elseCondition, initialState); + + }); + + + describe('conditionalReducer with if/then', () => { + + const reducerUnderTest = conditionalReducer( + (state, action, originalReducerState) => action.type === thenAction, + thenReducer, + elseReducer + ); + + performTest(reducerUnderTest, thenAction, thenState); + performTest(reducerUnderTest, elseCondition, elseState); + + }); + + // TODO: test edge case: validating parameters +}); From b5a93ed053e7dd5cfd8714923856801af1561b44 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Mon, 27 Feb 2017 10:45:42 -0600 Subject: [PATCH 19/75] added joinReducers unit tests --- src/reducer/joinReducers.js | 2 +- src/reducer/spec/joinReducers.spec.js | 27 +++++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) create mode 100644 src/reducer/spec/joinReducers.spec.js diff --git a/src/reducer/joinReducers.js b/src/reducer/joinReducers.js index 4990b89..f81d740 100644 --- a/src/reducer/joinReducers.js +++ b/src/reducer/joinReducers.js @@ -29,7 +29,7 @@ import {} from '../reduxAPI'; // TODO: placebo import required for JSDoc (ISSUE: */ export default function joinReducers(...reducerFns) { - // TODO: consider validation of joinReducers() params ... two or more reducerFns + // TODO: consider validation of joinReducers() params ... an array WITH 0,1,2? or more reducerFns // expose our new higher-order reducer return (state, action, originalReducerState) => { diff --git a/src/reducer/spec/joinReducers.spec.js b/src/reducer/spec/joinReducers.spec.js new file mode 100644 index 0000000..dc879e4 --- /dev/null +++ b/src/reducer/spec/joinReducers.spec.js @@ -0,0 +1,27 @@ +'use strict'; + +import expect from 'expect'; +import {joinReducers} from '../../index'; // module under test (NOTE: we purposely vary import techniques) + +const initialState = 1; + +const reducerIncrement = (state, action) => state + 1; +const reducerDecrement = (state, action) => state - 1; +const reducerDouble = (state, action) => state * 2; + +function performTest(desc, reducer, expectedState) { + it(`process: '${desc}'`, () => { + expect(reducer(initialState, {type: 'notUsed'})).toBe(expectedState); + }); +} + +describe('joinReducers() tests', () => { + // TODO: test parameter validation +//performTest('increment, double (with array params)', joinReducers([reducerIncrement, reducerDouble]), 4); // TODO: this errors, should be validated + performTest('NO REDUCERS', joinReducers(), 1); // TODO: should this be a validation error? + performTest('increment', joinReducers(reducerIncrement), 2); + performTest('increment, increment', joinReducers(reducerIncrement, reducerIncrement), 3); + performTest('increment, double', joinReducers(reducerIncrement, reducerDouble), 4); + performTest('increment, double, double', joinReducers(reducerIncrement, reducerDouble, reducerDouble), 8); + performTest('increment, double, double, decrement', joinReducers(reducerIncrement, reducerDouble, reducerDouble, reducerDecrement), 7); +}); From 6161d8f9f8dcd997ae8622a8359f1021acc9a238 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Tue, 28 Feb 2017 09:30:39 -0600 Subject: [PATCH 20/75] verify/repair syntax/functionality of documentation sample: reducerHash() --- src/docs/guide/conceptHash.md | 6 +-- .../samples/hash/verifySampleHash.spec.js | 39 +++++++++++++++++++ src/reducer/spec/samples/hash/widgetNew.js | 12 ++++++ src/reducer/spec/samples/hash/widgetOld.js | 16 ++++++++ 4 files changed, 70 insertions(+), 3 deletions(-) create mode 100644 src/reducer/spec/samples/hash/verifySampleHash.spec.js create mode 100644 src/reducer/spec/samples/hash/widgetNew.js create mode 100644 src/reducer/spec/samples/hash/widgetOld.js diff --git a/src/docs/guide/conceptHash.md b/src/docs/guide/conceptHash.md index a1e3762..daa39be 100644 --- a/src/docs/guide/conceptHash.md +++ b/src/docs/guide/conceptHash.md @@ -29,9 +29,9 @@ action.type. import { reducerHash } from 'astx-redux-util'; const reduceWidget = reducerHash({ - ['widget.edit'] (widget, action) => action.widget, - ['widget.edit.close'] (widget, action) => null, - }); + "widget.edit": (widget, action) => action.widget, + "widget.edit.close": (widget, action) => null, + }); export default function widget(widget=null, action) { return reduceWidget(widget, action); diff --git a/src/reducer/spec/samples/hash/verifySampleHash.spec.js b/src/reducer/spec/samples/hash/verifySampleHash.spec.js new file mode 100644 index 0000000..83f9009 --- /dev/null +++ b/src/reducer/spec/samples/hash/verifySampleHash.spec.js @@ -0,0 +1,39 @@ +'use strict'; + +import expect from 'expect'; +import widgetOld from './widgetOld'; // sample reducer (old style) +import widgetNew from './widgetNew'; // sample reducer (new style) + +const widgetUnderEdit = 'widgetUnderEdit'; + +function performTestSeries(reducer) { + let state = undefined; + // runningState, action, reducer, expectedNextState + // ===== ============================================ ======= ================= + state = performTest(state, {type:'app.bootstrap.init'}, reducer, null); + state = performTest(state, {type:'widget.edit', widget:widgetUnderEdit}, reducer, widgetUnderEdit); + state = performTest(state, {type:'other.action.while.edit'}, reducer, widgetUnderEdit); + state = performTest(state, {type:'widget.edit.close'}, reducer, null); + state = performTest(state, {type:'widget.edit.close'}, reducer, null); + state = performTest(state, {type:'other.action.while.not.edit'}, reducer, null); +} + +function performTest(state, action, reducer, expectedNextState) { + const nextState = reducer(state, action); + it(`process: '${action.type}'`, () => { + expect(nextState).toBe(expectedNextState); + }); + return nextState; +} + +describe('example reducerHash(): verify sample code found in documentation', () => { + + describe('old way', () => { + performTestSeries(widgetOld); + }); + + describe('new way', () => { + performTestSeries(widgetNew); + }); + +}); diff --git a/src/reducer/spec/samples/hash/widgetNew.js b/src/reducer/spec/samples/hash/widgetNew.js new file mode 100644 index 0000000..84feb39 --- /dev/null +++ b/src/reducer/spec/samples/hash/widgetNew.js @@ -0,0 +1,12 @@ +'use strict'; + +import { reducerHash } from '../../../../index'; // REALLY: 'astx-redux-util' + +const reduceWidget = reducerHash({ + "widget.edit": (widget, action) => action.widget, + "widget.edit.close": (widget, action) => null, +}); + +export default function widget(widget=null, action) { + return reduceWidget(widget, action); +} diff --git a/src/reducer/spec/samples/hash/widgetOld.js b/src/reducer/spec/samples/hash/widgetOld.js new file mode 100644 index 0000000..4ba298c --- /dev/null +++ b/src/reducer/spec/samples/hash/widgetOld.js @@ -0,0 +1,16 @@ +'use strict'; + +export default function widget(widget=null, action) { + + switch (action.type) { + + case 'widget.edit': + return action.widget; + + case 'widget.edit.close': + return null; + + default: + return widget; + } +} From c2cd48a4464b9a1b481ce280ae2b91de75c4e9d3 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Tue, 28 Feb 2017 10:29:11 -0600 Subject: [PATCH 21/75] added redux dev dependency for verification tests only --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index c7f213a..bc376d0 100644 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "jsdoc-babel": "^0.3.0", "mocha": "^3.2.0", "npm-run-all": "^4.0.1", + "redux": "^3.6.0", "rimraf": "^2.5.4", "webpack": "^2.2.1" }, From 8fccb4b5a64398bc2effaaa59a2466dbaee3c4ba Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Tue, 28 Feb 2017 10:30:06 -0600 Subject: [PATCH 22/75] verify syntax/functionality of documentation sample: conditionalReducer() --- src/docs/guide/conceptConditional.md | 4 +-- src/reducer/spec/samples/appReducer/x.js | 12 ++++++++ src/reducer/spec/samples/appReducer/y.js | 12 ++++++++ .../verifySampleConditional.spec.js | 30 +++++++++++++++++++ .../spec/samples/conditional/widget.js | 19 ++++++++++++ 5 files changed, 75 insertions(+), 2 deletions(-) create mode 100644 src/reducer/spec/samples/appReducer/x.js create mode 100644 src/reducer/spec/samples/appReducer/y.js create mode 100644 src/reducer/spec/samples/conditional/verifySampleConditional.spec.js create mode 100644 src/reducer/spec/samples/conditional/widget.js diff --git a/src/docs/guide/conceptConditional.md b/src/docs/guide/conceptConditional.md index a17d6eb..1ae65f3 100644 --- a/src/docs/guide/conceptConditional.md +++ b/src/docs/guide/conceptConditional.md @@ -10,8 +10,8 @@ This can be accomplished through the {@link conditionalReducer} utility. ```JavaScript import * as Redux from 'redux'; import * as AstxReduxUtil from 'astx-redux-util'; -import x from './myAppReducer.x'; -import y from './myAppReducer.y'; +import x from '../appReducer/x'; +import y from '../appReducer/y'; const reduceWidget = AstxReduxUtil.conditionalReducer( diff --git a/src/reducer/spec/samples/appReducer/x.js b/src/reducer/spec/samples/appReducer/x.js new file mode 100644 index 0000000..e98a159 --- /dev/null +++ b/src/reducer/spec/samples/appReducer/x.js @@ -0,0 +1,12 @@ +'use strict'; + +import * as AstxReduxUtil from '../../../../index'; // REALLY: 'astx-redux-util' + +const reduceX = AstxReduxUtil.reducerHash({ + "widget.edit.x.increment": (x, action) => x+1, + "widget.edit.x.decrement": (x, action) => x-1, +}); + +export default function x(x=0, action) { + return reduceX(x, action); +} diff --git a/src/reducer/spec/samples/appReducer/y.js b/src/reducer/spec/samples/appReducer/y.js new file mode 100644 index 0000000..a518940 --- /dev/null +++ b/src/reducer/spec/samples/appReducer/y.js @@ -0,0 +1,12 @@ +'use strict'; + +import * as AstxReduxUtil from '../../../../index'; // REALLY: 'astx-redux-util' + +const reduceY = AstxReduxUtil.reducerHash({ + "widget.edit.y.increment": (y, action) => y+1, + "widget.edit.y.decrement": (y, action) => y-1, +}); + +export default function y(y=0, action) { + return reduceY(y, action); +} diff --git a/src/reducer/spec/samples/conditional/verifySampleConditional.spec.js b/src/reducer/spec/samples/conditional/verifySampleConditional.spec.js new file mode 100644 index 0000000..156f0d5 --- /dev/null +++ b/src/reducer/spec/samples/conditional/verifySampleConditional.spec.js @@ -0,0 +1,30 @@ +'use strict'; + +import expect from 'expect'; +import widget from './widget'; // sample reducer + +function performTestSeries(reducer) { + let state = undefined; + // runningState, action, reducer, expectedNextState + // ===== ============================================ ======= ================= + state = performTest(state, {type:'app.bootstrap.init'}, reducer, {}); // NOTE: proves condidition reducer did NOT execute (because no x/y properties) + state = performTest(state, {type:'widget.edit'}, reducer, {x:0, y:0}); + state = performTest(state, {type:'other.action'}, reducer, {x:0, y:0}); + state = performTest(state, {type:'widget.edit.x.increment'}, reducer, {x:1, y:0}); + state = performTest(state, {type:'widget.edit.x.increment'}, reducer, {x:2, y:0}); + state = performTest(state, {type:'widget.edit.y.increment'}, reducer, {x:2, y:1}); + state = performTest(state, {type:'widget.edit.x.decrement'}, reducer, {x:1, y:1}); + state = performTest(state, {type:'other.action'}, reducer, {x:1, y:1}); +} + +function performTest(state, action, reducer, expectedNextState) { + const nextState = reducer(state, action); + it(`process: '${action.type}'`, () => { + expect(nextState).toEqual(expectedNextState); + }); + return nextState; +} + +describe('example conditionalReducer(): verify sample code found in documentation', () => { + performTestSeries(widget); +}); diff --git a/src/reducer/spec/samples/conditional/widget.js b/src/reducer/spec/samples/conditional/widget.js new file mode 100644 index 0000000..ce83766 --- /dev/null +++ b/src/reducer/spec/samples/conditional/widget.js @@ -0,0 +1,19 @@ +'use strict'; + +import * as Redux from 'redux'; +import * as AstxReduxUtil from '../../../../index'; // REALLY: 'astx-redux-util' +import x from '../appReducer/x'; +import y from '../appReducer/y'; + +const reduceWidget = + AstxReduxUtil.conditionalReducer( + // conditionally apply when action.type begins with 'widget.edit' + (curState, action, originalReducerState) => action.type.startsWith('widget.edit'), + Redux.combineReducers({ + x, + y + })); + +export default function widget(widget={}, action) { + return reduceWidget(widget, action); +} From a209557aabf5ec2ca5fc405a43dc61629b047769 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Tue, 28 Feb 2017 11:35:31 -0600 Subject: [PATCH 23/75] remove duplicate test --- src/reducer/spec/samples/hash/verifySampleHash.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/reducer/spec/samples/hash/verifySampleHash.spec.js b/src/reducer/spec/samples/hash/verifySampleHash.spec.js index 83f9009..326b39a 100644 --- a/src/reducer/spec/samples/hash/verifySampleHash.spec.js +++ b/src/reducer/spec/samples/hash/verifySampleHash.spec.js @@ -14,7 +14,6 @@ function performTestSeries(reducer) { state = performTest(state, {type:'widget.edit', widget:widgetUnderEdit}, reducer, widgetUnderEdit); state = performTest(state, {type:'other.action.while.edit'}, reducer, widgetUnderEdit); state = performTest(state, {type:'widget.edit.close'}, reducer, null); - state = performTest(state, {type:'widget.edit.close'}, reducer, null); state = performTest(state, {type:'other.action.while.not.edit'}, reducer, null); } From 456f3e17bcce218e447575e74d03cb909c305302 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Tue, 28 Feb 2017 11:42:16 -0600 Subject: [PATCH 24/75] verify/repair syntax/functionality of documentation sample: joinReducers() --- src/docs/guide/conceptJoin.md | 18 ++++----- .../samples/join/verifySampleJoin.spec.js | 40 +++++++++++++++++++ src/reducer/spec/samples/join/widgetNew.js | 28 +++++++++++++ src/reducer/spec/samples/join/widgetOld.js | 39 ++++++++++++++++++ 4 files changed, 116 insertions(+), 9 deletions(-) create mode 100644 src/reducer/spec/samples/join/verifySampleJoin.spec.js create mode 100644 src/reducer/spec/samples/join/widgetNew.js create mode 100644 src/reducer/spec/samples/join/widgetOld.js diff --git a/src/docs/guide/conceptJoin.md b/src/docs/guide/conceptJoin.md index 82c6f77..c554b5a 100644 --- a/src/docs/guide/conceptJoin.md +++ b/src/docs/guide/conceptJoin.md @@ -20,8 +20,8 @@ The individual x/y properties are nicely managed by the standard ```JavaScript import * as Redux from 'redux'; -import x from './myAppReducer.x'; -import y from './myAppReducer.y'; +import x from '../appReducer/x'; +import y from '../appReducer/y'; const contentReducer = Redux.combineReducers({ @@ -46,8 +46,8 @@ One way to accomplish this is through the following procedural logic: ```JavaScript import * as Redux from 'redux'; -import x from './myAppReducer.x'; -import y from './myAppReducer.y'; +import x from '../appReducer/x'; +import y from '../appReducer/y'; const contentReducer = Redux.combineReducers({ @@ -76,7 +76,7 @@ export default function widget(widget=null, action) { // SECOND: maintain individual x/y fields // ONLY when widget has content (i.e. is being edited) if (nextState !== null) { - nextState = contentReducer.reduce(nextState, action); + nextState = contentReducer(nextState, action); } // are we done yet? ... that was painful!! @@ -94,15 +94,15 @@ more reducers logically executing each in sequence. ```JavaScript import * as Redux from 'redux'; import * as AstxReduxUtil from 'astx-redux-util'; -import x from './myAppReducer.x'; -import y from './myAppReducer.y'; +import x from '../appReducer/x'; +import y from '../appReducer/y'; const reduceWidget = AstxReduxUtil.joinReducers( // FIRST: determine content shape (i.e. {} or null) AstxReduxUtil.reducerHash({ - ['widget.edit'] (widget, action) => action.widget, - ['widget.edit.close'] (widget, action) => null + "widget.edit": (widget, action) => action.widget, + "widget.edit.close": (widget, action) => null }), AstxReduxUtil.conditionalReducer( diff --git a/src/reducer/spec/samples/join/verifySampleJoin.spec.js b/src/reducer/spec/samples/join/verifySampleJoin.spec.js new file mode 100644 index 0000000..3a1c54d --- /dev/null +++ b/src/reducer/spec/samples/join/verifySampleJoin.spec.js @@ -0,0 +1,40 @@ +'use strict'; + +import expect from 'expect'; +import widgetOld from './widgetOld'; // sample reducer (old style) +import widgetNew from './widgetNew'; // sample reducer (new style) + +function performTestSeries(reducer) { + let state = undefined; + // runningState, action, reducer, expectedNextState + // ===== ============================================ ======= ================= + state = performTest(state, {type:'app.bootstrap.init'}, reducer, null); + state = performTest(state, {type:'widget.edit', widget:{x:11, y:22}}, reducer, {x:11, y:22}); + state = performTest(state, {type:'widget.edit.x.increment'}, reducer, {x:12, y:22}); + state = performTest(state, {type:'widget.edit.x.increment'}, reducer, {x:13, y:22}); + state = performTest(state, {type:'widget.edit.y.increment'}, reducer, {x:13, y:23}); + state = performTest(state, {type:'other.action.while.edit'}, reducer, {x:13, y:23}); + state = performTest(state, {type:'widget.edit.x.decrement'}, reducer, {x:12, y:23}); + state = performTest(state, {type:'widget.edit.close'}, reducer, null); + state = performTest(state, {type:'other.action.while.not.edit'}, reducer, null); +} + +function performTest(state, action, reducer, expectedNextState) { + const nextState = reducer(state, action); + it(`process: '${action.type}'`, () => { + expect(nextState).toEqual(expectedNextState); + }); + return nextState; +} + +describe('example joinReducers(): verify sample code found in documentation', () => { + + describe('old way', () => { + performTestSeries(widgetOld); + }); + + describe('new way', () => { + performTestSeries(widgetNew); + }); + +}); diff --git a/src/reducer/spec/samples/join/widgetNew.js b/src/reducer/spec/samples/join/widgetNew.js new file mode 100644 index 0000000..04ad091 --- /dev/null +++ b/src/reducer/spec/samples/join/widgetNew.js @@ -0,0 +1,28 @@ +'use strict'; + +import * as Redux from 'redux'; +import * as AstxReduxUtil from '../../../../index'; // REALLY: 'astx-redux-util' +import x from '../appReducer/x'; +import y from '../appReducer/y'; + +const reduceWidget = + AstxReduxUtil.joinReducers( + // FIRST: determine content shape (i.e. {} or null) + AstxReduxUtil.reducerHash({ + "widget.edit": (widget, action) => action.widget, + "widget.edit.close": (widget, action) => null + }), + + AstxReduxUtil.conditionalReducer( + // SECOND: maintain individual x/y fields + // ONLY when widget has content (i.e. is being edited) + (widget, action, originalReducerState) => widget !== null, + Redux.combineReducers({ + x, + y + })) + ); + +export default function widget(widget=null, action) { + return reduceWidget(widget, action); +} diff --git a/src/reducer/spec/samples/join/widgetOld.js b/src/reducer/spec/samples/join/widgetOld.js new file mode 100644 index 0000000..8c9d804 --- /dev/null +++ b/src/reducer/spec/samples/join/widgetOld.js @@ -0,0 +1,39 @@ +'use strict'; + +import * as Redux from 'redux'; +import x from '../appReducer/x'; +import y from '../appReducer/y'; + +const contentReducer = + Redux.combineReducers({ + x, + y + }); + +export default function widget(widget=null, action) { + + // FIRST: determine content shape (i.e. {} or null) + let nextState = widget; + switch (action.type) { + + case 'widget.edit': + nextState = action.widget; + break; + + case 'widget.edit.close': + nextState = null; + break; + + default: + nextState = widget; + } + + // SECOND: maintain individual x/y fields + // ONLY when widget has content (i.e. is being edited) + if (nextState !== null) { + nextState = contentReducer(nextState, action); + } + + // are we done yet? ... that was painful!! + return nextState; +} From f1f1aec0b68e3fe77b722f5cc74e213038dd1c49 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Tue, 28 Feb 2017 12:30:13 -0600 Subject: [PATCH 25/75] verify/repair syntax/functionality of documentation sample: fullExample --- src/docs/guide/fullExample.md | 12 +++--- src/reducer/spec/samples/appReducer/Widget.js | 28 +++++++++++++ .../samples/full/verifySampleFull.spec.js | 31 ++++++++++++++ src/reducer/spec/samples/full/widget.js | 40 +++++++++++++++++++ 4 files changed, 105 insertions(+), 6 deletions(-) create mode 100644 src/reducer/spec/samples/appReducer/Widget.js create mode 100644 src/reducer/spec/samples/full/verifySampleFull.spec.js create mode 100644 src/reducer/spec/samples/full/widget.js diff --git a/src/docs/guide/fullExample.md b/src/docs/guide/fullExample.md index 33f95b5..fe450f1 100644 --- a/src/docs/guide/fullExample.md +++ b/src/docs/guide/fullExample.md @@ -28,16 +28,16 @@ to our reduceWidget function. ```JavaScript import * as Redux from 'redux'; import * as AstxReduxUtil from 'astx-redux-util'; -import x from './myAppReducer.x'; -import y from './myAppReducer.y'; -import Widget from './myWidgetUtil'; +import x from '../appReducer/x'; +import y from '../appReducer/y'; +import Widget from '../appReducer/Widget'; const reduceWidget = AstxReduxUtil.joinReducers( // FIRST: determine content shape (i.e. {} or null) AstxReduxUtil.reducerHash({ - ['widget.edit'] (widget, action) => action.widget, - ['widget.edit.close'] (widget, action) => null + "widget.edit": (widget, action) => action.widget, + "widget.edit.close": (widget, action) => null }), AstxReduxUtil.conditionalReducer( @@ -57,8 +57,8 @@ const reduceWidget = widget.curHash = Widget.hash(widget); // OK to mutate (because of changed instance) return widget; }) - ) ) + ) ); export default function widget(widget=null, action) { diff --git a/src/reducer/spec/samples/appReducer/Widget.js b/src/reducer/spec/samples/appReducer/Widget.js new file mode 100644 index 0000000..3987968 --- /dev/null +++ b/src/reducer/spec/samples/appReducer/Widget.js @@ -0,0 +1,28 @@ +'use strict'; + +/** + * Widget provides a number of utilities in support of the widget + * JSON structure. In essance it is a pseudo class definition for + * widget (as close as we can get for a JSON structure). + */ +const Widget = { + + /** + * Calculate the hash for the supplied widget object. + * + * @param {Widget} widget the widget object to hash. + * + * @return {string} the hash representing the supplied widget object. + */ + hash(widget) { + + // YES: I know this is NOT a hash! + // HOWEVER: + // - this is sample code - that I don't really want more dev dependencies on + // - AND it can be systematically validated + return 'x:' + widget.x + ',y:' + widget.y; + }, + +} + +export default Widget; diff --git a/src/reducer/spec/samples/full/verifySampleFull.spec.js b/src/reducer/spec/samples/full/verifySampleFull.spec.js new file mode 100644 index 0000000..106b305 --- /dev/null +++ b/src/reducer/spec/samples/full/verifySampleFull.spec.js @@ -0,0 +1,31 @@ +'use strict'; + +import expect from 'expect'; +import widget from './widget'; + +function performTestSeries(reducer) { + let state = undefined; + // runningState, action, reducer, expectedNextState + // ===== ============================================ ======= ================= + state = performTest(state, {type:'app.bootstrap.init'}, reducer, null); + state = performTest(state, {type:'widget.edit', widget:{x:11, y:22}}, reducer, {x:11, y:22, curHash: "x:11,y:22"}); + state = performTest(state, {type:'widget.edit.x.increment'}, reducer, {x:12, y:22, curHash: "x:12,y:22"}); + state = performTest(state, {type:'widget.edit.x.increment'}, reducer, {x:13, y:22, curHash: "x:13,y:22"}); + state = performTest(state, {type:'widget.edit.y.increment'}, reducer, {x:13, y:23, curHash: "x:13,y:23"}); + state = performTest(state, {type:'other.action.while.edit'}, reducer, {x:13, y:23, curHash: "x:13,y:23"}); + state = performTest(state, {type:'widget.edit.x.decrement'}, reducer, {x:12, y:23, curHash: "x:12,y:23"}); + state = performTest(state, {type:'widget.edit.close'}, reducer, null); + state = performTest(state, {type:'other.action.while.not.edit'}, reducer, null); +} + +function performTest(state, action, reducer, expectedNextState) { + const nextState = reducer(state, action); + it(`process: '${action.type}'`, () => { + expect(nextState).toEqual(expectedNextState); + }); + return nextState; +} + +describe('example full example: verify sample code found in documentation', () => { + performTestSeries(widget); +}); diff --git a/src/reducer/spec/samples/full/widget.js b/src/reducer/spec/samples/full/widget.js new file mode 100644 index 0000000..bf50cc5 --- /dev/null +++ b/src/reducer/spec/samples/full/widget.js @@ -0,0 +1,40 @@ +'use strict'; + +import * as Redux from 'redux'; +import * as AstxReduxUtil from '../../../../index'; // REALLY: 'astx-redux-util' +import x from '../appReducer/x'; +import y from '../appReducer/y'; +import Widget from '../appReducer/Widget'; + +const reduceWidget = + AstxReduxUtil.joinReducers( + // FIRST: determine content shape (i.e. {} or null) + AstxReduxUtil.reducerHash({ + "widget.edit": (widget, action) => action.widget, + "widget.edit.close": (widget, action) => null + }), + + AstxReduxUtil.conditionalReducer( + // NEXT: maintain individual x/y fields + // ONLY when widget has content (i.e. is being edited) + (widget, action, originalReducerState) => widget !== null, + AstxReduxUtil.joinReducers( + Redux.combineReducers({ + x, + y + }), + AstxReduxUtil.conditionalReducer( + // LAST: maintain curHash + // ONLY when widget has content (see condition above) -AND- has changed + (widget, action, originalReducerState) => originalReducerState !== widget, + (widget, action) => { + widget.curHash = Widget.hash(widget); // OK to mutate (because of changed instance) + return widget; + }) + ) + ) + ); + +export default function widget(widget=null, action) { + return reduceWidget(widget, action); +} From 059a4efaa6976cc9e571ba1a5922d35290934226 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Tue, 28 Feb 2017 15:00:38 -0600 Subject: [PATCH 26/75] insure ALL reducers MUST maintain and pass-through originalReducerState --- src/docs/guide/originalReducerState.md | 37 +----- src/reducer/conditionalReducer.js | 19 +++- src/reducer/joinReducers.js | 5 +- src/reducer/reducerHash.js | 15 ++- src/reducer/spec/originalReducerState.spec.js | 106 ++++++++++++++++++ 5 files changed, 144 insertions(+), 38 deletions(-) create mode 100644 src/reducer/spec/originalReducerState.spec.js diff --git a/src/docs/guide/originalReducerState.md b/src/docs/guide/originalReducerState.md index a00efb7..59ecac6 100644 --- a/src/docs/guide/originalReducerState.md +++ b/src/docs/guide/originalReducerState.md @@ -43,35 +43,8 @@ By an overwhelming majority of the time (99.9%), **you should seldom have to worry about how originalReducerState is maintained**, because astx-redux-util does this for you. -The only time any of this concerns you is if your application reducer -is in a mid-stream execution chain which invokes a downstream {@link -conditionalReducer}. In this case (which is rare), your code is -responsible for passing originalReducerState (the 3rd reducer -parameter) to the downstream reducer. - - -??? OLD ... TRASH BELOW (once a confirmation that it is NOT NEEDED) - -Here are the significant take-away points of interest: - -- If your {@link conditionalReducer} conditionalFn never reasons about - originalReducerState: - * Then you have NO worries whatsoever! - -- If your {@link conditionalReducer} conditionalFn DOES reason about - originalReducerState: - - * In the normal use case (where your {@link conditionalReducer} is - orchestrated by a {@link joinReducers} - in the first order), then you - STILL have NOTHING to worry about! - - **Please Note:** These points cover 99.9% of all use cases! - - * If however, your {@link conditionalReducer} is invoked in a less - conventional way, then you must manually supply the appropriate - originalReducerState 3rd parameter when invoking the reducer. - - - This could be when you are invoking the {@link conditionalReducer} - directly (outside of a {@link joinReducers} utility). - - - Or if you have a nested {@link joinReducers} combination. +**The only time any of this concerns you** is if your application +reducer invokes one of the astx-redux-util reducers. In this case +(which is rare), your code is responsible for passing +originalReducerState (the 3rd reducer parameter) to the downstream +reducer. diff --git a/src/reducer/conditionalReducer.js b/src/reducer/conditionalReducer.js index 0382b05..6d9f59f 100644 --- a/src/reducer/conditionalReducer.js +++ b/src/reducer/conditionalReducer.js @@ -31,9 +31,22 @@ export default function conditionalReducer(conditionalFn, thenReducerFn, elseRed // expose our new higher-order reducer // NOTE: For more info on he originalReducerState parameter, refer to the User Guide {@tutorial originalReducerState} - return (state, action, originalReducerState) => conditionalFn(state, action, originalReducerState) - ? thenReducerFn(state, action, originalReducerState) - : elseReducerFn(state, action, originalReducerState); + return (state, action, originalReducerState) => { + + // maintain the originalReducerState as the immutable state + // at the time of the start of the reduction process + // ... in support of joinReducers() + // ... for more info, refer to the User Guide {@tutorial originalReducerState} + if (originalReducerState === undefined) { + originalReducerState = state; + } + + // execute either thenReducerFn or elseReducerFn, based on conditionalFn + return conditionalFn(state, action, originalReducerState) + ? thenReducerFn(state, action, originalReducerState) + : elseReducerFn(state, action, originalReducerState); + }; + } diff --git a/src/reducer/joinReducers.js b/src/reducer/joinReducers.js index f81d740..d83b46d 100644 --- a/src/reducer/joinReducers.js +++ b/src/reducer/joinReducers.js @@ -35,8 +35,9 @@ export default function joinReducers(...reducerFns) { return (state, action, originalReducerState) => { // maintain the originalReducerState as the immutable state - // at the time of the start of the reduction process. - // ... please refer to the User Guide {@tutorial originalReducerState} + // at the time of the start of the reduction process + // ... in support of joinReducers() + // ... for more info, refer to the User Guide {@tutorial originalReducerState} if (originalReducerState === undefined) { originalReducerState = state; } diff --git a/src/reducer/reducerHash.js b/src/reducer/reducerHash.js index bfb68a9..b44dd31 100644 --- a/src/reducer/reducerHash.js +++ b/src/reducer/reducerHash.js @@ -34,7 +34,20 @@ export default function reducerHash(actionHandlers) { const locateHandler = (action) => actionHandlers[action.type] || identity; // expose the new reducer fn, which resolves according the the supplied actionHandlers - return (state, action) => locateHandler(action)(state, action); + return (state, action, originalReducerState) => { + + // maintain the originalReducerState as the immutable state + // at the time of the start of the reduction process + // ... in support of joinReducers() + // ... for more info, refer to the User Guide {@tutorial originalReducerState} + if (originalReducerState === undefined) { + originalReducerState = state; + } + + // execute the handler indexed by the action.type (or the identity pass-through) + return locateHandler(action)(state, action, originalReducerState); + } + } diff --git a/src/reducer/spec/originalReducerState.spec.js b/src/reducer/spec/originalReducerState.spec.js new file mode 100644 index 0000000..82a1c17 --- /dev/null +++ b/src/reducer/spec/originalReducerState.spec.js @@ -0,0 +1,106 @@ +'use strict'; + +import expect from 'expect'; +import AstxReduxUtil from '../../index'; // module under test (NOTE: we purposely vary import techniques) + + +describe('verify originalReducerState is correctly passed through nested reducers', () => { + + function check(desc, value, expected) { + it(desc, () => { + expect(value).toBe(expected); + }); + } + + describe('with joinReducers() at the top', () => { + + AstxReduxUtil.joinReducers( + (state, action, originalReducerState) => { // myReducer1 + check('myReducer1 state', state, 'originalState'); + check('myReducer1 originalReducerState', originalReducerState, 'originalState'); + return 'myReducer1'; + }, + (state, action, originalReducerState) => { // myReducer2 + check('myReducer2 state', state, 'myReducer1'); + check('myReducer2 originalReducerState', originalReducerState, 'originalState'); + return 'myReducer2'; + }, + AstxReduxUtil.joinReducers( + (state, action, originalReducerState) => { // myReducer3 + check('myReducer3 state', state, 'myReducer2'); + check('myReducer3 originalReducerState', originalReducerState, 'originalState'); + return 'myReducer3'; + }, + AstxReduxUtil.conditionalReducer( + (state, action, originalReducerState) => { // CONDITION + check('myReducer4 CONDITION state', state, 'myReducer3'); + check('myReducer4 CONDITION originalReducerState', originalReducerState, 'originalState'); + return true; // always execute + }, + (state, action, originalReducerState) => { // myReducer4 + check('myReducer4 state', state, 'myReducer3'); + check('myReducer4 originalReducerState', originalReducerState, 'originalState'); + return 'myReducer4'; + } + ), + (state, action, originalReducerState) => { // myReducer5 + check('myReducer5 state', state, 'myReducer4'); + check('myReducer5 originalReducerState', originalReducerState, 'originalState'); + return 'myReducer5'; + }, + AstxReduxUtil.conditionalReducer( + (state, action, originalReducerState) => { // CONDITION + check('myReducer6 CONDITION state', state, 'myReducer5'); + check('myReducer6 CONDITION originalReducerState', originalReducerState, 'originalState'); + return true; // always execute + }, + AstxReduxUtil.reducerHash({ + 'myAction': (state, action, originalReducerState) => { // myReducer6 + check('myReducer6 state', state, 'myReducer5'); + check('myReducer6 originalReducerState', originalReducerState, 'originalState'); + return 'myReducer6'; + } + }), + ), + ), + )('originalState', {type: 'myAction'}); // execute reducer + + }); + + describe('with conditionalReducer() at the top', () => { + + AstxReduxUtil.conditionalReducer( + (state, action, originalReducerState) => { // CONDITION + check('myReducer1 CONDITION state', state, 'originalState'); + check('myReducer1 CONDITION originalReducerState', originalReducerState, 'originalState'); + return true; // always execute + }, + AstxReduxUtil.joinReducers( + (state, action, originalReducerState) => { // myReducer1 + check('myReducer1 state', state, 'originalState'); + check('myReducer1 originalReducerState', originalReducerState, 'originalState'); + return 'myReducer1'; + }, + (state, action, originalReducerState) => { // myReducer2 + check('myReducer2 state', state, 'myReducer1'); + check('myReducer2 originalReducerState', originalReducerState, 'originalState'); + return 'myReducer2'; + }, + ) + )('originalState', {type: 'myAction'}); // execute reducer + + }); + + describe('with reducerHash() at the top', () => { + + AstxReduxUtil.reducerHash({ + 'myAction': (state, action, originalReducerState) => { // myReducer1 + check('myReducer1 state', state, 'originalState'); + check('myReducer1 originalReducerState', originalReducerState, 'originalState'); + return 'myReducer1'; + } + })('originalState', {type: 'myAction'}); // execute reducer + + }); + +}); From 2dc611847ed4128c436481ac90fce6a2319d96ac Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Tue, 28 Feb 2017 15:15:47 -0600 Subject: [PATCH 27/75] added placeboReducer to avoid Redux.combineReducers() warnings/errors (see code comments) --- src/reducer/spec/samples/full/widget.js | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/reducer/spec/samples/full/widget.js b/src/reducer/spec/samples/full/widget.js index bf50cc5..38928f0 100644 --- a/src/reducer/spec/samples/full/widget.js +++ b/src/reducer/spec/samples/full/widget.js @@ -6,6 +6,19 @@ import x from '../appReducer/x'; import y from '../appReducer/y'; import Widget from '../appReducer/Widget'; +// NOTE: placeboReducer is slightly different than lodash.identity +// in that it defaults the state parameter to null +// ... Avoids following Redux.combineReducers() issues: +// - with NO curHash entry, WARNING: +// Unexpected key "curHash" found in previous state received by the reducer. +// Expected to find one of the known reducer keys instead: "x", "y". +// Unexpected keys will be ignored. +// - with curHash using lodash.identy, ERROR: +// Error: Reducer "curHash" returned undefined during initialization. +// If the state passed to the reducer is undefined, you must explicitly return the initial state. +// The initial state may not be undefined. +const placeboReducer = (state=null, action) => state; + const reduceWidget = AstxReduxUtil.joinReducers( // FIRST: determine content shape (i.e. {} or null) @@ -21,7 +34,8 @@ const reduceWidget = AstxReduxUtil.joinReducers( Redux.combineReducers({ x, - y + y, + curHash: placeboReducer }), AstxReduxUtil.conditionalReducer( // LAST: maintain curHash From a6ade31246af371d702b5a41933f8cd53b4152e5 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Wed, 1 Mar 2017 08:17:49 -0600 Subject: [PATCH 28/75] corrected README example --- README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index dee640c..7da9e80 100644 --- a/README.md +++ b/README.md @@ -33,16 +33,16 @@ npm install --save astx-redux-util ## Usage ```JavaScript - import { reducerHash } from 'astx-redux-util'; +import { reducerHash } from 'astx-redux-util'; - const reduceWidget = reducerHash({ - ['widget.edit'] (widget, action) => action.widget, - ['widget.edit.close'] (widget, action) => null, - }); +const reduceWidget = reducerHash({ + "widget.edit": (widget, action) => action.widget, + "widget.edit.close": (widget, action) => null, +}); - export default function widget(widget=null, action) { - return reduceWidget(widget, action); - } +export default function widget(widget=null, action) { + return reduceWidget(widget, action); +} ``` From 4192989c3b4f05aef1796e8a7703ca87618ea94f Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Wed, 1 Mar 2017 08:18:09 -0600 Subject: [PATCH 29/75] minor indentation correction to examples --- src/docs/guide/conceptHash.md | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/src/docs/guide/conceptHash.md b/src/docs/guide/conceptHash.md index daa39be..a566f1d 100644 --- a/src/docs/guide/conceptHash.md +++ b/src/docs/guide/conceptHash.md @@ -2,20 +2,20 @@ Reducers frequently reason about the action.type, very often using a switch statement to drive conditional logic: ```JavaScript - export default function widget(widget=null, action) { +export default function widget(widget=null, action) { - switch (action.type) { + switch (action.type) { - case 'widget.edit': - return action.widget; + case 'widget.edit': + return action.widget; - case 'widget.edit.close': - return null; + case 'widget.edit.close': + return null; - default: - return widget; - } + default: + return widget; } +} ``` The {@link reducerHash} function *(the most common composition @@ -26,16 +26,16 @@ action.type. *The following snippet, is equivalent to the one above:* ``` - import { reducerHash } from 'astx-redux-util'; +import { reducerHash } from 'astx-redux-util'; - const reduceWidget = reducerHash({ - "widget.edit": (widget, action) => action.widget, - "widget.edit.close": (widget, action) => null, - }); +const reduceWidget = reducerHash({ + "widget.edit": (widget, action) => action.widget, + "widget.edit.close": (widget, action) => null, +}); - export default function widget(widget=null, action) { - return reduceWidget(widget, action); - } +export default function widget(widget=null, action) { + return reduceWidget(widget, action); +} ``` Not only is the conditional logic better encapsulated, but the default From 84c349690cd4e9c3e9097ff4427ccab0120ae0c6 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Wed, 1 Mar 2017 09:00:26 -0600 Subject: [PATCH 30/75] qualify fullExample with placeboReducer (required by combineReducers) --- src/docs/guide/fullExample.md | 25 +++++++++++++++++++-- src/reducer/spec/samples/full/widget.js | 29 +++++++++++++++---------- 2 files changed, 41 insertions(+), 13 deletions(-) diff --git a/src/docs/guide/fullExample.md b/src/docs/guide/fullExample.md index fe450f1..aefbc2e 100644 --- a/src/docs/guide/fullExample.md +++ b/src/docs/guide/fullExample.md @@ -32,6 +32,9 @@ import x from '../appReducer/x'; import y from '../appReducer/y'; import Widget from '../appReducer/Widget'; +// placeboReducer WITH state initialization ... see NOTE (below) +const placeboReducer = (state=null, action) => state; + const reduceWidget = AstxReduxUtil.joinReducers( // FIRST: determine content shape (i.e. {} or null) @@ -47,7 +50,8 @@ const reduceWidget = AstxReduxUtil.joinReducers( Redux.combineReducers({ x, - y + y, + curHash: placeboReducer }), AstxReduxUtil.conditionalReducer( // LAST: maintain curHash @@ -87,10 +91,27 @@ functional decomposition! multiple reducers are combined. Please refer to the {@tutorial originalReducerState} discussion for more insight. -3. Contrary to any **red flags** that may have been raised on your +2. Contrary to any **red flags** that may have been raised on your initial glance of the code, **it is OK** to mutate the `widget` state in the last reducer, because we know one of the prior reducers has injected a new widget instance (via the `originalReducerState !== widget` condition). +3. The placeboReducer is slightly different than lodash.identity + in that it defaults the state parameter to null. + + This avoids following Redux.combineReducers() issues: + + - with NO curHash entry ... + WARNING: + Unexpected key "curHash" found in previous state received by the reducer. + Expected to find one of the known reducer keys instead: "x", "y". + Unexpected keys will be ignored. + + - with curHash entry, using lodash.identity ... + ERROR: + Reducer "curHash" returned undefined during initialization. + If the state passed to the reducer is undefined, you must explicitly return the initial state. + The initial state may not be undefined. + **Life is GOOD!** diff --git a/src/reducer/spec/samples/full/widget.js b/src/reducer/spec/samples/full/widget.js index 38928f0..1d6252c 100644 --- a/src/reducer/spec/samples/full/widget.js +++ b/src/reducer/spec/samples/full/widget.js @@ -6,17 +6,7 @@ import x from '../appReducer/x'; import y from '../appReducer/y'; import Widget from '../appReducer/Widget'; -// NOTE: placeboReducer is slightly different than lodash.identity -// in that it defaults the state parameter to null -// ... Avoids following Redux.combineReducers() issues: -// - with NO curHash entry, WARNING: -// Unexpected key "curHash" found in previous state received by the reducer. -// Expected to find one of the known reducer keys instead: "x", "y". -// Unexpected keys will be ignored. -// - with curHash using lodash.identy, ERROR: -// Error: Reducer "curHash" returned undefined during initialization. -// If the state passed to the reducer is undefined, you must explicitly return the initial state. -// The initial state may not be undefined. +// placeboReducer WITH state initialization ... see NOTE (below) const placeboReducer = (state=null, action) => state; const reduceWidget = @@ -52,3 +42,20 @@ const reduceWidget = export default function widget(widget=null, action) { return reduceWidget(widget, action); } + +// NOTE: The placeboReducer is slightly different than lodash.identity +// in that it defaults the state parameter to null. +// +// This avoids following Redux.combineReducers() issues: +// +// - with NO curHash entry ... +// WARNING: +// Unexpected key "curHash" found in previous state received by the reducer. +// Expected to find one of the known reducer keys instead: "x", "y". +// Unexpected keys will be ignored. +// +// - with curHash entry, using lodash.identity ... +// ERROR: +// Reducer "curHash" returned undefined during initialization. +// If the state passed to the reducer is undefined, you must explicitly return the initial state. +// The initial state may not be undefined. From d64541994bd090edc3ebcdcec531e614e562078b Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Wed, 1 Mar 2017 09:53:23 -0600 Subject: [PATCH 31/75] standardize existing package.json fields --- package.json | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index bc376d0..630a7d8 100644 --- a/package.json +++ b/package.json @@ -30,10 +30,11 @@ "flux", "redux", "reducer", + "redux reducer", "action", "compose", "composition", - "higher-order", + "higher order", "switch", "case", "utility", @@ -42,7 +43,7 @@ "helper", "helpers" ], - "author": "Kevin J. Bridges", + "author": "Kevin J. Bridges (https://github.com/KevinAst)", "license": "MIT", "bugs": { "url": "https://github.com/KevinAst/astx-redux-util/issues" From f31cd4dbacb64d683b2dae7308c49e6e5423d039 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Wed, 1 Mar 2017 10:17:07 -0600 Subject: [PATCH 32/75] minor doc changes (mosly moving originalReducerState discussion up higher) --- src/docs/guide/fullExample.md | 15 +++++++-------- src/docs/guide/originalReducerState.md | 2 +- src/docs/guide/toc.json | 12 ++++++------ src/docs/home.md | 6 +++--- src/reducer/spec/samples/full/widget.js | 5 ++--- 5 files changed, 19 insertions(+), 21 deletions(-) diff --git a/src/docs/guide/fullExample.md b/src/docs/guide/fullExample.md index aefbc2e..e528ae8 100644 --- a/src/docs/guide/fullExample.md +++ b/src/docs/guide/fullExample.md @@ -32,7 +32,7 @@ import x from '../appReducer/x'; import y from '../appReducer/y'; import Widget from '../appReducer/Widget'; -// placeboReducer WITH state initialization ... see NOTE (below) +// placeboReducer WITH state initialization (see NOTE below) const placeboReducer = (state=null, action) => state; const reduceWidget = @@ -84,11 +84,11 @@ functional decomposition! **nesting**. In other words, the outer conditionalReducer insures the widget is non-null. - - The latter condition utilizes the {@tutorial - originalReducerState} parameter to determine when the widget has - changed from ANY of the prior sub-reducers. This parameter - provides visibility to the {@tutorial originalReducerState} when - multiple reducers are combined. Please refer to the {@tutorial + - The latter condition utilizes the `originalReducerState` + parameter to determine when the widget has changed from ANY of + the prior sub-reducers. This parameter is useful when multiple + reducers are combined, because it represents the state prior to + the start of reduction process. Please refer to the {@tutorial originalReducerState} discussion for more insight. 2. Contrary to any **red flags** that may have been raised on your @@ -99,8 +99,7 @@ functional decomposition! 3. The placeboReducer is slightly different than lodash.identity in that it defaults the state parameter to null. - - This avoids following Redux.combineReducers() issues: + This avoids the following Redux.combineReducers() issues: - with NO curHash entry ... WARNING: diff --git a/src/docs/guide/originalReducerState.md b/src/docs/guide/originalReducerState.md index 59ecac6..ca8fce0 100644 --- a/src/docs/guide/originalReducerState.md +++ b/src/docs/guide/originalReducerState.md @@ -1,7 +1,7 @@ This sidebar discussion provides some insight into **originalReducerState** (*mostly an internal implementation detail*). -A fundamental problem in sequentially joining reducers is that each +A fundamental aspect of sequentially joining reducers is that each reducer should be able to build on what has been accomplished by a prior reducer. In essence it is an accumulative process. diff --git a/src/docs/guide/toc.json b/src/docs/guide/toc.json index 964aacf..1335b69 100644 --- a/src/docs/guide/toc.json +++ b/src/docs/guide/toc.json @@ -19,17 +19,17 @@ "order": 5, "title": "A Most Excellent Example" }, - "why": { + "originalReducerState": { "order": 6, - "title": "Why astx-redux-util?" + "title": "originalReducerState" }, - "logExt": { + "why": { "order": 7, - "title": "Logging Extension" + "title": "Why astx-redux-util?" }, - "originalReducerState": { + "logExt": { "order": 8, - "title": "originalReducerState" + "title": "Logging Extension" }, "LICENSE": { "order": 9, diff --git a/src/docs/home.md b/src/docs/home.md index 717a946..2760fd8 100644 --- a/src/docs/home.md +++ b/src/docs/home.md @@ -38,11 +38,11 @@ can be used in conjunction with one another. - {@tutorial fullExample} ... a more complete example employing many of the astx-redux-util utility functions +- {@tutorial originalReducerState} ... a sidebar discussion of + originalReducerState + - {@tutorial why} ... why was astx-redux-util created, and how does it compare to other utilities - {@tutorial logExt} ... conceptual extension for reducer-based centralized logging - -- {@tutorial originalReducerState} ... a sidebar discussion of - originalReducerState diff --git a/src/reducer/spec/samples/full/widget.js b/src/reducer/spec/samples/full/widget.js index 1d6252c..b016ecc 100644 --- a/src/reducer/spec/samples/full/widget.js +++ b/src/reducer/spec/samples/full/widget.js @@ -6,7 +6,7 @@ import x from '../appReducer/x'; import y from '../appReducer/y'; import Widget from '../appReducer/Widget'; -// placeboReducer WITH state initialization ... see NOTE (below) +// placeboReducer WITH state initialization (see NOTE below) const placeboReducer = (state=null, action) => state; const reduceWidget = @@ -45,8 +45,7 @@ export default function widget(widget=null, action) { // NOTE: The placeboReducer is slightly different than lodash.identity // in that it defaults the state parameter to null. -// -// This avoids following Redux.combineReducers() issues: +// This avoids the following Redux.combineReducers() issues: // // - with NO curHash entry ... // WARNING: From 6b3e847f00a9a81d3b60678f53ec1f8356019ade Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Wed, 1 Mar 2017 10:40:22 -0600 Subject: [PATCH 33/75] move placeboReducer to bottom and clarified it's usage --- src/docs/guide/fullExample.md | 25 ++++++++++++++++++------- src/reducer/spec/samples/full/widget.js | 25 +++++++++++++++++++------ 2 files changed, 37 insertions(+), 13 deletions(-) diff --git a/src/docs/guide/fullExample.md b/src/docs/guide/fullExample.md index e528ae8..3d5ce77 100644 --- a/src/docs/guide/fullExample.md +++ b/src/docs/guide/fullExample.md @@ -32,9 +32,6 @@ import x from '../appReducer/x'; import y from '../appReducer/y'; import Widget from '../appReducer/Widget'; -// placeboReducer WITH state initialization (see NOTE below) -const placeboReducer = (state=null, action) => state; - const reduceWidget = AstxReduxUtil.joinReducers( // FIRST: determine content shape (i.e. {} or null) @@ -68,6 +65,11 @@ const reduceWidget = export default function widget(widget=null, action) { return reduceWidget(widget, action); } + +// placeboReducer WITH state initialization (see NOTE below) +function placeboReducer(state=null, action) { + return state; +} ``` This represents a very comprehensive example of how **Reducer @@ -97,9 +99,17 @@ functional decomposition! reducers has injected a new widget instance (via the `originalReducerState !== widget` condition). -3. The placeboReducer is slightly different than lodash.identity - in that it defaults the state parameter to null. - This avoids the following Redux.combineReducers() issues: +3. The placeboReducer is slightly different than lodash.identity in + that it defaults the state parameter to null. + + This is required in conjunction Redux.combineReducers(), and is + related to our technique of maintaining curHash in the parent + widget reducer (which has visibility to all widget properties), + verses using an individual property reducer (which does NOT have + visibility to other widget properties). + + The placeboReducer works around the following + Redux.combineReducers() issues: - with NO curHash entry ... WARNING: @@ -113,4 +123,5 @@ functional decomposition! If the state passed to the reducer is undefined, you must explicitly return the initial state. The initial state may not be undefined. -**Life is GOOD!** + - with curHash entry, using placeboReducer ... + Life is GOOD! diff --git a/src/reducer/spec/samples/full/widget.js b/src/reducer/spec/samples/full/widget.js index b016ecc..96fe23f 100644 --- a/src/reducer/spec/samples/full/widget.js +++ b/src/reducer/spec/samples/full/widget.js @@ -6,9 +6,6 @@ import x from '../appReducer/x'; import y from '../appReducer/y'; import Widget from '../appReducer/Widget'; -// placeboReducer WITH state initialization (see NOTE below) -const placeboReducer = (state=null, action) => state; - const reduceWidget = AstxReduxUtil.joinReducers( // FIRST: determine content shape (i.e. {} or null) @@ -43,9 +40,22 @@ export default function widget(widget=null, action) { return reduceWidget(widget, action); } -// NOTE: The placeboReducer is slightly different than lodash.identity -// in that it defaults the state parameter to null. -// This avoids the following Redux.combineReducers() issues: +// placeboReducer WITH state initialization (required for Redux.combineReducers()) +function placeboReducer(state=null, action) { + return state; +} + +// NOTE: The placeboReducer is slightly different than lodash.identity in +// that it defaults the state parameter to null. +// +// This is required in conjunction Redux.combineReducers(), and is +// related to our technique of maintaining curHash in the parent +// widget reducer (which has visibility to all widget properties), +// verses using an individual property reducer (which does NOT have +// visibility to other widget properties). +// +// The placeboReducer works around the following +// Redux.combineReducers() issues: // // - with NO curHash entry ... // WARNING: @@ -58,3 +68,6 @@ export default function widget(widget=null, action) { // Reducer "curHash" returned undefined during initialization. // If the state passed to the reducer is undefined, you must explicitly return the initial state. // The initial state may not be undefined. +// +// - with curHash entry, using placeboReducer ... +// Life is GOOD! From f73688a01a0d3e29b8e583468737a406061675ff Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Wed, 1 Mar 2017 10:55:15 -0600 Subject: [PATCH 34/75] better placeboReducer explaination --- src/docs/guide/fullExample.md | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/docs/guide/fullExample.md b/src/docs/guide/fullExample.md index 3d5ce77..6df6aee 100644 --- a/src/docs/guide/fullExample.md +++ b/src/docs/guide/fullExample.md @@ -66,7 +66,7 @@ export default function widget(widget=null, action) { return reduceWidget(widget, action); } -// placeboReducer WITH state initialization (see NOTE below) +// placeboReducer WITH state initialization (required for Redux.combineReducers()) function placeboReducer(state=null, action) { return state; } @@ -102,14 +102,16 @@ functional decomposition! 3. The placeboReducer is slightly different than lodash.identity in that it defaults the state parameter to null. - This is required in conjunction Redux.combineReducers(), and is - related to our technique of maintaining curHash in the parent - widget reducer (which has visibility to all widget properties), - verses using an individual property reducer (which does NOT have - visibility to other widget properties). + This is required in conjunction + [Redux.combineReducers()](http://redux.js.org/docs/api/combineReducers.html), + and is related to our technique of maintaining curHash in the + parent widget reducer (*which has visibility to all widget + properties*), verses using an individual property reducer (*which + does NOT have visibility to other widget properties*). The placeboReducer works around the following - Redux.combineReducers() issues: + [Redux.combineReducers()](http://redux.js.org/docs/api/combineReducers.html) + issues: - with NO curHash entry ... WARNING: From eeccabf63c51e51d546caf9052aa72077a46a45d Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Thu, 2 Mar 2017 06:39:54 -0600 Subject: [PATCH 35/75] expose full examples in README --- README.md | 130 +++++++++++++++++++++++++++++++++++++++++++++-- package.json | 1 + src/docs/home.md | 10 ++-- 3 files changed, 134 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7da9e80..dc37b36 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,17 @@ # astx-redux-util The [astx-redux-util] library promotes several redux reducer -composition utilities. +composition utilities, which blend multiple reducers together forming +a richer abstraction through functional decomposition +(i.e. higher-order functions). Reducer composition is not new. Redux itself provides the innovative [combineReducers](http://redux.js.org/docs/api/combineReducers.html) -utility which allows you to blend individual reducers together to build +utility which allows you to fuse individual reducers together to build up the overall shape of your application state. The most prevalent [astx-redux-util] utility is **reducerHash()**, -which allows you to combine sub-reducers in such a way as to eliminate +which lets you combine sub-reducers in such a way as to eliminate the switch statement commonly used to delineate action type. **Additionally**, [astx-redux-util] promotes other reducer compositions that @@ -32,6 +34,17 @@ npm install --save astx-redux-util ## Usage +### Basics + +The following example uses **reducerHash()** to combine a set of +sub-reducer functions (indexed by the standard action.type), +eliminating the switch statement commonly used to delineate action +type. + +*Don't miss the [astx-redux-util] documentation, which fully explores +this example, and details the API.* + +widget.js ```JavaScript import { reducerHash } from 'astx-redux-util'; @@ -46,4 +59,115 @@ export default function widget(widget=null, action) { ``` +### Joining Reducers + +Building on the previous example, our widget now takes on more detail: + - we manage x/y properties (through the standard + [combineReducers](http://redux.js.org/docs/api/combineReducers.html)) + - the widget itself can take on a null value (when not being edited) + +We manage these new requirements by combining multiple reducers +through a functional decomposition (as opposed to procedural code). +To accomplish this, we add to our repertoire by introducing the +**joinReducers()** and **conditionalReducer()**. + +*Don't miss the [astx-redux-util] documentation, which fully explores +this example, and details the API.* + +widget.js +```JavaScript +import * as Redux from 'redux'; +import * as AstxReduxUtil from 'astx-redux-util'; +import x from '../appReducer/x'; +import y from '../appReducer/y'; + +const reduceWidget = + AstxReduxUtil.joinReducers( + // FIRST: determine content shape (i.e. {} or null) + AstxReduxUtil.reducerHash({ + "widget.edit": (widget, action) => action.widget, + "widget.edit.close": (widget, action) => null + }), + + AstxReduxUtil.conditionalReducer( + // SECOND: maintain individual x/y fields + // ONLY when widget has content (i.e. is being edited) + (widget, action, originalReducerState) => widget !== null, + Redux.combineReducers({ + x, + y + })) + ); + +export default function widget(widget=null, action) { + return reduceWidget(widget, action); +} +``` + +### Full Example + +Building even more on the previous example, our widget now: + - adds a curHash property (which is a determinate of whether + application content has changed) + +We manage this new property in the parent widget reducer, because it +has a unique vantage point of knowing when the widget has changed +(under any circumstance, regardless of how many properties are +involved). + +We accomplish this by simply combining yet another reducer (using a +functional approach). + + +*Don't miss the [astx-redux-util] documentation, which fully explores +this example, and details the API.* + +widget.js +```JavaScript +import * as Redux from 'redux'; +import * as AstxReduxUtil from 'astx-redux-util'; +import x from '../appReducer/x'; +import y from '../appReducer/y'; +import Widget from '../appReducer/Widget'; + +const reduceWidget = + AstxReduxUtil.joinReducers( + // FIRST: determine content shape (i.e. {} or null) + AstxReduxUtil.reducerHash({ + "widget.edit": (widget, action) => action.widget, + "widget.edit.close": (widget, action) => null + }), + + AstxReduxUtil.conditionalReducer( + // NEXT: maintain individual x/y fields + // ONLY when widget has content (i.e. is being edited) + (widget, action, originalReducerState) => widget !== null, + AstxReduxUtil.joinReducers( + Redux.combineReducers({ + x, + y, + curHash: placeboReducer + }), + AstxReduxUtil.conditionalReducer( + // LAST: maintain curHash + // ONLY when widget has content (see condition above) -AND- has changed + (widget, action, originalReducerState) => originalReducerState !== widget, + (widget, action) => { + widget.curHash = Widget.hash(widget); // OK to mutate (because of changed instance) + return widget; + }) + ) + ) + ); + +export default function widget(widget=null, action) { + return reduceWidget(widget, action); +} + +// placeboReducer WITH state initialization (required for Redux.combineReducers()) +function placeboReducer(state=null, action) { + return state; +} +``` + [astx-redux-util]: https://astx-redux-util.js.org/ diff --git a/package.json b/package.json index 630a7d8..de7b05c 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "compose", "composition", "higher order", + "functional decomposition", "switch", "case", "utility", diff --git a/src/docs/home.md b/src/docs/home.md index 2760fd8..e676824 100644 --- a/src/docs/home.md +++ b/src/docs/home.md @@ -1,15 +1,17 @@ # astx-redux-util -The astx-redux-util library promotes several redux reducer -composition utilities. +The astx-redux-util library promotes several redux reducer composition +utilities, which blend multiple reducers together forming a richer +abstraction through functional decomposition (i.e. higher-order +functions). Reducer composition is not new. Redux itself provides the innovative [combineReducers](http://redux.js.org/docs/api/combineReducers.html) -utility which allows you to blend individual reducers together to build +utility which allows you to fuse individual reducers together to build up the overall shape of your application state. The most prevalent astx-redux-util utility is {@link reducerHash}, -which allows you to combine sub-reducers in such a way as to eliminate +which lets you combine sub-reducers in such a way as to eliminate the switch statement commonly used to delineate action type. Additionally, astx-redux-util promotes other reducer compositions that From fca9f4696e5add8c726dc5be15c8b6c77c5bb157 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Thu, 2 Mar 2017 07:08:45 -0600 Subject: [PATCH 36/75] minor doc changes --- README.md | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index dc37b36..38fa508 100644 --- a/README.md +++ b/README.md @@ -41,10 +41,9 @@ sub-reducer functions (indexed by the standard action.type), eliminating the switch statement commonly used to delineate action type. -*Don't miss the [astx-redux-util] documentation, which fully explores +*Don't miss the **[astx-redux-util] documentation**, which fully explores this example, and details the API.* -widget.js ```JavaScript import { reducerHash } from 'astx-redux-util'; @@ -64,17 +63,17 @@ export default function widget(widget=null, action) { Building on the previous example, our widget now takes on more detail: - we manage x/y properties (through the standard [combineReducers](http://redux.js.org/docs/api/combineReducers.html)) - - the widget itself can take on a null value (when not being edited) + - the widget itself can take on a null value (an indication it is NOT + being edited) We manage these new requirements by combining multiple reducers through a functional decomposition (as opposed to procedural code). -To accomplish this, we add to our repertoire by introducing the +To accomplish this, we add to our repertoire by introducing **joinReducers()** and **conditionalReducer()**. -*Don't miss the [astx-redux-util] documentation, which fully explores +*Don't miss the **[astx-redux-util] documentation**, which fully explores this example, and details the API.* -widget.js ```JavaScript import * as Redux from 'redux'; import * as AstxReduxUtil from 'astx-redux-util'; @@ -106,9 +105,9 @@ export default function widget(widget=null, action) { ### Full Example -Building even more on the previous example, our widget now: - - adds a curHash property (which is a determinate of whether - application content has changed) +Building even more on the prior examples: + - our widget adds a curHash property (which is a determinate of + whether application content has changed) We manage this new property in the parent widget reducer, because it has a unique vantage point of knowing when the widget has changed @@ -119,10 +118,9 @@ We accomplish this by simply combining yet another reducer (using a functional approach). -*Don't miss the [astx-redux-util] documentation, which fully explores +*Don't miss the **[astx-redux-util] documentation**, which fully explores this example, and details the API.* -widget.js ```JavaScript import * as Redux from 'redux'; import * as AstxReduxUtil from 'astx-redux-util'; From 8a448aea107d091742cd03e7e625eca294e719b3 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Thu, 2 Mar 2017 07:10:25 -0600 Subject: [PATCH 37/75] highlight documentation --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 38fa508..7c5084b 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,7 @@ sub-reducer functions (indexed by the standard action.type), eliminating the switch statement commonly used to delineate action type. -*Don't miss the **[astx-redux-util] documentation**, which fully explores +**Don't miss the [astx-redux-util] documentation**, *which fully explores this example, and details the API.* ```JavaScript @@ -71,7 +71,7 @@ through a functional decomposition (as opposed to procedural code). To accomplish this, we add to our repertoire by introducing **joinReducers()** and **conditionalReducer()**. -*Don't miss the **[astx-redux-util] documentation**, which fully explores +**Don't miss the [astx-redux-util] documentation**, *which fully explores this example, and details the API.* ```JavaScript @@ -118,7 +118,7 @@ We accomplish this by simply combining yet another reducer (using a functional approach). -*Don't miss the **[astx-redux-util] documentation**, which fully explores +**Don't miss the [astx-redux-util] documentation**, *which fully explores this example, and details the API.* ```JavaScript From 18a6da7712d5b32578eead09634ad178d3f77be1 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Thu, 2 Mar 2017 07:19:13 -0600 Subject: [PATCH 38/75] more readme tweaks --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 7c5084b..e6f17d1 100644 --- a/README.md +++ b/README.md @@ -115,8 +115,8 @@ has a unique vantage point of knowing when the widget has changed involved). We accomplish this by simply combining yet another reducer (using a -functional approach). - +functional approach). This also demonstrates how **composition can be +nested!** **Don't miss the [astx-redux-util] documentation**, *which fully explores this example, and details the API.* @@ -168,4 +168,10 @@ function placeboReducer(state=null, action) { } ``` +This represents a very comprehensive example of how **Reducer +Composition** can **simplify your life**! We have combined multiple +reducers into one, applying conditional logic as needed through +functional decomposition! + + [astx-redux-util]: https://astx-redux-util.js.org/ From 324798e67152265db5604ddbc0121780d4daa048 Mon Sep 17 00:00:00 2001 From: Kevin Bridges Date: Thu, 2 Mar 2017 14:33:43 -0600 Subject: [PATCH 39/75] added lint tooling (production code: realtime via webpack plugin, all code: npm lint script) --- .eslintrc | 23 +++++++++++++++++++++++ package.json | 5 +++++ webpack.config.babel.js | 11 +++++++---- 3 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 .eslintrc diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..1fec5b0 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,23 @@ +{ + "env": { + "es6": true, + "browser": true, + "node": true + }, + "extends": "eslint:recommended", + "parserOptions": { + "sourceType": "module" + }, + "globals": { + "describe": true, // describe() is part of our unit test framework + "it": true // it() is part of our unit test framework + }, + "rules": { + "strict": ["error", "never"], // ES6 Modules imply a 'use strict'; ... specifying this is redundant + "indent": ["off", 2], // allow any indentation + "no-unused-vars": ["error", {"args": "none"}], // allow unsed parameter declaration + "linebreak-style": "off", // allow both unix/windows carriage returns + "quotes": "off", // allow single or double quotes string literals + "semi": ["error", "always"] // enforce semicolons + } +} diff --git a/package.json b/package.json index de7b05c..6bcc567 100644 --- a/package.json +++ b/package.json @@ -18,6 +18,8 @@ "docs:COMMENT": "docs: documentation builder", "docs": "./node_modules/.bin/jsdoc --configure ./src/docs/jsdoc.conf.json --verbose", "docs:clean": "rimraf docs", + "lint:COMMENT": "lint: verify code quality NOTE: This lints both production and test code; real-time linting is also accomplished through our webpack bundler!", + "lint": "eslint src", "clean:COMMENT": "clean: orchestrate ALL clean processes", "clean": "npm run build:clean && npm run docs:clean" }, @@ -52,10 +54,13 @@ "homepage": "https://github.com/KevinAst/astx-redux-util", "devDependencies": { "babel-core": "^6.22.1", + "babel-eslint": "^7.1.1", "babel-loader": "^6.2.10", "babel-preset-es2015": "^6.22.0", "babel-preset-stage-1": "^6.22.0", "docdash": "^0.4.0", + "eslint": "^3.16.1", + "eslint-loader": "^1.6.3", "expect": "^1.20.2", "jsdoc": "^3.4.3", "jsdoc-babel": "^0.3.0", diff --git a/webpack.config.babel.js b/webpack.config.babel.js index 08cdcb1..a64c069 100644 --- a/webpack.config.babel.js +++ b/webpack.config.babel.js @@ -20,7 +20,7 @@ * * diff --git a/src/spec/ModuleUnderTest.js b/src/spec/ModuleUnderTest.js index 8913587..2878808 100644 --- a/src/spec/ModuleUnderTest.js +++ b/src/spec/ModuleUnderTest.js @@ -1,6 +1,8 @@ import moduleFromDevSrc from '../index'; +import moduleFromBundle from '../../dist/astx-redux-util'; import moduleFromBundleMin from '../../dist/astx-redux-util.min'; + /* * This export module allows our unit tests to dynamically reference * different module platforms (i.e. the JS module ecosystem), which @@ -42,6 +44,7 @@ import moduleFromBundleMin from '../../dist/astx-redux-util.min'; //*** const { MODULE_PLATFORM } = process.env; + let moduleUnderTest = moduleFromDevSrc; switch (MODULE_PLATFORM) { @@ -50,6 +53,10 @@ switch (MODULE_PLATFORM) { console.log(`*** Testing Module Platform found in: src/*.js (MODULE_PLATFORM: ${MODULE_PLATFORM})`); // eslint-disable-line no-console moduleUnderTest = moduleFromDevSrc; break; + case 'bundle': + console.log(`*** Testing Module Platform found in: dist/astx-redux-util.js (MODULE_PLATFORM: ${MODULE_PLATFORM})`); // eslint-disable-line no-console + moduleUnderTest = moduleFromBundle; + break; case 'bundle.min': console.log(`*** Testing Module Platform found in: dist/astx-redux-util.min.js (MODULE_PLATFORM: ${MODULE_PLATFORM})`); // eslint-disable-line no-console moduleUnderTest = moduleFromBundleMin; diff --git a/webpack.config.babel.js b/webpack.config.babel.js index a64c069..6cab768 100644 --- a/webpack.config.babel.js +++ b/webpack.config.babel.js @@ -9,7 +9,7 @@ * - Consumable by least-common-denominator (ES5) * * transpiled via babel * - * - Bundle accessable through all module + * - Bundle accessable through all module * * npm package (via $ npm install) * * - UMD (Universal Module Definition) @@ -36,67 +36,33 @@ * *******************************************************************************/ -'use strict'; - import webpack from 'webpack'; // webpack built-in plugins import path from 'path'; import packageInfo from './package.json'; -const devEnv = true; // ?? interpret via some command-line or env var or some such thing - -const libraryName = packageInfo.name; -const outFileName = libraryName + (devEnv ? '.min.js' : '.js'); - -const sourceMaps = 'source-map'; // ?? vary this based on prod/dev needs ... https://webpack.js.org/configuration/devtool/ - -const plugins = []; - -// ??## http://survivejs.com/webpack/optimizing-build/minifying/ -const minifyOps = { -//compress: false, // default: true - // (function e(t,r){if(typeof exports==="object"&&typeof module==="object")module.exports=r();else if(typeof define==="function"&&define.amd)define("astx-redux-util",[],r);else if(typeof exports==="object")exports["astx-redux-util"]=r();else t["astx-redux-util"]=r()})(this,function(){return function(e){var t={};function r(u){if(t[u])return t[u].exports;var n=t[u]={i:u,l:false,exports:{}};e[u].call(n.exports,n,n.exports,r);n.l=true;return n.exports}r.m=e;r.c=t;r.i=function(e){return e};r.d=function(e,t,u){if(!r.o(e,t)){Object.defineProperty(e,t,{configurable:false,enumerable:true,get:u})}};r.n=function(e){var t=e&&e.__esModule?function t(){return e["default"]}:function t(){return e};r.d(t,"a",t);return t};r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)};r.p="";return r(r.s=4)}([function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:true});t.default=u;function u(e,t){return e}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:true});t.default=u;function u(e,t){return function(r,u,n){return e(r,u,n)?t(r,u,n):r}}},function(e,t,r){"use strict";Object.defineProperty(t,"__esModule",{value:true});t.default=u;function u(){for(var e=arguments.length,t=Array(e),r=0;r { + const nodes = packageInfo.name.split('-'); + const libName = nodes.reduce( (libName, node) => libName + node.charAt(0).toUpperCase() + node.slice(1), ''); + console.log(`*** Using libraryName(): '${libName}' (FROM: '${packageInfo.name}')\n`); + return libName; }; -if (devEnv) { - plugins.push ( new webpack.optimize.UglifyJsPlugin(minifyOps) ); // ??## see parameter options ... ex: { minimize: true } -} - -const config = { - entry: path.resolve(__dirname, 'src/index.js'), // the traversal entry point - devtool: sourceMaps, +// define our WebPack configuration +const webpackConfig = { + entry: path.resolve(__dirname, 'src/index.js'), // our traversal entry point output: { - path: path.resolve(__dirname, 'dist'), // ex: {project}/dist - filename: outFileName, // ex: astx-redux-util.min.js + path: path.resolve(__dirname, 'dist'), // bundlePath ....... ex: {projectDir}/dist + filename: packageInfo.name + (productionEnv ? '.min.js' : '.js'), // bundleFileName ... ex: astx-redux-util.min.js - library: libraryName, // bundle as a library (i.e. for external consumption) - libraryTarget: 'umd', // UMD compliance (Universal Module Definition) promotes library support for ALL module environments - umdNamedDefine: true // ditto ?? research diff with/without ?? research access via