├── .travis.yml ├── src ├── index.js ├── set.js ├── remove.js ├── merge.js ├── __tests__ │ ├── set-test.js │ ├── merge-test.js │ └── remove-test.js └── tree.js ├── demo ├── index.html └── index.js ├── .gitignore ├── webpack.config.js ├── package.json └── README.md /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "4" 5 | 6 | after_success: 7 | - npm run coveralls -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | exports.set = require("./set");; 2 | exports.merge = require('./merge'); 3 | exports.remove = require('./remove'); -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Title 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea/ 3 | .ipr 4 | .iws 5 | *~ 6 | ~* 7 | *.diff 8 | *.patch 9 | *.bak 10 | *.log 11 | .DS_Store 12 | Thumbs.db 13 | .project 14 | .*proj 15 | .svn/ 16 | *.swp 17 | *.swo 18 | *.pyc 19 | *.pyo 20 | .build 21 | node_modules 22 | _site 23 | sea-modules 24 | spm_modules 25 | .cache 26 | dist 27 | coverage 28 | lib 29 | .coveralls.yml -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | module.exports = function(webpackConfig, dora) { 3 | 4 | if (dora){ 5 | webpackConfig.entry.index= './demo/index.js'; 6 | return webpackConfig 7 | } 8 | 9 | webpackConfig.plugins.splice(0,1); 10 | webpackConfig.output.libraryTarget = 'commonjs2'; 11 | webpackConfig.externals = fs.readdirSync('node_modules'); 12 | return webpackConfig; 13 | } -------------------------------------------------------------------------------- /src/set.js: -------------------------------------------------------------------------------- 1 | var parse = require('object-path-parse'); 2 | var map = require('array-map'); 3 | var keys = Object.keys || require('object-keys'); 4 | var {createTree, getNodeValue} = require('./tree'); 5 | 6 | 7 | module.exports = function set(data, obj = {}) { 8 | if (typeof data !== 'object') { 9 | throw new Error('data should be Object or Array'); 10 | } 11 | var array = keys(obj); 12 | if (array.length === 0) { 13 | return data; 14 | } 15 | array = map(array, function(path){ 16 | return { 17 | // Just use split if there is no '[' in path 18 | // eg: obj["list"] => parse, obj.list => split 19 | path: path.indexOf('[') > -1 ? parse(path) : path.split('.'), 20 | data: obj[path], 21 | }; 22 | }); 23 | var tree = createTree(data, array); 24 | var value = getNodeValue(tree); 25 | return value; 26 | } 27 | -------------------------------------------------------------------------------- /src/remove.js: -------------------------------------------------------------------------------- 1 | var parse = require('object-path-parse'); 2 | var map = require('array-map'); 3 | var keys = Object.keys || require('object-keys'); 4 | var isArray = require('is-array'); 5 | var {createTree, getNodeValue} = require('./tree'); 6 | 7 | // remove(data, String or Array) 8 | module.exports = function remove(data, array = []) { 9 | if (typeof data !== 'object') { 10 | throw new Error('data should be Object or Array'); 11 | } 12 | 13 | if (!isArray(array)){ 14 | array = [array]; 15 | } 16 | 17 | if (array.length === 0){ 18 | return data; 19 | } 20 | 21 | array = map(array, function(path){ 22 | path = String(path); 23 | return { 24 | // Just use split if there is no '[' in path 25 | // eg: obj["list"] => parse, obj.list => split 26 | path: path.indexOf('[') > -1 ? parse(path) : path.split('.'), 27 | data: null, 28 | }; 29 | }); 30 | 31 | var tree = createTree(data, array); 32 | var value = getNodeValue(tree, 'remove'); 33 | return value; 34 | } 35 | 36 | -------------------------------------------------------------------------------- /demo/index.js: -------------------------------------------------------------------------------- 1 | import {set, merge, remove} from '../src/index.js'; 2 | 3 | var obj1 = { 4 | a:{ 5 | b:[{},{}] 6 | }, 7 | c:{} 8 | } 9 | 10 | var obj2 = set(obj1, { 11 | "a.b[0]":1, 12 | "c":2, 13 | "d":"x" 14 | }); 15 | 16 | var array1 = [{}]; 17 | 18 | var array2 = set(array1, { 19 | "1":{} 20 | }); 21 | 22 | console.log(JSON.stringify(obj2, null, 2)); 23 | console.log(JSON.stringify(array2, null, 2)); 24 | 25 | 26 | var obj3 = merge(obj1,{ 27 | a:{ 28 | b:{ 29 | "0":1 30 | } 31 | } 32 | }); 33 | 34 | console.log(JSON.stringify(obj3, null, 2)); 35 | console.log(merge({list:[1,2]}, {list:[0]})); 36 | console.log(merge({list:[1,2]}, {list:{"0":0}})); 37 | 38 | console.log(remove({ 39 | a:{ 40 | b:1 41 | }, 42 | b:{} 43 | },'b.x')) 44 | 45 | 46 | var obj1 = { 47 | a: { 48 | x: 1, 49 | y: 2, 50 | z: {} 51 | }, 52 | b: {} 53 | }; 54 | 55 | var obj2 = remove(obj1, ['a.x','a.y','b.z']); 56 | console.log(obj2) 57 | 58 | var list = [0,1]; 59 | 60 | var list2 = remove(list,0); 61 | 62 | console.log(list === list2); -------------------------------------------------------------------------------- /src/merge.js: -------------------------------------------------------------------------------- 1 | var isPlainObject = require('is-plain-object'); 2 | var forEach = require('array-foreach'); 3 | var keys = Object.keys || require('object-keys'); 4 | var {createTree, getNodeValue} = require('./tree'); 5 | 6 | // Get the tree path array 7 | // return Array of Structure({path: Array of String, data: node value}) 8 | // 9 | // eg: 10 | // value: 11 | // a 12 | // / \ 13 | // b c 14 | // | | 15 | // 1 2 16 | // return: 17 | // [{path:["a","b"], data:1}, {path:["a","c"], data:2}] 18 | // 19 | // If the data is not a plain object, assign it to the element, 20 | // 21 | // eg: 22 | // merge({list:[1,2]}, {list:[0]}) => {list:[0]} 23 | // merge({list:[1,2]}, {list:{"0":0}}) => {list:[0,2]} 24 | function getObjectPath(value) { 25 | var list = []; 26 | function traverse(data, path = []) { 27 | if (isPlainObject(data)) { 28 | forEach(keys(data), function(key){ 29 | traverse(data[key], path.concat(key)); 30 | }); 31 | return; 32 | } 33 | list.push({ 34 | path, 35 | data, 36 | }); 37 | } 38 | traverse(value); 39 | return list; 40 | } 41 | 42 | // deep merge data 43 | module.exports = function merge(data, obj) { 44 | if (typeof data !== 'object'){ 45 | throw new Error('data should be Object or Array'); 46 | } 47 | if (!obj){ 48 | return data; 49 | } 50 | var array = getObjectPath(obj); 51 | var tree = createTree(data, array); 52 | var value = getNodeValue(tree); 53 | return value; 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "immutable-data", 3 | "version": "2.0.5", 4 | "description": "Easily update nested objects and arrays in a declarative and immutable manner", 5 | "main": "lib/index.js", 6 | "entry": { 7 | "index": "./src/index.js" 8 | }, 9 | "scripts": { 10 | "test": "atool-test", 11 | "build": "atool-build -o ./lib --no-compress", 12 | "coveralls": "cat ./coverage/report-lcov/lcov.info | coveralls", 13 | "prepublish": "npm run build", 14 | "dev": "dora -p 8001 --plugins webpack?publicPath=/demo" 15 | }, 16 | "author": "https://github.com/zinkey", 17 | "homepage": "https://github.com/flutejs/immutable-data", 18 | "repository": { 19 | "type": "git", 20 | "url": "https://github.com/flutejs/immutable-data" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/flutejs/immutable-data/issues" 24 | }, 25 | "licenses": "MIT", 26 | "dependencies": { 27 | "array-foreach": "^1.0.1", 28 | "array-map": "0.0.0", 29 | "is-array": "~1.0.1", 30 | "is-plain-object": "^2.0.1", 31 | "object-assign": "~4.1.0", 32 | "object-keys": "^1.0.9", 33 | "object-path-parse": "^0.1.0" 34 | }, 35 | "devDependencies": { 36 | "atool-build": "0.7.1", 37 | "atool-test": "~0.4.x", 38 | "coveralls": "~2.11.8", 39 | "dora": "0.3.x", 40 | "dora-plugin-webpack": "0.6.4" 41 | }, 42 | "files": [ 43 | "lib" 44 | ], 45 | "keywords": [ 46 | "immutable", 47 | "persistent", 48 | "data", 49 | "datastructure", 50 | "declarative", 51 | "nested" 52 | ] 53 | } 54 | -------------------------------------------------------------------------------- /src/__tests__/set-test.js: -------------------------------------------------------------------------------- 1 | var set = require('../index').set; 2 | 3 | describe('object', () => { 4 | 5 | it('object property', () => { 6 | 7 | var obj1 = { 8 | a:{ 9 | b:[{},{}] 10 | }, 11 | c:{}, 12 | }; 13 | 14 | var obj2 = set(obj1, { 15 | "a.b[0]":{}, 16 | "d":"d", 17 | }); 18 | 19 | expect(obj2).to.not.equal(obj1); 20 | expect(obj2.a.b).to.not.equal(obj1.a.b); 21 | expect(obj2.a.b[0]).to.not.equal(obj1.a.b[0]); 22 | 23 | expect(obj2.a.b[1]).to.equal(obj1.a.b[1]); 24 | expect(obj2.a.b.c).to.equal(obj1.a.b.c); 25 | expect(obj2.d).to.equal('d'); 26 | 27 | }); 28 | 29 | it('array property', () => { 30 | 31 | var array1 = [{ 32 | list:[] 33 | },{ 34 | list:[] 35 | },{ 36 | list:[] 37 | }]; 38 | 39 | var list = []; 40 | var array2 = set(array1, { 41 | "1.list":list, 42 | }); 43 | 44 | expect(array2).to.not.equal(array1); 45 | expect(array2[0]).to.equal(array1[0]); 46 | expect(array2[1]).to.not.equal(array1[1]); 47 | expect(array2[1].list).to.equal(list); 48 | expect(array2[2]).to.equal(array1[2]); 49 | 50 | }); 51 | 52 | it('should throw error if data is not an object', () => { 53 | expect(() => set('a', {})).to.throw('data should be Object or Array'); 54 | }); 55 | 56 | it('should return data if obj not set', () => { 57 | var obj = { 58 | "a": 1 59 | }; 60 | 61 | var obj1 = set(obj); 62 | expect(obj1).to.eql(obj); 63 | 64 | var obj2 = set(obj, {}); 65 | expect(obj2).to.eql(obj); 66 | }); 67 | 68 | }); -------------------------------------------------------------------------------- /src/__tests__/merge-test.js: -------------------------------------------------------------------------------- 1 | var merge = require('../index').merge; 2 | 3 | describe('object', () => { 4 | 5 | it('object property', () => { 6 | 7 | var obj1 = { 8 | a: { 9 | b: [{},{}], 10 | }, 11 | c: {}, 12 | }; 13 | 14 | var obj2 = merge(obj1, { 15 | a: { 16 | b: { 17 | "0": { 18 | x: 1, 19 | } 20 | } 21 | }, 22 | d: "d", 23 | }); 24 | 25 | expect(obj2).to.not.equal(obj1); 26 | expect(obj2.a.b).to.not.equal(obj1.a.b); 27 | expect(obj2.a.b[0]).to.not.equal(obj1.a.b[0]); 28 | 29 | expect(obj2.a.b[1]).to.equal(obj1.a.b[1]); 30 | expect(obj2.a.b.c).to.equal(obj1.a.b.c); 31 | expect(obj2.d).to.equal('d'); 32 | expect(obj2).to.eql({ 33 | a: { 34 | b:[{x: 1},{}], 35 | }, 36 | c: {}, 37 | d: "d", 38 | }); 39 | 40 | }); 41 | 42 | it('array property', () => { 43 | 44 | var array1 = [{ 45 | list:[] 46 | },{ 47 | list:[] 48 | },{ 49 | list:[] 50 | }]; 51 | 52 | var list = []; 53 | var array2 = merge(array1, { 54 | "1":{ 55 | list, 56 | } 57 | }); 58 | 59 | expect(array2).to.not.equal(array1); 60 | expect(array2[0]).to.equal(array1[0]); 61 | expect(array2[1]).to.not.equal(array1[1]); 62 | expect(array2[1].list).to.equal(list); 63 | expect(array2[2]).to.equal(array1[2]); 64 | 65 | }); 66 | 67 | it('should throw error if data is not an object', () => { 68 | expect(() => merge('a', {})).to.throw('data should be Object or Array'); 69 | }); 70 | 71 | it('should return data if obj not set', () => { 72 | var obj = { 73 | "a": 1 74 | }; 75 | 76 | var obj1 = merge(obj); 77 | expect(obj1).to.eql(obj); 78 | 79 | }); 80 | 81 | }); -------------------------------------------------------------------------------- /src/__tests__/remove-test.js: -------------------------------------------------------------------------------- 1 | var remove = require('../index').remove; 2 | 3 | describe('object', () => { 4 | 5 | it('object property', () => { 6 | 7 | expect(remove({ 8 | list: [ 9 | { 10 | x: 1, 11 | y: 2, 12 | }, 13 | { 14 | a: 1, 15 | b: 2, 16 | }, 17 | ] 18 | },'list[1]["a"]')).to.eql({ 19 | list: [ 20 | { 21 | x: 1, 22 | y: 2, 23 | }, 24 | { 25 | b: 2, 26 | }, 27 | ], 28 | }); 29 | 30 | expect(remove({ 31 | a: '1', 32 | b: '2', 33 | }, 'a')).to.eql({ 34 | b: '2', 35 | }); 36 | 37 | var obj1 = { 38 | a: { 39 | x: 1, 40 | y: 2, 41 | z: {}, 42 | }, 43 | b: {}, 44 | }; 45 | 46 | var obj2 = remove(obj1, ['a.x','a.y']); 47 | 48 | expect(obj2).to.eql({ 49 | a: { 50 | z: {}, 51 | }, 52 | b: {}, 53 | }); 54 | 55 | expect(obj1.a.z).to.equal(obj2.a.z); 56 | expect(obj1.b).to.equal(obj2.b); 57 | 58 | }); 59 | 60 | it('array property', () => { 61 | 62 | var array1 = [ 63 | { 64 | x: 1, 65 | }, 66 | { 67 | y: 1, 68 | }, 69 | ]; 70 | 71 | var array2 = remove(array1,"0"); 72 | 73 | expect(array2).to.eql([ 74 | { 75 | y: 1, 76 | } 77 | ]); 78 | 79 | expect(array1[0]).to.not.equal(array2[0]); 80 | 81 | var list1 = [0,1]; 82 | var list2 = remove(list1,0); 83 | 84 | expect(list2).to.eql([1]); 85 | expect(list1).to.not.equal(list2); 86 | 87 | }); 88 | 89 | it('should throw error if data is not an object', () => { 90 | expect(() => remove('a', {})).to.throw('data should be Object or Array'); 91 | }); 92 | 93 | it('should return data if obj not set', () => { 94 | var obj = { 95 | "a": 1, 96 | }; 97 | 98 | var obj1 = remove(obj); 99 | expect(obj1).to.eql(obj); 100 | 101 | var obj2 = remove(obj, []); 102 | expect(obj2).to.eql(obj); 103 | }); 104 | 105 | }); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # immutable-data 2 | 3 | [![Build Status](https://travis-ci.org/flutejs/immutable-data.svg)](https://travis-ci.org/flutejs/immutable-data) 4 | [![Coverage Status](https://coveralls.io/repos/flutejs/immutable-data/badge.svg?branch=master&service=github)](https://coveralls.io/github/flutejs/immutable-data?branch=master) 5 | [![NPM version](https://img.shields.io/npm/v/immutable-data.svg?style=flat)](https://npmjs.org/package/immutable-data) 6 | [![NPM downloads](http://img.shields.io/npm/dm/immutable-data.svg?style=flat)](https://npmjs.org/package/immutable-data) 7 | 8 | > Easily "set", "merge", "remove" nested objects and arrays in an immutable manner. 9 | 10 | "immutable-data" lets you specify the part of the data you want to change, and then set the value, then it can change the part that you want to change, at the same time, the rest data that you do not want to change will not be affected. 11 | 12 | ## api 13 | 14 | ### set(data, map) 15 | 16 | - data: original data you want to change 17 | 18 | - map: the path map 19 | 20 | ```javascript 21 | { 22 | 'a.b.0': 1 // or 'a["b"][0]': 1 23 | } 24 | ``` 25 | 26 | eg: 27 | 28 | ```javascript 29 | var set = require("immutable-data").set; 30 | 31 | var data = { 32 | a: { 33 | b: 2 34 | }, 35 | c: [{ 36 | d: 2 37 | }] 38 | }; 39 | 40 | set(data, { 41 | 'a.b': 1, 42 | 'c.0.d': 1 43 | }) 44 | 45 | //return 46 | { 47 | a: { 48 | b: 1 49 | }, 50 | c: [{ 51 | d: 1 52 | }] 53 | } 54 | ``` 55 | 56 | 57 | ### merge(data, object) 58 | 59 | - data: original data you want to change 60 | 61 | - object: deep merge object 62 | 63 | 64 | eg: 65 | 66 | ```javascript 67 | var merge = require("immutable-data").merge; 68 | 69 | var data = { 70 | a: { 71 | b: 2 72 | }, 73 | c: [{ 74 | d: 2 75 | }] 76 | }; 77 | 78 | merge(data, { 79 | a: { 80 | b: 1 81 | }, 82 | c: { 83 | "0": { 84 | d: 1 85 | } 86 | } 87 | }) 88 | 89 | //return 90 | { 91 | a: { 92 | b: 1 93 | }, 94 | c: [{ 95 | d: 1 96 | }] 97 | } 98 | ``` 99 | 100 | Tip: If the type of a value is an array, it will be assigned a direct value. 101 | 102 | ```javascript 103 | merge({list:[1,2]}, {list:[0]}) 104 | 105 | //return 106 | {list:[0]} 107 | 108 | merge({list:[1,2]}, {list:{"0":0}}) 109 | 110 | //return 111 | {list:[0,2]} 112 | ``` 113 | 114 | ### remove(data, path) 115 | 116 | - data: original data you want to change 117 | 118 | - path: String or Array 119 | 120 | 121 | ```javascript 122 | "a.b" 123 | ["a.b","a.c"] 124 | ``` 125 | 126 | eg: 127 | 128 | ```javascript 129 | var remove = require("immutable-data").remove; 130 | 131 | var data = { 132 | a: { 133 | b: 2 134 | }, 135 | c: [{ 136 | d: 2 137 | }] 138 | }; 139 | 140 | remove(data, [ 141 | 'a.b', 142 | 'c.0' 143 | ]) 144 | 145 | // return 146 | { 147 | a: {}, 148 | c: [] 149 | } 150 | ``` 151 | 152 | ## dev 153 | 154 | ``` 155 | $ npm install 156 | $ npm run dev 157 | $ npm test 158 | $ npm run build 159 | ``` 160 | 161 | ## License 162 | 163 | MIT License 164 | -------------------------------------------------------------------------------- /src/tree.js: -------------------------------------------------------------------------------- 1 | var assign = require('object-assign'); 2 | var isArray = require('is-array'); 3 | var forEach = require('array-foreach'); 4 | var map = require('array-map'); 5 | var keys = Object.keys || require('object-keys'); 6 | 7 | // node structure {key, value, data, parentNode} 8 | class Node{ 9 | constructor({key, value, data, parentNode}) { 10 | this.key = key; 11 | this.value = value; 12 | this.data = data; 13 | this.parentNode = parentNode; 14 | this.children = {}; 15 | } 16 | setChild(key ,child) { 17 | this.children[key] = child; 18 | } 19 | getChild(key){ 20 | return this.children[key]; 21 | } 22 | } 23 | 24 | 25 | // Assign data with array: {key, value, type='set'} 26 | // data : array => replace the same value as the index(obj.key) 27 | // object => assign object 28 | // 29 | // eg:obj1:{key:1,value},obj2:{key:3,value} 30 | // 31 | // array: 32 | // [0, 1, 2, 3, 4] => 33 | // [0, obj1.value, 2, obj2.value, 4] 34 | // 35 | // object: 36 | // { 37 | // "1":1, 38 | // "3":3 39 | // } => 40 | // { 41 | // "1":obj1.value, 42 | // "3":obj2.value 43 | // } 44 | function assignData(node, array, type = 'set') { 45 | var data = node.data; 46 | if (type === 'remove' && node.secondNode){ 47 | if (isArray(data)) { 48 | data = data.concat(); 49 | forEach(array, function(obj, index) { 50 | // splice 1 item and index - 1 51 | data.splice(obj.key - index, 1); 52 | }); 53 | return data; 54 | } 55 | data = assign({}, data); 56 | forEach(array, function(obj) { 57 | if (obj.key in data){ 58 | delete data[obj.key]; 59 | } 60 | }); 61 | return data; 62 | } 63 | 64 | if (isArray(data)) { 65 | data = data.concat(); 66 | forEach(array, function(obj) { 67 | data[obj.key] = obj.value; 68 | }); 69 | return data; 70 | } 71 | var assignObject = {}; 72 | forEach(array, function(obj) { 73 | assignObject[obj.key] = obj.value; 74 | }); 75 | return assign({}, data, assignObject); 76 | } 77 | 78 | // Create a tree that can be used to handle multiple data 79 | // @param data (Object or Array) 80 | // @param array (Array of Structure {path, data}) 81 | // 82 | // eg:[{path:["a","b"],data:1},{path:["a","c"],data:2}] => 83 | // a 84 | // / \ 85 | // b c 86 | // | | 87 | // 1 2 88 | function createTree(data, array) { 89 | var tree = new Node({ 90 | data, 91 | }); 92 | 93 | forEach(array, function(item) { 94 | var pointer = tree; 95 | var dataPointer = data; 96 | 97 | var len = item.path.length; 98 | 99 | forEach(item.path, function(key, index) { 100 | 101 | var child = pointer.getChild(key) || new Node({ 102 | // node name 103 | key, 104 | // leaf node value 105 | value: len === index +1 ? item.data : null, 106 | // real data 107 | data: dataPointer[key], 108 | // parent node 109 | parentNode: pointer, 110 | }); 111 | 112 | dataPointer = dataPointer[key]; 113 | pointer.setChild(key, child); 114 | pointer = child; 115 | 116 | }); 117 | 118 | }); 119 | return tree; 120 | } 121 | 122 | // Recursive access node and get the assignData, 123 | // and then return the root node value 124 | function getNodeValue(node, type) { 125 | var array = keys(node.children); 126 | 127 | // Just get the leaf node value, 128 | // if a node is not a leaf node and its value is not undefined, 129 | // then the value is ignored. 130 | if (array.length === 0){ 131 | // Mark the parent node is the second last node, 132 | // so it is convenient to know in which node can remove attributes 133 | node.parentNode.secondNode = true; 134 | return node.value; 135 | } 136 | 137 | var assignArray = map(array, function(key) { 138 | var child = node.children[key]; 139 | return { 140 | key: child.key, 141 | value: getNodeValue(child, type), 142 | }; 143 | }); 144 | 145 | return assignData(node, assignArray, type); 146 | 147 | } 148 | 149 | exports.createTree = createTree; 150 | exports.getNodeValue = getNodeValue; 151 | --------------------------------------------------------------------------------