├── .eslintrc.json ├── .gitignore ├── EXAMPLES.md ├── ImmutableComparison.md ├── ImmutableComparison ├── PerformanceComparison.gif ├── PerformanceComparison │ ├── index.html │ ├── package.json │ └── src │ │ ├── diffEqualObjects.js │ │ └── index.js ├── SyntaxComparison.png ├── SyntaxComparison │ ├── index.html │ ├── package.json │ └── src │ │ ├── defaultObject.js │ │ ├── diffEqualObjects.js │ │ ├── exampleDeepMutation.js │ │ ├── exampleImmutable.js │ │ └── index.js └── performanceResult.png ├── LICENSE.md ├── README.md ├── babel.config.json ├── deepMutationTodo ├── package.json ├── public │ └── index.html └── src │ ├── App.js │ ├── components │ ├── Modal.jsx │ ├── Steps.jsx │ ├── TodoEditor.jsx │ ├── TodoRemovingConfirmation.jsx │ ├── TodoView.jsx │ ├── TodosList.jsx │ ├── modal.css │ ├── steps.css │ ├── todoEditor.css │ ├── todoView.css │ └── todosList.css │ ├── index.js │ └── styles.css ├── dist └── index.js ├── jest.config.js ├── package-lock.json ├── package.json └── src ├── index.js └── tests ├── checkIsExists.test.js ├── deepToMutate.test.js ├── extToArray.test.js ├── extToTree.test.js ├── getObjectPaths.test.js ├── getPairValue.test.js ├── getRealIndex.test.js ├── getValue.test.js ├── index_data.test.js ├── isArrayElement.test.js ├── mutate.test.js ├── mutateDeep.test.js ├── mutate_function.test.js ├── object_immutable.test.js ├── path_array.test.js ├── separatePath.test.js └── splitPath.test.js /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "env": { 4 | "browser": true, 5 | "jest":true 6 | }, 7 | "extends": [ 8 | "eslint:recommended" 9 | ], 10 | "plugins": [ 11 | "babel" 12 | ], 13 | "rules": { 14 | "keyword-spacing": ["warn"], 15 | "key-spacing": ["warn"], 16 | "max-len": ["warn", 120, { 17 | "ignoreComments": true , 18 | "ignoreStrings": true, 19 | "ignoreUrls": true, 20 | "ignoreTemplateLiterals": true, 21 | "ignoreRegExpLiterals": true 22 | }], 23 | "object-curly-spacing": ["warn", "always"], 24 | "arrow-parens": "off", 25 | "space-in-parens": "off", 26 | "no-case-declarations": ["warn"], 27 | "no-mixed-operators": "off", 28 | "eqeqeq": "off", 29 | "no-debugger": "warn", 30 | "no-empty": "off", 31 | "indent": ["error", 2, {"SwitchCase": 1}], 32 | "quotes": ["error", "single"], 33 | "semi": ["error", "always", {"omitLastInOneLineBlock": true}], 34 | "no-unused-vars": ["warn"], 35 | "no-const-assign": ["error"], 36 | "no-console": [ "warn", { 37 | "allow": [ 38 | "warn", 39 | "error", 40 | "debug" 41 | ] 42 | }], 43 | "comma-dangle": ["error", "only-multiline"], 44 | "callback-return": "error" 45 | }, 46 | "globals": { 47 | "module": true 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled binary addons (http://nodejs.org/api/addons.html) 2 | build 3 | 4 | # Dependency directory 5 | # https://docs.npmjs.com/misc/faq#should-i-check-my-node-modules-folder-into-git 6 | node_modules 7 | 8 | # Coverage directory used by tools like istanbul 9 | coverage 10 | 11 | .idea 12 | -------------------------------------------------------------------------------- /EXAMPLES.md: -------------------------------------------------------------------------------- 1 | # Only examples 2 | 3 | ```javascript 4 | import mutate from 'deep-mutation'; 5 | 6 | const cat = { 7 | size: { 8 | weight: 5, 9 | height: 20, 10 | }, 11 | breed: 'siam', 12 | kitten: [ 13 | 'Lian', 14 | 'Semion' 15 | ], 16 | sex: 'w' 17 | }; 18 | 19 | return mutate(cat, { 20 | 'size.height': 22, 21 | 'kitten.[+1]': 'Lukan', 22 | 'kitten.[+2]': 'Munya' 23 | }); 24 | ``` 25 | 26 | ```javascript 27 | import mutate, { deepPatch } from 'deep-mutation'; 28 | 29 | const todos = [ 30 | { 31 | id: 998941425, 32 | title: 'Add useful somthing', 33 | description: 'I would like to make something interesting and useful' 34 | steps: [ 35 | { id: 1, text: 'idea', isFinished: false }, 36 | { id: 2, text: 'prepare', isFinished: false }, 37 | { id: 3, text: 'make', isFinished: false } 38 | ] 39 | }, 40 | ... 41 | ]; 42 | 43 | ... 44 | 45 | function findTodo(todoId) { 46 | return todos.find(el => el.id == todoId); 47 | } 48 | 49 | function findStep(todo, stepId) { 50 | return todos.find(el => el.id == todoId); 51 | } 52 | 53 | function changeStepState(todoId, stepId, isFinished) { 54 | const todoPos = findTodo(todoId); 55 | const stepPos = findStep(todos[todoPos], stepId); 56 | 57 | return mutate( 58 | todos, 59 | { [`[${todoPos}].steps.[${stepPos}].isFinished`]: isFinished } 60 | ); 61 | } 62 | 63 | function changeStepText(todoId, stepId, text) { 64 | const todoPos = findTodo(todoId); 65 | const stepPos = findStep(todos[todoPos], stepId); 66 | 67 | return mutate( 68 | todos, 69 | { [`[${todoPos}].steps.[${stepPos}].text`]: text } 70 | ); 71 | } 72 | 73 | function addStep(todoId, text) { 74 | const todoPos = findTodo(todoId); 75 | const newStep = { id: Math.floor(Math.random() * 10000), text, isFinished: false }; 76 | return mutate( 77 | todos, 78 | { [`[${todoPos}].steps.[]`]: newStep }; 79 | ) 80 | } 81 | 82 | function removeStep(todoId, stepId) { 83 | const todoPos = findTodo(todoId); 84 | const stepPos = findStep(todos[todoPos], stepId); 85 | return mutate( 86 | todos, 87 | { [`[${todoPos}].steps.[${stepPos}].text`]: undefined } 88 | ) 89 | } 90 | ``` -------------------------------------------------------------------------------- /ImmutableComparison.md: -------------------------------------------------------------------------------- 1 | ## Performance comparison 2 | **Sandbox editor**: https://codesandbox.io/s/l9ovomzv99 3 | 4 | **Sandbox view**: https://l9ovomzv99.csb.app/ 5 | 6 | ![deep-mutation vs immutable performance](./ImmutableComparison/performanceResult.png) 7 | 8 | ## Syntax comparison 9 | **Sandbox editor**: https://codesandbox.io/s/j4wkq2znj5 10 | 11 | **Sandbox view**: https://j4wkq2znj5.codesandbox.io/ 12 | 13 | ![deep-mutation vs immutable performance](./ImmutableComparison/SyntaxComparison.png) -------------------------------------------------------------------------------- /ImmutableComparison/PerformanceComparison.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axules/deep-mutation/41b1ab55be5a27b98019192e861f9cdfd0c0dbe9/ImmutableComparison/PerformanceComparison.gif -------------------------------------------------------------------------------- /ImmutableComparison/PerformanceComparison/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parcel Sandbox 5 | 6 | 7 | 8 | 9 |
10 | https://codesandbox.io/s/km3w4jowo5 11 |
12 |
13 | 16 |
17 |
18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /ImmutableComparison/PerformanceComparison/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep-mutation-vs-immutable", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "deep-mutation": "1.0.3", 12 | "immutable": "3.8.2", 13 | "lodash.isequal": "4.5.0" 14 | }, 15 | "devDependencies": { 16 | "parcel-bundler": "^1.6.1" 17 | }, 18 | "keywords": [] 19 | } -------------------------------------------------------------------------------- /ImmutableComparison/PerformanceComparison/src/diffEqualObjects.js: -------------------------------------------------------------------------------- 1 | export function diffToHtml(diff) { 2 | return diff 3 | .map(el => `
${el[0]}
`) 4 | .join(''); 5 | } 6 | 7 | export default function deepDiffEqual(a, b, prefix = '') { 8 | if (!(a instanceof Object)) return null; 9 | return Object.keys(a).reduce((R, key) => { 10 | const currentKey = `${prefix}${prefix ? '.' : ''}${key}`; 11 | R.push([currentKey, a[key] === b[key]]); 12 | if (a[key] instanceof Object) { 13 | return R.concat(deepDiffEqual(a[key], b[key], currentKey)); 14 | } 15 | return R; 16 | }, []); 17 | } 18 | -------------------------------------------------------------------------------- /ImmutableComparison/PerformanceComparison/src/index.js: -------------------------------------------------------------------------------- 1 | import mutate from 'deep-mutation'; 2 | import { OrderedMap } from 'immutable'; 3 | import isEqual from 'lodash.isequal'; 4 | 5 | const abc = 'abcdefghijkmnpqrstuvwxz-_0123456789'; 6 | const keyLen = [10, 15]; 7 | const deepLevel = 5; 8 | 9 | document.getElementById('runButton').addEventListener('click', onClickRun); 10 | 11 | function onClickRun() { 12 | runner(5); 13 | } 14 | 15 | function runner(runCount = 1) { 16 | let testData = null; 17 | let immutableResult = null; 18 | let mutateResult = null; 19 | let immutableMergeResult = null; 20 | const median = { 21 | immutable: [], 22 | immutableMerge: [], 23 | deepMutation: [] 24 | }; 25 | 26 | for (let i = 0; i < runCount; i++) { 27 | testData = generateData(generateDataLevel(3, ''), deepLevel, 3).filter( 28 | el => el[0].split('.').length > deepLevel 29 | ); 30 | 31 | immutableResult = immutableTest(testData); 32 | immutableMergeResult = immutableMergeTest(immutableResult.result); 33 | mutateResult = mutateTest(testData); 34 | 35 | median.immutable.push(immutableResult.time); 36 | median.immutableMerge.push(immutableMergeResult.time); 37 | median.deepMutation.push(mutateResult.time); 38 | } 39 | 40 | const equals = 41 | isEqual(mutateResult.result, immutableResult.result) && 42 | isEqual(mutateResult.result, immutableMergeResult.result); 43 | 44 | const medianImmutable = 45 | median.immutable.reduce((R, v) => R + v, 0) / runCount; 46 | const medianImmutableMerge = 47 | median.immutableMerge.reduce((R, v) => R + v, 0) / runCount; 48 | const medianDeepMutation = 49 | median.deepMutation.reduce((R, v) => R + v, 0) / runCount; 50 | 51 | document.getElementById('app').innerHTML = ` 52 |
Calls count: ${runCount}
53 |
54 |
results are equal: ${equals}
55 |
56 |
Changes count: ${testData.length}
57 |
58 |
59 | deep-mutation: ${medianDeepMutation} ms 60 |
${median.deepMutation.join(' ms
')} 61 |
62 |
63 |
64 | immutable: ${medianImmutable} ms 65 |
${median.immutable.join(' ms
')} 66 |
67 |
68 |
69 | immutable-merge: ${medianImmutableMerge} ms 70 |
${median.immutableMerge.join(' ms
')} 71 |
72 |
73 | 74 | `; 75 | } 76 | 77 | function mutateTest(pChanges) { 78 | const begin = performance.now(); 79 | const result = mutate({}, pChanges); 80 | const end = performance.now(); 81 | return { 82 | time: end - begin, 83 | result 84 | }; 85 | } 86 | 87 | function immutableMergeTest(pChanges) { 88 | const begin = performance.now(); 89 | let iMap = OrderedMap({}); 90 | const result = iMap.mergeDeep(pChanges).toJS(); 91 | const end = performance.now(); 92 | return { 93 | time: end - begin, 94 | result 95 | }; 96 | } 97 | 98 | function immutableTest(pChanges) { 99 | const begin = performance.now(); 100 | let iMap = OrderedMap({}); 101 | for (let i = 0; i < pChanges.length; i++) { 102 | iMap = iMap.setIn(pChanges[i][0].split('.'), pChanges[i][1]); 103 | } 104 | // pChanges.forEach(el => (iMap = iMap.setIn(el[0].split("."), el[1]))); 105 | const result = iMap.toJS(); 106 | const end = performance.now(); 107 | return { 108 | time: end - begin, 109 | result 110 | }; 111 | } 112 | 113 | function rnd(min, max) { 114 | return Math.floor(Math.random() * (max - min + 1)) + min; 115 | } 116 | 117 | function genKey() { 118 | const len = rnd(...keyLen); 119 | const result = []; 120 | for (let i = 0; i < len; i++) { 121 | result.push(abc[rnd(0, abc.length - 1)]); 122 | } 123 | return result.join(''); 124 | } 125 | 126 | function generateDataLevel(count, path = '') { 127 | const result = []; 128 | const prefix = path ? `${path}.` : ''; 129 | 130 | function generateData(val) { 131 | if (val > 0.3 && val < 0.5) { 132 | return { 133 | value: Math.random() * 100000000, 134 | key: genKey() 135 | }; 136 | } 137 | 138 | if (val >= 0.5) { 139 | return [1, 2, 3, 4, 5]; 140 | } 141 | 142 | return Math.random() * 100000000; 143 | } 144 | 145 | for (let i = 0; i < count; i++) { 146 | result.push([`${prefix}${genKey()}`, generateData(Math.random())]); 147 | } 148 | return result; 149 | } 150 | 151 | function generateData(parents, deep, count) { 152 | let result = [].concat(parents); 153 | for (let i = 0; i < parents.length; i++) { 154 | const data = generateDataLevel(count, parents[i][0]); 155 | if (deep > 0) { 156 | result = result.concat(generateData(data, deep - 1, count)); 157 | } 158 | } 159 | 160 | return result; 161 | } 162 | -------------------------------------------------------------------------------- /ImmutableComparison/SyntaxComparison.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axules/deep-mutation/41b1ab55be5a27b98019192e861f9cdfd0c0dbe9/ImmutableComparison/SyntaxComparison.png -------------------------------------------------------------------------------- /ImmutableComparison/SyntaxComparison/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Parcel Sandbox 5 | 6 | 15 | 16 | 17 | 18 |
19 | https://codesandbox.io/s/j4wkq2znj5 20 |
21 |
22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /ImmutableComparison/SyntaxComparison/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep-mutation-vs-immutable-syntax", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.html", 6 | "scripts": { 7 | "start": "parcel index.html --open", 8 | "build": "parcel build index.html" 9 | }, 10 | "dependencies": { 11 | "deep-mutation": "1.0.3", 12 | "immutable": "3.8.2", 13 | "lodash.isequal": "4.5.0" 14 | }, 15 | "devDependencies": { 16 | "parcel-bundler": "^1.6.1" 17 | }, 18 | "keywords": [] 19 | } -------------------------------------------------------------------------------- /ImmutableComparison/SyntaxComparison/src/defaultObject.js: -------------------------------------------------------------------------------- 1 | const defaultObject = { 2 | count: 0, 3 | offset: 0, 4 | searchText: '', 5 | items: null, 6 | requestId: null, 7 | status: { 8 | isError: false, 9 | isLoading: false, 10 | isSuccess: false 11 | }, 12 | error: {}, 13 | selectedItem: {}, 14 | editedItem: {} 15 | }; 16 | 17 | export default defaultObject; 18 | -------------------------------------------------------------------------------- /ImmutableComparison/SyntaxComparison/src/diffEqualObjects.js: -------------------------------------------------------------------------------- 1 | export function diffToHtml(diff) { 2 | return diff 3 | .map(el => `
${el[0]}
`) 4 | .join(''); 5 | } 6 | 7 | export default function deepDiffEqual(a, b, prefix = '') { 8 | if (!(a instanceof Object)) return null; 9 | const isArray = Array.isArray(a); 10 | return (isArray ? a : Object.keys(a)).reduce((R, key) => { 11 | const currentKey = isArray 12 | ? `${prefix}${prefix ? '.' : ''}[${key}]` 13 | : `${prefix}${prefix ? '.' : ''}${key}`; 14 | R.push([currentKey, a[key] === b[key]]); 15 | if (a[key] instanceof Object) { 16 | return R.concat(deepDiffEqual(a[key], b[key], currentKey)); 17 | } 18 | return R; 19 | }, []); 20 | } 21 | -------------------------------------------------------------------------------- /ImmutableComparison/SyntaxComparison/src/exampleDeepMutation.js: -------------------------------------------------------------------------------- 1 | import mutate from 'deep-mutation'; 2 | import defaultObject from './defaultObject'; 3 | 4 | let byMutate = mutate(defaultObject, { 5 | count: 120, 6 | offset: 10, 7 | searchText: 'text', 8 | items: null, 9 | requestId: Math.floor(Math.random() * 1000000), 10 | status: { 11 | isLoading: true, 12 | isError: false, 13 | isSuccess: false 14 | }, 15 | selectedItem: undefined, 16 | editedItem: undefined, 17 | error: undefined 18 | }); 19 | 20 | // after response 21 | byMutate = mutate(byMutate, { 22 | items: [1, 2, 3, 4, 5], 23 | 'status.isLoading': false, 24 | 'status.isSuccess': true 25 | }); 26 | 27 | // once item is selected 28 | byMutate = mutate(byMutate, { selectedItem: { value: 10, id: 1 } }); 29 | 30 | // once selected item value is changed 31 | byMutate = mutate(byMutate, { 'selectedItem.value': [1, 2, 3] }); 32 | 33 | // add new elements to selected item value 34 | byMutate = mutate(byMutate, { 35 | 'selectedItem.value.[+1]': 10, 36 | 'selectedItem.value.[+2]': 20, 37 | 'selectedItem.value.[+3]': 30 38 | }); 39 | 40 | export default byMutate; 41 | -------------------------------------------------------------------------------- /ImmutableComparison/SyntaxComparison/src/exampleImmutable.js: -------------------------------------------------------------------------------- 1 | import { OrderedMap, List } from 'immutable'; 2 | import defaultObject from './defaultObject'; 3 | 4 | let byImmutable = new OrderedMap(defaultObject) 5 | .mergeDeep({ 6 | count: 120, 7 | offset: 10, 8 | searchText: 'text', 9 | items: null, 10 | requestId: Math.floor(Math.random() * 1000000), 11 | status: { 12 | isLoading: true, 13 | isError: false, 14 | isSuccess: false 15 | } 16 | }) 17 | .remove('selectedItem') 18 | .remove('editedItem') 19 | .remove('error'); 20 | 21 | // after response 22 | byImmutable = byImmutable 23 | .setIn(['status', 'isLoading'], false) 24 | .setIn(['status', 'isSuccess'], true) 25 | .set('items', [1, 2, 3, 4, 5]); 26 | 27 | // once item is selected 28 | byImmutable = byImmutable.set('selectedItem', OrderedMap({ value: 10, id: 1 })); 29 | 30 | // once selected item value is changed 31 | byImmutable = byImmutable.setIn(['selectedItem', 'value'], List([1, 2, 3])); 32 | 33 | // add new elements to selected item value 34 | byImmutable = byImmutable.setIn( 35 | ['selectedItem', 'value'], 36 | byImmutable.getIn(['selectedItem', 'value']).push(10, 20, 30) 37 | ); 38 | 39 | export default byImmutable; 40 | -------------------------------------------------------------------------------- /ImmutableComparison/SyntaxComparison/src/index.js: -------------------------------------------------------------------------------- 1 | import deepDiffEqual, { diffToHtml } from './diffEqualObjects'; 2 | import byImmutable from './exampleImmutable'; 3 | import byDMutation from './exampleDeepMutation'; 4 | 5 | document.getElementById('app').innerHTML = ` 6 | `; 7 | 8 | console.log(byImmutable.toJS(), byDMutation); 9 | const diff = deepDiffEqual(byImmutable.toJS(), byDMutation, ''); 10 | 11 | document.getElementById('objectTree').innerHTML = diffToHtml(diff); 12 | -------------------------------------------------------------------------------- /ImmutableComparison/performanceResult.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/axules/deep-mutation/41b1ab55be5a27b98019192e861f9cdfd0c0dbe9/ImmutableComparison/performanceResult.png -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2018 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # deep-mutation 2 | 3 | 1. [What is it?](#what-is-it) 4 | 2. [Installation](#installation) 5 | 3. [What does it do?](#installation) 6 | * [Simple example](#simple-example) 7 | * [Array specific keys](#array-specific-keys) 8 | * [mutate.deep(..) or deepPatch(...)](#mutatedeep-or-deeppatch) 9 | * [Deep-mutation can return updater-function](#deep-mutation-can-return-updater-function) 10 | 4. [Immutable comparison](#immutable-comparison) 11 | 5. [Tests cases / code example](#tests-cases--code-example) 12 | 6. [Use cases for 'deep-mutation'](#use-cases-for-deep-mutation) 13 | 7. [Live TODO Example in Codesandbox](https://codesandbox.io/s/deep-mutation-todo-1w022) 14 | 8. [Examples](./EXAMPLES.md) 15 | 16 | ## What is it? 17 | 18 | It is a simple function which gets an object and a list of changes and returns a new updated object. 19 | 20 | **Since the version 2.0.0 `deep-mutation` returns a new object only when something was changed** 21 | 22 | ## Installation 23 | 24 | ``` 25 | npm install --save deep-mutation 26 | ``` 27 | 28 | ## What does it do? 29 | 30 | ### Getting a new object with changes 31 | #### with plain JavaScript 32 | ```javascript 33 | const obj = { a: 10, b: 20, c: { c1: 1, c2: 2, c3: { c31: 31 } }}; 34 | const result = { 35 | ...obj, 36 | c: { 37 | ...obj.c, 38 | c3: { 39 | ...obj.c3, 40 | c32: 25 41 | } 42 | } 43 | }; 44 | ``` 45 | 46 | #### doing the same with `deep-mutation` 47 | 48 | ```javascript 49 | import mutate from 'deep-mutation'; 50 | const resultMutate = mutate(obj, { 'c.c3.c32': 25 }); 51 | // OR 52 | const resultMutate = mutate(obj, [['c.c3.c32', 25]]); 53 | // OR since v2.1.0 54 | const resultMutate = mutate(obj, [[['c', 'c3', 'c32'], 25]]); 55 | // OR since v3.0.0 56 | const resultMutate = mutate.deep(obj, { c: { c3: { c32: 25 } } }); 57 | ``` 58 | 59 | ### Simple example 60 | ```javascript 61 | import mutate from 'deep-mutation'; 62 | 63 | const myObject = { 64 | a: 100, 65 | b: 200, 66 | c: { 67 | c1: 1, 68 | c2: 2 69 | }, 70 | d: [] 71 | }; 72 | 73 | const changes = [ 74 | ['a', 111], 75 | ['b.b1', 222], 76 | ['b.b2', 'text'], 77 | ['c.c1', 20], 78 | ['c.c2'], 79 | ['d.[]', 10], 80 | ['d.[]', 20], 81 | ['e', [1,2,3]] 82 | ]; 83 | 84 | const result = mutate(myObject, changes); 85 | ``` 86 | 87 | #### 'result' will be 88 | ```javascript 89 | const obj = { 90 | a: 111, 91 | b: { 92 | b1: 222, 93 | b2: 'text' 94 | }, 95 | c: { 96 | c1: 20 97 | }, 98 | d: [10,20], 99 | e: [1,2,3] 100 | }; 101 | ``` 102 | 103 | ### Changes can be specified as an array of arrays or an object where each key is a path or object converted by `deepPatch` function: 104 | 105 | ```javascript 106 | // array of arrays 107 | const changes = [ 108 | ['a', 111], 109 | ['b.b1', 222], 110 | ['b.b2', 'text'], 111 | ['c.c1', 20], 112 | ['c.c2'], 113 | ['d.[]', 10], 114 | ['d.[]', 20], 115 | ['e', [1,2,3]] 116 | ]; 117 | ``` 118 | 119 | **OR** 120 | 121 | ```javascript 122 | // object 123 | const changes = { 124 | a: 111, 125 | 'b.b1': 222, 126 | 'b.b2': 'text', 127 | 'c.c1': 20, 128 | 'c.c2': undefined, 129 | 'd.[+123412]': 10, 130 | 'd.[+544555]': 20, 131 | e: [1,2,3] 132 | }; 133 | ``` 134 | 135 | **OR** 136 | 137 | ```javascript 138 | // deep patch 139 | import { deepPatch } from 'deep-mutation'; 140 | 141 | const changes = deepPatch({ 142 | a: 111, 143 | b: { b1: 222, b2: 'text' }, 144 | c: { c1: 20, c2: undefined }, 145 | d: { '[+123412]': 10, '[+544555]': 20 }, 146 | e: [1,2,3] 147 | }); 148 | ``` 149 | 150 | ## Array specific keys 151 | 152 | ### a.[] (or a.[+242234]) 153 | 154 | If a key for an array item starts from `+`, then the **value will be appended to the end of the array** like `[].push()`. 155 | ```javascript 156 | const patch = { 157 | 'arr.[+123312312]': 100, 158 | // OR 159 | 'arr.[+6]': 100, 160 | // will be equal to 161 | 'arr.[]': 100, 162 | }; 163 | ``` 164 | 165 | It is useful when you need to add some items to an array and use changes as an Object. 166 | ```javascript 167 | import muatate from 'deep-mutation'; 168 | // ... 169 | return mutate( 170 | { arr: [] }, 171 | { 172 | // It is an error because JS object can't have values with the same keys! 173 | // 'arr.[]': 1, 174 | // 'arr.[]': 2, 175 | 176 | //It is the correct way 177 | 'arr.[+1123]': 1, 178 | 'arr.[+232]': 2, 179 | 'arr.[+43534]': 3, 180 | 'arr.[+64]': 4, 181 | 'arr.[]': 5, 182 | } 183 | ); 184 | 185 | // the result will be = { arr: [1,2,3,4,5] } 186 | ``` 187 | 188 | ### arr.[=10] or arr.[=id=15] 189 | 190 | If a key for an array item starts from `=` ([=10] or [=data.id=99]), then index will be found by comparison item or item's property and value. `[=field.path=value]`. 191 | ```javascript 192 | import muatate from 'deep-mutation'; 193 | // ... 194 | return mutate( 195 | { arr: [1,2,3,4,5,6,7,8] }, 196 | { 197 | 'arr.[=2]': 200, 198 | // index for element with value `2` will be `1` 199 | 'arr.[=8]': 800, 200 | // index for element with value `8` will be `7` 201 | 'arr.[=100]': 'undefined', 202 | // `100` is not found in arr and will be ignored, index is `-1` 203 | } 204 | ); 205 | 206 | // the result will be = { arr: [1,200,3,4,5,6,7,800] } 207 | ``` 208 | `arr.[=]` or `arr.[=value=]` - to find empty string value in array (or item.value = '') 209 | `arr.[=false]` or `arr.[=value=]` - to find 'false' value in array (or item.value = false) 210 | 211 | Example for objects 212 | ```javascript 213 | import muatate from 'deep-mutation'; 214 | // ... 215 | return mutate( 216 | { arr: [{ id: 10 }, { id: 20 }] }, 217 | { 218 | 'arr.[=id=20].name': 'Name 20', 219 | // index for element with `id=20` will be `1` 220 | 'arr.[=id=999]': 'undefined', 221 | // it is not found, ignored 222 | } 223 | ); 224 | 225 | // the result will be = { arr: [{ id: 10 }, { id: 20, name: 'Name 20' }] } 226 | ``` 227 | 228 | Example with deep path 229 | ```javascript 230 | import muatate from 'deep-mutation'; 231 | // ... 232 | return mutate( 233 | { arr: [{ data: { id: 12 }}, { data: { id: 30 }}] }, 234 | { 235 | 'arr.[=data.id=12].data.v': 'value1', 236 | // index for element with `data.id=12` will be `0` 237 | 'arr.[=data.id=999]': 'undefined', 238 | // it is not found, ignored 239 | } 240 | ); 241 | 242 | // the result will be = { arr: [{ data: { id: 12, v: 'value1' }}, { data: { id: 30 }}] } 243 | ``` 244 | 245 | ### arr.[-1] 246 | 247 | It will be **ignored**. 248 | 249 | ```javascript 250 | import muatate from 'deep-mutation'; 251 | // ... 252 | return mutate( 253 | { arr: [1,2,3,4] }, 254 | { 'arr.[-1]': 999 } 255 | ); 256 | 257 | // the result will be = { arr: [1,2,3,4] } 258 | ``` 259 | 260 | ### arr.[>2] 261 | 262 | It will insert element onto provided position. 263 | 264 | ```javascript 265 | import muatate from 'deep-mutation'; 266 | // ... 267 | return mutate( 268 | { arr: [1,2,3,4] }, 269 | { 'arr.[>2]': 555 }, 270 | { 'arr.[>3]': 999 }, 271 | ); 272 | 273 | // the result will be = { arr: [1,2,555,999,3,4] } 274 | ``` 275 | 276 | ## mutate.deep(...) or deepPatch(...) 277 | 278 | `deepPatch(patchObject: Object): PatchObject` 279 | 280 | `mutate.deep(sourceObject: Object, patchObject: Object): Object` 281 | 282 | ```javascript 283 | import mutate from 'deep-mutation'; 284 | 285 | return mutate.deep( 286 | { a: 10, b: { b1: 1, b2: 2 }}, // main object 287 | { c: 50, b: { b2: 100 } } // changes 288 | ); 289 | 290 | // result = { a: 10, b: { b1: 1, b2: 100 }, c: 50} 291 | ``` 292 | 293 | **OR** 294 | 295 | ```javascript 296 | import mutate, { deepPatch } from 'deep-mutation'; 297 | 298 | return mutate( 299 | { a: 10, b: { b1: 1, b2: 2 }}, // main object 300 | deepPatch({ c: 50, b: { b2: 100 } }) // changes 301 | ); 302 | 303 | // result = { a: 10, b: { b1: 1, b2: 100 }, c: 50} 304 | ``` 305 | 306 | * [deepPatch test cases / examples](./src/tests/deepToMutate.test.js) 307 | 308 | * [mutate.deep test cases / examples](./src/tests/mutateDeep.test.js) 309 | 310 | ## Deep-mutation can return updater-function 311 | 312 | If `deep-mutation` function is called only with one argument (an object without changes) then it will return a **function** which can take one argument as changes. When called, it will save the changes and return an updated object. 313 | 314 | 315 | ```javascript 316 | import mutate from 'deep-mutation'; 317 | 318 | const patch = mutate({ a: 1, b: 2}); 319 | 320 | const result1 = patch({ c: 3 }); 321 | // result1 === { a: 1, b: 2, c: 3} 322 | 323 | const result2 = patch({ d: 4 }); 324 | // result2 === { a: 1, b: 2, c: 3, d: 4} 325 | 326 | const result3 = patch(); 327 | // result3 === result2 === { a: 1, b: 2, c: 3, d: 4} 328 | ``` 329 | 330 | ## `deep-mutation` supports dots in path since v2.1.0 331 | 332 | In order to use dots in the path of changes you should use the path as an Array of keys: 333 | 334 | `mutate({ a: { 'a.b': { 'a.b.c': 10 }} }, [[['a', 'a.b', 'a.b.c'], newValue]])` 335 | 336 | **OR** 337 | 338 | `mutate({ a: { 'a.b': { 'a.b.c': 10 }} }, [['a-a.b-a.b.c'.split('-'), newValue]])` 339 | 340 | 341 | ```javascript 342 | import mutate from 'deep-mutation'; 343 | 344 | const obj = { 345 | a: { 346 | 'a.1': { 347 | 'a.1.1': 100 348 | } 349 | } 350 | }; 351 | 352 | const changes = [['a-a.1-a.1.1'.split('-'), 15]] 353 | 354 | const result = mutate(obj, changes); 355 | // result === { a: { a.1: { a.1.1: 15 } } } 356 | ``` 357 | 358 | ## Immutable comparison 359 | [ImmutableComparison.md](./ImmutableComparison.md) 360 | 361 | ## Performance comparison 362 | **Sandbox editor**: https://codesandbox.io/s/l9ovomzv99 363 | 364 | **Sandbox view**: https://l9ovomzv99.csb.app/ 365 | 366 | ![deep-mutation vs immutable performance](./ImmutableComparison/performanceResult.png) 367 | 368 | ## Syntax comparison 369 | **Sandbox editor**: https://codesandbox.io/s/j4wkq2znj5 370 | 371 | **Sandbox view**: https://j4wkq2znj5.codesandbox.io/ 372 | 373 | ![deep-mutation vs immutable performance](./ImmutableComparison/SyntaxComparison.png) 374 | 375 | # Tests cases / code example 376 | ### [Go to tests](./src/tests) 377 | 378 | 379 | ```javascript 380 | mutate({ a: 10 }, [['a', 5]]); // { a: 5 } 381 | mutate({ a: 10 }, [['b', 5]]); // { a: 10, b: 5 } 382 | mutate({}, [['a', 10], ['b', 5]]); // { a: 10, b: 5 } 383 | mutate({ a: 10 }, [['a']]); // { } 384 | mutate({ a: 10 }, [null]); // { a: 10 } 385 | mutate({ a: 10 }, [['a']]); // ['b']]); // { } 386 | mutate({ a: 10 }, ['a', 'b']); // { } 387 | mutate({ a: 10 }, [['a']]); // ['b', 5]]); // { b: 5 } 388 | mutate({ a: 10 }, [['a', [1,2,3]]]); // { a: [1,2,3] } 389 | mutate({ a: 10 }, [['a', { aa: 1 }]]); // { a: { aa: 1 } } 390 | mutate({ a: 10 }, [['a', 5], ['b', { bb: 2 }]]); // { a: 5, b: { bb: 2 } } 391 | // extend an object 392 | mutate({ a: { aa: 10 } }, [['a.aa', 5]]); // { a: { aa: 5 } } 393 | mutate({ a: { aa: 10 } }, [['a.aa']]); // { a: { } } 394 | mutate({ a: { aa: { aaa: 10 } } }, [['a.aa'], ['a.aa.aaa']]) // { a: { } } 395 | mutate({ a: { aa: 10 } }, [['a.aa.[]', 1]]); // { a: { aa: [1] } } 396 | mutate({ a: { aa: 10 } }, [['a.aa'], ['a']]); // { } 397 | mutate({ a: { aa: 10 } }, ['a.aa', 'a']); // { } 398 | mutate({ a: 10 }, [['a.aa', 5]]); // { a: { aa: 5 } } 399 | mutate({ a: 10 }, [['a.aa.aaa', 5]]); // { a: { aa: { aaa: 5 } } } 400 | mutate({ a: 10 }, [['a.aa.aaa', 5], ['a.aa.aaa.aaaa', 2]]); // { a: { aa: { aaa: { aaaa: 2 } } } } 401 | mutate({ a: 10 }, [['a.aa', 5], ['a.aa2', 2]]); // { a: { aa: 5, aa2: 2 } } 402 | mutate({ a: 10 }, [['a.aa', 5], ['b.bb', 2]]); // { a: { aa: 5 }, b: { bb: 2 } } 403 | // extend an array 404 | mutate([], [['[]', 5]]); // [5] 405 | mutate({ a: [] }, [['a.[]', 5]]); // { a: [5] } 406 | mutate({ a: [] }, [['a.[0]', 5]]); // { a: [5] } 407 | mutate({ a: [] }, [['a[0]', 5]]); // { a: [5] } 408 | mutate({ a: [] }, [['a[][]', 5]]); // { a: [[5]] } 409 | mutate({ a: [] }, [['a.[].[]', 5]]); // { a: [[5]] } 410 | mutate({ a: [] }, [['a.[2]', 5]]); // { a: [undefined, undefined, 5] } 411 | mutate({ a: [1] }, [['a.[]', 5]]); // { a: [1, 5] } 412 | mutate({ a: [1] }, [['a.[]', 5],['a.[]', 7]]); // { a: [1, 5, 7] } 413 | mutate({ a: [1] }, [['a.[0]', 5]]); // { a: [5] } 414 | mutate({ a: [1] }, [['a.[0]']]); // { a: [] } 415 | // changes as an object 416 | mutate({ a: [] }, { 'a.[]': 5 }); // { a: [5] } 417 | mutate({ a: [] }, { 'a.[0]': 5 }); // { a: [5] } 418 | mutate({ a: [] }, { 'a.[2]': 5 }); // { a: [undefined, undefined, 5] } 419 | mutate({ a: [1] }, { 'a.[]': 5 }); // { a: [1, 5] } 420 | mutate({ a: [1] }, { 'a.[0]': 5 }); // { a: [5] } 421 | mutate({ a: { aa: 10 } }, { 'a.aa': 5 }); // { a: { aa: 5 } } 422 | mutate({ a: { aa: 10 } }, { 'a.aa': undefined, 'a.aaa': 99 }); // { a: { aaa: 99 } } 423 | mutate({ }, { 'a.aa.aaa': undefined }); // { } 424 | mutate({ }, [['a.aa.aaa']]); // { } 425 | mutate({ a: { 0: 'v0', 1: 'v1' } }, [['a.0']]); // { a: { 1: 'v1' } } 426 | mutate({ a: [1,2,3] }, [['a.[]']]); // { a: [1,2,3] } 427 | mutate({ a: [1,2,3] }, [['a.[0]']]); // { a: [2,3] } 428 | mutate({ a: [1,2,3] }, [['a.0']]); // { a: [undefined, 2,3] } 429 | // set the object, extend the object 430 | mutate({ }, [['a', { aa: 5 }]]) // { a: { aa: 5 } } 431 | mutate({ a: 10 }, [['a', { aa: 5 }]]); // { a: { aa: 5 } } 432 | mutate({ a: 10 }, [['a', { aa: { aaa: 5 } }]]) // { a: { aa: { aaa: 5 } } } 433 | mutate({ a: 10 }, [['a', { aa: { aaa: 5 } }]]); // { a: { aa: { aaa: 5 } } } 434 | mutate({ a: 10 }, [['a', { aa: { aaa: 5 } }], ['a.aa.aaa2', 1]]); // { a: { aa: { aaa: 5, aaa2: 1 } } } 435 | mutate({ a: 10 }, [['a', { aa: { aaa: 5, aaa2: 1 } }], ['a.aa.aaa2']]); // { a: { aa: { aaa: 5 } } } 436 | mutate({ a: 10 }, [['a', { aa: 5 }], ['a', [1,2,3]]]) // { a: [1,2,3] } 437 | mutate({ a: 10 }, [['a', { aa: 5 }], ['a.aa', 12]]) // { a: { aa: 12 } } 438 | mutate({ b: 20 }, [['a', { aa: 5 }], ['a']]) // { b: 20 } 439 | mutate({ b: 20 }, [['a', { aa: 5 }], ['a.aa']]) // { a: { }, b: 20 } 440 | ``` 441 | ### Tests for complex changes 442 | ```javascript 443 | mutate({ a: 10, b: [], c: {} }, { a: 50, b: { b1: 10 }, c: [1,2,3] }) 444 | // { a: 50, b: { b1: 10 }, c: [1,2,3] } 445 | 446 | mutate( 447 | { a: 10, b: [], c: {}, d: { d1: 12 }, e: [9,8,7] }, 448 | { 449 | a: 50, 450 | b: { b1: 10 }, 451 | c: [1,2,3], 452 | 'c.[]': { cc: 22 }, 453 | 'b.b2': 17, 454 | 'd.d2': 15, 455 | 'e.[0]': 1, 456 | 'e.[]': 3 457 | } 458 | ) 459 | /* 460 | { 461 | a: 50, 462 | b: { b1: 10, b2: 17 }, 463 | c: [1,2,3, { cc: 22 }], 464 | d: { d1: 12, d2: 15 }, 465 | e: [1,8,7,3] 466 | } 467 | */ 468 | 469 | mutate( 470 | { a: { a1: { a1_1: 22 } }, b: [{ b1: 10 }], c: [{ c1: 1 }] }, 471 | { 472 | 'a.a1.a1_1': 33, 473 | 'a.a1.a1_2': 9, 474 | 'a.a2': 14, 475 | 'b.[0].b1': 11, 476 | 'b.[]': 15, 477 | 'b.[0].b2': null, 478 | 'c[0].c1': undefined, 479 | 'c[0]': 7 480 | } 481 | ) 482 | /* 483 | { 484 | a: { 485 | a1: { a1_1: 33, a1_2: 9 }, 486 | a2: 14 487 | }, 488 | b: [{ b1: 11, b2: null }, 15], 489 | c: [7] 490 | } 491 | */ 492 | 493 | mutate( 494 | { a: 10, b: 20 }, 495 | { 496 | a: { a1: 1, a2: 2 }, 497 | 'a.a3.a3_1': 20, b: [1,2,3,{ b1: 1 }], 498 | 'b.[]': 11, 499 | 'b[3].b2.b2_1.b2_1_1': 'b2_1_1 value', 500 | 'c.[]': 14 501 | } 502 | ) 503 | /* 504 | { 505 | a: { 506 | a1: 1, 507 | a2: 2, 508 | a3: { a3_1: 20 } 509 | }, 510 | b: [ 511 | 1,2,3, { 512 | b1: 1, 513 | b2: { 514 | b2_1: { b2_1_1: 'b2_1_1 value' } 515 | } 516 | }, 517 | 11 518 | ], 519 | c: [14] 520 | } 521 | */ 522 | ``` 523 | 524 | ### It returns the same object (**works since version 2.0.0**) 525 | ```javascript 526 | const obj = { a: 10 }; 527 | const result = mutate(obj, []); 528 | expect(result).not.toBe(obj); 529 | ``` 530 | 531 | # (!!!) Attention! Важно! Achtung! 532 | If you use an instance of `Object` (or `Array`) to construct a list of changes then this instance of `Object` (or `Array`) will be changed. 533 | ### Test cases 534 | ```javascript 535 | test('should change object value', () => { 536 | const obj = { b: [] }; 537 | const patchObject = { b1: 1, b2: 2, b3: 3 }; 538 | const changes = [ 539 | ['b', patchObject], 540 | ['b.b4', 4] 541 | ]; 542 | const resut = mutate(obj, changes); 543 | 544 | expect(resut.b).toEqual(patchObject); 545 | expect(resut.b).toBe(patchObject); 546 | expect(patchObject).toEqual({ b1: 1, b2: 2, b3: 3, b4: 4 }); 547 | }); 548 | 549 | test('should change array value', () => { 550 | const obj = { b: [5,6] }; 551 | const patchArray = [1,2,3]; 552 | const changes = [ 553 | ['b', patchArray], 554 | ['b.[]', 4] 555 | ]; 556 | const resut = mutate(obj, changes); 557 | 558 | expect(resut.b).toEqual(patchArray); 559 | expect(resut.b).toBe(patchArray); 560 | expect(patchArray).toEqual([1,2,3,4]); 561 | }); 562 | ``` 563 | 564 | # Use cases for 'deep-mutation' 565 | 566 | ## In redux 567 | ```javascript 568 | import mutate from 'deep-mutation'; 569 | 570 | export default (state = {}, action) => { 571 | const { type, payload } = action; 572 | const { uid } = payload; 573 | 574 | switch (type) { 575 | case 'API_REQUEST': 576 | return mutate(state, [ 577 | [`${uid}._status`, 'request'] 578 | ]); 579 | 580 | case 'API_REQUEST__OK': 581 | return mutate(state, [ 582 | [`${uid}._status`, 'success'], 583 | [`${uid}.result`, payload.body], 584 | ]); 585 | 586 | case 'API_REQUEST__FAIL': 587 | return mutate(state, [ 588 | [`${uid}._status`, 'fail'], 589 | [`${uid}.result`], 590 | [`${uid}.error`, payload.error] 591 | ]); 592 | 593 | default: 594 | return state; 595 | } 596 | }; 597 | // ... 598 | ``` 599 | 600 | ## In component's state 601 | ```javascript 602 | import mutate from 'deep-mutation'; 603 | 604 | class ExampleComponent extends Component { 605 | // ... 606 | onClick = () => this.setState(state => 607 | mutate(state, { 608 | isFetching: true, 609 | 'data.value': 'default', 610 | 'data.key': null, 611 | validation: null 612 | }) 613 | ); 614 | // ... 615 | } 616 | ``` 617 | -------------------------------------------------------------------------------- /babel.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": [ 3 | ], 4 | "presets": [ 5 | ["@babel/preset-env", { 6 | "useBuiltIns": "entry", 7 | "corejs": "3.24", 8 | "loose": true, 9 | "forceAllTransforms": false, 10 | "targets": "ie 11" 11 | }] 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /deepMutationTodo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "deep-mutation-todo", 3 | "version": "1.0.0", 4 | "description": null, 5 | "keywords": [ 6 | "deep-mutation", 7 | "immutable", 8 | "todo" 9 | ], 10 | "main": "src/index.js", 11 | "dependencies": { 12 | "classnames": "2.2.6", 13 | "deep-mutation": "2.1.0", 14 | "react": "16.12.0", 15 | "react-dom": "16.12.0", 16 | "react-scripts": "3.0.1" 17 | }, 18 | "devDependencies": { 19 | "typescript": "3.3.3" 20 | }, 21 | "scripts": { 22 | "start": "react-scripts start", 23 | "build": "react-scripts build", 24 | "test": "react-scripts test --env=jsdom", 25 | "eject": "react-scripts eject" 26 | }, 27 | "browserslist": [ 28 | ">0.2%", 29 | "not dead", 30 | "not ie <= 11", 31 | "not op_mini all" 32 | ] 33 | } -------------------------------------------------------------------------------- /deepMutationTodo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /deepMutationTodo/src/App.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./styles.css"; 3 | 4 | import TodosList from "./components/TodosList"; 5 | 6 | export default function App() { 7 | return ( 8 |
9 |

Hello deep-mutatuon Todo example

10 | 11 |
12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /deepMutationTodo/src/components/Modal.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import "./modal.css"; 3 | 4 | export default class Modal extends PureComponent { 5 | render() { 6 | const { children } = this.props; 7 | 8 | return ( 9 |
10 |
{children}
11 |
12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /deepMutationTodo/src/components/Steps.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import cn from "classnames"; 3 | import "./steps.css"; 4 | 5 | export default class Steps extends PureComponent { 6 | onChangeStep = ({ target }) => { 7 | const { toggleStep } = this.props; 8 | toggleStep(Number(target.dataset.id)); 9 | }; 10 | 11 | onRemoveStep = ({ target }) => { 12 | const { removeStep } = this.props; 13 | removeStep(Number(target.dataset.id)); 14 | }; 15 | 16 | render() { 17 | const { steps } = this.props; 18 | if (steps.length === 0) return null; 19 | return ( 20 | 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /deepMutationTodo/src/components/TodoEditor.jsx: -------------------------------------------------------------------------------- 1 | import React, { PureComponent } from "react"; 2 | import Modal from "./Modal"; 3 | import "./todoEditor.css"; 4 | 5 | export default class TodoEditor extends PureComponent { 6 | state = { 7 | errors: [] 8 | }; 9 | 10 | onSubmit = event => { 11 | event.preventDefault(); 12 | const { target } = event; 13 | const { onConfirm } = this.props; 14 | const errors = []; 15 | const title = target.title.value.trim(); 16 | const description = target.description.value.trim(); 17 | 18 | if (!title) errors.push("Title can not be empty"); 19 | if (errors.length > 0) return this.setState({ errors }); 20 | 21 | onConfirm(title, description); 22 | }; 23 | 24 | render() { 25 | const { title, description, id, onCancel } = this.props; 26 | const { errors } = this.state; 27 | 28 | return ( 29 | 30 |
31 | 37 |