├── .gitignore ├── .travis.yml ├── bower.json ├── gulpfile.js ├── package.json ├── simple-diff.d.ts ├── LICENSE ├── simple-diff.min.js ├── README.md ├── simple-diff.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | node_modules 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "5" -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-diff", 3 | "main": "simple-diff.js", 4 | "version": "1.6.0", 5 | "homepage": "https://github.com/redexp/simple-diff", 6 | "authors": [ 7 | "Sergii Kliuchnyk " 8 | ], 9 | "description": "simple diff for object and arrays with options", 10 | "keywords": [ 11 | "diff" 12 | ], 13 | "license": "MIT", 14 | "ignore": [ 15 | "**/.*", 16 | "test" 17 | ], 18 | "dependencies": {}, 19 | "devDependencies": {} 20 | } 21 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | var gulp = require('gulp'); 2 | var replace = require('gulp-replace'); 3 | var rename = require('gulp-rename'); 4 | var uglify = require('gulp-uglify'); 5 | 6 | gulp.task('default', function() { 7 | gulp.src('simple-diff.js') 8 | .pipe(replace('var _DEV_ = true;', '')) 9 | .pipe(uglify({ 10 | mangle: false, 11 | compress: { 12 | global_defs: { 13 | '_DEV_': false 14 | }, 15 | unused: true 16 | } 17 | })) 18 | .pipe(rename('simple-diff.min.js')) 19 | .pipe(gulp.dest('./')) 20 | ; 21 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "simple-diff", 3 | "version": "1.7.2", 4 | "description": "simple diff for object and arrays with options", 5 | "main": "simple-diff.js", 6 | "types": "simple-diff.d.ts", 7 | "scripts": { 8 | "test": "mocha -R spec" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/redexp/simple-diff.git" 13 | }, 14 | "keywords": [ 15 | "diff" 16 | ], 17 | "author": "Sergii Kliuchnyk", 18 | "license": "MIT", 19 | "bugs": { 20 | "url": "https://github.com/redexp/simple-diff/issues" 21 | }, 22 | "homepage": "https://github.com/redexp/simple-diff#readme", 23 | "devDependencies": { 24 | "chai": "^3.5.0", 25 | "gulp": "^3.9.1", 26 | "gulp-rename": "^1.2.2", 27 | "gulp-replace": "^0.5.4", 28 | "gulp-uglify": "^1.5.4", 29 | "mocha": "^2.5.3", 30 | "sinon": "^1.17.7", 31 | "sinon-chai": "^2.8.0" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /simple-diff.d.ts: -------------------------------------------------------------------------------- 1 | declare function diff(oldObj: any, newObj: any, ops?: DiffOptions): DiffEvent[]; 2 | 3 | export default diff; 4 | 5 | export interface DiffOptions extends Partial { 6 | idProp?: string, 7 | idProps?: {[path: string]: string}, 8 | addEvent?: string, 9 | removeEvent?: string, 10 | changeEvent?: string, 11 | addItemEvent?: string, 12 | removeItemEvent?: string, 13 | moveItemEvent?: string, 14 | callback?: (event: DiffEvent) => void, 15 | comparators?: Array<[any, Comparator]>, 16 | ignore?: Comparator, 17 | } 18 | 19 | export type Path = Array; 20 | 21 | export interface PathChange { 22 | oldPath: Path, 23 | newPath: Path, 24 | } 25 | 26 | export type Comparator = (oldValue: any, newValue: any, options: PathChange) => boolean; 27 | 28 | export interface DiffEvent extends PathChange { 29 | type: 'add' | 'remove' | 'change' | 'add-item' | 'remove-item' | 'move-item', 30 | oldValue: any, 31 | newValue: any, 32 | oldIndex?: number, 33 | curIndex?: number, 34 | newIndex?: number, 35 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Sergii Kliuchnyk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /simple-diff.min.js: -------------------------------------------------------------------------------- 1 | !function(root,factory){"function"==typeof define&&define.amd?define([],factory):"object"==typeof exports?module.exports=factory():root.simpleDiff=factory()}(this,function(){function diff(oldObj,newObj,ops){ops=ops||{};var i,len,prop,id,changes=[],oldPath=ops.oldPath||[],newPath=ops.newPath||[],ID_PROP=ops.idProp||"id",ADD_EVENT=ops.addEvent||"add",REMOVE_EVENT=ops.removeEvent||"remove",CHANGE_EVENT=ops.changeEvent||"change",ADD_ITEM_EVENT=ops.addItemEvent||"add-item",REMOVE_ITEM_EVENT=ops.removeItemEvent||"remove-item",MOVE_ITEM_EVENT=ops.moveItemEvent||"move-item",callback=ops.callback||function(item){changes.push(item)},comparators=ops.comparators||[],ignore=ops.ignore;if(!(isObject(oldObj)&&isObject(newObj)||oldObj===newObj))return callback({oldPath:oldPath,newPath:newPath,type:CHANGE_EVENT,oldValue:oldObj,newValue:newObj}),changes;if(ignore&&ignore(oldObj,newObj,{oldPath:oldPath,newPath:newPath}))return changes;if(isArray(oldObj)){var idProp=ops.idProps&&(ops.idProps[oldPath.map(numberToAsterisk).join(".")]||ops.idProps[oldPath.join(".")])||ID_PROP;if("*"===idProp){var oldLength=oldObj.length,newLength=newObj.length;for(i=0,len=oldLength>newLength?oldLength:newLength;i=oldLength?callback({oldPath:oldPath,newPath:newPath,type:ADD_ITEM_EVENT,oldIndex:-1,curIndex:-1,newIndex:i,newValue:newObj[i]}):i>=newLength&&callback({oldPath:oldPath,newPath:newPath,type:REMOVE_ITEM_EVENT,oldIndex:i,curIndex:newLength,newIndex:-1,oldValue:oldObj[i]});return changes}var sample=oldObj.length>0?oldObj[0]:newObj[0];if(sample===UNDEFINED)return changes;var curIndex,oldIndex,objective="object"==typeof sample,oldHash=objective?indexBy(oldObj,idProp):hashOf(oldObj),newHash=objective?indexBy(newObj,idProp):hashOf(newObj),curArray=[].concat(oldObj);for(i=0,len=oldObj.length;i=curArray.length?curArray.push(newObj[i]):curArray.splice(i,0,newObj[i]));for(i=0,len=newObj.length;i0)for(i=0,len=comparators.length;i newLength ? oldLength : newLength; i < len; i++) { 70 | if (i < oldLength && i < newLength) { 71 | diff(oldObj[i], newObj[i], extend({}, ops, { 72 | callback: callback, 73 | oldPath: oldPath.concat(i), 74 | newPath: newPath.concat(i) 75 | })); 76 | } 77 | else if (i >= oldLength) { 78 | callback({ 79 | oldPath: oldPath, 80 | newPath: newPath, 81 | type: ADD_ITEM_EVENT, 82 | oldIndex: -1, 83 | curIndex: -1, 84 | newIndex: i, 85 | newValue: newObj[i] 86 | }); 87 | } 88 | else if (i >= newLength) { 89 | callback({ 90 | oldPath: oldPath, 91 | newPath: newPath, 92 | type: REMOVE_ITEM_EVENT, 93 | oldIndex: i, 94 | curIndex: newLength, 95 | newIndex: -1, 96 | oldValue: oldObj[i] 97 | }); 98 | } 99 | } 100 | 101 | return changes; 102 | } 103 | 104 | var sample = oldObj.length > 0 ? oldObj[0] : newObj[0]; 105 | 106 | if (sample === UNDEFINED) return changes; 107 | 108 | var objective = typeof sample === 'object'; 109 | 110 | var oldHash = objective ? indexBy(oldObj, idProp) : hashOf(oldObj), 111 | newHash = objective ? indexBy(newObj, idProp) : hashOf(newObj), 112 | curArray = [].concat(oldObj), 113 | curIndex, oldIndex; 114 | 115 | for (i = 0, len = oldObj.length; i < len; i++) { 116 | id = objective ? oldObj[i][idProp] : oldObj[i]; 117 | 118 | if (!newHash.hasOwnProperty(id)) { 119 | curIndex = curArray.indexOf(oldObj[i]); 120 | curArray.splice(curIndex, 1); 121 | 122 | callback({ 123 | oldPath: oldPath, 124 | newPath: newPath, 125 | type: REMOVE_ITEM_EVENT, 126 | oldIndex: i, 127 | curIndex: curIndex, 128 | newIndex: -1, 129 | oldValue: oldObj[i] 130 | }); 131 | } 132 | } 133 | 134 | for (i = 0, len = newObj.length; i < len; i++) { 135 | id = objective ? newObj[i][idProp] : newObj[i]; 136 | 137 | if (!oldHash.hasOwnProperty(id)) { 138 | callback({ 139 | oldPath: oldPath, 140 | newPath: newPath, 141 | type: ADD_ITEM_EVENT, 142 | oldIndex: -1, 143 | curIndex: -1, 144 | newIndex: i, 145 | newValue: newObj[i] 146 | }); 147 | 148 | if (i >= curArray.length) { 149 | curArray.push(newObj[i]); 150 | } 151 | else { 152 | curArray.splice(i, 0, newObj[i]); 153 | } 154 | } 155 | } 156 | 157 | for (i = 0, len = newObj.length; i < len; i++) { 158 | id = objective ? newObj[i][idProp] : newObj[i]; 159 | 160 | if (!oldHash.hasOwnProperty(id)) continue; 161 | 162 | oldIndex = oldObj.indexOf(oldHash[id]); 163 | curIndex = curArray.indexOf(oldHash[id]); 164 | 165 | if (i !== curIndex) { 166 | callback({ 167 | oldPath: oldPath, 168 | newPath: newPath, 169 | type: MOVE_ITEM_EVENT, 170 | oldIndex: oldIndex, 171 | curIndex: curIndex, 172 | newIndex: i 173 | }); 174 | 175 | curArray.splice(curIndex, 1); 176 | curArray.splice(i, 0, oldHash[id]); 177 | } 178 | 179 | diff(oldHash[id], newObj[i], extend({}, ops, { 180 | callback: callback, 181 | oldPath: oldPath.concat(oldIndex), 182 | newPath: newPath.concat(i) 183 | })); 184 | } 185 | } 186 | else { 187 | if (comparators.length > 0) { 188 | for (i = 0, len = comparators.length; i < len; i++) { 189 | if (oldObj instanceof comparators[i][0] === false && newObj instanceof comparators[i][0] === false) continue; 190 | 191 | var objEqual = comparators[i][1](oldObj, newObj, { 192 | oldPath: oldPath, 193 | newPath: newPath 194 | }); 195 | 196 | if (!objEqual) { 197 | callback({ 198 | oldPath: oldPath, 199 | newPath: newPath, 200 | type: CHANGE_EVENT, 201 | oldValue: oldObj, 202 | newValue: newObj 203 | }); 204 | } 205 | 206 | return changes; 207 | } 208 | } 209 | 210 | for (prop in oldObj) { 211 | if (!oldObj.hasOwnProperty(prop)) continue; 212 | 213 | if (!newObj.hasOwnProperty(prop)) { 214 | callback({ 215 | oldPath: oldPath.concat(prop), 216 | newPath: newPath.concat(prop), 217 | type: REMOVE_EVENT, 218 | oldValue: oldObj[prop], 219 | newValue: UNDEFINED 220 | }); 221 | } 222 | else if (isObject(oldObj[prop]) && isObject(newObj[prop])) { 223 | diff(oldObj[prop], newObj[prop], extend({}, ops, { 224 | callback: callback, 225 | oldPath: oldPath.concat(prop), 226 | newPath: newPath.concat(prop) 227 | })); 228 | } 229 | else if (oldObj[prop] !== newObj[prop]) { 230 | callback({ 231 | oldPath: oldPath.concat(prop), 232 | newPath: newPath.concat(prop), 233 | type: CHANGE_EVENT, 234 | oldValue: oldObj[prop], 235 | newValue: newObj[prop] 236 | }); 237 | } 238 | } 239 | 240 | for (prop in newObj) { 241 | if (!newObj.hasOwnProperty(prop)) continue; 242 | 243 | if (!oldObj.hasOwnProperty(prop)) { 244 | callback({ 245 | oldPath: oldPath.concat(prop), 246 | newPath: newPath.concat(prop), 247 | type: ADD_EVENT, 248 | oldValue: UNDEFINED, 249 | newValue: newObj[prop] 250 | }); 251 | } 252 | } 253 | } 254 | 255 | return changes; 256 | } 257 | 258 | return diff; 259 | 260 | function isObject(object) { 261 | return !!object && typeof object === 'object'; 262 | } 263 | 264 | function isArray(array) { 265 | if (Array.isArray) { 266 | return Array.isArray(array); 267 | } 268 | else { 269 | return Object.prototype.toString.call(array) === '[object Array]'; 270 | } 271 | } 272 | 273 | function indexBy(array, id) { 274 | var hash = {}; 275 | 276 | for (var i = 0, len = array.length; i < len; i++) { 277 | hash[array[i][id]] = array[i]; 278 | } 279 | 280 | return hash; 281 | } 282 | 283 | function hashOf(array) { 284 | var hash = {}; 285 | 286 | for (var i = 0, len = array.length; i < len; i++) { 287 | hash[array[i]] = array[i]; 288 | } 289 | 290 | return hash; 291 | } 292 | 293 | function extend(target) { 294 | for (var i = 1, len = arguments.length; i < len; i++) { 295 | var source = arguments[i]; 296 | for (var prop in source) { 297 | if (!source.hasOwnProperty(prop)) continue; 298 | 299 | target[prop] = source[prop]; 300 | } 301 | } 302 | 303 | return target; 304 | } 305 | 306 | function numberToAsterisk(value) { 307 | return typeof value === 'number' ? '*' : value; 308 | } 309 | 310 | })); -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var expect = require('chai').expect, 2 | sinon = require('sinon'), 3 | diff = require('../simple-diff'), 4 | undefined; 5 | 6 | require('chai').use(require('sinon-chai')); 7 | 8 | describe('diff', function () { 9 | it('should find changes in objects', function () { 10 | var changes = diff( 11 | { 12 | prop1: 'value1', 13 | prop2: 'value2', 14 | prop3: 'value3' 15 | }, 16 | { 17 | prop1: 'value01', 18 | prop2: 'value2', 19 | prop4: 'value4' 20 | } 21 | ); 22 | 23 | expect(changes).to.deep.equal([ 24 | { 25 | oldPath: ['prop1'], 26 | newPath: ['prop1'], 27 | type: 'change', 28 | oldValue: 'value1', 29 | newValue: 'value01' 30 | }, 31 | { 32 | oldPath: ['prop3'], 33 | newPath: ['prop3'], 34 | type: 'remove', 35 | oldValue: 'value3', 36 | newValue: undefined 37 | }, 38 | { 39 | oldPath: ['prop4'], 40 | newPath: ['prop4'], 41 | type: 'add', 42 | oldValue: undefined, 43 | newValue: 'value4' 44 | } 45 | ]); 46 | }); 47 | 48 | it('should find changes in deep object', function () { 49 | var changes = diff( 50 | { 51 | prop0: { 52 | prop1: 'value1', 53 | prop2: 'value2', 54 | prop3: 'value3' 55 | }, 56 | prop5: 'value5' 57 | }, 58 | { 59 | prop0: { 60 | prop1: 'value01', 61 | prop2: 'value2', 62 | prop4: 'value4' 63 | }, 64 | prop5: 'value05' 65 | } 66 | ); 67 | 68 | expect(changes).to.deep.equal([ 69 | { 70 | oldPath: ['prop0', 'prop1'], 71 | newPath: ['prop0', 'prop1'], 72 | type: 'change', 73 | oldValue: 'value1', 74 | newValue: 'value01' 75 | }, 76 | { 77 | oldPath: ['prop0', 'prop3'], 78 | newPath: ['prop0', 'prop3'], 79 | type: 'remove', 80 | oldValue: 'value3', 81 | newValue: undefined 82 | }, 83 | { 84 | oldPath: ['prop0', 'prop4'], 85 | newPath: ['prop0', 'prop4'], 86 | type: 'add', 87 | oldValue: undefined, 88 | newValue: 'value4' 89 | }, 90 | { 91 | oldPath: ['prop5'], 92 | newPath: ['prop5'], 93 | type: 'change', 94 | oldValue: 'value5', 95 | newValue: 'value05' 96 | } 97 | ]); 98 | }); 99 | 100 | it('should find changes in array with id', function () { 101 | var changes = diff( 102 | [ 103 | { 104 | id: 1, 105 | prop: 'value1' 106 | }, 107 | { 108 | id: 2, 109 | prop: 'value2' 110 | }, 111 | { 112 | id: 3, 113 | prop: 'value3' 114 | }, 115 | { 116 | id: 4, 117 | prop: 'value4' 118 | } 119 | ], 120 | [ 121 | { 122 | id: 3, 123 | prop: 'value3' 124 | }, 125 | { 126 | id: 2, 127 | prop: 'value2' 128 | }, 129 | { 130 | id: 1, 131 | prop: 'value1' 132 | }, 133 | { 134 | id: 5, 135 | prop: 'value5' 136 | } 137 | ] 138 | ); 139 | 140 | expect(changes).to.deep.equal([ 141 | { 142 | oldPath: [], 143 | newPath: [], 144 | type: 'remove-item', 145 | oldIndex: 3, 146 | curIndex: 3, 147 | newIndex: -1, 148 | oldValue: { 149 | id: 4, 150 | prop: 'value4' 151 | } 152 | }, 153 | { 154 | oldPath: [], 155 | newPath: [], 156 | type: 'add-item', 157 | oldIndex: -1, 158 | curIndex: -1, 159 | newIndex: 3, 160 | newValue: { 161 | id: 5, 162 | prop: 'value5' 163 | } 164 | }, 165 | { 166 | oldPath: [], 167 | newPath: [], 168 | type: 'move-item', 169 | oldIndex: 2, 170 | curIndex: 2, 171 | newIndex: 0 172 | }, 173 | { 174 | oldPath: [], 175 | newPath: [], 176 | type: 'move-item', 177 | oldIndex: 1, 178 | curIndex: 2, 179 | newIndex: 1 180 | } 181 | ]); 182 | }); 183 | 184 | it('should find changes in array of simple types', function () { 185 | var changes = diff( 186 | [ 187 | 'one', 188 | 'two', 189 | 'three', 190 | 'four' 191 | ], 192 | [ 193 | 'three', 194 | 'two', 195 | 'one', 196 | 'five' 197 | ] 198 | ); 199 | 200 | expect(changes).to.deep.equal([ 201 | { 202 | oldPath: [], 203 | newPath: [], 204 | type: 'remove-item', 205 | oldIndex: 3, 206 | curIndex: 3, 207 | newIndex: -1, 208 | oldValue: 'four' 209 | }, 210 | { 211 | oldPath: [], 212 | newPath: [], 213 | type: 'add-item', 214 | oldIndex: -1, 215 | curIndex: -1, 216 | newIndex: 3, 217 | newValue: 'five' 218 | }, 219 | { 220 | oldPath: [], 221 | newPath: [], 222 | type: 'move-item', 223 | oldIndex: 2, 224 | curIndex: 2, 225 | newIndex: 0 226 | }, 227 | { 228 | oldPath: [], 229 | newPath: [], 230 | type: 'move-item', 231 | oldIndex: 1, 232 | curIndex: 2, 233 | newIndex: 1 234 | } 235 | ]); 236 | }); 237 | 238 | it('should not handle changed index by removed previous items', function () { 239 | var changes = diff( 240 | [ 241 | 1, 242 | 2, 243 | 3, 244 | 4, 245 | 5, 246 | 6, 247 | 7, 248 | 8, 249 | 9, 250 | 10 251 | ], 252 | [ 253 | 11, 254 | 3, 255 | 1, 256 | 4, 257 | 5, 258 | 13, 259 | 7, 260 | 6, 261 | 9, 262 | 10, 263 | 12 264 | ] 265 | ); 266 | 267 | expect(changes).to.deep.equal([ 268 | { 269 | oldPath: [], 270 | newPath: [], 271 | type: 'remove-item', 272 | oldIndex: 1, 273 | curIndex: 1, 274 | newIndex: -1, 275 | oldValue: 2 276 | }, 277 | { 278 | oldPath: [], 279 | newPath: [], 280 | type: 'remove-item', 281 | oldIndex: 7, 282 | curIndex: 6, 283 | newIndex: -1, 284 | oldValue: 8 285 | }, 286 | { 287 | oldPath: [], 288 | newPath: [], 289 | type: 'add-item', 290 | oldIndex: -1, 291 | curIndex: -1, 292 | newIndex: 0, 293 | newValue: 11 294 | }, 295 | { 296 | oldPath: [], 297 | newPath: [], 298 | type: 'add-item', 299 | oldIndex: -1, 300 | curIndex: -1, 301 | newIndex: 5, 302 | newValue: 13 303 | }, 304 | { 305 | oldPath: [], 306 | newPath: [], 307 | type: 'add-item', 308 | oldIndex: -1, 309 | curIndex: -1, 310 | newIndex: 10, 311 | newValue: 12 312 | }, 313 | { 314 | oldPath: [], 315 | newPath: [], 316 | type: 'move-item', 317 | oldIndex: 2, 318 | curIndex: 2, 319 | newIndex: 1 320 | }, 321 | { 322 | oldPath: [], 323 | newPath: [], 324 | type: 'move-item', 325 | oldIndex: 6, 326 | curIndex: 7, 327 | newIndex: 6 328 | } 329 | ]); 330 | }); 331 | 332 | it('should find changes in deep array', function () { 333 | var changes = diff( 334 | { 335 | prop: [ 336 | 1, 337 | 2 338 | ] 339 | }, 340 | { 341 | prop: [ 342 | 2, 343 | 3 344 | ] 345 | } 346 | ); 347 | 348 | expect(changes).to.deep.equal([ 349 | { 350 | oldPath: ['prop'], 351 | newPath: ['prop'], 352 | type: 'remove-item', 353 | oldIndex: 0, 354 | curIndex: 0, 355 | newIndex: -1, 356 | oldValue: 1 357 | }, 358 | { 359 | oldPath: ['prop'], 360 | newPath: ['prop'], 361 | type: 'add-item', 362 | oldIndex: -1, 363 | curIndex: -1, 364 | newIndex: 1, 365 | newValue: 3 366 | } 367 | ]); 368 | }); 369 | 370 | it('should handle idProps option', function () { 371 | var changes = diff( 372 | { 373 | prop1: { 374 | prop2: [ 375 | { 376 | _id: 1, 377 | prop3: [ 378 | { 379 | cid: 1, 380 | name: 'name1' 381 | }, 382 | { 383 | cid: 2, 384 | name: 'name2' 385 | } 386 | ] 387 | }, 388 | { 389 | _id: 2, 390 | name: 'name2' 391 | } 392 | ] 393 | } 394 | }, 395 | { 396 | prop1: { 397 | prop2: [ 398 | { 399 | _id: 2, 400 | name: 'name2' 401 | }, 402 | { 403 | _id: 1, 404 | prop3: [ 405 | { 406 | cid: 2, 407 | name: 'name02' 408 | }, 409 | { 410 | cid: 1, 411 | name: 'name1' 412 | } 413 | ] 414 | } 415 | ] 416 | } 417 | }, 418 | { 419 | idProp: '_id', 420 | idProps: { 421 | "prop1.prop2.*.prop3": "cid" 422 | } 423 | } 424 | ); 425 | 426 | expect(changes).to.deep.equal([ 427 | { 428 | oldPath: ['prop1', 'prop2'], 429 | newPath: ['prop1', 'prop2'], 430 | type: 'move-item', 431 | oldIndex: 1, 432 | curIndex: 1, 433 | newIndex: 0 434 | }, 435 | { 436 | oldPath: ['prop1', 'prop2', 0, 'prop3'], 437 | newPath: ['prop1', 'prop2', 1, 'prop3'], 438 | type: 'move-item', 439 | oldIndex: 1, 440 | curIndex: 1, 441 | newIndex: 0 442 | }, 443 | { 444 | oldPath: ['prop1', 'prop2', 0, 'prop3', 1, 'name'], 445 | newPath: ['prop1', 'prop2', 1, 'prop3', 0, 'name'], 446 | type: 'change', 447 | oldValue: 'name2', 448 | newValue: 'name02' 449 | } 450 | ]); 451 | }); 452 | 453 | it('should compare non object', function () { 454 | var changes = diff({test: 1}, 'test'); 455 | 456 | expect(changes).to.deep.equal([ 457 | { 458 | type: 'change', 459 | oldPath: [], 460 | newPath: [], 461 | oldValue: {test: 1}, 462 | newValue: 'test' 463 | } 464 | ]); 465 | }); 466 | 467 | it('should handle idProp: * to compare arrays as is', function () { 468 | var changes = diff( 469 | [ 470 | {a: 1}, 471 | {a: 2} 472 | ], 473 | [ 474 | {a: 1}, 475 | {a: 2} 476 | ], 477 | {idProp: '*'} 478 | ); 479 | 480 | expect(changes.length).to.equal(0); 481 | 482 | changes = diff( 483 | [ 484 | {a: 1}, 485 | {a: 2} 486 | ], 487 | [ 488 | {a: 2}, 489 | {a: 1}, 490 | {a: 3} 491 | ], 492 | {idProp: '*'} 493 | ); 494 | 495 | expect(changes).to.deep.equal([ 496 | { 497 | oldPath: [0, 'a'], 498 | newPath: [0, 'a'], 499 | type: 'change', 500 | oldValue: 1, 501 | newValue: 2 502 | }, 503 | { 504 | oldPath: [1, 'a'], 505 | newPath: [1, 'a'], 506 | type: 'change', 507 | oldValue: 2, 508 | newValue: 1 509 | }, 510 | { 511 | oldPath: [], 512 | newPath: [], 513 | type: 'add-item', 514 | oldIndex: -1, 515 | curIndex: -1, 516 | newIndex: 2, 517 | newValue: {a: 3} 518 | } 519 | ]); 520 | 521 | changes = diff( 522 | [ 523 | {a: 1}, 524 | {a: 2}, 525 | {a: 3}, 526 | {a: 4} 527 | ], 528 | [ 529 | {a: 1}, 530 | {a: 2} 531 | ], 532 | {idProp: '*'} 533 | ); 534 | 535 | expect(changes).to.deep.equal([ 536 | { 537 | oldPath: [], 538 | newPath: [], 539 | type: 'remove-item', 540 | oldIndex: 2, 541 | curIndex: 2, 542 | newIndex: -1, 543 | oldValue: {a: 3} 544 | }, 545 | { 546 | oldPath: [], 547 | newPath: [], 548 | type: 'remove-item', 549 | oldIndex: 3, 550 | curIndex: 2, 551 | newIndex: -1, 552 | oldValue: {a: 4} 553 | } 554 | ]); 555 | }); 556 | 557 | it('should handle comparators: [] to compare custom class objects', function () { 558 | var cb = sinon.spy(function (a, b, ops) { 559 | expect(a).to.be.instanceOf(Date); 560 | expect(b).to.be.instanceOf(Date); 561 | 562 | expect(ops).to.deep.equal({ 563 | oldPath: ['prop', 'date'], 564 | newPath: ['prop', 'date'] 565 | }); 566 | 567 | return a.toString() === b.toString(); 568 | }); 569 | 570 | var changes = diff( 571 | { 572 | prop: { 573 | test1: {}, 574 | test2: [{}], 575 | date: new Date() 576 | } 577 | }, 578 | { 579 | prop: { 580 | test1: {}, 581 | test2: [{}], 582 | date: new Date() 583 | } 584 | }, 585 | { 586 | comparators: [ 587 | [Date, cb] 588 | ] 589 | } 590 | ); 591 | 592 | expect(changes).to.deep.equal([]); 593 | expect(cb).to.have.callCount(1); 594 | 595 | var nowDate = new Date(); 596 | var prevDate = new Date(); 597 | prevDate.setHours(-1); 598 | 599 | changes = diff( 600 | { 601 | prop: { 602 | test1: 1, 603 | date: nowDate 604 | } 605 | }, 606 | { 607 | prop: { 608 | test1: 2, 609 | date: prevDate 610 | } 611 | }, 612 | { 613 | comparators: [ 614 | [Date, cb] 615 | ] 616 | } 617 | ); 618 | 619 | expect(cb).to.have.callCount(2); 620 | expect(changes).to.deep.equal([ 621 | { 622 | oldPath: ['prop', 'test1'], 623 | newPath: ['prop', 'test1'], 624 | type: 'change', 625 | oldValue: 1, 626 | newValue: 2 627 | }, 628 | { 629 | oldPath: ['prop', 'date'], 630 | newPath: ['prop', 'date'], 631 | type: 'change', 632 | oldValue: nowDate, 633 | newValue: prevDate 634 | } 635 | ]); 636 | }); 637 | 638 | it('should ignore', function () { 639 | var changes = diff( 640 | { 641 | prop: { 642 | test1: { 643 | test11: 1 644 | }, 645 | test2: 1 646 | } 647 | }, 648 | { 649 | prop: { 650 | test1: { 651 | test11: 2 652 | }, 653 | test2: 2 654 | } 655 | }, 656 | { 657 | ignore: function (a, b, ops) { 658 | return ops.oldPath.join('.') === 'prop.test1'; 659 | } 660 | } 661 | ); 662 | 663 | expect(changes).to.deep.equal([ 664 | { 665 | oldPath: ['prop', 'test2'], 666 | newPath: ['prop', 'test2'], 667 | type: 'change', 668 | oldValue: 1, 669 | newValue: 2 670 | } 671 | ]); 672 | 673 | var root = { 674 | prop: { 675 | test2: 2 676 | } 677 | }; 678 | 679 | root.prop.test1 = root; 680 | 681 | changes = diff( 682 | root, 683 | { 684 | prop: { 685 | test1: {}, 686 | test2: 3 687 | } 688 | }, 689 | { 690 | ignore: function (oldObj, newObj, options) { 691 | var parent = root; 692 | 693 | if (options.oldPath.length === 0) return false; 694 | if (parent === oldObj) return true; 695 | 696 | return options.oldPath.slice(0, -1).find(function (prop) { 697 | parent = parent[prop]; 698 | return parent === oldObj; 699 | }); 700 | } 701 | } 702 | ); 703 | 704 | expect(changes).to.deep.equal([ 705 | { 706 | oldPath: ['prop', 'test2'], 707 | newPath: ['prop', 'test2'], 708 | type: 'change', 709 | oldValue: 2, 710 | newValue: 3 711 | } 712 | ]); 713 | }); 714 | }); --------------------------------------------------------------------------------