fix($animate): ensure guarded animations consider AJAX requests upon bootstrap

Prior to this fix when an Angular application is bootstrapped it would only
place an animation guard to prevent animations from running when the application
starts for the first two digest cycles. However, if any controllers or directives,
that are executed upon boostrap, trigger any remote code to be downloaded (via $http)
then the guard does not put that into consideration. This fix now properly addresses
that circumstance and removes the guard once all outbound HTTP requests are complete
when an Angular application is bootstrapped.

Closes #8275
Closes #5262
This commit is contained in:
Matias Niemelä
2014-08-27 20:31:07 -04:00
parent a70e2833ea
commit 4bca4c44b9
3 changed files with 82 additions and 17 deletions

View File

@@ -73,6 +73,16 @@
* When the `on` expression value changes and an animation is triggered then each of the elements within
* will all animate without the block being applied to child elements.
*
* ## Are animations run when the application starts?
* No they are not. When an application is bootstrapped Angular will disable animations from running to avoid
* a frenzy of animations from being triggered as soon as the browser has rendered the screen. For this to work,
* Angular will wait for two digest cycles until enabling animations. From there on, any animation-triggering
* layout changes in the application will trigger animations as normal.
*
* In addition, upon bootstrap, if the routing system or any directives or load remote data (via $http) then Angular
* will automatically extend the wait time to enable animations once **all** of the outbound HTTP requests
* are complete.
*
* <h2>CSS-defined Animations</h2>
* The animate service will automatically apply two CSS classes to the animated element and these two CSS classes
* are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported
@@ -396,24 +406,40 @@ angular.module('ngAnimate', ['ng'])
}
$provide.decorator('$animate',
['$delegate', '$$q', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document',
function($delegate, $$q, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document) {
['$delegate', '$$q', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', '$templateRequest',
function($delegate, $$q, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document, $templateRequest) {
var globalAnimationCounter = 0;
$rootElement.data(NG_ANIMATE_STATE, rootAnimateState);
// disable animations during bootstrap, but once we bootstrapped, wait again
// for another digest until enabling animations. The reason why we digest twice
// is because all structural animations (enter, leave and move) all perform a
// post digest operation before animating. If we only wait for a single digest
// to pass then the structural animation would render its animation on page load.
// (which is what we're trying to avoid when the application first boots up.)
$rootScope.$$postDigest(function() {
$rootScope.$$postDigest(function() {
rootAnimateState.running = false;
});
});
// for another digest until enabling animations. Enter, leave and move require
// a follow-up digest so having a watcher here is enough to let both digests pass.
// However, when any directive or view templates are downloaded then we need to
// handle postpone enabling animations until they are fully completed and then...
var watchFn = $rootScope.$watch(
function() { return $templateRequest.totalPendingRequests; },
function(val, oldVal) {
if (oldVal === 0) {
if (val === 0) {
$rootScope.$$postDigest(onApplicationReady);
}
} else if(val === 0) {
// ...when the template has been downloaded we digest twice again until the
// animations are set to enabled (since enter, leave and move require a
// follow-up).
$rootScope.$$postDigest(function() {
$rootScope.$$postDigest(onApplicationReady);
});
}
}
);
function onApplicationReady() {
rootAnimateState.running = false;
watchFn();
}
var globalAnimationCounter = 0;
var classNameFilter = $animateProvider.classNameFilter();
var isAnimatableClassName = !classNameFilter
? function() { return true; }

View File

@@ -427,10 +427,7 @@ describe('ngClass animations', function() {
});
inject(function($compile, $rootScope, $browser, $rootElement, $animate, $timeout, $document) {
// Enable animations by triggering the first item in the postDigest queue
digestQueue.shift()();
// wait for the 2nd animation bootstrap digest to pass
// Animations are enabled right away since there are no remote HTTP template requests
$rootScope.$digest();
digestQueue.shift()();

View File

@@ -50,6 +50,48 @@ describe("ngAnimate", function() {
});
});
it("should disable animations for two digests until all pending HTTP requests are complete during bootstrap", function() {
var animateSpy = jasmine.createSpy();
module(function($animateProvider, $compileProvider) {
$compileProvider.directive('myRemoteDirective', function() {
return {
templateUrl : 'remote.html'
};
});
$animateProvider.register('.my-structrual-animation', function() {
return {
enter : animateSpy,
leave : animateSpy
};
});
});
inject(function($rootScope, $compile, $animate, $rootElement, $document, $httpBackend) {
$httpBackend.whenGET('remote.html').respond(200, '<strong>content</strong>');
var element = $compile('<div my-remote-directive class="my-structrual-animation">...</div>')($rootScope);
$rootElement.append(element);
jqLite($document[0].body).append($rootElement);
// running this twice just to prove that the dual post digest is run
$rootScope.$digest();
$rootScope.$digest();
$animate.enter(element, $rootElement);
$rootScope.$digest();
expect(animateSpy).not.toHaveBeenCalled();
$httpBackend.flush();
$rootScope.$digest();
$animate.leave(element);
$rootScope.$digest();
expect(animateSpy).toHaveBeenCalled();
});
});
//we use another describe block because the before/after operations below
//are used across all animations tests and we don't want that same behavior