Skip to content

Commit 393fc1b

Browse files
committed
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 angular#8275 Closes angular#5262
1 parent b3c6c26 commit 393fc1b

File tree

3 files changed

+82
-16
lines changed

3 files changed

+82
-16
lines changed

src/ngAnimate/animate.js

+40-13
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,16 @@
7373
* When the `on` expression value changes and an animation is triggered then each of the elements within
7474
* will all animate without the block being applied to child elements.
7575
*
76+
* ## Are animations run when the application starts?
77+
* No they are not. When an application is bootstrapped Angular will disable animations from running to avoid
78+
* a frenzy of animations from being triggered as soon as the browser has rendered the screen. For this to work,
79+
* Angular will wait for two digest cycles until enabling animations. From there on, any animation-triggering
80+
* layout changes in the application will be handled naturally.
81+
*
82+
* In addition, upon bootstrap, if the routing system or any directives or load remote data (via $http) then Angular
83+
* will automatically extend the wait time to enable animations once **all** of the outbound HTTP requests
84+
* are complete.
85+
*
7686
* <h2>CSS-defined Animations</h2>
7787
* The animate service will automatically apply two CSS classes to the animated element and these two CSS classes
7888
* are designed to contain the start and end CSS styling. Both CSS transitions and keyframe animations are supported
@@ -394,24 +404,41 @@ angular.module('ngAnimate', ['ng'])
394404
return extractElementNode(elm1) == extractElementNode(elm2);
395405
}
396406

397-
$provide.decorator('$animate', ['$delegate', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document',
398-
function($delegate, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document) {
407+
$provide.decorator('$animate',
408+
['$delegate', '$injector', '$sniffer', '$rootElement', '$$asyncCallback', '$rootScope', '$document', '$$templateRequest',
409+
function($delegate, $injector, $sniffer, $rootElement, $$asyncCallback, $rootScope, $document, $$templateRequest) {
399410

400-
var globalAnimationCounter = 0;
401411
$rootElement.data(NG_ANIMATE_STATE, rootAnimateState);
402412

403413
// disable animations during bootstrap, but once we bootstrapped, wait again
404-
// for another digest until enabling animations. The reason why we digest twice
405-
// is because all structural animations (enter, leave and move) all perform a
406-
// post digest operation before animating. If we only wait for a single digest
407-
// to pass then the structural animation would render its animation on page load.
408-
// (which is what we're trying to avoid when the application first boots up.)
409-
$rootScope.$$postDigest(function() {
410-
$rootScope.$$postDigest(function() {
411-
rootAnimateState.running = false;
412-
});
413-
});
414+
// for another digest until enabling animations. Enter, leave and move require
415+
// a follow-up digest so having a watcher here is enough to let both digests pass.
416+
// However, when any directive or view templates are downloaded then we need to
417+
// handle postpone enabling animations until they are fully completed and then...
418+
var watchFn = $rootScope.$watch(
419+
function() { return $$templateRequest.totalPendingRequests; },
420+
function(val, oldVal) {
421+
if (oldVal === 0) {
422+
if (val === 0) {
423+
$rootScope.$$postDigest(onApplicationReady);
424+
}
425+
} else if(val === 0) {
426+
// ...when the template has been downloaded we digest twice again until the
427+
// animations are set to enabled (since enter, leave and move require a
428+
// follow-up).
429+
$rootScope.$$postDigest(function() {
430+
$rootScope.$$postDigest(onApplicationReady);
431+
});
432+
}
433+
}
434+
);
414435

436+
function onApplicationReady() {
437+
rootAnimateState.running = false;
438+
watchFn();
439+
}
440+
441+
var globalAnimationCounter = 0;
415442
var classNameFilter = $animateProvider.classNameFilter();
416443
var isAnimatableClassName = !classNameFilter
417444
? function() { return true; }

test/ng/directive/ngClassSpec.js

-3
Original file line numberDiff line numberDiff line change
@@ -427,9 +427,6 @@ describe('ngClass animations', function() {
427427
inject(function($compile, $rootScope, $browser, $rootElement, $animate, $timeout, $document) {
428428

429429
// Enable animations by triggering the first item in the postDigest queue
430-
digestQueue.shift()();
431-
432-
// wait for the 2nd animation bootstrap digest to pass
433430
$rootScope.$digest();
434431
digestQueue.shift()();
435432

test/ngAnimate/animateSpec.js

+42
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,48 @@ describe("ngAnimate", function() {
3939
});
4040
});
4141

42+
it("should disable animations for two digests until all pending HTTP requests are complete during bootstrap", function() {
43+
var animateSpy = jasmine.createSpy();
44+
module(function($animateProvider, $compileProvider) {
45+
$compileProvider.directive('myRemoteDirective', function() {
46+
return {
47+
templateUrl : 'remote.html'
48+
};
49+
});
50+
$animateProvider.register('.my-structrual-animation', function() {
51+
return {
52+
enter : animateSpy,
53+
leave : animateSpy
54+
};
55+
});
56+
});
57+
inject(function($rootScope, $compile, $animate, $rootElement, $document, $httpBackend) {
58+
59+
$httpBackend.whenGET('remote.html').respond(200, '<strong>content</strong>');
60+
61+
var element = $compile('<div my-remote-directive class="my-structrual-animation">...</div>')($rootScope);
62+
$rootElement.append(element);
63+
jqLite($document[0].body).append($rootElement);
64+
65+
// running this twice just to prove that the dual post digest is run
66+
$rootScope.$digest();
67+
$rootScope.$digest();
68+
69+
$animate.enter(element, $rootElement);
70+
$rootScope.$digest();
71+
72+
expect(animateSpy).not.toHaveBeenCalled();
73+
74+
$httpBackend.flush();
75+
$rootScope.$digest();
76+
77+
$animate.leave(element);
78+
$rootScope.$digest();
79+
80+
expect(animateSpy).toHaveBeenCalled();
81+
});
82+
});
83+
4284

4385
//we use another describe block because the before/after operations below
4486
//are used across all animations tests and we don't want that same behavior

0 commit comments

Comments
 (0)