Skip to content

Instantly share code, notes, and snippets.

@cameron
Created August 30, 2014 00:54
Show Gist options
  • Save cameron/51604827fb7ba94b744b to your computer and use it in GitHub Desktop.
Save cameron/51604827fb7ba94b744b to your computer and use it in GitHub Desktop.
angular's $scope extensions
// Hyperscope
// Upgrading $scope.$watch, making some new friends
angular.module('hyperscope', [])
// service for extending $scope objects
.factory('Hyperscope', function($parse, $rootScope){
var vanillaAdditions = {
// Apply only if angular is not already doing so.
$applyWithCare: function(fn){
_.contains(['$apply','$digest'], this.$root.$$phase) ?
fn() :
this.$apply(fn);
},
// Like $watch, but assigns the value of `expression` to `this[`alias`]`
$alias: function(expr, alias, objectEquality){
return this.$watch(expr, function(val, old, scope){
scope[alias] = val;
}, objectEquality);
},
$expr: function(expr, objectEquality){
return this.$watch(expr, function(){}, objectEquality);
}
};
var upgradeable = {
// Like $watch, but only runs when the watch expression's new value
// is truthy.
$if: function(expr, fn, objectEquality){
return this.$watch(expr, function(is, was, scope){
if(!is) return true;
return fn(is, was, scope);
}, objectEquality);
},
};
/* Upgrade watchHelpers
* - accept an angular expression as a callback
* - $watch(...).once() to deregister after one invocation
* - $watch(...).times(n) to deregister after N invocations
*
* MIND-BLOW: $watch supports this by default, but it's undocumented
* and a comment in the source suggests it may disappear. Leaving it
* here for now, but worth noting that it's a native feature.
*/
var upgradeWatchHelper = function(obj, funcName){
var unwrappedWatcher = obj[funcName];
// keep the original around: eg., $watch -> $$watch
obj['$' + funcName] = unwrappedWatcher;
obj[funcName] = function(expr, _fnOrExpr, _objectEq, _exprCtx){
var objectEq = _objectEq,
exprCtx = _exprCtx || this; // object against which fnOrExpr is evald
// TODO dont cop out on this case.
// Handle the $watch(fn) form.
if(typeof expr == "function")
return unwrappedWatcher.call(this, expr, _fnOrExpr);
// handle the short function signature:
// $watch(expr, expr, exprCtx)
if(typeof objectEq == 'object')
exprCtx = objectEq, objectEq = false;
// xform callback expression to function
var fn = typeof _fnOrExpr == 'string'
? function(newV, oldV, scope){ return $parse(_fnOrExpr)(exprCtx);}
: _fnOrExpr;
// Wrap the callback so we can capture and use its return value
// to determine whether or not to count that invocation against
// the max invocations.
var deregister;
var maxInvocations = 0;
var numInvocations = 0;
var debug = angular.noop;
var callback = function(n, o, s){
// If `fn` returns true, do not count this invocation against the count.
// This is useful for implementing $if(...).once(), which should not be
// deregistered if its listener is triggered by a falsy expression value.
var countIt = !fn(n, o, s);
countIt && numInvocations++;
debug(n, o, s);
countIt;
if(maxInvocations && numInvocations >= maxInvocations){
deregister();
}
};
deregister = unwrappedWatcher.call(this, expr, callback, objectEq);
deregister.once = function(){
maxInvocations = 1;
return deregister;
};
deregister.times = function(n){
maxInvocations = n;
return deregister;
}
deregister.debug = function(args){
debug = function(n, o, s){
console.log(
s.id,
expr,
typeof _fnOrExpr == 'string' ?
_fnOrExpr :
'fn',
n, o
);
}
return deregister;
};
return deregister;
};
};
_.keys(upgradeable).map(function(key){
upgradeWatchHelper(upgradeable, key);
});
var extensions = _.extend({}, _.extend(upgradeable, vanillaAdditions));
return function(scope){
_.extend(scope, extensions);
upgradeWatchHelper(scope, '$watch');
return scope;
};
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment