diff --git a/angularFiles.js b/angularFiles.js index 6c6dc1e59af5..924fcd487fb2 100755 --- a/angularFiles.js +++ b/angularFiles.js @@ -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', diff --git a/src/AngularPublic.js b/src/AngularPublic.js index e859a0da7577..14c8383a9b99 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -78,6 +78,7 @@ $SceDelegateProvider, $SnifferProvider, $TemplateCacheProvider, + $TemplateRequestProvider, $TimeoutProvider, $$RAFProvider, $$AsyncCallbackProvider, @@ -227,6 +228,7 @@ function publishExternalAPI(angular){ $sceDelegate: $SceDelegateProvider, $sniffer: $SnifferProvider, $templateCache: $TemplateCacheProvider, + $templateRequest: $TemplateRequestProvider, $timeout: $TimeoutProvider, $window: $WindowProvider, $$rAF: $$RAFProvider, diff --git a/src/ng/compile.js b/src/ng/compile.js index ac0eab395690..6c40d9b0db7a 100644 --- a/src/ng/compile.js +++ b/src/ng/compile.js @@ -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) { @@ -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); @@ -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) { diff --git a/src/ng/directive/ngInclude.js b/src/ng/directive/ngInclude.js index 8aea896b2dd8..e78e72bc1910 100644 --- a/src/ng/directive/ngInclude.js +++ b/src/ng/directive/ngInclude.js @@ -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, @@ -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; @@ -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'); diff --git a/src/ng/templateRequest.js b/src/ng/templateRequest.js new file mode 100644 index 000000000000..7155718e89a4 --- /dev/null +++ b/src/ng/templateRequest.js @@ -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; + }]; +} diff --git a/src/ngAnimate/animate.js b/src/ngAnimate/animate.js index 2b2de1abe367..d8c65712ca79 100644 --- a/src/ngAnimate/animate.js +++ b/src/ngAnimate/animate.js @@ -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. + * *

CSS-defined Animations

* 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; } diff --git a/src/ngMessages/messages.js b/src/ngMessages/messages.js index ecf5bc21f435..3254390a5a51 100644 --- a/src/ngMessages/messages.js +++ b/src/ngMessages/messages.js @@ -228,8 +228,8 @@ angular.module('ngMessages', []) * * */ - .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'; @@ -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('
').html(html); angular.forEach(container.children(), function(elm) { elm = angular.element(elm); diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js index 53b1927d6ad5..b140ddfbc1b8 100644 --- a/src/ngRoute/route.js +++ b/src/ngRoute/route.js @@ -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 @@ -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)) { diff --git a/test/ng/directive/ngClassSpec.js b/test/ng/directive/ngClassSpec.js index a00708f18d5d..2d4f28cce701 100644 --- a/test/ng/directive/ngClassSpec.js +++ b/test/ng/directive/ngClassSpec.js @@ -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()(); diff --git a/test/ng/directive/ngIncludeSpec.js b/test/ng/directive/ngIncludeSpec.js index 1d2f1ec4c89a..4dca3f803dc8 100644 --- a/test/ng/directive/ngIncludeSpec.js +++ b/test/ng/directive/ngIncludeSpec.js @@ -199,6 +199,7 @@ describe('ngInclude', function() { $rootScope.url = 'url2'; $rootScope.$digest(); $httpBackend.flush(); + expect($rootScope.$$childHead).toBeFalsy(); expect(element.text()).toBe(''); diff --git a/test/ng/templateRequestSpec.js b/test/ng/templateRequestSpec.js new file mode 100644 index 000000000000..efc6a182116c --- /dev/null +++ b/test/ng/templateRequestSpec.js @@ -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('
abc
'); + + var content; + $templateRequest('tpl.html').then(function(html) { content = html; }); + + $rootScope.$digest(); + $httpBackend.flush(); + + expect(content).toBe('
abc
'); + })); + + 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); + })); + +}); diff --git a/test/ngAnimate/animateSpec.js b/test/ngAnimate/animateSpec.js index a8f67b1087e4..ef1e0fecab0b 100644 --- a/test/ngAnimate/animateSpec.js +++ b/test/ngAnimate/animateSpec.js @@ -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, 'content'); + + var element = $compile('
...
')($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 diff --git a/test/ngRoute/directive/ngViewSpec.js b/test/ngRoute/directive/ngViewSpec.js index a832a7b32cc8..6113a2ca2ec8 100644 --- a/test/ngRoute/directive/ngViewSpec.js +++ b/test/ngRoute/directive/ngViewSpec.js @@ -56,7 +56,7 @@ describe('ngView', function() { }); - it('should instantiate controller for empty template', function() { + it('should not instantiate the associated controller when an empty template is downloaded', function() { var log = [], controllerScope, Ctrl = function($scope) { controllerScope = $scope; @@ -70,11 +70,12 @@ describe('ngView', function() { inject(function($route, $rootScope, $templateCache, $location) { $templateCache.put('/tpl.html', [200, '', {}]); $location.path('/some'); - $rootScope.$digest(); - expect(controllerScope.$parent).toBe($rootScope); - expect(controllerScope).toBe($route.current.scope); - expect(log).toEqual(['ctrl-init']); + expect(function() { + $rootScope.$digest(); + }).toThrowMinErr('$compile', 'tpload', 'Failed to load template: /tpl.html'); + + expect(controllerScope).toBeUndefined(); }); }); diff --git a/test/ngRoute/routeSpec.js b/test/ngRoute/routeSpec.js index 5dcf96edcb32..220d4f47b631 100644 --- a/test/ngRoute/routeSpec.js +++ b/test/ngRoute/routeSpec.js @@ -671,35 +671,36 @@ describe('$route', function() { }); - it('should drop in progress route change when new route change occurs and old fails', function() { - module(function($routeProvider) { + it('should throw an error when a template is empty or not found', function() { + module(function($routeProvider, $exceptionHandlerProvider) { + $exceptionHandlerProvider.mode('log'); $routeProvider. when('/r1', { templateUrl: 'r1.html' }). - when('/r2', { templateUrl: 'r2.html' }); + when('/r2', { templateUrl: 'r2.html' }). + when('/r3', { templateUrl: 'r3.html' }); }); - inject(function($route, $httpBackend, $location, $rootScope) { - var log = ''; - $rootScope.$on('$routeChangeError', function(e, next, last, error) { - log += '$failed(' + next.templateUrl + ', ' + error.status + ');'; - }); - $rootScope.$on('$routeChangeStart', function(e, next) { log += '$before(' + next.templateUrl + ');'; }); - $rootScope.$on('$routeChangeSuccess', function(e, next) { log += '$after(' + next.templateUrl + ');'; }); - + inject(function($route, $httpBackend, $location, $rootScope, $exceptionHandler) { $httpBackend.expectGET('r1.html').respond(404, 'R1'); - $httpBackend.expectGET('r2.html').respond('R2'); - $location.path('/r1'); $rootScope.$digest(); - expect(log).toBe('$before(r1.html);'); + $httpBackend.flush(); + expect($exceptionHandler.errors.pop().message).toContain("[$compile:tpload] Failed to load template: r1.html"); + + $httpBackend.expectGET('r2.html').respond(''); $location.path('/r2'); $rootScope.$digest(); - expect(log).toBe('$before(r1.html);$before(r2.html);'); $httpBackend.flush(); - expect(log).toBe('$before(r1.html);$before(r2.html);$after(r2.html);'); - expect(log).not.toContain('$after(r1.html);'); + expect($exceptionHandler.errors.pop().message).toContain("[$compile:tpload] Failed to load template: r2.html"); + + $httpBackend.expectGET('r3.html').respond('abc'); + $location.path('/r3'); + $rootScope.$digest(); + + $httpBackend.flush(); + expect($exceptionHandler.errors.length).toBe(0); }); });