Skip to content

Instantly share code, notes, and snippets.

@katowulf
Last active July 28, 2016 18:42
Show Gist options
  • Star 9 You must be signed in to star a gist
  • Fork 5 You must be signed in to fork a gist
  • Save katowulf/6193808 to your computer and use it in GitHub Desktop.
Save katowulf/6193808 to your computer and use it in GitHub Desktop.
Diff services for monitoring objects and arrays, and creating changelists in Angular.js
/**
* A diff utility that compares arrays and returns a list of added, removed, and updated items
*
* Returns an object with two methods:
* diff: do a one-time diff of two arrays
* watch: observe a variable on scope and report any changes to a callback
*
* Invoking the factory is done like so:
* <code>
* function(listDiff) {
* // watch an object
* var watcher = listDiff();
*
* // a function that creates a unique hash from each object (e.g. it's key or id)
* function hashFn(obj) { return obj.id; }
*
* // a function to read the changeList when a change occurs
* function processChanges(changeList) { console.log('changes detected', changeList); }
*
* // attach a listener to the watcher
* listDiff.watch($scope, 'variableName', processChanges, hashFn);
*
* // directly process two arrays
* var changeList = watcher.diff( oldArray, newArray, hashFn );
* }
* </code>
*/
appUtils.factory('listDiff', [function() {
function _map(list, hashFn) {
var out = {};
_.each(list, function(x) {
out[ hashFn(x) ] = x;
});
return out;
}
function diff(old, curr, hashFn) {
var out = {
count: 0,
added: [],
removed: []
};
if( !old && curr ) {
out.added = curr.slice(0);
}
else if( !curr && old ) {
out.removed = old.slice(0);
}
else if( hashFn ) {
//todo this could be more efficient (it's possibly worse than o(n) right now)
var oldMap = _map(old, hashFn), newMap = _map(curr, hashFn);
out.removed = _.filter(oldMap, function(x,k) { return !_.has(newMap, k); });
out.added = _.filter(newMap, function(x,k) { return !_.has(oldMap, k); });
}
else {
// these don't work for angularFire because it returns different objects in each set and === is used to compare
out.removed = _.difference(old, curr);
out.added = _.difference(curr, old);
}
out.count = out.removed.length + out.added.length;
return out;
}
return {
diff: diff,
watch: function($scope, varName, callback, hashFn) {
//todo add a dispose method
return $scope.$watch(varName, function(newVal, oldVal) {
var out = diff(oldVal, newVal, hashFn);
// console.log('listDiff', out);
if( out.count ) {
callback(out);
}
}, true);
}
};
}]);
/**
* A diff utility that watches a variable in scope, which is assumed to be an object.
* Any time it changes, the objects are compared (only one level deep) and a list
* of added, removed, and updated elements is returned to anybody listening on watch().
*
* Invoking the factory is done like so:
* <code>
* function(treeDiff) {
* // watch an object
* var watcher = treeDiff($scope, 'variableName');
*
* // attach a listener to the watcher
* watcher.watch(function(changeList) {
* console.log('changes detected', changeList);
* });
*
* // manually process an object
* var changeList = watcher.diff( oldObject, newObject );
* }
* </code>
*
* The return value of that call contains an object with three methods:
* diff: do a one-time diff of two arrays
* watch: add an observer to be notified of any changes
* orig: when using watch() this method returns the old object before it changed
*/
appUtils.factory('treeDiff', function() {
return function($scope, variableName) {
var orig = copy($scope[variableName]);
var listeners = [];
function copy(orig) {
var cloned = {};
orig && _.each(orig, function(v,k) {
cloned[k] = _.isArray(v)? v.slice(0) : (_.isObject(v)? _.clone(v) : v);
});
return cloned;
}
function update(newVal) {
newVal || (newVal = {});
var changes = diff(orig, newVal);
if( changes.count ) {
notify(changes, newVal, orig);
orig = copy(newVal);
}
}
function diff(orig, updated) {
var newKeys = _.keys(updated), oldKeys = _.keys(orig);
var removed = _.difference(oldKeys, newKeys);
var added = _.difference(newKeys, oldKeys);
var union = _.union(newKeys, oldKeys);
var changes = {
count: removed.length+added.length,
added: added,
removed: removed,
updated: []
};
_.each(union, function(k) {
if( !_.isEqual(orig[k], updated[k]) ) {
changes.updated.push(k);
changes.count++;
}
});
return changes;
}
function notify(changes, newVal, orig) {
_.each(listeners, function(fn) { fn(changes, newVal, orig); });
}
$scope.$watch(variableName, update, true);
return {
orig: function() {
return orig;
},
diff: diff,
watch: function(callback) {
listeners.push(callback);
}
}
}
});
@amcdnl
Copy link

amcdnl commented Sep 11, 2014

Forked and removed underscore in favor of ES5 Array helpers : https://gist.github.com/amcdnl/df3344de6ae9ed400a56

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment