Last active
August 29, 2015 14:10
-
-
Save thatmarvin/cc3500cf8df9be1a04c8 to your computer and use it in GitHub Desktop.
Patches Angular 1.2.x's $interval with fix from 1.3.x
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* The following is stolen from the patch that landed in 1.3.0-beta.13: | |
* https://github.com/angular/angular.js/pull/7999 | |
* | |
* Remove this if upgraded to Angular 1.3.x. | |
*/ | |
(function () { | |
'use strict'; | |
function $$QProvider() { | |
this.$get = ['$browser', '$exceptionHandler', function($browser, $exceptionHandler) { | |
return qFactory(function(callback) { | |
$browser.defer(callback); | |
}, $exceptionHandler); | |
}]; | |
} | |
function $IntervalProvider() { | |
this.$get = ['$rootScope', '$window', '$q', '$$q', | |
function($rootScope, $window, $q, $$q) { | |
var intervals = {}; | |
/** | |
* @memberOf $IntervalProvider | |
* @name $interval | |
* | |
* @description | |
* Angular's wrapper for `window.setInterval`. The `fn` function is executed every `delay` | |
* milliseconds. | |
* | |
* The return value of registering an interval function is a promise. This promise will be | |
* notified upon each tick of the interval, and will be resolved after `count` iterations, or | |
* run indefinitely if `count` is not defined. The value of the notification will be the | |
* number of iterations that have run. | |
* To cancel an interval, call `$interval.cancel(promise)`. | |
* | |
* In tests you can use {@link ngMock.$interval#flush `$interval.flush(millis)`} to | |
* move forward by `millis` milliseconds and trigger any functions scheduled to run in that | |
* time. | |
* | |
* <div class="alert alert-warning"> | |
* **Note**: Intervals created by this service must be explicitly destroyed when you are finished | |
* with them. In particular they are not automatically destroyed when a controller's scope or a | |
* directive's element are destroyed. | |
* You should take this into consideration and make sure to always cancel the interval at the | |
* appropriate moment. See the example below for more details on how and when to do this. | |
* </div> | |
* | |
* @param {function()} fn A function that should be called repeatedly. | |
* @param {number} delay Number of milliseconds between each function call. | |
* @param {number=} [count=0] Number of times to repeat. If not set, or 0, will repeat | |
* indefinitely. | |
* @param {boolean=} [invokeApply=true] If set to `false` skips model dirty checking, otherwise | |
* will invoke `fn` within the {@link ng.$rootScope.Scope#$apply $apply} block. | |
* @returns {promise} A promise which will be notified on each iteration. | |
* | |
* @example | |
* <example module="time"> | |
* <file name="index.html"> | |
* <script> | |
* function Ctrl2($scope,$interval) { | |
* $scope.format = 'M/d/yy h:mm:ss a'; | |
* $scope.blood_1 = 100; | |
* $scope.blood_2 = 120; | |
* | |
* var stop; | |
* $scope.fight = function() { | |
* // Don't start a new fight if we are already fighting | |
* if ( angular.isDefined(stop) ) return; | |
* | |
* stop = $interval(function() { | |
* if ($scope.blood_1 > 0 && $scope.blood_2 > 0) { | |
* $scope.blood_1 = $scope.blood_1 - 3; | |
* $scope.blood_2 = $scope.blood_2 - 4; | |
* } else { | |
* $scope.stopFight(); | |
* } | |
* }, 100); | |
* }; | |
* | |
* $scope.stopFight = function() { | |
* if (angular.isDefined(stop)) { | |
* $interval.cancel(stop); | |
* stop = undefined; | |
* } | |
* }; | |
* | |
* $scope.resetFight = function() { | |
* $scope.blood_1 = 100; | |
* $scope.blood_2 = 120; | |
* } | |
* | |
* $scope.$on('$destroy', function() { | |
* // Make sure that the interval is destroyed too | |
* $scope.stopFight(); | |
* }); | |
* } | |
* | |
* angular.module('time', []) | |
* // Register the 'myCurrentTime' directive factory method. | |
* // We inject $interval and dateFilter service since the factory method is DI. | |
* .directive('myCurrentTime', function($interval, dateFilter) { | |
* // return the directive link function. (compile function not needed) | |
* return function(scope, element, attrs) { | |
* var format, // date format | |
* stopTime; // so that we can cancel the time updates | |
* | |
* // used to update the UI | |
* function updateTime() { | |
* element.text(dateFilter(new Date(), format)); | |
* } | |
* | |
* // watch the expression, and update the UI on change. | |
* scope.$watch(attrs.myCurrentTime, function(value) { | |
* format = value; | |
* updateTime(); | |
* }); | |
* | |
* stopTime = $interval(updateTime, 1000); | |
* | |
* // listen on DOM destroy (removal) event, and cancel the next UI update | |
* // to prevent updating time ofter the DOM element was removed. | |
* element.on('$destroy', function() { | |
* $interval.cancel(stopTime); | |
* }); | |
* } | |
* }); | |
* </script> | |
* | |
* <div> | |
* <div ng-controller="Ctrl2"> | |
* Date format: <input ng-model="format"> <hr/> | |
* Current time is: <span my-current-time="format"></span> | |
* <hr/> | |
* Blood 1 : <font color='red'>{{blood_1}}</font> | |
* Blood 2 : <font color='red'>{{blood_2}}</font> | |
* <button type="button" data-ng-click="fight()">Fight</button> | |
* <button type="button" data-ng-click="stopFight()">StopFight</button> | |
* <button type="button" data-ng-click="resetFight()">resetFight</button> | |
* </div> | |
* </div> | |
* | |
* </file> | |
* </example> | |
*/ | |
function interval(fn, delay, count, invokeApply) { | |
var setInterval = $window.setInterval, | |
clearInterval = $window.clearInterval, | |
iteration = 0, | |
skipApply = (isDefined(invokeApply) && !invokeApply), | |
deferred = (skipApply ? $$q : $q).defer(), | |
promise = deferred.promise; | |
count = isDefined(count) ? count : 0; | |
promise.then(null, null, fn); | |
promise.$$intervalId = setInterval(function tick() { | |
deferred.notify(iteration++); | |
if (count > 0 && iteration >= count) { | |
deferred.resolve(iteration); | |
clearInterval(promise.$$intervalId); | |
delete intervals[promise.$$intervalId]; | |
} | |
if (!skipApply) $rootScope.$apply(); | |
}, delay); | |
intervals[promise.$$intervalId] = deferred; | |
return promise; | |
} | |
/** | |
* @ngdoc method | |
* @name $interval#cancel | |
* | |
* @description | |
* Cancels a task associated with the `promise`. | |
* | |
* @param {promise} promise returned by the `$interval` function. | |
* @returns {boolean} Returns `true` if the task was successfully canceled. | |
*/ | |
interval.cancel = function(promise) { | |
if (promise && promise.$$intervalId in intervals) { | |
intervals[promise.$$intervalId].reject('canceled'); | |
clearInterval(promise.$$intervalId); | |
delete intervals[promise.$$intervalId]; | |
return true; | |
} return false; | |
}; | |
return interval; | |
}]; | |
} | |
// Define these for qFactory | |
var isDefined = angular.isDefined; | |
var isFunction = angular.isFunction; | |
var isArray = angular.isArray; | |
var forEach = angular.forEach; | |
/** | |
* @memberOf intervalProviderPatch | |
* | |
* Constructs a promise manager. | |
* | |
* @param {function(Function)} nextTick Function for executing functions in the next turn. | |
* @param {function(...*)} exceptionHandler Function into which unexpected exceptions are passed for | |
* debugging purposes. | |
* @returns {object} Promise manager. | |
*/ | |
function qFactory(nextTick, exceptionHandler) { | |
/** | |
* @ngdoc method | |
* @name $q#defer | |
* @kind function | |
* | |
* @description | |
* Creates a `Deferred` object which represents a task which will finish in the future. | |
* | |
* @returns {Deferred} Returns a new instance of deferred. | |
*/ | |
var defer = function() { | |
var pending = [], | |
value, deferred; | |
deferred = { | |
resolve: function(val) { | |
if (pending) { | |
var callbacks = pending; | |
pending = undefined; | |
value = ref(val); | |
if (callbacks.length) { | |
nextTick(function() { | |
var callback; | |
for (var i = 0, ii = callbacks.length; i < ii; i++) { | |
callback = callbacks[i]; | |
value.then(callback[0], callback[1], callback[2]); | |
} | |
}); | |
} | |
} | |
}, | |
reject: function(reason) { | |
deferred.resolve(createInternalRejectedPromise(reason)); | |
}, | |
notify: function(progress) { | |
if (pending) { | |
var callbacks = pending; | |
if (pending.length) { | |
nextTick(function() { | |
var callback; | |
for (var i = 0, ii = callbacks.length; i < ii; i++) { | |
callback = callbacks[i]; | |
callback[2](progress); | |
} | |
}); | |
} | |
} | |
}, | |
promise: { | |
then: function(callback, errback, progressback) { | |
var result = defer(); | |
var wrappedCallback = function(value) { | |
try { | |
result.resolve((isFunction(callback) ? callback : defaultCallback)(value)); | |
} catch(e) { | |
result.reject(e); | |
exceptionHandler(e); | |
} | |
}; | |
var wrappedErrback = function(reason) { | |
try { | |
result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); | |
} catch(e) { | |
result.reject(e); | |
exceptionHandler(e); | |
} | |
}; | |
var wrappedProgressback = function(progress) { | |
try { | |
result.notify((isFunction(progressback) ? progressback : defaultCallback)(progress)); | |
} catch(e) { | |
exceptionHandler(e); | |
} | |
}; | |
if (pending) { | |
pending.push([wrappedCallback, wrappedErrback, wrappedProgressback]); | |
} else { | |
value.then(wrappedCallback, wrappedErrback, wrappedProgressback); | |
} | |
return result.promise; | |
}, | |
"catch": function(callback) { | |
return this.then(null, callback); | |
}, | |
"finally": function(callback) { | |
function makePromise(value, resolved) { | |
var result = defer(); | |
if (resolved) { | |
result.resolve(value); | |
} else { | |
result.reject(value); | |
} | |
return result.promise; | |
} | |
function handleCallback(value, isResolved) { | |
var callbackOutput = null; | |
try { | |
callbackOutput = (callback ||defaultCallback)(); | |
} catch(e) { | |
return makePromise(e, false); | |
} | |
if (callbackOutput && isFunction(callbackOutput.then)) { | |
return callbackOutput.then(function() { | |
return makePromise(value, isResolved); | |
}, function(error) { | |
return makePromise(error, false); | |
}); | |
} else { | |
return makePromise(value, isResolved); | |
} | |
} | |
return this.then(function(value) { | |
return handleCallback(value, true); | |
}, function(error) { | |
return handleCallback(error, false); | |
}); | |
} | |
} | |
}; | |
return deferred; | |
}; | |
var ref = function(value) { | |
if (value && isFunction(value.then)) return value; | |
return { | |
then: function(callback) { | |
var result = defer(); | |
nextTick(function() { | |
result.resolve(callback(value)); | |
}); | |
return result.promise; | |
} | |
}; | |
}; | |
/** | |
* @ngdoc method | |
* @name $q#reject | |
* @kind function | |
* | |
* @description | |
* Creates a promise that is resolved as rejected with the specified `reason`. This api should be | |
* used to forward rejection in a chain of promises. If you are dealing with the last promise in | |
* a promise chain, you don't need to worry about it. | |
* | |
* When comparing deferreds/promises to the familiar behavior of try/catch/throw, think of | |
* `reject` as the `throw` keyword in JavaScript. This also means that if you "catch" an error via | |
* a promise error callback and you want to forward the error to the promise derived from the | |
* current promise, you have to "rethrow" the error by returning a rejection constructed via | |
* `reject`. | |
* | |
* ```js | |
* promiseB = promiseA.then(function(result) { | |
* // success: do something and resolve promiseB | |
* // with the old or a new result | |
* return result; | |
* }, function(reason) { | |
* // error: handle the error if possible and | |
* // resolve promiseB with newPromiseOrValue, | |
* // otherwise forward the rejection to promiseB | |
* if (canHandle(reason)) { | |
* // handle the error and recover | |
* return newPromiseOrValue; | |
* } | |
* return $q.reject(reason); | |
* }); | |
* ``` | |
* | |
* @param {*} reason Constant, message, exception or an object representing the rejection reason. | |
* @returns {Promise} Returns a promise that was already resolved as rejected with the `reason`. | |
*/ | |
var reject = function(reason) { | |
var result = defer(); | |
result.reject(reason); | |
return result.promise; | |
}; | |
var createInternalRejectedPromise = function(reason) { | |
return { | |
then: function(callback, errback) { | |
var result = defer(); | |
nextTick(function() { | |
try { | |
result.resolve((isFunction(errback) ? errback : defaultErrback)(reason)); | |
} catch(e) { | |
result.reject(e); | |
exceptionHandler(e); | |
} | |
}); | |
return result.promise; | |
} | |
}; | |
}; | |
/** | |
* @ngdoc method | |
* @name $q#when | |
* @kind function | |
* | |
* @description | |
* Wraps an object that might be a value or a (3rd party) then-able promise into a $q promise. | |
* This is useful when you are dealing with an object that might or might not be a promise, or if | |
* the promise comes from a source that can't be trusted. | |
* | |
* @param {*} value Value or a promise | |
* @returns {Promise} Returns a promise of the passed value or promise | |
*/ | |
var when = function(value, callback, errback, progressback) { | |
var result = defer(), | |
done; | |
var wrappedCallback = function(value) { | |
try { | |
return (isFunction(callback) ? callback : defaultCallback)(value); | |
} catch (e) { | |
exceptionHandler(e); | |
return reject(e); | |
} | |
}; | |
var wrappedErrback = function(reason) { | |
try { | |
return (isFunction(errback) ? errback : defaultErrback)(reason); | |
} catch (e) { | |
exceptionHandler(e); | |
return reject(e); | |
} | |
}; | |
var wrappedProgressback = function(progress) { | |
try { | |
return (isFunction(progressback) ? progressback : defaultCallback)(progress); | |
} catch (e) { | |
exceptionHandler(e); | |
} | |
}; | |
nextTick(function() { | |
ref(value).then(function(value) { | |
if (done) return; | |
done = true; | |
result.resolve(ref(value).then(wrappedCallback, wrappedErrback, wrappedProgressback)); | |
}, function(reason) { | |
if (done) return; | |
done = true; | |
result.resolve(wrappedErrback(reason)); | |
}, function(progress) { | |
if (done) return; | |
result.notify(wrappedProgressback(progress)); | |
}); | |
}); | |
return result.promise; | |
}; | |
function defaultCallback(value) { | |
return value; | |
} | |
function defaultErrback(reason) { | |
return reject(reason); | |
} | |
/** | |
* @ngdoc method | |
* @name $q#all | |
* @kind function | |
* | |
* @description | |
* Combines multiple promises into a single promise that is resolved when all of the input | |
* promises are resolved. | |
* | |
* @param {Array.<Promise>|Object.<Promise>} promises An array or hash of promises. | |
* @returns {Promise} Returns a single promise that will be resolved with an array/hash of values, | |
* each value corresponding to the promise at the same index/key in the `promises` array/hash. | |
* If any of the promises is resolved with a rejection, this resulting promise will be rejected | |
* with the same rejection value. | |
*/ | |
function all(promises) { | |
var deferred = defer(), | |
counter = 0, | |
results = isArray(promises) ? [] : {}; | |
forEach(promises, function(promise, key) { | |
counter++; | |
ref(promise).then(function(value) { | |
if (results.hasOwnProperty(key)) return; | |
results[key] = value; | |
if (!(--counter)) deferred.resolve(results); | |
}, function(reason) { | |
if (results.hasOwnProperty(key)) return; | |
deferred.reject(reason); | |
}); | |
}); | |
if (counter === 0) { | |
deferred.resolve(results); | |
} | |
return deferred.promise; | |
} | |
return { | |
defer: defer, | |
reject: reject, | |
when: when, | |
all: all | |
}; | |
} | |
angular.module('intervalProviderPatch', []).config(function ($provide) { | |
$provide.provider('$$q', $$QProvider); | |
$provide.provider('$interval', $IntervalProvider); | |
}); | |
})(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment