├── .babelrc ├── .gitignore ├── .npmignore ├── README.md ├── dist ├── merge.js └── reducers.js ├── docs ├── 2-nesting.md ├── 3-convenience-reducers.md ├── 4-initial-state.md ├── 5-debugging.md ├── 6-third-party-reducers.md ├── 7-standalone-usage.md └── convenience-reducers │ └── README.md ├── examples ├── README.md ├── basic.js ├── convenience-reducers.js ├── initial-state.js ├── react-router-redux │ ├── .babelrc │ ├── README.md │ ├── actions │ │ └── count.js │ ├── app.js │ ├── components │ │ ├── App.js │ │ ├── Bar.js │ │ ├── Foo.js │ │ ├── Home.js │ │ └── index.js │ ├── index.html │ ├── package.json │ ├── reducers │ │ ├── count.js │ │ └── index.js │ └── webpack.config.js ├── standalone │ └── index.html └── todos │ ├── .babelrc │ ├── README.md │ ├── actions │ └── index.js │ ├── components │ ├── App.js │ ├── Footer.js │ ├── Link.js │ ├── Todo.js │ └── TodoList.js │ ├── containers │ ├── AddTodo.js │ ├── FilterLink.js │ └── VisibleTodoList.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── reducers │ └── index.js │ ├── server.js │ ├── test │ ├── .eslintrc │ ├── actions │ │ └── todos.spec.js │ ├── reducers │ │ └── todos.spec.js │ └── setup.js │ └── webpack.config.js ├── package.json ├── src ├── arrayReducers.js ├── index.js ├── mapReducers.js ├── objectReducers.js └── utils.js ├── test ├── array.test.js ├── legacy.test.js ├── map.test.js └── object.test.js ├── webpack.config.js └── webpack ├── merge.js └── reducers.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2"] 3 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /lib/ 2 | node_modules/ -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /src/ 2 | /examples/ 3 | /test/ 4 | .gitignore 5 | .babelrc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Remerge 2 | 3 | State simplified. 4 | 5 | The sole purpose of Remerge is to provide a consistent interface for defining and manipulating state. It's extremely easy and intuitive to use once you get the hang of it. While there is a slight learning curve, hopefully our examples will ease the learning process. 6 | 7 | Although Remerge was built for use with Redux, it can also be used standalone. (full example apps coming soon!) It is completely framework-agnostic. 8 | 9 | ## Quick links 10 | 11 | Part 1 - Basics (this) 12 | 13 | [Part 2 - Nesting](docs/2-nesting.md) 14 | 15 | [Part 3 - Convenience reducers](docs/3-convenience-reducers.md) 16 | 17 | [Part 4 - Initial state](docs/4-initial-state.md) 18 | 19 | [Part 5 - Debugging](docs/5-debugging.md) 20 | 21 | [Part 6 - Working with third party reducers](docs/6-third-party-reducers.md) 22 | 23 | [Part 7 - Standalone usage](docs/7-standalone-usage.md) 24 | 25 | #### Examples 26 | 27 | [Redux TodoMVC, refactored with Remerge](examples/todos) 28 | 29 | [React Router Redux basic example, refactored with Remerge](examples/react-router-redux) 30 | 31 | This example shows how Remerge can be used in conjunction with libraries that expose reducers to hook into Redux state, such as React Router Redux and Redux Form. 32 | 33 | ## Getting started 34 | 35 | >Remerge is also distributed as a standalone script. See [Part 7 - Standalone usage](docs/7-standalone-usage.md) for more detail. 36 | 37 | **Install** 38 | 39 | ``` 40 | npm install remerge --save 41 | ``` 42 | 43 | >This example can be found in [`examples/basic`](examples/basic.js). 44 | 45 | **Define a Remerge schema** 46 | 47 | ```js 48 | import merge from 'remerge' 49 | 50 | const reducer = merge({ 51 | todos: { 52 | _: [], 53 | add: todoAddReducer, 54 | delete: todoDeleteReducer 55 | } 56 | }) 57 | ``` 58 | 59 | **Define reducers** 60 | 61 | ```js 62 | const todoAddReducer = ( 63 | state = [], 64 | action 65 | ) => { 66 | return state.concat(action.todo) 67 | } 68 | 69 | const todoDeleteReducer = ( 70 | state = [], 71 | ) => { 72 | state.pop() 73 | return state 74 | } 75 | ``` 76 | 77 | **Initialize the state tree** 78 | 79 | ```js 80 | const initialState = reducer() 81 | ``` 82 | 83 | ```js 84 | // initialState 85 | { 86 | todos: [] 87 | } 88 | ``` 89 | 90 | **Mutate the state tree with actions** 91 | 92 | ```js 93 | const addTodo = { 94 | type: 'todos.add', 95 | todo: { 96 | title: 'Buy milk' 97 | } 98 | } 99 | 100 | const state1 = reducer(initialState, addTodo) 101 | 102 | const deleteTodo = { 103 | type: 'todos.delete' 104 | } 105 | 106 | const state2 = reducer(state1, deleteTodo) 107 | ``` 108 | 109 | ```js 110 | // state1 111 | { todos: 112 | [ 113 | { title: 'Buy milk' } 114 | ] 115 | } 116 | 117 | // state2 118 | { 119 | todos: [] 120 | } 121 | ``` 122 | 123 | **Tada! You now have a live and working state tree.** 124 | 125 | These steps are explained in more detail below. 126 | 127 | ### Remerge Schema 128 | 129 | Remerge exposes a single top-level `merge` function, which takes a single object as an argument. 130 | 131 | ```js 132 | const reducer = merge({ 133 | todos: { 134 | _: [], 135 | add: todoAddReducer, 136 | delete: todoDeleteReducer 137 | } 138 | }) 139 | ``` 140 | 141 | This object, also called a **schema**, is a Remerge convention. It specifies the shape and behavior of your state tree using a familiar and intuitive syntax. 142 | 143 | With this schema object, `merge` returns a pure function that serves two purposes: **setting up the initial state tree**, and **mutating it**. 144 | 145 | ### Initial state 146 | 147 | The initial state of the state tree can be constructed by using the special `_` key. In this example, the initial state of `todos` is an empty array. 148 | 149 | Now, we can construct the initial state tree by calling `reducer` with no arguments. 150 | 151 | ```js 152 | const initialState = reducer() 153 | ``` 154 | 155 | ### Mutation Actions 156 | 157 | In order to mutate/populate the state tree, we use actions. Actions in Remerge are plain objects that represent a mutation to the state tree. They are heavily inspired from Redux actions. 158 | 159 | ```js 160 | const addTodo = { 161 | type: 'todos.add', 162 | todo: { 163 | title: 'Buy milk' 164 | } 165 | } 166 | 167 | const state1 = reducer(initialState, addTodo) 168 | ``` 169 | 170 | The function returned by `merge` takes a state tree as the first argument, the action object as the second argument, and returns the new mutated state tree. 171 | 172 | Another convention of Remerge is that it expects actions to consist of a `type` key. The `type` key represents the path that the action takes as it navigates through the state tree. 173 | 174 | ### Convenience Reducers 175 | 176 | Remerge ships with some generic reducers to manipulate commonly-used collections such as arrays, plain objects, and Maps. We recommend using them extensively in your schema, only falling back your own custom reducers for more complex situations. 177 | 178 | To use them, simply import them from the `lib` directory. Below is the same example using `arrayInsertReducer` and `arrayDeleteReducer`: 179 | 180 | ```js 181 | import { arrayInsertReducer, arrayDeleteReducer } from 'remerge/lib/arrayReducers' 182 | 183 | const reducer = merge({ 184 | todos: { 185 | _: [], 186 | add: arrayInsertReducer, 187 | delete: arrayDeleteReducer 188 | } 189 | }) 190 | ``` 191 | 192 | For in-depth documentation and an exhausive list of reducers, take a look at [`docs/convenience-reducers/README.md`](docs/convenience-reducers/README.md). 193 | 194 | ## Documentation 195 | 196 | Continue the second part of this README, starting in [Part 2 - Nesting](docs/2-nesting.md), to get a full low-down on how to use Remerge effectively! 197 | 198 | Then take a look at [`examples/convenience-reducers`](examples/convenience-reducers.js) for a more extensive example on using Remerge. 199 | 200 | Example apps with ~~Redux integration~~ and generic JavaScript apps are coming soon! 201 | 202 | The `examples` folder now include [an example of using Remerge in a Redux app](examples/todos). The app itself was taken from [Redux's examples](https://github.com/reactjs/redux/tree/master/examples/todos), and refactored to use Remerge. The original tests have been rewritten to test Remerge's functionality as well, so if you're thinking of writing tests in your own app, this is a good starting point. 203 | 204 | **Update**: The `examples` folder has been updated to include [an example of using Remerge together with React Router Redux](examples/react-router-redux). 205 | 206 | ## Tests 207 | 208 | Remerge includes a fairly comprehensive test suite that also doubles as documentation. Run it with `npm test`. 209 | 210 | ## License 211 | 212 | MIT -------------------------------------------------------------------------------- /dist/merge.js: -------------------------------------------------------------------------------- 1 | var merge=function(t){function e(r){if(n[r])return n[r].exports;var o=n[r]={exports:{},id:r,loaded:!1};return t[r].call(o.exports,o,o.exports,e),o.loaded=!0,o.exports}var n={};return e.m=t,e.c=n,e.p="",e(0)}([function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}var o=n(1),c=r(o);t.exports=c["default"]},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}Object.defineProperty(e,"__esModule",{value:!0});var o=Object.assign||function(t){for(var e=1;e-1&&t%1==0&&e>t}function s(t){var e=-1,n=Array(t.size);return t.forEach(function(t,r){n[++e]=[r,t]}),n}function p(t){var e=-1,n=Array(t.size);return t.forEach(function(t){n[++e]=t}),n}function y(){}function g(t,e){return v(t,e)&&delete t[e]}function d(t,e){if(Ae){var n=t[e];return n===_t?void 0:n}return pe.call(t,e)?t[e]:void 0}function v(t,e){return Ae?void 0!==t[e]:pe.call(t,e)}function h(t,e,n){t[e]=Ae&&void 0===n?_t:n}function b(t){var e=-1,n=t?t.length:0;for(this.clear();++en)return!1;var r=t.length-1;return n==r?t.pop():_e.call(t,n,1),!0}function N(t,e){var n=k(t,e);return 0>n?void 0:t[n][1]}function $(t,e){return k(t,e)>-1}function k(t,e){for(var n=t.length;n--;)if(at(t[n][0],e))return n;return-1}function F(t,e,n){var r=k(t,e);0>r?t.push([e,n]):t[r][1]=n}function D(t,e,n){var r=t[e];pe.call(t,e)&&at(r,n)&&(void 0!==n||e in t)||(t[e]=n)}function P(t,e){return t&&V(e,mt(e),t)}function B(t,e,n,r,o,i,u){var l;if(r&&(l=i?r(t,o,i,u):r(t)),void 0!==l)return l;if(!dt(t))return t;var f=Pe(t);if(f){if(l=rt(t),!e)return Q(t,l)}else{var s=nt(t),p=s==zt||s==Mt;if(Be(t))return J(t,e);if(s==kt||s==xt||p&&!i){if(a(t))return i?t:{};if(l=ot(p?{}:t),!e)return l=P(l,t),n?Y(t,l):l}else{if(!Yt[s])return i?t:{};l=ct(t,s,e)}}u||(u=new x);var y=u.get(t);return y?y:(u.set(t,l),(f?c:I)(t,function(o,c){D(l,c,B(o,e,n,r,c,t,u))}),n&&!f?Y(t,l):l)}function U(t){return dt(t)?je(t):{}}function I(t,e){return t&&ke(t,e,mt)}function R(t,e){return pe.call(t,e)||"object"==typeof t&&e in t&&null===be(t)}function G(t){return Oe(Object(t))}function T(t){return function(e){return null==e?void 0:e[t]}}function J(t,e){if(e)return t.slice();var n=new t.constructor(t.length);return t.copy(n),n}function K(t){var e=new t.constructor(t.byteLength);return new he(e).set(new he(t)),e}function L(t){return i(s(t),r,new t.constructor)}function H(t){var e=new t.constructor(t.source,Qt.exec(t));return e.lastIndex=t.lastIndex,e}function W(t){return i(p(t),o,new t.constructor)}function Z(t){return $e?Object($e.call(t)):{}}function q(t,e){var n=e?K(t.buffer):t.buffer;return new t.constructor(n,t.byteOffset,t.length)}function Q(t,e){var n=-1,r=t.length;for(e||(e=Array(r));++n-1&&t%1==0&&Ot>=t}function dt(t){var e=typeof t;return!!t&&("object"==e||"function"==e)}function vt(t){return!!t&&"object"==typeof t}function ht(t){return null==t?!1:yt(t)?ge.test(se.call(t)):vt(t)&&(a(t)?ge:Vt).test(t)}function bt(t){return"string"==typeof t||!Pe(t)&&vt(t)&&ye.call(t)==Pt}function mt(t){var e=lt(t);if(!e&&!st(t))return G(t);var n=it(t),r=!!n,o=n||[],c=o.length;for(var i in t)!R(t,i)||r&&("length"==i||f(i,c))||e&&"constructor"==i||o.push(i);return o}function jt(t){return function(){return t}}var wt=200,_t="__lodash_hash_undefined__",Ot=9007199254740991,xt="[object Arguments]",Et="[object Array]",St="[object Boolean]",At="[object Date]",Ct="[object Error]",zt="[object Function]",Mt="[object GeneratorFunction]",Nt="[object Map]",$t="[object Number]",kt="[object Object]",Ft="[object RegExp]",Dt="[object Set]",Pt="[object String]",Bt="[object Symbol]",Ut="[object WeakMap]",It="[object ArrayBuffer]",Rt="[object Float32Array]",Gt="[object Float64Array]",Tt="[object Int8Array]",Jt="[object Int16Array]",Kt="[object Int32Array]",Lt="[object Uint8Array]",Ht="[object Uint8ClampedArray]",Wt="[object Uint16Array]",Zt="[object Uint32Array]",qt=/[\\^$.*+?()[\]{}|]/g,Qt=/\w*$/,Vt=/^\[object .+?Constructor\]$/,Xt=/^(?:0|[1-9]\d*)$/,Yt={};Yt[xt]=Yt[Et]=Yt[It]=Yt[St]=Yt[At]=Yt[Rt]=Yt[Gt]=Yt[Tt]=Yt[Jt]=Yt[Kt]=Yt[Nt]=Yt[$t]=Yt[kt]=Yt[Ft]=Yt[Dt]=Yt[Pt]=Yt[Bt]=Yt[Lt]=Yt[Ht]=Yt[Wt]=Yt[Zt]=!0,Yt[Ct]=Yt[zt]=Yt[Ut]=!1;var te={"function":!0,object:!0},ee=te[typeof e]&&e&&!e.nodeType?e:void 0,ne=te[typeof t]&&t&&!t.nodeType?t:void 0,re=ne&&ne.exports===ee?ee:void 0,oe=l(ee&&ne&&"object"==typeof n&&n),ce=l(te[typeof self]&&self),ie=l(te[typeof window]&&window),ue=l(te[typeof this]&&this),le=oe||ie!==(ue&&ue.window)&&ie||ce||ue||Function("return this")(),ae=Array.prototype,fe=Object.prototype,se=Function.prototype.toString,pe=fe.hasOwnProperty,ye=fe.toString,ge=RegExp("^"+se.call(pe).replace(qt,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),de=re?le.Buffer:void 0,ve=le.Symbol,he=le.Uint8Array,be=Object.getPrototypeOf,me=Object.getOwnPropertySymbols,je=Object.create,we=fe.propertyIsEnumerable,_e=ae.splice,Oe=Object.keys,xe=et(le,"Map"),Ee=et(le,"Set"),Se=et(le,"WeakMap"),Ae=et(Object,"create"),Ce=xe?se.call(xe):"",ze=Ee?se.call(Ee):"",Me=Se?se.call(Se):"",Ne=ve?ve.prototype:void 0,$e=Ne?Ne.valueOf:void 0,ke=tt(),Fe=T("length"),De=me||function(){return[]};(xe&&nt(new xe)!=Nt||Ee&&nt(new Ee)!=Dt||Se&&nt(new Se)!=Ut)&&(nt=function(t){var e=ye.call(t),n=e==kt?t.constructor:null,r="function"==typeof n?se.call(n):"";if(r)switch(r){case Ce:return Nt;case ze:return Dt;case Me:return Ut}return e});var Pe=Array.isArray,Be=de?function(t){return t instanceof de}:jt(!1);y.prototype=Ae?Ae(null):fe,b.prototype.clear=m,b.prototype["delete"]=j,b.prototype.get=w,b.prototype.has=_,b.prototype.set=O,x.prototype.clear=E,x.prototype["delete"]=S,x.prototype.get=A,x.prototype.has=C,x.prototype.set=z,t.exports=B}).call(e,n(5)(t),function(){return this}())},function(t,e){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}},function(t,e,n){"use strict";function r(t){return t&&t.__esModule?t:{"default":t}}function o(t){return Object.getPrototypeOf(t)===Map.prototype||"Map"===t.constructor.name}function c(t,e,n){console.log("----------------------"),console.log("Running process"),console.log("current map"),console.log(t),console.log("current state"),console.log(e),console.log("current action"),console.log(n),console.log("----------------------")}function i(t){console.log(v["default"].inspect(t,!1,null))}function u(t,e){return o(t)?t.get(e):t[e]}function l(t,e,n){o(t)?t.set(e,n):t[e]=n}function a(t,e){e&&console.log("%c[remerge]%c "+t,h.black,"")}function f(t,e){e&&console.log("%c[remerge]%c "+t,h.yellow,"")}function s(t,e){e&&console.log("%c[remerge]%c "+t,h.green,"")}function p(t,e){e&&console.error("%c[remerge]%c "+t,h.red,"")}function y(t,e){e&&console.groupCollapsed?console.groupCollapsed("%c[remerge]%c "+t,h.black,""):e&&console.log("%c[remerge]%c "+t,h.black,"")}function g(t,e){e&&console.groupEnd&&console.groupEnd()}Object.defineProperty(e,"__esModule",{value:!0}),e.isMap=o,e.debug=c,e.printTree=i,e.getCollectionElement=u,e.setCollectionElement=l,e.consoleMessage=a,e.consoleWarning=f,e.consoleSuccess=s,e.consoleError=p,e.consoleGrouped=y,e.consoleEndGrouped=g;var d=n(7),v=r(d),h={black:"font-weight : bold; color : #000000;",gray:"font-weight : bold; color : #1B2B34;",red:"font-weight : bold; color : #EC5f67;",orange:"font-weight : bold; color : #F99157;",yellow:"font-weight : bold; color : #FAC863;",green:"font-weight : bold; color : #99C794;",teal:"font-weight : bold; color : #5FB3B3;",blue:"font-weight : bold; color : #6699CC;",purple:"font-weight : bold; color : #C594C5;",brown:"font-weight : bold; color : #AB7967;"}},function(t,e,n){(function(t,r){function o(t,n){var r={seen:[],stylize:i};return arguments.length>=3&&(r.depth=arguments[2]),arguments.length>=4&&(r.colors=arguments[3]),d(n)?r.showHidden=n:n&&e._extend(r,n),w(r.showHidden)&&(r.showHidden=!1),w(r.depth)&&(r.depth=2),w(r.colors)&&(r.colors=!1),w(r.customInspect)&&(r.customInspect=!0),r.colors&&(r.stylize=c),l(r,t,r.depth)}function c(t,e){var n=o.styles[e];return n?"["+o.colors[n][0]+"m"+t+"["+o.colors[n][1]+"m":t}function i(t,e){return t}function u(t){var e={};return t.forEach(function(t,n){e[t]=!0}),e}function l(t,n,r){if(t.customInspect&&n&&S(n.inspect)&&n.inspect!==e.inspect&&(!n.constructor||n.constructor.prototype!==n)){var o=n.inspect(r,t);return m(o)||(o=l(t,o,r)),o}var c=a(t,n);if(c)return c;var i=Object.keys(n),d=u(i);if(t.showHidden&&(i=Object.getOwnPropertyNames(n)),E(n)&&(i.indexOf("message")>=0||i.indexOf("description")>=0))return f(n);if(0===i.length){if(S(n)){var v=n.name?": "+n.name:"";return t.stylize("[Function"+v+"]","special")}if(_(n))return t.stylize(RegExp.prototype.toString.call(n),"regexp");if(x(n))return t.stylize(Date.prototype.toString.call(n),"date");if(E(n))return f(n)}var h="",b=!1,j=["{","}"];if(g(n)&&(b=!0,j=["[","]"]),S(n)){var w=n.name?": "+n.name:"";h=" [Function"+w+"]"}if(_(n)&&(h=" "+RegExp.prototype.toString.call(n)),x(n)&&(h=" "+Date.prototype.toUTCString.call(n)),E(n)&&(h=" "+f(n)),0===i.length&&(!b||0==n.length))return j[0]+h+j[1];if(0>r)return _(n)?t.stylize(RegExp.prototype.toString.call(n),"regexp"):t.stylize("[Object]","special");t.seen.push(n);var O;return O=b?s(t,n,r,d,i):i.map(function(e){return p(t,n,r,d,e,b)}),t.seen.pop(),y(O,h,j)}function a(t,e){if(w(e))return t.stylize("undefined","undefined");if(m(e)){var n="'"+JSON.stringify(e).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return t.stylize(n,"string")}return b(e)?t.stylize(""+e,"number"):d(e)?t.stylize(""+e,"boolean"):v(e)?t.stylize("null","null"):void 0}function f(t){return"["+Error.prototype.toString.call(t)+"]"}function s(t,e,n,r,o){for(var c=[],i=0,u=e.length;u>i;++i)N(e,String(i))?c.push(p(t,e,n,r,String(i),!0)):c.push("");return o.forEach(function(o){o.match(/^\d+$/)||c.push(p(t,e,n,r,o,!0))}),c}function p(t,e,n,r,o,c){var i,u,a;if(a=Object.getOwnPropertyDescriptor(e,o)||{value:e[o]},a.get?u=a.set?t.stylize("[Getter/Setter]","special"):t.stylize("[Getter]","special"):a.set&&(u=t.stylize("[Setter]","special")),N(r,o)||(i="["+o+"]"),u||(t.seen.indexOf(a.value)<0?(u=v(n)?l(t,a.value,null):l(t,a.value,n-1),u.indexOf("\n")>-1&&(u=c?u.split("\n").map(function(t){return" "+t}).join("\n").substr(2):"\n"+u.split("\n").map(function(t){return" "+t}).join("\n"))):u=t.stylize("[Circular]","special")),w(i)){if(c&&o.match(/^\d+$/))return u;i=JSON.stringify(""+o),i.match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(i=i.substr(1,i.length-2),i=t.stylize(i,"name")):(i=i.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),i=t.stylize(i,"string"))}return i+": "+u}function y(t,e,n){var r=0,o=t.reduce(function(t,e){return r++,e.indexOf("\n")>=0&&r++,t+e.replace(/\u001b\[\d\d?m/g,"").length+1},0);return o>60?n[0]+(""===e?"":e+"\n ")+" "+t.join(",\n ")+" "+n[1]:n[0]+e+" "+t.join(", ")+" "+n[1]}function g(t){return Array.isArray(t)}function d(t){return"boolean"==typeof t}function v(t){return null===t}function h(t){return null==t}function b(t){return"number"==typeof t}function m(t){return"string"==typeof t}function j(t){return"symbol"==typeof t}function w(t){return void 0===t}function _(t){return O(t)&&"[object RegExp]"===C(t)}function O(t){return"object"==typeof t&&null!==t}function x(t){return O(t)&&"[object Date]"===C(t)}function E(t){return O(t)&&("[object Error]"===C(t)||t instanceof Error)}function S(t){return"function"==typeof t}function A(t){return null===t||"boolean"==typeof t||"number"==typeof t||"string"==typeof t||"symbol"==typeof t||"undefined"==typeof t}function C(t){return Object.prototype.toString.call(t)}function z(t){return 10>t?"0"+t.toString(10):t.toString(10)}function M(){var t=new Date,e=[z(t.getHours()),z(t.getMinutes()),z(t.getSeconds())].join(":");return[t.getDate(),D[t.getMonth()],e].join(" ")}function N(t,e){return Object.prototype.hasOwnProperty.call(t,e)}var $=/%[sdj%]/g;e.format=function(t){if(!m(t)){for(var e=[],n=0;n=c)return t;switch(t){case"%s":return String(r[n++]);case"%d":return Number(r[n++]);case"%j":try{return JSON.stringify(r[n++])}catch(e){return"[Circular]"}default:return t}}),u=r[n];c>n;u=r[++n])i+=v(u)||!O(u)?" "+u:" "+o(u);return i},e.deprecate=function(n,o){function c(){if(!i){if(r.throwDeprecation)throw new Error(o);r.traceDeprecation?console.trace(o):console.error(o),i=!0}return n.apply(this,arguments)}if(w(t.process))return function(){return e.deprecate(n,o).apply(this,arguments)};if(r.noDeprecation===!0)return n;var i=!1;return c};var k,F={};e.debuglog=function(t){if(w(k)&&(k=r.env.NODE_DEBUG||""),t=t.toUpperCase(),!F[t])if(new RegExp("\\b"+t+"\\b","i").test(k)){var n=r.pid;F[t]=function(){var r=e.format.apply(e,arguments);console.error("%s %d: %s",t,n,r)}}else F[t]=function(){};return F[t]},e.inspect=o,o.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},o.styles={special:"cyan",number:"yellow","boolean":"yellow",undefined:"grey","null":"bold",string:"green",date:"magenta",regexp:"red"},e.isArray=g,e.isBoolean=d,e.isNull=v,e.isNullOrUndefined=h,e.isNumber=b,e.isString=m,e.isSymbol=j,e.isUndefined=w,e.isRegExp=_,e.isObject=O,e.isDate=x,e.isError=E,e.isFunction=S,e.isPrimitive=A,e.isBuffer=n(9);var D=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];e.log=function(){console.log("%s - %s",M(),e.format.apply(e,arguments))},e.inherits=n(10),e._extend=function(t,e){if(!e||!O(e))return t;for(var n=Object.keys(e),r=n.length;r--;)t[n[r]]=e[n[r]];return t}}).call(e,function(){return this}(),n(8))},function(t,e){function n(){a=!1,i.length?l=i.concat(l):f=-1,l.length&&r()}function r(){if(!a){var t=setTimeout(n);a=!0;for(var e=l.length;e;){for(i=l,l=[];++f1)for(var n=1;n-1&&t%1==0&&r>t}function s(t){var r=-1,e=Array(t.size);return t.forEach(function(t,n){e[++r]=[n,t]}),e}function p(t){var r=-1,e=Array(t.size);return t.forEach(function(t){e[++r]=t}),e}function v(){}function y(t,r){return h(t,r)&&delete t[r]}function d(t,r){if(Pr){var e=t[r];return e===mt?void 0:e}return pr.call(t,r)?t[r]:void 0}function h(t,r){return Pr?void 0!==t[r]:pr.call(t,r)}function b(t,r,e){t[r]=Pr&&void 0===e?mt:e}function _(t){var r=-1,e=t?t.length:0;for(this.clear();++re)return!1;var n=t.length-1;return e==n?t.pop():mr.call(t,e,1),!0}function $(t,r){var e=E(t,r);return 0>e?void 0:t[e][1]}function k(t,r){return E(t,r)>-1}function E(t,r){for(var e=t.length;e--;)if(ft(t[e][0],r))return e;return-1}function F(t,r,e){var n=E(t,r);0>n?t.push([r,e]):t[n][1]=e}function U(t,r,e){var n=t[r];pr.call(t,r)&&ft(n,e)&&(void 0!==e||r in t)||(t[r]=e)}function D(t,r){return t&&X(r,gt(r),t)}function K(t,r,e,n,o,c,a){var i;if(n&&(i=c?n(t,o,c,a):n(t)),void 0!==i)return i;if(!dt(t))return t;var l=Dr(t);if(l){if(i=nt(t),!r)return V(t,i)}else{var s=et(t),p=s==Mt||s==St;if(Kr(t))return G(t,r);if(s==Et||s==At||p&&!c){if(f(t))return c?t:{};if(i=ot(p?{}:t),!r)return i=D(i,t),e?Z(t,i):i}else{if(!Zt[s])return c?t:{};i=ut(t,s,r)}}a||(a=new A);var v=a.get(t);return v?v:(a.set(t,i),(l?u:z)(t,function(o,u){U(i,u,K(o,r,e,n,u,t,a))}),e&&!l?Z(t,i):i)}function B(t){return dt(t)?jr(t):{}}function z(t,r){return t&&Er(t,r,gt)}function C(t,r){return pr.call(t,r)||"object"==typeof t&&r in t&&null===_r(t)}function T(t){return Or(Object(t))}function W(t){return function(r){return null==r?void 0:r[t]}}function G(t,r){if(r)return t.slice();var e=new t.constructor(t.length);return t.copy(e),e}function L(t){var r=new t.constructor(t.byteLength);return new br(r).set(new br(t)),r}function N(t){return c(s(t),n,new t.constructor)}function q(t){var r=new t.constructor(t.source,Vt.exec(t));return r.lastIndex=t.lastIndex,r}function H(t){return c(p(t),o,new t.constructor)}function J(t){return kr?Object(kr.call(t)):{}}function Q(t,r){var e=r?L(t.buffer):t.buffer;return new t.constructor(e,t.byteOffset,t.length)}function V(t,r){var e=-1,n=t.length;for(r||(r=Array(n));++e-1&&t%1==0&&Ot>=t}function dt(t){var r=typeof t;return!!t&&("object"==r||"function"==r)}function ht(t){return!!t&&"object"==typeof t}function bt(t){return null==t?!1:vt(t)?yr.test(sr.call(t)):ht(t)&&(f(t)?yr:Xt).test(t)}function _t(t){return"string"==typeof t||!Dr(t)&&ht(t)&&vr.call(t)==Dt}function gt(t){var r=it(t);if(!r&&!st(t))return T(t);var e=ct(t),n=!!e,o=e||[],u=o.length;for(var c in t)!C(t,c)||n&&("length"==c||l(c,u))||r&&"constructor"==c||o.push(c);return o}function jt(t){return function(){return t}}var wt=200,mt="__lodash_hash_undefined__",Ot=9007199254740991,At="[object Arguments]",xt="[object Array]",It="[object Boolean]",Pt="[object Date]",Rt="[object Error]",Mt="[object Function]",St="[object GeneratorFunction]",$t="[object Map]",kt="[object Number]",Et="[object Object]",Ft="[object RegExp]",Ut="[object Set]",Dt="[object String]",Kt="[object Symbol]",Bt="[object WeakMap]",zt="[object ArrayBuffer]",Ct="[object Float32Array]",Tt="[object Float64Array]",Wt="[object Int8Array]",Gt="[object Int16Array]",Lt="[object Int32Array]",Nt="[object Uint8Array]",qt="[object Uint8ClampedArray]",Ht="[object Uint16Array]",Jt="[object Uint32Array]",Qt=/[\\^$.*+?()[\]{}|]/g,Vt=/\w*$/,Xt=/^\[object .+?Constructor\]$/,Yt=/^(?:0|[1-9]\d*)$/,Zt={};Zt[At]=Zt[xt]=Zt[zt]=Zt[It]=Zt[Pt]=Zt[Ct]=Zt[Tt]=Zt[Wt]=Zt[Gt]=Zt[Lt]=Zt[$t]=Zt[kt]=Zt[Et]=Zt[Ft]=Zt[Ut]=Zt[Dt]=Zt[Kt]=Zt[Nt]=Zt[qt]=Zt[Ht]=Zt[Jt]=!0,Zt[Rt]=Zt[Mt]=Zt[Bt]=!1;var tr={"function":!0,object:!0},rr=tr[typeof r]&&r&&!r.nodeType?r:void 0,er=tr[typeof t]&&t&&!t.nodeType?t:void 0,nr=er&&er.exports===rr?rr:void 0,or=i(rr&&er&&"object"==typeof e&&e),ur=i(tr[typeof self]&&self),cr=i(tr[typeof window]&&window),ar=i(tr[typeof this]&&this),ir=or||cr!==(ar&&ar.window)&&cr||ur||ar||Function("return this")(),fr=Array.prototype,lr=Object.prototype,sr=Function.prototype.toString,pr=lr.hasOwnProperty,vr=lr.toString,yr=RegExp("^"+sr.call(pr).replace(Qt,"\\$&").replace(/hasOwnProperty|(function).*?(?=\\\()| for .+?(?=\\\])/g,"$1.*?")+"$"),dr=nr?ir.Buffer:void 0,hr=ir.Symbol,br=ir.Uint8Array,_r=Object.getPrototypeOf,gr=Object.getOwnPropertySymbols,jr=Object.create,wr=lr.propertyIsEnumerable,mr=fr.splice,Or=Object.keys,Ar=rt(ir,"Map"),xr=rt(ir,"Set"),Ir=rt(ir,"WeakMap"),Pr=rt(Object,"create"),Rr=Ar?sr.call(Ar):"",Mr=xr?sr.call(xr):"",Sr=Ir?sr.call(Ir):"",$r=hr?hr.prototype:void 0,kr=$r?$r.valueOf:void 0,Er=tt(),Fr=W("length"),Ur=gr||function(){return[]};(Ar&&et(new Ar)!=$t||xr&&et(new xr)!=Ut||Ir&&et(new Ir)!=Bt)&&(et=function(t){var r=vr.call(t),e=r==Et?t.constructor:null,n="function"==typeof e?sr.call(e):"";if(n)switch(n){case Rr:return $t;case Mr:return Ut;case Sr:return Bt}return r});var Dr=Array.isArray,Kr=dr?function(t){return t instanceof dr}:jt(!1);v.prototype=Pr?Pr(null):lr,_.prototype.clear=g,_.prototype["delete"]=j,_.prototype.get=w,_.prototype.has=m,_.prototype.set=O,A.prototype.clear=x,A.prototype["delete"]=I,A.prototype.get=P,A.prototype.has=R,A.prototype.set=M,t.exports=K}).call(r,e(5)(t),function(){return this}())},function(t,r){t.exports=function(t){return t.webpackPolyfill||(t.deprecate=function(){},t.paths=[],t.children=[],t.webpackPolyfill=1),t}},,,,,,function(t,r){"use strict";function e(t){if(Array.isArray(t)){for(var r=0,e=Array(t.length);rThis section showcases the concept of nesting. The example code here can be found in [examples/convenience-reducers](../examples/convenience-reducers.js) 4 | 5 | From this part onwards, we will consider the following state tree, which represents `users`. Each of these `users` may be associated with some `items`. When populated, the state tree might look something like this when populated with data: 6 | 7 | ```js 8 | const stateTree = { 9 | users: { 10 | john: { 11 | name: 'John', 12 | items: [ 13 | { itemName: 'Apple' }, 14 | { itemName: 'Orange' } 15 | ] 16 | }, 17 | mary: { 18 | name: 'Mary', 19 | items: [ 20 | { itemName: 'Pear' }, 21 | { itemName: 'Mango' } 22 | ] 23 | } 24 | } 25 | } 26 | ``` 27 | 28 | We can represent the structure of such a state tree, as well as the mutations that are allowed on it, with the following Remerge schema: 29 | 30 | ```js 31 | import merge from 'remerge' 32 | import { arrayInsertReducer, arrayDeleteReducer } from 'remerge/lib/arrayReducers' 33 | import { objectInsertReducer, objectDeleteReducer, objectUpdateReducer } from 'remerge/lib/objectReducers' 34 | 35 | const reducer = merge({ 36 | users: { 37 | _: {}, 38 | add: objectInsertReducer, 39 | delete: objectDeleteReducer, 40 | $userId: { 41 | update: objectUpdateReducer, 42 | items: { 43 | add: arrayInsertReducer, 44 | delete: arrayDeleteReducer, 45 | $itemIndex: { 46 | update: objectUpdateReducer 47 | } 48 | } 49 | } 50 | } 51 | }) 52 | ``` 53 | 54 | As you can see, the update mutation, as well as the `items` key, are found under the `$userId` key. This `$userId` key represents operations that involve a single element in a collection1, rather than on the entire collection. 55 | 56 | The part after the `$` symbol, `userId` (and `itemIndex`), will be used to access individual elements in the collection. Note that this is an arbitrary choice - as long as the action provides the correct key when it is needed, it is fine. 57 | 58 | Some example action types that involve nested elements would look like: 59 | 60 | ```js 61 | let action1 = { 62 | type: 'users.update', 63 | userId: 'john' 64 | } 65 | ``` 66 | 67 | or 68 | 69 | ```js 70 | let action2 = { 71 | type: 'users.items.update', 72 | userId: 'mary', 73 | itemIndex: 0 74 | } 75 | ``` 76 | 77 | Continue to [Part 3 - Convenience Reducers](./3-convenience-reducers.md). 78 | 79 | 1. A collection here refers to data structures like arrays, objects, or maps that can contain multiple elements and are accessable by key. -------------------------------------------------------------------------------- /docs/3-convenience-reducers.md: -------------------------------------------------------------------------------- 1 | # Part 3 - Convenience Reducers 2 | 3 | >This section showcases the usage of Remerge's convenience reducers. 4 | 5 | Regarding mutations that are allowed on the state tree, we've defined the schema so that we can add, delete, or update users, and also add, delete or update items from a user: 6 | 7 | ```js 8 | const reducer = merge({ 9 | users: { 10 | _: {}, 11 | add: objectInsertReducer, 12 | delete: objectDeleteReducer, 13 | $userId: { 14 | update: objectUpdateReducer, 15 | items: { 16 | add: arrayInsertReducer, 17 | delete: arrayDeleteReducer, 18 | $itemIndex: { 19 | update: objectUpdateReducer 20 | } 21 | } 22 | } 23 | } 24 | }) 25 | ``` 26 | 27 | Let's initialize the state tree: 28 | 29 | ```js 30 | const initialStateTree = reducer() 31 | ``` 32 | 33 | ```js 34 | // initialStateTree 35 | { 36 | users: {} 37 | } 38 | ``` 39 | 40 | Then, add a user: 41 | 42 | ```js 43 | const addUserAction = { 44 | type: 'users.add', 45 | insertKey: 'john', 46 | data: { 47 | name: 'John' 48 | } 49 | } 50 | 51 | const state1 = reducer(initialStateTree, addUserAction) 52 | ``` 53 | 54 | Now, our state tree looks like this: 55 | 56 | ```js 57 | // state1 58 | { 59 | users: { 60 | john: { 61 | name: 'John' 62 | } 63 | } 64 | } 65 | ``` 66 | 67 | Let's give John an apple. If the path involves accessing elements in a collection, then those collection accessor keys need to be defined as well. Therefore, we now need to provide `userId`, since we are accessing John's items: 68 | 69 | ```js 70 | const addAppleAction = { 71 | type: 'users.items.add', 72 | userId: 'john', 73 | data: { 74 | itemName: 'apple' 75 | } 76 | } 77 | 78 | const state2 = reducer(state1, addAppleAction) 79 | ``` 80 | 81 | ```js 82 | // state2 83 | { 84 | users: { 85 | john: { 86 | name: 'John', 87 | items: [ { itemName: 'apple' } ] 88 | } 89 | } 90 | } 91 | ``` 92 | 93 | Now let's change John's name. We still need to provide `userId`, since the update mutation works on a single element in a collection: 94 | 95 | ```js 96 | const changeUserNameAction = { 97 | type: 'users.update', 98 | userId: 'john', 99 | data: { 100 | name: 'Jim' 101 | } 102 | } 103 | 104 | const state3 = reducer(state2, changeUserNameAction) 105 | ``` 106 | 107 | ```js 108 | // state3 109 | { 110 | users: { 111 | john: { 112 | name: 'Jim', 113 | items: [ { itemName: 'apple' } ] 114 | } 115 | } 116 | } 117 | ``` 118 | 119 | As a trickier example, let's change the apple that John has to an orange. Now, we need to provide `itemId` as well as `userId`. Notice that since `items` is represented by an array, `itemId` is a numerical index: 120 | 121 | ```js 122 | const changeItemNameAction = { 123 | type: 'users.items.update', 124 | userId: 'john', 125 | itemId: 0, 126 | data: { 127 | itemName: 'orange' 128 | } 129 | } 130 | 131 | const state4 = reducer(state3, changeItemNameAction) 132 | ``` 133 | 134 | ```js 135 | // state4 136 | { 137 | users: { 138 | john: { 139 | name: 'Jim', 140 | items: [ { itemName: 'orange' } ] 141 | } 142 | } 143 | } 144 | ``` 145 | 146 | Finally, let's remove Jim from our state tree. Notice that we don't need to provide `userId`, since the delete mutation works on the entire collection. 147 | 148 | ```js 149 | const deleteUserAction = { 150 | type: 'users.delete', 151 | deleteKey: 'john' 152 | } 153 | 154 | const state5 = reducer(state4, deleteUserAction) 155 | ``` 156 | 157 | ```js 158 | // state5 159 | { 160 | users: {} 161 | } 162 | ``` 163 | 164 | Continue to [Part 4 - Initial State](./4-initial-state.md). -------------------------------------------------------------------------------- /docs/4-initial-state.md: -------------------------------------------------------------------------------- 1 | # Part 4 - Initial State 2 | 3 | >This section discusses the finer points about setting up initial state in Remerge. The example code here can be found in [examples/initial-state](../examples/initial-state.js) 4 | 5 | For the purposes of this discussion, let's consider such a schema. In addition to holding the state for an array of todos, the state tree also holds the state for two modals on the frontend - one for adding a todo, and another for removing a todo. A value of `true` indicates that the modal is visible. 6 | 7 | ```js 8 | const toggleModalReducer = ( 9 | state, 10 | action 11 | ) => { 12 | state[action.modalName] = action.modalOpen 13 | return state 14 | } 15 | 16 | const reducer = merge({ 17 | todos: { 18 | _: [], 19 | add: arrayInsertReducer, 20 | delete: arrayDeleteReducer 21 | }, 22 | ui: { 23 | modals: { 24 | addTodo: { 25 | _: false 26 | }, 27 | deleteTodo: { 28 | _: false 29 | }, 30 | toggle: toggleModalReducer 31 | } 32 | } 33 | }) 34 | ``` 35 | 36 | In addition to `todos`, we've also defined the initial state for `addTodo` and `deleteTodo`. The first reason we do this is to allow the views to (correctly) hide the modal when it reads a value of `false` from the state tree for its initial render. The second reason we need to do this is because this allows `toggleModalReducer` to access the keys when it sets them. 37 | 38 | ```js 39 | const initialStateTree = reducer() 40 | const openAddTodoModal = { 41 | type: 'ui.modals.toggle', 42 | modalName: 'addTodo', 43 | modalOpen: true 44 | } 45 | 46 | const state1 = reducer(initialStateTree, openAddTodoModal) 47 | ``` 48 | 49 | ```js 50 | // initialStateTree 51 | { 52 | todos: [], 53 | ui: { 54 | modals: { 55 | addTodo: false, 56 | deleteTodo: false 57 | } 58 | } 59 | } 60 | 61 | // state1 62 | { 63 | todos: [], 64 | ui: { 65 | modals: { 66 | addTodo: true, 67 | deleteTodo: false 68 | } 69 | } 70 | } 71 | ``` 72 | 73 | ### Alternative Pattern 74 | 75 | The schema could also have been written like this: 76 | 77 | ```js 78 | const reducer = merge({ 79 | todos: { 80 | _: [], 81 | add: arrayInsertReducer, 82 | delete: arrayDeleteReducer 83 | }, 84 | ui: { 85 | modals: { 86 | _: { 87 | addTodo: false, 88 | deleteTodo: false 89 | }, 90 | toggle: toggleModalReducer 91 | } 92 | } 93 | }) 94 | ``` 95 | 96 | This is a matter of taste. 97 | 98 | ### Missing initial state 99 | 100 | If you omit the initial state, Remerge will default to a value of `null`: 101 | 102 | ```js 103 | const reducer = merge({ 104 | todos: { 105 | add: arrayInsertReducer, 106 | delete: arrayDeleteReducer 107 | }, 108 | ui: { 109 | modals: { 110 | addTodo: { 111 | _: false 112 | }, 113 | deleteTodo: { 114 | _: false 115 | }, 116 | toggle: toggleModalReducer 117 | } 118 | } 119 | }) 120 | ``` 121 | 122 | will result in a initial state tree of: 123 | 124 | ```js 125 | { 126 | todos: null, 127 | ui: { 128 | modals: { 129 | addTodo: false, 130 | deleteTodo: false 131 | } 132 | } 133 | } 134 | ``` 135 | 136 | This may or may not affect the operation of the state tree, depending on how your app handles `null` values, and how your reducers handle `null` values. 137 | 138 | Continue to [Part 5 - Debugging](./5-debugging.md). -------------------------------------------------------------------------------- /docs/5-debugging.md: -------------------------------------------------------------------------------- 1 | # Debugging Remerge 2 | 3 | >This section discusses how to debug when working with Remerge. 4 | 5 | For debugging purposes, the `merge` function also accepts a second optional argument. If set to `true`, the reducer will log out the path that actions take through the state tree. For example: 6 | 7 | ```js 8 | const reducer = merge({ 9 | users: { 10 | _: {}, 11 | add: objectInsertReducer, 12 | delete: objectDeleteReducer, 13 | $userId: { 14 | update: objectUpdateReducer, 15 | items: { 16 | add: arrayInsertReducer, 17 | delete: arrayDeleteReducer, 18 | $itemId: { 19 | update: objectUpdateReducer 20 | } 21 | } 22 | } 23 | } 24 | }, true) 25 | ``` 26 | 27 | The logs will look like this: 28 | 29 | ``` 30 | [remerge] Setting up initial state tree 31 | 32 | [remerge] Received action with type: users.add 33 | [remerge] Navigating element node: users 34 | [remerge] Executing action at leaf node: add 35 | 36 | [remerge] Received action with type: users.items.add 37 | [remerge] Navigating collection node: users 38 | [remerge] Navigating element node: items 39 | [remerge] Executing action at leaf node: add 40 | 41 | [remerge] Received action with type: users.update 42 | [remerge] Navigating collection node: users 43 | [remerge] Executing action at leaf node: update 44 | 45 | [remerge] Received action with type: users.items.update 46 | [remerge] Navigating collection node: users 47 | [remerge] Navigating collection node: items 48 | [remerge] Executing action at leaf node: update 49 | 50 | [remerge] Received action with type: users.delete 51 | [remerge] Navigating element node: users 52 | [remerge] Executing action at leaf node: delete 53 | ``` 54 | 55 | Continue to [Part 6 - Working with third party reducers](./6-third-party-reducers.md). -------------------------------------------------------------------------------- /docs/6-third-party-reducers.md: -------------------------------------------------------------------------------- 1 | # Working with third party reducers 2 | 3 | >This section discusses how Remerge can be used in conjunction with third party libraries such as React Router Redux and Redux Form, which expose reducers to hook into Redux state. 4 | 5 | In order to allow Remerge to work with third party libraries, it includes a way to handle action types that do not follow Remerge's navigational pattern. For example, React Router Redux emits an action with the type `@@router/LOCATION_CHANGE`. 6 | 7 | In a vanilla Redux app, one would include third party reducers like this: 8 | 9 | ```js 10 | import reducers from './reducers' 11 | import { routerReducer } from 'react-router-redux' 12 | import {reducer as formReducer} from 'redux-form' 13 | 14 | const reducer = combineReducers({ 15 | ...reducers, 16 | routing: routerReducer, 17 | form: formReducer 18 | }) 19 | ``` 20 | 21 | In Remerge, third party reducers are indicated with the special `__key__` syntax, like so: 22 | 23 | 24 | ```js 25 | import { routerReducer } from 'react-router-redux' 26 | import {reducer as formReducer} from 'redux-form' 27 | 28 | const reducer = merge({ 29 | __routing__: routerReducer, 30 | __form__: formReducer, 31 | users: { 32 | // rest of Remerge schema 33 | } 34 | }) 35 | ``` 36 | 37 | Whatever is between the leading and trailing underscores will be the key that the reducer is assigned to. 38 | 39 | Continue to [Part 7 - Standalone usage](./7-standalone-usage.md). -------------------------------------------------------------------------------- /docs/7-standalone-usage.md: -------------------------------------------------------------------------------- 1 | # Standalone usage 2 | 3 | >This section discusses how Remerge can be used as a standalone script. 4 | 5 | Remerge is also distributed as a standalone script, which exposes a global `merge` function. 6 | 7 | Remerge's built-in reducers are distributed in another script, which exposes a global `reducers` object whose properties are the reducers. 8 | 9 | These scripts can be found in the [dist](../dist) folder. 10 | 11 | Usage of the script is mostly the same, except that the reducers are namespaced under `reducers`: 12 | 13 | ```html 14 | 15 | 16 | 17 | 26 | ``` 27 | 28 | A full example can be found in [examples/standalone](../examples/standalone). -------------------------------------------------------------------------------- /docs/convenience-reducers/README.md: -------------------------------------------------------------------------------- 1 | # Convenience Reducers 2 | 3 | A exhaustive list of reducers that ship with Remerge include: 4 | 5 | ```js 6 | import { arrayInsertReducer, arrayDeleteReducer } from 'remerge/lib/arrayReducers' 7 | import { objectInsertReducer, objectDeleteReducer, objectUpdateReducer } from 'remerge/lib/objectReducers' 8 | import { mapInsertReducer, mapDeleteReducer } from 'remerge/lib/mapReducers' 9 | ``` 10 | 11 | This page serves as documentation for all of these reducers. 12 | 13 | ### Array Reducers 14 | 15 | #### Adding array elements 16 | 17 | `arrayInsertReducer` is used to append an object to the state, which is expected to be an array. `arrayInsertReducer` expects the object to be appended to be indicated with the `data` key and optionally, the index `insertIndex`: 18 | 19 | ```js 20 | const addItemAction = { 21 | type: 'items.add', 22 | insertIndex: 0, 23 | data: { 24 | name: 'model' 25 | } 26 | } 27 | ``` 28 | 29 | #### Deleting array elements 30 | 31 | `arrayDeleteReducer` is used to remove an object by index from the state, which is expeced to be an array. `arrayDeleteReducer` expects the index of the object to be indicated with the `deleteIndex` key: 32 | 33 | 34 | ```js 35 | const deleteItemAction = { 36 | type: 'items.delete', 37 | deleteIndex: 1 38 | } 39 | ``` 40 | 41 | ### Object Reducers 42 | 43 | #### Inserting object keys 44 | 45 | `objectInsertReducer` is used to add a key-value pair to the state, which is expected to be an object. `objectInsertReducer` expects the key to be indicated with the `insertKey` key, and the value to be indicated with the `data` key: 46 | 47 | ```js 48 | const addItemAction = { 49 | type: 'items.add', 50 | insertKey: 'apple', 51 | data: { name: 'a yummy apple' } 52 | } 53 | ``` 54 | 55 | If there is an existing key in the object, it is overwritten. 56 | 57 | #### Deleting object keys 58 | 59 | `objectDeleteReducer` is used to remove a key-value pair from the state, which is expected to be an object. `objectDeleteReducer` expects the key to be indicated with the `deleteKey` key: 60 | 61 | ```js 62 | const deleteItemAction = { 63 | type: 'models.delete', 64 | deleteKey: 'apple' 65 | } 66 | ``` 67 | 68 | #### objectUpdateReducer 69 | 70 | `objectUpdateReducer` is used to update the state, which is expected to be an object. `objectUpdateReducer` expects the updated object to be indicated with the `data` key: 71 | 72 | ```js 73 | const updateItemAction = { 74 | type: 'items.update', 75 | data: { name: 'a yummier apple' } 76 | } 77 | ``` 78 | 79 | ### Map Reducers 80 | 81 | Map reducers - `mapInsertReducer` and `mapDeleteReducer` - are used exactly the same as their object reducer counterparts. -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | # Examples 2 | 3 | All the examples for Remerge can be found here. For the standalone `.js` fies, you can run them with `./node_modules/.bin/babel-node examples/[test].js` (don't forget to `npm install`!) 4 | 5 | The rest of the folders (except `standalone`) are larger examples that have been refactored to use Remerge. To run them, `cd` into the directory and `npm install` followed by `npm start`. Those that contain tests have also been rewritten for Remerge, and can be run with `npm test`. 6 | 7 | `standalone` is an example of how Remerge can be used as a standalone script. It consists of a plain HTML page that can be viewed with a browser. -------------------------------------------------------------------------------- /examples/basic.js: -------------------------------------------------------------------------------- 1 | import merge from '../src' 2 | import { printTree } from '../src/utils' 3 | 4 | const todoAddReducer = ( 5 | state = [], 6 | action 7 | ) => { 8 | return state.concat(action.todo) 9 | } 10 | 11 | const todoDeleteReducer = ( 12 | state = [], 13 | ) => { 14 | state.pop() 15 | return state 16 | } 17 | 18 | const reducer = merge({ 19 | todos: { 20 | _: [], 21 | add: todoAddReducer, 22 | delete: todoDeleteReducer 23 | } 24 | }, true) 25 | 26 | const initialStateTree = reducer() 27 | printTree(initialStateTree) 28 | 29 | const addTodo = { 30 | type: 'todos.add', 31 | todo: { 32 | title: 'Buy milk' 33 | } 34 | } 35 | 36 | const state1 = reducer(initialStateTree, addTodo) 37 | printTree(state1) 38 | 39 | const deleteTodo = { 40 | type: 'todos.delete' 41 | } 42 | 43 | const state2 = reducer(state1, deleteTodo) 44 | printTree(state2) 45 | -------------------------------------------------------------------------------- /examples/convenience-reducers.js: -------------------------------------------------------------------------------- 1 | 2 | import merge from '../src' 3 | import { arrayInsertReducer, arrayDeleteReducer } from '../src/arrayReducers' 4 | import { objectInsertReducer, objectDeleteReducer, objectUpdateReducer } from '../src/objectReducers' 5 | import { printTree } from '../src/utils' 6 | 7 | const reducer = merge({ 8 | users: { 9 | _: {}, 10 | add: objectInsertReducer, 11 | delete: objectDeleteReducer, 12 | $userId: { 13 | update: objectUpdateReducer, 14 | items: { 15 | add: arrayInsertReducer, 16 | delete: arrayDeleteReducer, 17 | $itemId: { 18 | update: objectUpdateReducer 19 | } 20 | } 21 | } 22 | } 23 | }, true) 24 | 25 | const initialStateTree = reducer() 26 | printTree(initialStateTree) 27 | 28 | const addUserAction = { 29 | type: 'users.add', 30 | insertKey: 'user0id', 31 | data: { 32 | name: 'John' 33 | } 34 | } 35 | 36 | const state1 = reducer(initialStateTree, addUserAction) 37 | printTree(state1) 38 | 39 | const addAppleAction = { 40 | type: 'users.items.add', 41 | userId: 'user0id', 42 | data: { 43 | itemName: 'apple' 44 | } 45 | } 46 | 47 | const state2 = reducer(state1, addAppleAction) 48 | printTree(state2) 49 | 50 | const changeUserNameAction = { 51 | type: 'users.update', 52 | userId: 'user0id', 53 | data: { 54 | name: 'Jim' 55 | } 56 | } 57 | 58 | const state3 = reducer(state2, changeUserNameAction) 59 | printTree(state3) 60 | 61 | const changeItemNameAction = { 62 | type: 'users.items.update', 63 | userId: 'user0id', 64 | itemId: 0, 65 | data: { 66 | itemName: 'orange' 67 | } 68 | } 69 | 70 | const state4 = reducer(state3, changeItemNameAction) 71 | printTree(state4) 72 | 73 | const deleteUserAction = { 74 | type: 'users.delete', 75 | deleteKey: 'user0id' 76 | } 77 | 78 | const state5 = reducer(state4, deleteUserAction) 79 | printTree(state5) 80 | -------------------------------------------------------------------------------- /examples/initial-state.js: -------------------------------------------------------------------------------- 1 | 2 | import merge from '../src' 3 | import { arrayInsertReducer, arrayDeleteReducer } from '../src/arrayReducers' 4 | import { printTree } from '../src/utils' 5 | 6 | const toggleModalReducer = ( 7 | state, 8 | action 9 | ) => { 10 | state[action.modalName] = action.modalOpen 11 | return state 12 | } 13 | 14 | const reducer = merge({ 15 | todos: { 16 | add: arrayInsertReducer, 17 | delete: arrayDeleteReducer 18 | }, 19 | ui: { 20 | modals: { 21 | addTodo: { 22 | _: false 23 | }, 24 | deleteTodo: { 25 | _: false 26 | }, 27 | toggle: toggleModalReducer 28 | } 29 | } 30 | }, true) 31 | 32 | const initialStateTree = reducer() 33 | printTree(initialStateTree) 34 | 35 | const openAddTodoModal = { 36 | type: 'ui.modals.toggle', 37 | modalName: 'addTodo', 38 | modalOpen: true 39 | } 40 | 41 | const state1 = reducer(initialStateTree, openAddTodoModal) 42 | printTree(state1) 43 | -------------------------------------------------------------------------------- /examples/react-router-redux/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "react", "stage-1"] 3 | } 4 | -------------------------------------------------------------------------------- /examples/react-router-redux/README.md: -------------------------------------------------------------------------------- 1 | **Note from Remerge**: 2 | 3 | The refactor diff can be found [here](https://github.com/siawyoung/remerge/commit/5b0123b0840c5cf755497207ec5070b50485f6e0). This diff shows how we refactored this example to use Remerge. 4 | 5 | The below is the original README. 6 | 7 | react-router-redux basic example 8 | ================================= 9 | 10 | This is a basic example that demonstrates rendering components based 11 | on URLs with `react-router` as well as connecting them to Redux state. 12 | It uses both elements as well as the `push` action creator 13 | provided by react-router-redux. 14 | 15 | This example also demonstrates integration with 16 | **[redux-devtools](https://github.com/gaearon/redux-devtools) ^3.0.0** 17 | 18 | **To run, follow these steps:** 19 | 20 | 1. Install dependencies with `npm install` in this directory (make sure it creates a local node_modules) 21 | 2. By default, it uses the local version from `src` of react-router-redux, so you need to run `npm install` from there first. If you want to use a version straight from npm, remove the lines in `webpack.config.js` at the bottom. 22 | 3. Start build with `npm start` 23 | 4. Open [http://localhost:8080/](http://localhost:8080/) 24 | 25 | - 26 | 27 | If you want to run the example from the npm published version of 28 | **react-router-redux**, remove the alias in `webpack.config` 29 | to the source from line 21. 30 | 31 | This example uses the latest version, switch to a specific tag to use a stable version: 32 | 33 | e.g. [react-router-redux tag 1.0.2](https://github.com/reactjs/react-router-redux/tree/1.0.2/examples/basic) 34 | -------------------------------------------------------------------------------- /examples/react-router-redux/actions/count.js: -------------------------------------------------------------------------------- 1 | export function increase(n) { 2 | return { 3 | type: 'count.increase', 4 | amount: n 5 | } 6 | } 7 | 8 | export function decrease(n) { 9 | return { 10 | type: 'count.decrease', 11 | amount: n 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /examples/react-router-redux/app.js: -------------------------------------------------------------------------------- 1 | import { createDevTools } from 'redux-devtools' 2 | import LogMonitor from 'redux-devtools-log-monitor' 3 | import DockMonitor from 'redux-devtools-dock-monitor' 4 | 5 | import React from 'react' 6 | import ReactDOM from 'react-dom' 7 | import { createStore, combineReducers } from 'redux' 8 | import { Provider } from 'react-redux' 9 | import { Router, Route, IndexRoute, browserHistory } from 'react-router' 10 | import { syncHistoryWithStore, routerReducer } from 'react-router-redux' 11 | 12 | import * as reducers from './reducers' 13 | import { App, Home, Foo, Bar } from './components' 14 | 15 | import merge from 'remerge' 16 | 17 | const increaseReducer = ( 18 | state, 19 | action 20 | ) => { 21 | return state + action.amount 22 | } 23 | 24 | const decreaseReducer = ( 25 | state, 26 | action 27 | ) => { 28 | return state - action.amount 29 | } 30 | 31 | const reducer = merge({ 32 | __routing__: routerReducer, 33 | count: { 34 | _: 1, 35 | increase: increaseReducer, 36 | decrease: decreaseReducer 37 | } 38 | }, true) 39 | 40 | const DevTools = createDevTools( 41 | 42 | 43 | 44 | ) 45 | 46 | const store = createStore( 47 | reducer, 48 | DevTools.instrument() 49 | ) 50 | const history = syncHistoryWithStore(browserHistory, store) 51 | 52 | ReactDOM.render( 53 | 54 |
55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 |
64 |
, 65 | document.getElementById('mount') 66 | ) 67 | -------------------------------------------------------------------------------- /examples/react-router-redux/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { Link, browserHistory } from 'react-router' 3 | 4 | export default function App({ children }) { 5 | return ( 6 |
7 |
8 | Links: 9 | {' '} 10 | Home 11 | {' '} 12 | Foo 13 | {' '} 14 | Bar 15 |
16 |
17 | 18 |
19 |
{children}
20 |
21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /examples/react-router-redux/components/Bar.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Bar() { 4 | return
And I am Bar!
5 | } 6 | -------------------------------------------------------------------------------- /examples/react-router-redux/components/Foo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | export default function Foo() { 4 | return
I am Foo!
5 | } 6 | -------------------------------------------------------------------------------- /examples/react-router-redux/components/Home.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { increase, decrease } from '../actions/count' 4 | 5 | function Home({ number, increase, decrease }) { 6 | return ( 7 |
8 | Some state changes: 9 | {number} 10 | 11 | 12 |
13 | ) 14 | } 15 | 16 | export default connect( 17 | state => ({ number: state.count.number }), 18 | { increase, decrease } 19 | )(Home) 20 | -------------------------------------------------------------------------------- /examples/react-router-redux/components/index.js: -------------------------------------------------------------------------------- 1 | export App from './App' 2 | export Home from './Home' 3 | export Foo from './Foo' 4 | export Bar from './Bar' 5 | -------------------------------------------------------------------------------- /examples/react-router-redux/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | react-router-redux basic example 5 | 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/react-router-redux/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rrr-basic-example", 3 | "version": "0.0.0", 4 | "repository": "reactjs/react-router-redux", 5 | "license": "MIT", 6 | "dependencies": { 7 | "react": "^0.14.7", 8 | "react-dom": "^0.14.7", 9 | "react-redux": "^4.3.0", 10 | "react-router": "^2.0.0", 11 | "redux": "^3.2.1", 12 | "react-router-redux": "^4.0.0", 13 | "remerge": "../../../remerge" 14 | }, 15 | "devDependencies": { 16 | "babel-core": "^6.4.5", 17 | "babel-eslint": "^5.0.0-beta9", 18 | "babel-loader": "^6.2.2", 19 | "babel-preset-es2015": "^6.3.13", 20 | "babel-preset-react": "^6.3.13", 21 | "babel-preset-stage-1": "^6.3.13", 22 | "eslint": "^1.10.3", 23 | "eslint-config-rackt": "^1.1.1", 24 | "eslint-plugin-react": "^3.16.1", 25 | "redux-devtools": "^3.1.0", 26 | "redux-devtools-dock-monitor": "^1.0.1", 27 | "redux-devtools-log-monitor": "^1.0.4", 28 | "webpack": "^1.12.13", 29 | "webpack-dev-server": "^1.14.1" 30 | }, 31 | "scripts": { 32 | "start": "webpack-dev-server --history-api-fallback --no-info --open" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /examples/react-router-redux/reducers/count.js: -------------------------------------------------------------------------------- 1 | const initialState = { 2 | number: 1 3 | } 4 | 5 | export default function update(state = initialState, action) { 6 | if(action.type === INCREASE) { 7 | return { number: state.number + action.amount } 8 | } 9 | else if(action.type === DECREASE) { 10 | return { number: state.number - action.amount } 11 | } 12 | return state 13 | } 14 | -------------------------------------------------------------------------------- /examples/react-router-redux/reducers/index.js: -------------------------------------------------------------------------------- 1 | export count from './count' 2 | -------------------------------------------------------------------------------- /examples/react-router-redux/webpack.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const path = require('path'); 3 | 4 | module.exports = { 5 | entry: './app.js', 6 | output: { 7 | path: path.join(__dirname, 'dist'), 8 | filename: 'bundle.js' 9 | }, 10 | module: { 11 | loaders: [{ 12 | test: /\.js$/, 13 | loader: 'babel', 14 | exclude: /node_modules/, 15 | include: __dirname 16 | }] 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/standalone/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Standalone Remerge Demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 |

Standalone Remerge Demo (with jQuery)

17 | 18 |

This example demonstrates how Remerge can be used in a standalone manner as a way to store and manipulate state. No React or Redux here, ladies and gentlemen.

19 | 20 |

As a way to demonstrate two things at once, I've written it so that adding todos is handled by a user-defined reducer, while deleting todos is handled by Remerge's built-in arrayRemoveReducer. This is purely meant as a showcase.

21 | 22 |
23 | 24 | 25 |
26 |
    27 |
    28 | 29 | 102 | 103 | 104 | -------------------------------------------------------------------------------- /examples/todos/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-2", "react"], 3 | "env": { 4 | "development": { 5 | "presets": ["react-hmre"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /examples/todos/README.md: -------------------------------------------------------------------------------- 1 | **Note from Remerge**: 2 | 3 | The refactor diff can be found [here](https://github.com/siawyoung/remerge/commit/8196139485c08ae77d9bd5cea89f56a62fd2c928). This diff shows how we refactored this example to use Remerge. -------------------------------------------------------------------------------- /examples/todos/actions/index.js: -------------------------------------------------------------------------------- 1 | let nextTodoId = 0 2 | 3 | export const addTodo = (text) => { 4 | return { 5 | type: 'todos.add', 6 | data: { 7 | id: nextTodoId++, 8 | text, 9 | completed: false 10 | } 11 | } 12 | } 13 | 14 | export const setVisibilityFilter = (filter) => { 15 | return { 16 | type: 'visibilityFilter.set', 17 | data: filter 18 | } 19 | } 20 | 21 | export const toggleTodo = (id) => { 22 | return { 23 | type: 'todos.toggle', 24 | id 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /examples/todos/components/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Footer from './Footer' 3 | import AddTodo from '../containers/AddTodo' 4 | import VisibleTodoList from '../containers/VisibleTodoList' 5 | 6 | const App = () => ( 7 |
    8 | 9 | 10 |
    11 |
    12 | ) 13 | 14 | export default App 15 | -------------------------------------------------------------------------------- /examples/todos/components/Footer.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import FilterLink from '../containers/FilterLink' 3 | 4 | const Footer = () => ( 5 |

    6 | Show: 7 | {" "} 8 | 9 | All 10 | 11 | {", "} 12 | 13 | Active 14 | 15 | {", "} 16 | 17 | Completed 18 | 19 |

    20 | ) 21 | 22 | export default Footer 23 | -------------------------------------------------------------------------------- /examples/todos/components/Link.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | const Link = ({ active, children, onClick }) => { 4 | if (active) { 5 | return {children} 6 | } 7 | 8 | return ( 9 | { 11 | e.preventDefault() 12 | onClick() 13 | }} 14 | > 15 | {children} 16 | 17 | ) 18 | } 19 | 20 | Link.propTypes = { 21 | active: PropTypes.bool.isRequired, 22 | children: PropTypes.node.isRequired, 23 | onClick: PropTypes.func.isRequired 24 | } 25 | 26 | export default Link 27 | -------------------------------------------------------------------------------- /examples/todos/components/Todo.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | 3 | const Todo = ({ onClick, completed, text }) => ( 4 |
  • 10 | {text} 11 |
  • 12 | ) 13 | 14 | Todo.propTypes = { 15 | onClick: PropTypes.func.isRequired, 16 | completed: PropTypes.bool.isRequired, 17 | text: PropTypes.string.isRequired 18 | } 19 | 20 | export default Todo 21 | -------------------------------------------------------------------------------- /examples/todos/components/TodoList.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react' 2 | import Todo from './Todo' 3 | 4 | const TodoList = ({ todos, onTodoClick }) => ( 5 |
      6 | {todos.map(todo => 7 | onTodoClick(todo.id)} 11 | /> 12 | )} 13 |
    14 | ) 15 | 16 | TodoList.propTypes = { 17 | todos: PropTypes.arrayOf(PropTypes.shape({ 18 | id: PropTypes.number.isRequired, 19 | completed: PropTypes.bool.isRequired, 20 | text: PropTypes.string.isRequired 21 | }).isRequired).isRequired, 22 | onTodoClick: PropTypes.func.isRequired 23 | } 24 | 25 | export default TodoList 26 | -------------------------------------------------------------------------------- /examples/todos/containers/AddTodo.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { connect } from 'react-redux' 3 | import { addTodo } from '../actions' 4 | 5 | let AddTodo = ({ dispatch }) => { 6 | let input 7 | 8 | return ( 9 |
    10 |
    { 11 | e.preventDefault() 12 | if (!input.value.trim()) { 13 | return 14 | } 15 | dispatch(addTodo(input.value)) 16 | input.value = '' 17 | }}> 18 | { 19 | input = node 20 | }} /> 21 | 24 |
    25 |
    26 | ) 27 | } 28 | AddTodo = connect()(AddTodo) 29 | 30 | export default AddTodo 31 | -------------------------------------------------------------------------------- /examples/todos/containers/FilterLink.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { setVisibilityFilter } from '../actions' 3 | import Link from '../components/Link' 4 | 5 | const mapStateToProps = (state, ownProps) => { 6 | return { 7 | active: ownProps.filter === state.visibilityFilter 8 | } 9 | } 10 | 11 | const mapDispatchToProps = (dispatch, ownProps) => { 12 | return { 13 | onClick: () => { 14 | dispatch(setVisibilityFilter(ownProps.filter)) 15 | } 16 | } 17 | } 18 | 19 | const FilterLink = connect( 20 | mapStateToProps, 21 | mapDispatchToProps 22 | )(Link) 23 | 24 | export default FilterLink 25 | -------------------------------------------------------------------------------- /examples/todos/containers/VisibleTodoList.js: -------------------------------------------------------------------------------- 1 | import { connect } from 'react-redux' 2 | import { toggleTodo } from '../actions' 3 | import TodoList from '../components/TodoList' 4 | 5 | const getVisibleTodos = (todos, filter) => { 6 | switch (filter) { 7 | case 'SHOW_ALL': 8 | return todos 9 | case 'SHOW_COMPLETED': 10 | return todos.filter(t => t.completed) 11 | case 'SHOW_ACTIVE': 12 | return todos.filter(t => !t.completed) 13 | } 14 | } 15 | 16 | const mapStateToProps = (state) => { 17 | return { 18 | todos: getVisibleTodos(state.todos, state.visibilityFilter) 19 | } 20 | } 21 | 22 | const mapDispatchToProps = (dispatch) => { 23 | return { 24 | onTodoClick: (id) => { 25 | dispatch(toggleTodo(id)) 26 | } 27 | } 28 | } 29 | 30 | const VisibleTodoList = connect( 31 | mapStateToProps, 32 | mapDispatchToProps 33 | )(TodoList) 34 | 35 | export default VisibleTodoList 36 | -------------------------------------------------------------------------------- /examples/todos/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Redux Todos example 5 | 6 | 7 |
    8 |
    9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/todos/index.js: -------------------------------------------------------------------------------- 1 | import 'babel-polyfill' 2 | import React from 'react' 3 | import { render } from 'react-dom' 4 | import { Provider } from 'react-redux' 5 | import { createStore } from 'redux' 6 | import todoApp from './reducers' 7 | import App from './components/App' 8 | 9 | let store = createStore(todoApp) 10 | 11 | render( 12 | 13 | 14 | , 15 | document.getElementById('root') 16 | ) 17 | -------------------------------------------------------------------------------- /examples/todos/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "redux-todos-example", 3 | "version": "0.0.0", 4 | "description": "Redux Todos example", 5 | "scripts": { 6 | "start": "node server.js", 7 | "test": "cross-env NODE_ENV=test mocha --recursive --compilers js:babel-register --require ./test/setup.js", 8 | "test:watch": "npm test -- --watch" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "https://github.com/reactjs/redux.git" 13 | }, 14 | "license": "MIT", 15 | "bugs": { 16 | "url": "https://github.com/reactjs/redux/issues" 17 | }, 18 | "homepage": "http://redux.js.org", 19 | "dependencies": { 20 | "babel-polyfill": "^6.3.14", 21 | "react": "^0.14.7", 22 | "react-dom": "^0.14.7", 23 | "react-redux": "^4.1.2", 24 | "redux": "^3.1.2", 25 | "remerge": "../../../remerge" 26 | }, 27 | "devDependencies": { 28 | "babel-core": "^6.3.15", 29 | "babel-loader": "^6.2.0", 30 | "babel-preset-es2015": "^6.3.13", 31 | "babel-preset-react": "^6.3.13", 32 | "babel-preset-react-hmre": "^1.1.1", 33 | "babel-preset-stage-2": "^6.5.0", 34 | "babel-register": "^6.3.13", 35 | "cross-env": "^1.0.7", 36 | "expect": "^1.8.0", 37 | "express": "^4.13.3", 38 | "jsdom": "^5.6.1", 39 | "mocha": "^2.2.5", 40 | "node-libs-browser": "^0.5.2", 41 | "react-addons-test-utils": "^0.14.7", 42 | "webpack": "^1.9.11", 43 | "webpack-dev-middleware": "^1.2.0", 44 | "webpack-hot-middleware": "^2.9.1" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /examples/todos/reducers/index.js: -------------------------------------------------------------------------------- 1 | import merge from 'remerge' 2 | import { arrayInsertReducer } from 'remerge/lib/arrayReducers' 3 | 4 | const todoToggleReducer = ( 5 | state, 6 | _action 7 | ) => ({ 8 | ...state, 9 | completed: !state.completed 10 | }) 11 | 12 | const updateVisibilityReducer = ( 13 | _state, 14 | action 15 | ) => action.data 16 | 17 | const todoApp = merge({ 18 | todos: { 19 | _: [], 20 | add: arrayInsertReducer, 21 | $id: { 22 | toggle: todoToggleReducer 23 | } 24 | }, 25 | visibilityFilter: { 26 | _: 'SHOW_ALL', 27 | set: updateVisibilityReducer 28 | } 29 | }, true) 30 | 31 | export default todoApp -------------------------------------------------------------------------------- /examples/todos/server.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | var webpackDevMiddleware = require('webpack-dev-middleware') 3 | var webpackHotMiddleware = require('webpack-hot-middleware') 4 | var config = require('./webpack.config') 5 | 6 | var app = new (require('express'))() 7 | var port = 3000 8 | 9 | var compiler = webpack(config) 10 | app.use(webpackDevMiddleware(compiler, { noInfo: true, publicPath: config.output.publicPath })) 11 | app.use(webpackHotMiddleware(compiler)) 12 | 13 | app.get("/", function(req, res) { 14 | res.sendFile(__dirname + '/index.html') 15 | }) 16 | 17 | app.listen(port, function(error) { 18 | if (error) { 19 | console.error(error) 20 | } else { 21 | console.info("==> 🌎 Listening on port %s. Open up http://localhost:%s/ in your browser.", port, port) 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /examples/todos/test/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "mocha": true 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /examples/todos/test/actions/todos.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import * as actions from '../../actions' 3 | 4 | describe('todo actions', () => { 5 | it('addTodo should create correct Remerge action', () => { 6 | expect(actions.addTodo('Use Redux')).toEqual({ 7 | type: 'todos.add', 8 | data: { 9 | id: 0, 10 | text: 'Use Redux', 11 | completed: false 12 | } 13 | }) 14 | }) 15 | 16 | it('setVisibilityFilter should create correct Remerge action', () => { 17 | expect(actions.setVisibilityFilter('active')).toEqual({ 18 | type: 'visibilityFilter.set', 19 | data: 'active' 20 | }) 21 | }) 22 | 23 | it('toogleTodo should create correct Remerge action', () => { 24 | expect(actions.toggleTodo(1)).toEqual({ 25 | type: 'todos.toggle', 26 | id: 1 27 | }) 28 | }) 29 | }) 30 | -------------------------------------------------------------------------------- /examples/todos/test/reducers/todos.spec.js: -------------------------------------------------------------------------------- 1 | import expect from 'expect' 2 | import todoApp from '../../reducers' 3 | import * as actions from '../../actions' 4 | 5 | describe('todoApp reducer', () => { 6 | 7 | it("should handle initial state", () => { 8 | expect( 9 | todoApp()) 10 | .toEqual({ 11 | todos: [], 12 | "visibilityFilter": "SHOW_ALL" 13 | }) 14 | }) 15 | 16 | it("should handle todos.add", () => { 17 | expect( 18 | todoApp( 19 | todoApp(), 20 | actions.addTodo('Use Redux') 21 | )) 22 | .toEqual({ 23 | todos: [ 24 | { 25 | id: 1, 26 | text: 'Use Redux', 27 | completed: false 28 | } 29 | ], 30 | "visibilityFilter": "SHOW_ALL" 31 | }) 32 | }) 33 | 34 | it("should handle todos.toggle", () => { 35 | const initialState = todoApp(todoApp(), actions.addTodo('Use Redux')) 36 | expect(todoApp( 37 | initialState, 38 | actions.toggleTodo(0) 39 | )) 40 | .toEqual({ 41 | todos: [ 42 | { 43 | id: 2, 44 | text: 'Use Redux', 45 | completed: true 46 | } 47 | ], 48 | "visibilityFilter": "SHOW_ALL" 49 | }) 50 | }) 51 | 52 | it("should handle visibilityFilter.set", () => { 53 | expect( 54 | todoApp( 55 | todoApp(), 56 | actions.setVisibilityFilter('active') 57 | )) 58 | .toEqual({ 59 | todos: [], 60 | "visibilityFilter": "active" 61 | }) 62 | }) 63 | 64 | }) 65 | -------------------------------------------------------------------------------- /examples/todos/test/setup.js: -------------------------------------------------------------------------------- 1 | import { jsdom } from 'jsdom' 2 | 3 | global.document = jsdom('') 4 | global.window = document.defaultView 5 | global.navigator = global.window.navigator 6 | -------------------------------------------------------------------------------- /examples/todos/webpack.config.js: -------------------------------------------------------------------------------- 1 | var path = require('path') 2 | var webpack = require('webpack') 3 | 4 | module.exports = { 5 | devtool: 'cheap-module-eval-source-map', 6 | entry: [ 7 | 'webpack-hot-middleware/client', 8 | './index' 9 | ], 10 | output: { 11 | path: path.join(__dirname, 'dist'), 12 | filename: 'bundle.js', 13 | publicPath: '/static/' 14 | }, 15 | plugins: [ 16 | new webpack.optimize.OccurenceOrderPlugin(), 17 | new webpack.HotModuleReplacementPlugin() 18 | ], 19 | module: { 20 | loaders: [ 21 | { 22 | test: /\.js$/, 23 | loaders: [ 'babel' ], 24 | exclude: /node_modules/, 25 | include: __dirname 26 | } 27 | ] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "remerge", 3 | "version": "0.0.8", 4 | "description": "State simplified.", 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "test": "ava --tap -v | tap-nyan", 8 | "prepublish": "rimraf lib && babel src --out-dir lib", 9 | "build:script": "webpack" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/siawyoung/remerge.git" 14 | }, 15 | "keywords": [ 16 | "javascript", 17 | "merge", 18 | "state", 19 | "redux", 20 | "react" 21 | ], 22 | "author": "Lau Siaw Young", 23 | "contributors": [ 24 | "Lau Siaw Young (https://github.com/siawyoung)", 25 | "Matthew Cheok (https://github.com/matthewcheok)" 26 | ], 27 | "license": "MIT", 28 | "bugs": { 29 | "url": "https://github.com/siawyoung/remerge/issues" 30 | }, 31 | "homepage": "https://github.com/siawyoung/remerge#readme", 32 | "devDependencies": { 33 | "ava": "^0.12.0", 34 | "babel-cli": "^6.5.1", 35 | "babel-core": "^6.5.2", 36 | "babel-loader": "^6.2.4", 37 | "babel-preset-es2015": "^6.5.0", 38 | "babel-preset-stage-2": "^6.5.0", 39 | "babel-register": "^6.5.2", 40 | "deep-freeze": "0.0.1", 41 | "rimraf": "^2.5.2", 42 | "tap-nyan": "0.0.2", 43 | "webpack": "^1.12.14" 44 | }, 45 | "dependencies": { 46 | "lodash.clone": "^4.3.1", 47 | "lodash.isfunction": "^3.0.8" 48 | }, 49 | "ava": { 50 | "require": [ 51 | "babel-register" 52 | ] 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/arrayReducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reducer that inserts a value to an array 3 | * @param {any} action.data The element to be added 4 | * @param {number} action.insertIndex The index to be inserted 5 | */ 6 | export const arrayInsertReducer = ( 7 | state = [], 8 | action 9 | ) => { 10 | const index = action.insertIndex == undefined ? state.length : action.insertIndex 11 | return [ 12 | ...state.slice(0, index), 13 | action.data, 14 | ...state.slice(index), 15 | ] 16 | } 17 | 18 | /** 19 | * Reducer that removes an element from an array 20 | * @param {number} action.deleteIndex The index to be removed 21 | */ 22 | export const arrayDeleteReducer = ( 23 | state, 24 | action 25 | ) => { 26 | const index = action.deleteIndex 27 | return [ 28 | ...state.slice(0, index), 29 | ...state.slice(index + 1), 30 | ] 31 | } 32 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The top-level merge function 3 | * 4 | * @param {object} remerge map 5 | * @return {function} reducer function 6 | * 7 | */ 8 | 9 | import isFunction from 'lodash.isfunction' 10 | import clone from 'lodash.clone' 11 | import { 12 | isMap, 13 | getCollectionElement, 14 | setCollectionElement, 15 | consoleMessage, 16 | consoleError, 17 | consoleSuccess, 18 | consoleWarning, 19 | consoleGrouped, 20 | consoleEndGrouped, 21 | } from './utils' 22 | 23 | const collectionRegex = /^\$(.+)/ 24 | const legacyRegex = /^__(.+)__$/ 25 | const remergeLegacyKey = '$legacy' 26 | 27 | const merge = (schema, debugMode = false) => { 28 | 29 | const _getAccessorKey = (key) => { 30 | // this regex tests if the key is of the form $abcd1234 31 | const captureAccessor = collectionRegex.exec(key) 32 | if (captureAccessor) { 33 | return captureAccessor[1] 34 | } else { 35 | return null 36 | } 37 | } 38 | 39 | const _getLegacyKey = (key) => { 40 | // this regex tests if the key is of the form __abcd__ 41 | const captureLegacy = legacyRegex.exec(key) 42 | if (captureLegacy) { 43 | return captureLegacy[1] 44 | } else { 45 | return null 46 | } 47 | } 48 | 49 | const _preprocess = (schema) => { 50 | const map = new Map() 51 | function _dive(subSchema, prefix = '', path = [], params = []) { 52 | for (const key in subSchema) { 53 | if (key === '_') { continue } 54 | const child = subSchema[key] 55 | const type = `${prefix}${key}` 56 | 57 | if (isFunction(child)) { 58 | const legacyKey = _getLegacyKey(key) 59 | if (legacyKey) { 60 | const node = { 61 | reducer: child, 62 | path: [legacyKey], 63 | } 64 | 65 | const nodes = map.get(remergeLegacyKey) || [] 66 | map.set(remergeLegacyKey, [...nodes, node]) 67 | } else { 68 | const node = { 69 | reducer: child, 70 | path, 71 | params, 72 | } 73 | 74 | const nodes = map.get(type) || [] 75 | map.set(type, [...nodes, node]) 76 | } 77 | } else { 78 | const param = _getAccessorKey(key) 79 | if (param) { 80 | _dive(child, prefix, [...path, key], [...params, param]) 81 | } else { 82 | _dive(child, `${type}.`, [...path, key], params) 83 | } 84 | } 85 | } 86 | } 87 | 88 | _dive(schema) 89 | return map 90 | } 91 | 92 | const _initial = (map) => { 93 | if (!map) { 94 | return undefined 95 | } else if (isFunction(map)) { 96 | return undefined 97 | } 98 | 99 | let newMap = map['_'] || {} 100 | let changed = map['_'] !== undefined 101 | 102 | for (const key in map) { 103 | if (key === '_') { continue } 104 | if (_getLegacyKey(key)) { 105 | newMap[_getLegacyKey(key)] = map[key](undefined, {}) 106 | changed = true 107 | } 108 | 109 | // to avoid adding keys to collections 110 | if (!_getAccessorKey(key)) { 111 | let result = _initial(map[key]) 112 | if (result !== undefined) { 113 | newMap[key] = result 114 | changed = true 115 | } 116 | } 117 | } 118 | 119 | return changed ? newMap : null 120 | } 121 | 122 | const _reduce = (state, action, node) => { 123 | let newState = clone(state) 124 | let current = newState 125 | let parent = null 126 | 127 | for (let key of node.path) { 128 | let accessorKey = _getAccessorKey(key) 129 | if (accessorKey) { 130 | parent = current 131 | current = clone(getCollectionElement(current, action[accessorKey])) 132 | setCollectionElement(parent, action[accessorKey], current) 133 | } else { 134 | parent = current 135 | current = clone(getCollectionElement(current, key)) 136 | setCollectionElement(parent, key, current) 137 | } 138 | } 139 | 140 | current = node.reducer(current, action) 141 | 142 | const lastKey = node.path[node.path.length-1] 143 | let accessorKey = _getAccessorKey(lastKey) 144 | if (accessorKey) { 145 | setCollectionElement(parent, action[accessorKey], current) 146 | } else { 147 | setCollectionElement(parent, lastKey, current) 148 | } 149 | return newState 150 | } 151 | 152 | const initialState = _initial(schema) 153 | const map = _preprocess(schema) 154 | 155 | return (state, action) => { 156 | if (action === undefined) { 157 | consoleMessage(debugMode, `Setting up initial state tree`) 158 | } else if (!action.type) { 159 | consoleError(debugMode, `Action is missing type`) 160 | } 161 | 162 | if (state === undefined) { 163 | return initialState 164 | } 165 | 166 | let successLogs = [] 167 | let errorLogs = [] 168 | 169 | let newState = state 170 | const nodes = map.get(action.type) 171 | if (nodes) { 172 | for (let node of nodes) { 173 | const valid = node.params.map((p) => action[p]).reduce((prev, curr) => (prev && curr !== undefined), true) 174 | if (valid) { 175 | successLogs.push(`Executing action ${action.type}`) 176 | for (let param of node.params) { 177 | successLogs.push(`$${param} = ${action[param]}`) 178 | } 179 | 180 | newState = _reduce(newState, action, node) 181 | break 182 | } else { 183 | errorLogs.push(`Could not execute action ${action.type} with params ${node.params}`) 184 | } 185 | } 186 | } else { 187 | const legacyNodes = map.get(remergeLegacyKey) 188 | for (let node of legacyNodes) { 189 | successLogs.push(`Executing legacy action ${action.type}`) 190 | newState = _reduce(newState, action, node) 191 | } 192 | } 193 | 194 | if (successLogs.length > 0) { 195 | consoleGrouped(debugMode, `Processing action ${action.type}`) 196 | for (let log of successLogs) { 197 | consoleSuccess(debugMode, log) 198 | } 199 | consoleEndGrouped(debugMode) 200 | } else if (errorLogs.length > 0) { 201 | consoleGrouped(debugMode, `Processing action ${action.type}`, false) 202 | for (let log of errorLogs) { 203 | consoleSuccess(debugMode, log) 204 | } 205 | consoleEndGrouped(debugMode) 206 | } else { 207 | consoleGrouped(debugMode, `Processing action ${action.type}`, false) 208 | consoleError(debugMode, 'No available action found!') 209 | consoleEndGrouped(debugMode) 210 | } 211 | 212 | return newState 213 | } 214 | } 215 | 216 | export default merge 217 | -------------------------------------------------------------------------------- /src/mapReducers.js: -------------------------------------------------------------------------------- 1 | 2 | import clone from 'lodash.clone' 3 | 4 | export const mapInsertReducer = ( 5 | state = new Map(), 6 | action 7 | ) => { 8 | const newState = clone(state) 9 | newState.set(action.insertKey, action.data) 10 | return newState 11 | } 12 | 13 | export const mapDeleteReducer = ( 14 | state, 15 | action 16 | ) => { 17 | const newState = clone(state) 18 | newState.delete(action.deleteKey) 19 | return newState 20 | } 21 | -------------------------------------------------------------------------------- /src/objectReducers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Reducer that inserts a value to an object 3 | * @param {any} action.data The element to be added 4 | * @param {string} action.insertKey The key to use 5 | */ 6 | export const objectInsertReducer = ( 7 | state = {}, 8 | action 9 | ) => { 10 | return { 11 | ...state, 12 | [`${action.insertKey}`]: action.data 13 | } 14 | } 15 | 16 | /** 17 | * Reducer that removes a value from an object 18 | * @param {string} action.deleteKey The key to use 19 | */ 20 | export const objectDeleteReducer = ( 21 | state, 22 | action 23 | ) => { 24 | let newState = {...state} 25 | delete newState[action.deleteKey] 26 | return newState 27 | } 28 | 29 | /** 30 | * Reducer that updates an object 31 | * @param {any} action.data The element to update 32 | */ 33 | export const objectUpdateReducer = ( 34 | state, 35 | action 36 | ) => ({ 37 | ...state, 38 | ...action.data 39 | }) 40 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | 2 | import util from 'util' 3 | 4 | export function isMap(obj) { 5 | return Object.getPrototypeOf(obj) === Map.prototype 6 | || obj.constructor.name === 'Map' // Ava has a map shim which doesn't play nicely 7 | } 8 | 9 | export function debug(map, state, action) { 10 | console.log('----------------------') 11 | console.log('Running process') 12 | console.log('current map') 13 | console.log(map) 14 | console.log('current state') 15 | console.log(state) 16 | console.log('current action') 17 | console.log(action) 18 | console.log('----------------------') 19 | } 20 | 21 | export function printTree(obj) { 22 | console.log(util.inspect(obj, false, null)) 23 | } 24 | 25 | export function getCollectionElement(collection, key) { 26 | if (isMap(collection)) { 27 | return collection.get(key) 28 | } else { 29 | return collection[key] 30 | } 31 | } 32 | 33 | export function setCollectionElement(collection, key, value) { 34 | if (isMap(collection)) { 35 | collection.set(key, value) 36 | } else { 37 | collection[key] = value 38 | } 39 | } 40 | 41 | const colors = { 42 | black : "font-weight : bold; color : #000000;", 43 | gray : "font-weight : bold; color : #1B2B34;", 44 | red : "font-weight : bold; color : #EC5f67;", 45 | orange : "font-weight : bold; color : #F99157;", 46 | yellow : "font-weight : bold; color : #FAC863;", 47 | green : "font-weight : bold; color : #99C794;", 48 | teal : "font-weight : bold; color : #5FB3B3;", 49 | blue : "font-weight : bold; color : #6699CC;", 50 | purple : "font-weight : bold; color : #C594C5;", 51 | brown : "font-weight : bold; color : #AB7967;" 52 | } 53 | 54 | function _console(msg, color, func = console.log) { 55 | if (typeof window === 'undefined') { 56 | func(`[remerge] ${msg}`) 57 | } else { 58 | func.call(console, `%c[remerge]%c ${msg}`, color, '') 59 | } 60 | } 61 | 62 | export function consoleMessage(debugMode, msg) { 63 | if (debugMode) { 64 | _console(msg, colors.black) 65 | } 66 | } 67 | 68 | export function consoleWarning(debugMode, msg) { 69 | if (debugMode) { 70 | _console(msg, colors.yellow) 71 | } 72 | } 73 | 74 | export function consoleSuccess(debugMode, msg) { 75 | if (debugMode) { 76 | _console(msg, colors.green) 77 | } 78 | } 79 | 80 | export function consoleError(debugMode, msg) { 81 | if (debugMode) { 82 | _console(msg, colors.red) 83 | } 84 | } 85 | 86 | export function consoleGrouped(debugMode, msg, collapsed = true) { 87 | if (debugMode && console.group) { 88 | _console(msg, colors.black, collapsed ? console.groupCollapsed : console.group) 89 | } else if (debugMode) { 90 | _console(msg, colors.black) 91 | } 92 | } 93 | 94 | export function consoleEndGrouped(debugMode) { 95 | if (debugMode && console.groupEnd) { 96 | console.groupEnd() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/array.test.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * 4 | * Test suite for array-based state 5 | * 6 | */ 7 | 8 | import test from 'ava' 9 | import deepFreeze from 'deep-freeze' 10 | 11 | import merge from '../src' 12 | import { arrayInsertReducer, arrayDeleteReducer } from '../src/arrayReducers' 13 | import { objectUpdateReducer } from '../src/objectReducers' 14 | 15 | const arrayReducer = merge({ 16 | models: { 17 | _: [], 18 | add: arrayInsertReducer, 19 | delete: arrayDeleteReducer, 20 | $modelId: { 21 | update: objectUpdateReducer, 22 | fields: { 23 | add: arrayInsertReducer, 24 | delete: arrayDeleteReducer, 25 | $fieldId: { 26 | update: objectUpdateReducer 27 | } 28 | } 29 | } 30 | } 31 | }, false) 32 | 33 | 34 | test('Initial state', (t) => { 35 | const stateAfter = { 36 | models: [] 37 | } 38 | 39 | deepFreeze(stateAfter) 40 | t.same(arrayReducer(), stateAfter) 41 | }) 42 | 43 | test('Add model', (t) => { 44 | const action = { 45 | type : 'models.add', 46 | data: { 47 | name: 'model' 48 | } 49 | } 50 | 51 | const stateBefore = { 52 | models: [] 53 | } 54 | const stateAfter = { 55 | models: [ {name: 'model'} ] 56 | } 57 | 58 | deepFreeze(action) 59 | deepFreeze(stateBefore) 60 | deepFreeze(stateAfter) 61 | 62 | t.same(arrayReducer(stateBefore, action), stateAfter) 63 | }) 64 | 65 | test('Insert model', (t) => { 66 | const action = { 67 | type : 'models.add', 68 | data: { 69 | name: 'model1' 70 | }, 71 | insertIndex: 0, 72 | } 73 | 74 | const stateBefore = { 75 | models: [ {name: 'model2'} ] 76 | } 77 | const stateAfter = { 78 | models: [ {name: 'model1'}, {name: 'model2'} ] 79 | } 80 | 81 | deepFreeze(action) 82 | deepFreeze(stateBefore) 83 | deepFreeze(stateAfter) 84 | 85 | t.same(arrayReducer(stateBefore, action), stateAfter) 86 | }) 87 | 88 | 89 | test('Update model', (t) => { 90 | const action = { 91 | type : 'models.update', 92 | modelId: 0, 93 | data: { 94 | name: 'updatedModel' 95 | } 96 | } 97 | 98 | const stateBefore = { 99 | models: [ { name: 'oldModel' } ] 100 | } 101 | const stateAfter = { 102 | models: [ {name: 'updatedModel'} ] 103 | } 104 | 105 | deepFreeze(action) 106 | deepFreeze(stateBefore) 107 | deepFreeze(stateAfter) 108 | 109 | t.same(arrayReducer(stateBefore, action), stateAfter) 110 | }) 111 | 112 | test('Delete model', (t) => { 113 | const action = { 114 | type: 'models.delete', 115 | deleteIndex: 1 116 | } 117 | 118 | const stateBefore = { 119 | models: [ 120 | { name: "model 1" }, 121 | { name: 'model 2' } 122 | ] 123 | } 124 | 125 | const stateAfter = { 126 | models: [ 127 | { name: "model 1" } 128 | ] 129 | } 130 | 131 | deepFreeze(action) 132 | deepFreeze(stateBefore) 133 | deepFreeze(stateAfter) 134 | 135 | t.same(arrayReducer(stateBefore, action), stateAfter) 136 | }) 137 | 138 | test('Add field to model', (t) => { 139 | const action = { 140 | type : 'models.fields.add', 141 | modelId: 0, 142 | data: { 143 | name: "field a", 144 | type: "integer" 145 | } 146 | } 147 | 148 | const stateBefore = { 149 | models: [ 150 | { 151 | name: "modelz", 152 | } 153 | ] 154 | } 155 | 156 | const stateAfter = { 157 | models: [ 158 | { 159 | name: "modelz", 160 | fields: [ {name: "field a", type: "integer"} ] 161 | } 162 | ] 163 | } 164 | 165 | deepFreeze(action) 166 | deepFreeze(stateBefore) 167 | deepFreeze(stateAfter) 168 | 169 | t.same(arrayReducer(stateBefore, action), stateAfter) 170 | 171 | }) 172 | 173 | test('Update field of model', (t) => { 174 | const action = { 175 | type : 'models.fields.update', 176 | modelId: 0, 177 | fieldId: 1, 178 | data: { 179 | name: "new field b", 180 | type: "integer" 181 | } 182 | } 183 | 184 | const stateBefore = { 185 | models: [ 186 | { 187 | name: "model", 188 | fields: [{ name: "field a", type: "string" }, { name: "old field b", type: "string" }] 189 | } 190 | ] 191 | } 192 | 193 | const stateAfter = { 194 | models: [ 195 | { 196 | name: "model", 197 | fields: [{ name: "field a", type: "string" }, { name: "new field b", type: "integer" }] 198 | } 199 | ] 200 | } 201 | 202 | deepFreeze(action) 203 | deepFreeze(stateBefore) 204 | deepFreeze(stateAfter) 205 | 206 | t.same(arrayReducer(stateBefore, action), stateAfter) 207 | 208 | }) 209 | 210 | test('Delete field of model', (t) => { 211 | const action = { 212 | type : 'models.fields.delete', 213 | modelId: 0, 214 | deleteIndex: 0 215 | } 216 | 217 | const stateBefore = { 218 | models: [ 219 | { 220 | name: "model", 221 | fields: [{ name: "field a", type: "string" }, { name: "old field b", type: "string" }] 222 | } 223 | ] 224 | } 225 | 226 | const stateAfter = { 227 | models: [ 228 | { 229 | name: "model", 230 | fields: [{ name: "old field b", type: "string" }] 231 | } 232 | ] 233 | } 234 | 235 | deepFreeze(action) 236 | deepFreeze(stateBefore) 237 | deepFreeze(stateAfter) 238 | 239 | t.same(arrayReducer(stateBefore, action), stateAfter) 240 | }) 241 | 242 | test('Array add test', (t) => { 243 |   const startTime = process.hrtime() 244 |   const action = { 245 |     type : 'models.add', 246 |     data: { 247 |       name: 'model' 248 |     } 249 |   } 250 | 251 |   let state = { 252 |     models: [] 253 |   } 254 | 255 |   for(let i = 0; i < 1000; i++) { 256 |     state = arrayReducer(state, action) 257 |   } 258 | 259 |   const endTime = process.hrtime(startTime) 260 |   console.info("Execution time: %ds %dms", endTime[0], endTime[1]/1000000) 261 | }) 262 | -------------------------------------------------------------------------------- /test/legacy.test.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * 4 | * Test suite for legacy reducer support 5 | * 6 | */ 7 | 8 | import test from 'ava' 9 | import deepFreeze from 'deep-freeze' 10 | 11 | import merge from '../src' 12 | import { arrayInsertReducer, arrayDeleteReducer } from '../src/arrayReducers' 13 | 14 | const legacyReducer = ( 15 | state = "initial state", 16 | action 17 | ) => { 18 | if (action.type === "LEGACY_ACTION_TYPE_1") { 19 | return "legacy state" 20 | } else { 21 | return state 22 | } 23 | } 24 | 25 | const anotherLegacyReducer = ( 26 | state = "initial state", 27 | action 28 | ) => { 29 | if (action.type === "LEGACY_ACTION_TYPE_2") { 30 | return "legacy state" 31 | } else { 32 | return state 33 | } 34 | } 35 | 36 | const reducer = merge({ 37 | _: { 38 | someTopLevelInitialState: 1 39 | }, 40 | models: { 41 | _: [], 42 | add: arrayInsertReducer, 43 | delete: arrayDeleteReducer, 44 | }, 45 | __legacy__: legacyReducer, 46 | __anotherLegacy__: anotherLegacyReducer 47 | }, true) 48 | 49 | test("Initial state for legacy reducer", (t) => { 50 | 51 | const stateAfter = { 52 | someTopLevelInitialState: 1, 53 | models: [], 54 | legacy: "initial state", 55 | anotherLegacy: "initial state" 56 | } 57 | 58 | deepFreeze(stateAfter) 59 | 60 | t.same(reducer(), stateAfter) 61 | 62 | }) 63 | 64 | // import { combineReducers } from 'redux' 65 | // import { routerReducer } from 'react-router-redux' 66 | 67 | // test.only("Routing test", (t) => { 68 | // const action = { 69 | // type: "@@router/LOCATION_CHANGE", 70 | // payload: { 71 | // pathname: "/fo", 72 | // search: "", 73 | // hash: "", 74 | // state: null, 75 | // action: "POP", 76 | // key: "k12jsad" 77 | // }, 78 | // query: {}, 79 | // $searhBase: { 80 | // search: "", 81 | // searchBase: "" 82 | // } 83 | // } 84 | 85 | // const stateBefore = { 86 | // // models: [], 87 | // routing: {} 88 | // } 89 | 90 | // const stateAfter = { 91 | // // models: [], 92 | // routing: { 93 | // locationBeforeTransitions: { 94 | // pathname: '/fo', 95 | // search: '', 96 | // hash: '', 97 | // state: null, 98 | // action: 'POP', 99 | // key: 'k12jsad' 100 | // } 101 | // }, 102 | // legacy: 'initial state' 103 | // } 104 | 105 | // deepFreeze(action) 106 | // deepFreeze(stateBefore) 107 | // deepFreeze(stateAfter) 108 | 109 | // let testReducer = combineReducers({ 110 | // routing: routerReducer 111 | // }) 112 | 113 | // // console.log('testReducer', testReducer(stateBefore, action)) 114 | // console.log('reducer', reducer(stateBefore, action)) 115 | // console.log('after', stateAfter) 116 | // t.same(reducer(stateBefore, action), stateAfter) 117 | // }) 118 | 119 | test("Legacy test", (t) => { 120 | 121 | const action = { 122 | type: "LEGACY_ACTION_TYPE_1" 123 | } 124 | 125 | const stateBefore = { 126 | models: [], 127 | legacy: undefined, 128 | anotherLegacy: undefined, 129 | } 130 | 131 | const stateAfter = { 132 | models: [], 133 | legacy: "legacy state", 134 | anotherLegacy: "initial state" 135 | } 136 | 137 | deepFreeze(action) 138 | deepFreeze(stateBefore) 139 | deepFreeze(stateAfter) 140 | 141 | t.same(reducer(stateBefore, action), stateAfter) 142 | 143 | }) 144 | 145 | test("Multiple legacy test", (t) => { 146 | 147 | const action = { 148 | type: "LEGACY_ACTION_TYPE_1" 149 | } 150 | 151 | const action2 = { 152 | type: "LEGACY_ACTION_TYPE_2" 153 | } 154 | 155 | const stateBefore = { 156 | models: [], 157 | legacy: undefined, 158 | anotherLegacy: undefined, 159 | } 160 | 161 | const stateAfter = { 162 | models: [], 163 | legacy: "legacy state", 164 | anotherLegacy: "legacy state", 165 | } 166 | 167 | deepFreeze(action) 168 | deepFreeze(stateBefore) 169 | deepFreeze(stateAfter) 170 | 171 | t.same(reducer(reducer(stateBefore, action), action2), stateAfter) 172 | 173 | }) -------------------------------------------------------------------------------- /test/map.test.js: -------------------------------------------------------------------------------- 1 | 2 | import test from 'ava' 3 | import deepFreeze from 'deep-freeze' 4 | 5 | import merge from '../src' 6 | import { mapInsertReducer, mapDeleteReducer } from '../src/mapReducers' 7 | import { objectUpdateReducer } from '../src/objectReducers' 8 | 9 | const mapReducer = merge({ 10 | models: { 11 | _: new Map(), 12 | add: mapInsertReducer, 13 | delete: mapDeleteReducer, 14 | $modelId: { 15 | update: objectUpdateReducer, 16 | fields: { 17 | add: mapInsertReducer 18 | } 19 | } 20 | } 21 | }, true) 22 | 23 | test('Initial state', (t) => { 24 | const stateAfter = { 25 | models: new Map() 26 | } 27 | 28 | deepFreeze(stateAfter) 29 | t.same(mapReducer(), stateAfter) 30 | }) 31 | 32 | test('Add model', (t) => { 33 | const action = { 34 | type: 'models.add', 35 | addIndex: 'abcde', 36 | data: { 37 | name: 'model abcde' 38 | } 39 | } 40 | 41 | const stateBefore = { 42 | models: new Map() 43 | } 44 | 45 | const stateAfter = { 46 | models: new Map([['abcde', { name: 'model abcde' }]]) 47 | } 48 | 49 | deepFreeze(action) 50 | deepFreeze(stateBefore) 51 | deepFreeze(stateAfter) 52 | t.same(mapReducer(stateBefore, action), stateAfter) 53 | }) 54 | 55 | test('Update model', (t) => { 56 | const action = { 57 | type: 'models.update', 58 | modelId: 'abcde', 59 | data: { 60 | name: 'updated model abcde' 61 | } 62 | } 63 | 64 | const stateBefore = { 65 | models: new Map([['abcde', { name: 'model abcde' }]]) 66 | } 67 | 68 | const stateAfter = { 69 | models: new Map([['abcde', { name: 'updated model abcde' }]]) 70 | } 71 | 72 | deepFreeze(action) 73 | deepFreeze(stateBefore) 74 | deepFreeze(stateAfter) 75 | 76 | t.same(mapReducer(stateBefore, action), stateAfter) 77 | }) 78 | 79 | test('Delete model', (t) => { 80 | const action = { 81 | type: 'models.delete', 82 | deleteKey: 'abcde' 83 | } 84 | 85 | const stateBefore = { 86 | models: new Map([['abcde', { name: 'model abcde' }]]) 87 | } 88 | 89 | const stateAfter = { 90 | models: new Map() 91 | } 92 | 93 | deepFreeze(action) 94 | deepFreeze(stateBefore) 95 | deepFreeze(stateAfter) 96 | 97 | t.same(mapReducer(stateBefore, action), stateAfter) 98 | }) 99 | 100 | test('Initial state of mapInsertReducer', (t) => { 101 | const action = { 102 | type: 'models.fields.add', 103 | modelId: 'abcde', 104 | insertKey: 'field 1', 105 | data: { 106 | name: 'field 1' 107 | } 108 | } 109 | 110 | const stateBefore = { 111 | models: new Map([['abcde', { 112 | name: 'model abcde' 113 | }]]) 114 | } 115 | 116 | const stateAfter = { 117 | models: new Map([['abcde', { 118 | name: 'model abcde', 119 | fields: new Map([['field 1', { name: 'field 1' }]]) 120 | }]]) 121 | } 122 | 123 | deepFreeze(action) 124 | deepFreeze(stateBefore) 125 | deepFreeze(stateAfter) 126 | t.same(mapReducer(stateBefore, action), stateAfter) 127 | }) 128 | -------------------------------------------------------------------------------- /test/object.test.js: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | * 4 | * Test suite for object-based state 5 | * 6 | */ 7 | 8 | import test from 'ava' 9 | import deepFreeze from 'deep-freeze' 10 | 11 | import merge from '../src' 12 | import { objectInsertReducer, objectDeleteReducer } from '../src/objectReducers' 13 | import { objectUpdateReducer } from '../src/objectReducers' 14 | 15 | const objectReducer = merge({ 16 | models: { 17 | _: {}, 18 | add: objectInsertReducer, 19 | delete: objectDeleteReducer, 20 | $modelId: { 21 | update: objectUpdateReducer, 22 | fields: { 23 | add: objectInsertReducer 24 | } 25 | } 26 | } 27 | }, true) 28 | 29 | test('Initial state', (t) => { 30 | const stateAfter = { 31 | models: {} 32 | } 33 | 34 | deepFreeze(stateAfter) 35 | t.same(objectReducer(), stateAfter) 36 | }) 37 | 38 | test('Insert model', (t) => { 39 | const action = { 40 | type: 'models.add', 41 | insertKey: 'abcde', 42 | data: { name: 'model abcde' } 43 | } 44 | 45 | const stateBefore = { 46 | models: { 47 | qwert: { name: 'model qwert' } 48 | } 49 | } 50 | 51 | const stateAfter = { 52 | models: { 53 | abcde: { name: 'model abcde' }, 54 | qwert: { name: 'model qwert' } 55 | } 56 | } 57 | 58 | deepFreeze(action) 59 | deepFreeze(stateBefore) 60 | deepFreeze(stateAfter) 61 | 62 | t.same(objectReducer(stateBefore, action), stateAfter) 63 | }) 64 | 65 | test('Update model', (t) => { 66 | const action = { 67 | type: 'models.update', 68 | modelId: 'abcde', 69 | data: { name: 'updated model abcde' } 70 | } 71 | 72 | const stateBefore = { 73 | models: { 74 | abcde: { name: 'model abcde' }, 75 | qwert: { name: 'model qwert' } 76 | } 77 | } 78 | 79 | const stateAfter = { 80 | models: { 81 | abcde: { name: 'updated model abcde' }, 82 | qwert: { name: 'model qwert' } 83 | } 84 | } 85 | 86 | deepFreeze(action) 87 | deepFreeze(stateBefore) 88 | deepFreeze(stateAfter) 89 | 90 | t.same(objectReducer(stateBefore, action), stateAfter) 91 | }) 92 | 93 | test('Delete model', (t) => { 94 | const action = { 95 | type: 'models.delete', 96 | deleteKey: 'abcde' 97 | } 98 | 99 | const stateBefore = { 100 | models: { 101 | abcde: { name: 'model abcde' }, 102 | qwert: { name: 'model qwert' } 103 | } 104 | } 105 | 106 | const stateAfter = { 107 | models: { 108 | qwert: { name: 'model qwert' } 109 | } 110 | } 111 | 112 | deepFreeze(action) 113 | deepFreeze(stateBefore) 114 | deepFreeze(stateAfter) 115 | 116 | t.same(objectReducer(stateBefore, action), stateAfter) 117 | }) 118 | 119 | test('Initial state of objectInsertReducer', (t) => { 120 | const action = { 121 | type: 'models.fields.add', 122 | modelId: 'abcde', 123 | insertKey: 'field 1', 124 | data: { 125 | name: 'field 1' 126 | } 127 | } 128 | 129 | const stateBefore = { 130 | models: { 131 | abcde: { 132 | name: 'abcde' 133 | } 134 | } 135 | } 136 | 137 | const stateAfter = { 138 | models: { 139 | abcde: { 140 | name: 'abcde', 141 | fields: { 142 | 'field 1': { 143 | name: 'field 1' 144 | } 145 | } 146 | } 147 | } 148 | } 149 | 150 | deepFreeze(action) 151 | deepFreeze(stateBefore) 152 | deepFreeze(stateAfter) 153 | t.same(objectReducer(stateBefore, action), stateAfter) 154 | }) 155 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack') 2 | 3 | module.exports = { 4 | 5 | entry: { 6 | merge: './webpack/merge.js', 7 | reducers: './webpack/reducers.js' 8 | }, 9 | 10 | output: { 11 | path: __dirname + '/dist', 12 | filename: '[name].js', 13 | libraryTarget: 'var', 14 | library: '[name]' 15 | }, 16 | 17 | module: { 18 | loaders: [ 19 | { test: /\.js$/, exclude: /node_modules/, loader: 'babel' } 20 | ] 21 | }, 22 | 23 | plugins: [ 24 | new webpack.optimize.UglifyJsPlugin({ minimize: true }) 25 | ] 26 | 27 | } -------------------------------------------------------------------------------- /webpack/merge.js: -------------------------------------------------------------------------------- 1 | import merge from '../src/index' 2 | 3 | module.exports = merge -------------------------------------------------------------------------------- /webpack/reducers.js: -------------------------------------------------------------------------------- 1 | import * as arrayReducers from '../src/arrayReducers' 2 | import * as objectReducers from '../src/objectReducers' 3 | import * as mapReducers from '../src/mapReducers' 4 | 5 | module.exports = { 6 | ...arrayReducers, 7 | ...objectReducers, 8 | ...mapReducers, 9 | } --------------------------------------------------------------------------------