Skip to content
This repository was archived by the owner on Apr 12, 2024. It is now read-only.

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

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions angularFiles.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ var angularFiles = {
'src/ng/sanitizeUri.js',
'src/ng/sce.js',
'src/ng/sniffer.js',
'src/ng/templateRequest.js',
'src/ng/timeout.js',
'src/ng/urlUtils.js',
'src/ng/window.js',
Expand Down
2 changes: 2 additions & 0 deletions src/AngularPublic.js
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@
$SceDelegateProvider,
$SnifferProvider,
$TemplateCacheProvider,
$TemplateRequestProvider,
$TimeoutProvider,
$$RAFProvider,
$$AsyncCallbackProvider,
Expand Down Expand Up @@ -227,6 +228,7 @@ function publishExternalAPI(angular){
$sceDelegate: $SceDelegateProvider,
$sniffer: $SnifferProvider,
$templateCache: $TemplateCacheProvider,
$templateRequest: $TemplateRequestProvider,
$timeout: $TimeoutProvider,
$window: $WindowProvider,
$$rAF: $$RAFProvider,
Expand Down
11 changes: 4 additions & 7 deletions src/ng/compile.js
Original file line number Diff line number Diff line change
Expand Up @@ -669,9 +669,9 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
};

this.$get = [
'$injector', '$interpolate', '$exceptionHandler', '$http', '$templateCache', '$parse',
'$injector', '$interpolate', '$exceptionHandler', '$templateRequest', '$parse',
'$controller', '$rootScope', '$document', '$sce', '$animate', '$$sanitizeUri',
function($injector, $interpolate, $exceptionHandler, $http, $templateCache, $parse,
function($injector, $interpolate, $exceptionHandler, $templateRequest, $parse,
$controller, $rootScope, $document, $sce, $animate, $$sanitizeUri) {

var Attributes = function(element, attributesToCopy) {
Expand Down Expand Up @@ -1827,8 +1827,8 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {

$compileNode.empty();

$http.get($sce.getTrustedResourceUrl(templateUrl), {cache: $templateCache}).
success(function(content) {
$templateRequest($sce.getTrustedResourceUrl(templateUrl))
.then(function(content) {
var compileNode, tempTemplateAttrs, $template, childBoundTranscludeFn;

content = denormalizeTemplate(content);
Expand Down Expand Up @@ -1903,9 +1903,6 @@ function $CompileProvider($provide, $$sanitizeUriProvider) {
childBoundTranscludeFn);
}
linkQueue = null;
}).
error(function(response, code, headers, config) {
throw $compileMinErr('tpload', 'Failed to load template: {0}', config.url);
});

return function delayedNodeLinkFn(ignoreChildLinkFn, scope, node, rootElement, boundTranscludeFn) {
Expand Down
10 changes: 6 additions & 4 deletions src/ng/directive/ngInclude.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,8 +169,8 @@
* @description
* Emitted when a template HTTP request yields an erronous response (status < 200 || status > 299)
*/
var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce',
function($http, $templateCache, $anchorScroll, $animate, $sce) {
var ngIncludeDirective = ['$templateRequest', '$anchorScroll', '$animate', '$sce',
function($templateRequest, $anchorScroll, $animate, $sce) {
return {
restrict: 'ECA',
priority: 400,
Expand Down Expand Up @@ -215,7 +215,9 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate'
var thisChangeId = ++changeCounter;

if (src) {
$http.get(src, {cache: $templateCache}).success(function(response) {
//set the 2nd param to true to ignore the template request error so that the inner
//contents and scope can be cleaned up.
$templateRequest(src, true).then(function(response) {
if (thisChangeId !== changeCounter) return;
var newScope = scope.$new();
ctrl.template = response;
Expand All @@ -236,7 +238,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate'

currentScope.$emit('$includeContentLoaded');
scope.$eval(onloadExp);
}).error(function() {
}, function() {
if (thisChangeId === changeCounter) {
cleanupLastIncludeContent();
scope.$emit('$includeContentError');
Expand Down
53 changes: 53 additions & 0 deletions src/ng/templateRequest.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

var $compileMinErr = minErr('$compile');

/**
* @ngdoc service
* @name $templateRequest
*
* @description
* The `$templateRequest` service downloads the provided template using `$http` and, upon success,
* stores the contents inside of `$templateCache`. If the HTTP request fails or the response data
* of the HTTP request is empty then a `$compile` error will be thrown (the exception can be thwarted
* by setting the 2nd parameter of the function to true).
*
* @param {string} tpl The HTTP request template URL
* @param {boolean=} ignoreRequestError Whether or not to ignore the exception when the request fails or the template is empty
*
* @return {Promise} the HTTP Promise for the given.
*
* @property {number} totalPendingRequests total amount of pending template requests being downloaded.
*/
function $TemplateRequestProvider() {
this.$get = ['$templateCache', '$http', '$q', function($templateCache, $http, $q) {
function handleRequestFn(tpl, ignoreRequestError) {
var self = handleRequestFn;
self.totalPendingRequests++;

return $http.get(tpl, { cache : $templateCache })
.then(function(response) {
var html = response.data;
if(!html || html.length === 0) {
return handleError();
}

self.totalPendingRequests--;
$templateCache.put(tpl, html);
return html;
}, handleError);

function handleError() {
self.totalPendingRequests--;
if (!ignoreRequestError) {
throw $compileMinErr('tpload', 'Failed to load template: {0}', tpl);
}
return $q.reject();
}
}

handleRequestFn.totalPendingRequests = 0;

return handleRequestFn;
}];
}
52 changes: 39 additions & 13 deletions src/ngAnimate/animate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

did we determine a $watch is the best way to do this? Maybe it would be worth making a change to the browser provider to do this without incurring additional cost during bootstrap of applications

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just tested variations of the code below:

      $rootScope.$$postDigest(function() {
        $browser.$$completeOutstandingRequest(function() {
          $rootScope.$$postDigest(function() {
            $rootScope.$$postDigest(onApplicationReady);
          });
        });
      });

Doesn't work.

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; }
Expand Down
8 changes: 4 additions & 4 deletions src/ngMessages/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -228,8 +228,8 @@ angular.module('ngMessages', [])
* </file>
* </example>
*/
.directive('ngMessages', ['$compile', '$animate', '$http', '$templateCache',
function($compile, $animate, $http, $templateCache) {
.directive('ngMessages', ['$compile', '$animate', '$templateRequest',
function($compile, $animate, $templateRequest) {
var ACTIVE_CLASS = 'ng-active';
var INACTIVE_CLASS = 'ng-inactive';

Expand Down Expand Up @@ -296,8 +296,8 @@ angular.module('ngMessages', [])

var tpl = $attrs.ngMessagesInclude || $attrs.include;
if(tpl) {
$http.get(tpl, { cache: $templateCache })
.success(function processTemplate(html) {
$templateRequest(tpl)
.then(function processTemplate(html) {
var after, container = angular.element('<div/>').html(html);
angular.forEach(container.children(), function(elm) {
elm = angular.element(elm);
Expand Down
8 changes: 3 additions & 5 deletions src/ngRoute/route.js
Original file line number Diff line number Diff line change
Expand Up @@ -226,10 +226,9 @@ function $RouteProvider(){
'$routeParams',
'$q',
'$injector',
'$http',
'$templateCache',
'$templateRequest',
'$sce',
function($rootScope, $location, $routeParams, $q, $injector, $http, $templateCache, $sce) {
function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) {

/**
* @ngdoc service
Expand Down Expand Up @@ -556,8 +555,7 @@ function $RouteProvider(){
templateUrl = $sce.getTrustedResourceUrl(templateUrl);
if (angular.isDefined(templateUrl)) {
next.loadedTemplateUrl = templateUrl;
template = $http.get(templateUrl, {cache: $templateCache}).
then(function(response) { return response.data; });
template = $templateRequest(templateUrl);
}
}
if (angular.isDefined(template)) {
Expand Down
5 changes: 1 addition & 4 deletions test/ng/directive/ngClassSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()();

Expand Down
1 change: 1 addition & 0 deletions test/ng/directive/ngIncludeSpec.js
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@ describe('ngInclude', function() {
$rootScope.url = 'url2';
$rootScope.$digest();
$httpBackend.flush();

expect($rootScope.$$childHead).toBeFalsy();
expect(element.text()).toBe('');

Expand Down
89 changes: 89 additions & 0 deletions test/ng/templateRequestSpec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
'use strict';

describe('$templateRequest', function() {

it('should download the provided template file',
inject(function($rootScope, $templateRequest, $httpBackend) {

$httpBackend.expectGET('tpl.html').respond('<div>abc</div>');

var content;
$templateRequest('tpl.html').then(function(html) { content = html; });

$rootScope.$digest();
$httpBackend.flush();

expect(content).toBe('<div>abc</div>');
}));

it('should cache the request using $templateCache to prevent extra downloads',
inject(function($rootScope, $templateRequest, $templateCache) {

$templateCache.put('tpl.html', 'matias');

var content;
$templateRequest('tpl.html').then(function(html) { content = html; });

$rootScope.$digest();
expect(content).toBe('matias');
}));

it('should throw an error when the template is not found',
inject(function($rootScope, $templateRequest, $httpBackend) {

$httpBackend.expectGET('tpl.html').respond(404);

$templateRequest('tpl.html');

$rootScope.$digest();

expect(function() {
$rootScope.$digest();
$httpBackend.flush();
}).toThrowMinErr('$compile', 'tpload', 'Failed to load template: tpl.html');
}));

it('should throw an error when the template is empty',
inject(function($rootScope, $templateRequest, $httpBackend) {

$httpBackend.expectGET('tpl.html').respond('');

$templateRequest('tpl.html');

$rootScope.$digest();

expect(function() {
$rootScope.$digest();
$httpBackend.flush();
}).toThrowMinErr('$compile', 'tpload', 'Failed to load template: tpl.html');
}));

it('should keep track of how many requests are going on',
inject(function($rootScope, $templateRequest, $httpBackend) {

$httpBackend.expectGET('a.html').respond('a');
$httpBackend.expectGET('b.html').respond('c');
$templateRequest('a.html');
$templateRequest('b.html');

expect($templateRequest.totalPendingRequests).toBe(2);

$rootScope.$digest();
$httpBackend.flush();

expect($templateRequest.totalPendingRequests).toBe(0);

$httpBackend.expectGET('c.html').respond(404);
$templateRequest('c.html');

expect($templateRequest.totalPendingRequests).toBe(1);
$rootScope.$digest();

try {
$httpBackend.flush();
} catch(e) {}

expect($templateRequest.totalPendingRequests).toBe(0);
}));

});
Loading