├── .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 | [](https://travis-ci.org/flutejs/immutable-data)
4 | [](https://coveralls.io/github/flutejs/immutable-data?branch=master)
5 | [](https://npmjs.org/package/immutable-data)
6 | [](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 |
--------------------------------------------------------------------------------