-
Notifications
You must be signed in to change notification settings - Fork 27.4k
feat(shutdown): Add the ability for an app to shutdown #8005
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1692,7 +1692,7 @@ function bootstrap(element, modules, config) { | |
|
||
modules = modules || []; | ||
modules.unshift(['$provide', function($provide) { | ||
$provide.value('$rootElement', element); | ||
$provide.provider('$rootElement', rootElementProviderFactory(element)); | ||
}]); | ||
|
||
if (config.debugInfoEnabled) { | ||
|
@@ -1772,6 +1772,48 @@ function getTestability(rootElement) { | |
return injector.get('$$testability'); | ||
} | ||
|
||
function rootElementProviderFactory(rootElement) { | ||
return ['$shutdownProvider', function($shutdownProvider) { | ||
$shutdownProvider.register(function() { | ||
if (rootElement.dealoc) { | ||
rootElement.dealoc(); | ||
} else { | ||
rootElement.find('*').removeData(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not a common case for sure, but this would also "clean" inside There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if there is an outer and an inner app, and the outer app is shutdown (remember that this will trigger a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, it shouldn't. These are independent apps, so destroying the outer, shouldn't destroy the inner. To be clear, this is what I am talking about: <body app1>
Hello from {{ 'app 1' }}!
<div ng-non-bindable>
<div app2>
Hello from {{ 'app 2' }}!
</div>
</div>
</body> angular.module('app1', []);
angular.module('app2', []);
angular.bootstrap(document.querySelector('[app1]'), ['app1']);
angular.bootstrap(document.querySelector('[app2]'), ['app2']); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, this is currently not possible, as in cases like this we do not keep track of what elements belong to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is possible to ignore anything under There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe something like this (which is not super efficient, but shouldn't really matter, when shutting down): var unvisitedNodes = [$rootElement[0]];
var foundNodes = [$rootElement[0]];
var node;
while ((node = unvisitedNodes.pop())) {
var newNodes = Array.prototype.filter.call(node.children, function (child) {
return directiveNormalize(nodeName_(child)) !== 'ngNonBindable';
});
Array.prototype.push.apply(unvisitedNodes, newNodes);
Array.prototype.push.apply(foundNodes, newNodes);
}
jqLite.cleanData(foundNodes); There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gkalpak if we want to support nested ng-apps in ng-non-bindable containers perhaps we should add at least basic tests verifying that it works? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Perhaps. But basic tests wouldn't help much (I mean even if basic tests worked, some less basic code could still be broken). The question is whether we want to break this usecase or not (I still wouldn't). |
||
rootElement.removeData(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not enough as it won't remove event listeners. How about: jqLite.cleanData(rootElement.find('*'));
jqLite.cleanData(rootElement); or something analogous to @gkalpak's code but using This, BTW, made me notice that jqLite's implementation of |
||
} | ||
}); | ||
this.$get = function() { | ||
return rootElement; | ||
}; | ||
}]; | ||
} | ||
|
||
function $ShutdownProvider() { | ||
var fns = []; | ||
this.$get = function() { | ||
return function() { | ||
while (fns.length) { | ||
var fn = fns.shift(); | ||
fn(); | ||
} | ||
}; | ||
}; | ||
|
||
this.register = function(fn) { | ||
fns.push(fn); | ||
}; | ||
} | ||
|
||
function shutdown(element) { | ||
var injector; | ||
|
||
injector = angular.element(element).injector(); | ||
if (!injector) { | ||
throw ngMinErr('shtdwn', 'Element not part of an app'); | ||
} | ||
injector.get('$shutdown')(); | ||
} | ||
|
||
var SNAKE_CASE_REGEXP = /[A-Z]/g; | ||
function snake_case(name, separator) { | ||
separator = separator || '_'; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,7 +28,11 @@ function Browser(window, document, $log, $sniffer) { | |
history = window.history, | ||
setTimeout = window.setTimeout, | ||
clearTimeout = window.clearTimeout, | ||
pendingDeferIds = {}; | ||
setInterval = window.setInterval, | ||
clearInterval = window.clearInterval, | ||
pendingDeferIds = {}, | ||
currentIntervalIds = {}, | ||
active = true; | ||
|
||
self.isMock = false; | ||
|
||
|
@@ -79,6 +83,19 @@ function Browser(window, document, $log, $sniffer) { | |
} | ||
}; | ||
|
||
self.shutdown = function() { | ||
active = false; | ||
forEach(currentIntervalIds, function(ignore, intervalId) { | ||
delete currentIntervalIds[intervalId]; | ||
clearInterval(+intervalId); | ||
}); | ||
forEach(pendingDeferIds, function(ignore, timeoutId) { | ||
delete pendingDeferIds[timeoutId]; | ||
clearTimeout(+timeoutId); | ||
}); | ||
jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange); | ||
}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I know it wasn't done before either, but wouldn't it make sense to clear timeouts as well? |
||
|
||
////////////////////////////////////////////////////////////// | ||
// URL API | ||
////////////////////////////////////////////////////////////// | ||
|
@@ -270,16 +287,6 @@ function Browser(window, document, $log, $sniffer) { | |
return callback; | ||
}; | ||
|
||
/** | ||
* @private | ||
* Remove popstate and hashchange handler from window. | ||
* | ||
* NOTE: this api is intended for use only by $rootScope. | ||
*/ | ||
self.$$applicationDestroyed = function() { | ||
jqLite(window).off('hashchange popstate', cacheStateAndFireUrlChange); | ||
}; | ||
|
||
/** | ||
* Checks whether the url has changed outside of Angular. | ||
* Needs to be exported to be able to check for changes that have been done in sync, | ||
|
@@ -321,12 +328,16 @@ function Browser(window, document, $log, $sniffer) { | |
*/ | ||
self.defer = function(fn, delay) { | ||
var timeoutId; | ||
outstandingRequestCount++; | ||
timeoutId = setTimeout(function() { | ||
delete pendingDeferIds[timeoutId]; | ||
completeOutstandingRequest(fn); | ||
}, delay || 0); | ||
pendingDeferIds[timeoutId] = true; | ||
if (active) { | ||
outstandingRequestCount++; | ||
timeoutId = setTimeout(function() { | ||
delete pendingDeferIds[timeoutId]; | ||
completeOutstandingRequest(fn); | ||
}, delay || 0); | ||
pendingDeferIds[timeoutId] = true; | ||
} else { | ||
timeoutId = 0; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What's the purpose of this line? |
||
} | ||
return timeoutId; | ||
}; | ||
|
||
|
@@ -351,11 +362,57 @@ function Browser(window, document, $log, $sniffer) { | |
return false; | ||
}; | ||
|
||
|
||
/** | ||
* @name $browser#interval | ||
* @param {function()} fn A function, whose execution should be executed. | ||
* @param {number=} interval Interval in milliseconds on how often to execute the function. | ||
* @returns {*} IntervalId that can be used to cancel the task via `$browser.interval.cancel()`. | ||
* | ||
* @description | ||
* Executes a fn asynchronously via `setInterval(fn, interval)`. | ||
* | ||
*/ | ||
self.interval = function(fn, interval) { | ||
var intervalId; | ||
if (active) { | ||
intervalId = setInterval(fn, interval); | ||
currentIntervalIds[intervalId] = true; | ||
} else { | ||
intervalId = 0; | ||
} | ||
return intervalId; | ||
}; | ||
|
||
|
||
/** | ||
* @name $browser#interval.cancel | ||
* | ||
* @description | ||
* Cancels an interval task identified with `intervalId`. | ||
* | ||
* @param {*} intervalId Token returned by the `$browser.interval` function. | ||
* @returns {boolean} Returns `true` if the task was successfully canceled, and | ||
* `false` if the task was already canceled. | ||
*/ | ||
self.interval.cancel = function(intervalId) { | ||
if (currentIntervalIds[intervalId]) { | ||
delete currentIntervalIds[intervalId]; | ||
clearInterval(intervalId); | ||
return true; | ||
} | ||
return false; | ||
}; | ||
} | ||
|
||
function $BrowserProvider() { | ||
function $BrowserProvider($shutdownProvider) { | ||
var browser; | ||
|
||
$shutdownProvider.register(function() { if (browser) { browser.shutdown(); } }); | ||
this.$get = ['$window', '$log', '$sniffer', '$document', | ||
function($window, $log, $sniffer, $document) { | ||
return new Browser($window, $document, $log, $sniffer); | ||
return browser = new Browser($window, $document, $log, $sniffer); | ||
}]; | ||
} | ||
|
||
$BrowserProvider.$inject = ['$shutdownProvider']; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -67,11 +67,13 @@ | |
* They also provide event emission/broadcast and subscription facility. See the | ||
* {@link guide/scope developer guide on scopes}. | ||
*/ | ||
function $RootScopeProvider() { | ||
$RootScopeProvider.$inject = ['$shutdownProvider']; | ||
function $RootScopeProvider($shutdownProvider) { | ||
var TTL = 10; | ||
var $rootScopeMinErr = minErr('$rootScope'); | ||
var lastDirtyWatch = null; | ||
var applyAsyncId = null; | ||
var $rootScope; | ||
|
||
this.digestTtl = function(value) { | ||
if (arguments.length) { | ||
|
@@ -94,8 +96,10 @@ function $RootScopeProvider() { | |
return ChildScope; | ||
} | ||
|
||
this.$get = ['$exceptionHandler', '$parse', '$browser', | ||
function($exceptionHandler, $parse, $browser) { | ||
$shutdownProvider.register(function() { if ($rootScope) { $rootScope.$destroy(); } }); | ||
|
||
this.$get = ['$exceptionHandler', '$parse', '$browser', '$shutdown', | ||
function($exceptionHandler, $parse, $browser, $shutdown) { | ||
|
||
function destroyChildScope($event) { | ||
$event.currentScope.$$destroyed = true; | ||
|
@@ -907,8 +911,7 @@ function $RootScopeProvider() { | |
this.$$destroyed = true; | ||
|
||
if (this === $rootScope) { | ||
//Remove handlers attached to window when $rootScope is removed | ||
$browser.$$applicationDestroyed(); | ||
$shutdown(); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I am not sure we need this, now that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @gkalpak What do you mean? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Well, it's been a while, but what I think I meant that this was a poor man's It doesn't seem too harmful though, so we might as well keep it to avoid the breaking change. |
||
} | ||
|
||
incrementWatchersCount(this, -this.$$watchersCount); | ||
|
@@ -1308,7 +1311,7 @@ function $RootScopeProvider() { | |
} | ||
}; | ||
|
||
var $rootScope = new Scope(); | ||
$rootScope = new Scope(); | ||
|
||
//The internal queues. Expose them on the $rootScope for debugging/testing purposes. | ||
var asyncQueue = $rootScope.$$asyncQueue = []; | ||
|
@@ -1374,3 +1377,5 @@ function $RootScopeProvider() { | |
} | ||
}]; | ||
} | ||
|
||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Under what circumstances can
rootElement.dealoc
be defined?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
oh, this is just proof of how old this PR is, this was removed with 9c5b407
WDYT about changing this to
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is basically the same as the
else
branch below, so we only need one branch.