From 73624b26a38abb262ce6000eedd0e05bf8711241 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 10 Dec 2013 16:45:54 -0800 Subject: [PATCH 1/2] fix(ngView): Add template to DOM before linking other directives The template needs to be added to the DOM before other directives at the same element as `ngView` are linked. Related to #5247. --- src/ngRoute/directive/ngView.js | 74 ++++++++++++++++++---------- test/ngRoute/directive/ngViewSpec.js | 40 +++++++++++++++ 2 files changed, 87 insertions(+), 27 deletions(-) diff --git a/src/ngRoute/directive/ngView.js b/src/ngRoute/directive/ngView.js index 280bf42324c1..b12529444715 100644 --- a/src/ngRoute/directive/ngView.js +++ b/src/ngRoute/directive/ngView.js @@ -1,6 +1,8 @@ 'use strict'; ngRouteModule.directive('ngView', ngViewFactory); +ngRouteModule.directive('ngView', ngViewFillContentFactory); + /** * @ngdoc directive @@ -166,8 +168,8 @@ ngRouteModule.directive('ngView', ngViewFactory); * @description * Emitted every time the ngView content is reloaded. */ -ngViewFactory.$inject = ['$route', '$anchorScroll', '$compile', '$controller', '$animate']; -function ngViewFactory( $route, $anchorScroll, $compile, $controller, $animate) { +ngViewFactory.$inject = ['$route', '$anchorScroll', '$animate']; +function ngViewFactory( $route, $anchorScroll, $animate) { return { restrict: 'ECA', terminal: true, @@ -199,6 +201,7 @@ function ngViewFactory( $route, $anchorScroll, $compile, $controller, if (template) { var newScope = scope.$new(); + var current = $route.current; // Note: This will also link all children of ng-view that were contained in the original // html. If that content contains controllers, ... they could pollute/change the scope. @@ -206,34 +209,18 @@ function ngViewFactory( $route, $anchorScroll, $compile, $controller, // Note: We can't remove them in the cloneAttchFn of $transclude as that // function is called before linking the content, which would apply child // directives to non existing elements. - var clone = $transclude(newScope, angular.noop); - clone.html(template); - $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () { - if (angular.isDefined(autoScrollExp) - && (!autoScrollExp || scope.$eval(autoScrollExp))) { - $anchorScroll(); - } + var clone = $transclude(newScope, function(clone) { + $animate.enter(clone, null, currentElement || $element, function onNgViewEnter () { + if (angular.isDefined(autoScrollExp) + && (!autoScrollExp || scope.$eval(autoScrollExp))) { + $anchorScroll(); + } + }); + cleanupLastView(); }); - cleanupLastView(); - - var link = $compile(clone.contents()), - current = $route.current; - - currentScope = current.scope = newScope; currentElement = clone; - - if (current.controller) { - locals.$scope = currentScope; - var controller = $controller(current.controller, locals); - if (current.controllerAs) { - currentScope[current.controllerAs] = controller; - } - clone.data('$ngControllerController', controller); - clone.children().data('$ngControllerController', controller); - } - - link(currentScope); + currentScope = current.scope = newScope; currentScope.$emit('$viewContentLoaded'); currentScope.$eval(onloadExp); } else { @@ -243,3 +230,36 @@ function ngViewFactory( $route, $anchorScroll, $compile, $controller, } }; } + +// This directive is called during the $transclude call of the first `ngView` directive. +// It will replace and compile the content of the element with the loaded template. +// We need this directive so that the element content is already filled when +// the link function of another directive on the same element as ngView +// is called. +ngViewFillContentFactory.$inject = ['$compile', '$controller', '$route']; +function ngViewFillContentFactory($compile, $controller, $route) { + return { + restrict: 'ECA', + priority: -400, + link: function(scope, $element) { + var current = $route.current, + locals = current.locals; + + $element.html(locals.$template); + + var link = $compile($element.contents()); + + if (current.controller) { + locals.$scope = scope; + var controller = $controller(current.controller, locals); + if (current.controllerAs) { + scope[current.controllerAs] = controller; + } + $element.data('$ngControllerController', controller); + $element.children().data('$ngControllerController', controller); + } + + link(scope); + } + }; +} diff --git a/test/ngRoute/directive/ngViewSpec.js b/test/ngRoute/directive/ngViewSpec.js index f4b28058aa0a..a8d1f4596e6a 100644 --- a/test/ngRoute/directive/ngViewSpec.js +++ b/test/ngRoute/directive/ngViewSpec.js @@ -582,6 +582,46 @@ describe('ngView and transcludes', function() { }); }); + + it('should link directives on the same element after the content has been loaded', function() { + var contentOnLink; + module(function($compileProvider, $routeProvider) { + $routeProvider.when('/view', {template: 'someContent'}); + $compileProvider.directive('test', function() { + return { + link: function(scope, element) { + contentOnLink = element.text(); + } + }; + }); + }); + inject(function($compile, $rootScope, $location) { + element = $compile('
')($rootScope); + $location.url('/view'); + $rootScope.$apply(); + expect(contentOnLink).toBe('someContent'); + }); + }); + + it('should add the content to the element before compiling it', function() { + var root; + module(function($compileProvider, $routeProvider) { + $routeProvider.when('/view', {template: ''}); + $compileProvider.directive('test', function() { + return { + link: function(scope, element) { + root = element.parent().parent(); + } + }; + }); + }); + inject(function($compile, $rootScope, $location) { + element = $compile('
')($rootScope); + $location.url('/view'); + $rootScope.$apply(); + expect(root[0]).toBe(element[0]); + }); + }); }); describe('ngView animations', function() { From a2d1962ad549a9d8fbb38000dceee27b74ba2b59 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 10 Dec 2013 16:46:29 -0800 Subject: [PATCH 2/2] fix(ngInclude): Add template to DOM before linking other directives The template needs to be added to the DOM before other directives at the same element as `ngInclude` are linked. Fixes #5247. --- src/AngularPublic.js | 4 +++ src/ng/directive/ngInclude.js | 34 +++++++++++++++++++------ test/ng/directive/ngIncludeSpec.js | 40 ++++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/AngularPublic.js b/src/AngularPublic.js index d2c325c552f6..14fe25aec20a 100644 --- a/src/AngularPublic.js +++ b/src/AngularPublic.js @@ -28,6 +28,7 @@ ngHideDirective, ngIfDirective, ngIncludeDirective, + ngIncludeFillContentDirective, ngInitDirective, ngNonBindableDirective, ngPluralizeDirective, @@ -181,6 +182,9 @@ function publishExternalAPI(angular){ ngRequired: requiredDirective, ngValue: ngValueDirective }). + directive({ + ngInclude: ngIncludeFillContentDirective + }). directive(ngAttributeAliasDirectives). directive(ngEventDirectives); $provide.provider({ diff --git a/src/ng/directive/ngInclude.js b/src/ng/directive/ngInclude.js index b721aa235fee..d6b386632641 100644 --- a/src/ng/directive/ngInclude.js +++ b/src/ng/directive/ngInclude.js @@ -147,13 +147,14 @@ * @description * Emitted every time the ngInclude content is reloaded. */ -var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile', '$animate', '$sce', - function($http, $templateCache, $anchorScroll, $compile, $animate, $sce) { +var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$animate', '$sce', + function($http, $templateCache, $anchorScroll, $animate, $sce) { return { restrict: 'ECA', priority: 400, terminal: true, transclude: 'element', + controller: angular.noop, compile: function(element, attr) { var srcExp = attr.ngInclude || attr.src, onloadExp = attr.onload || '', @@ -187,6 +188,7 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile' $http.get(src, {cache: $templateCache}).success(function(response) { if (thisChangeId !== changeCounter) return; var newScope = scope.$new(); + ctrl.template = response; // Note: This will also link all children of ng-include that were contained in the original // html. If that content contains controllers, ... they could pollute/change the scope. @@ -194,15 +196,14 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile' // Note: We can't remove them in the cloneAttchFn of $transclude as that // function is called before linking the content, which would apply child // directives to non existing elements. - var clone = $transclude(newScope, noop); - cleanupLastIncludeContent(); + var clone = $transclude(newScope, function(clone) { + cleanupLastIncludeContent(); + $animate.enter(clone, null, $element, afterAnimation); + }); currentScope = newScope; currentElement = clone; - currentElement.html(response); - $animate.enter(currentElement, null, $element, afterAnimation); - $compile(currentElement.contents())(currentScope); currentScope.$emit('$includeContentLoaded'); scope.$eval(onloadExp); }).error(function() { @@ -211,9 +212,28 @@ var ngIncludeDirective = ['$http', '$templateCache', '$anchorScroll', '$compile' scope.$emit('$includeContentRequested'); } else { cleanupLastIncludeContent(); + ctrl.template = null; } }); }; } }; }]; + +// This directive is called during the $transclude call of the first `ngInclude` directive. +// It will replace and compile the content of the element with the loaded template. +// We need this directive so that the element content is already filled when +// the link function of another directive on the same element as ngInclude +// is called. +var ngIncludeFillContentDirective = ['$compile', + function($compile) { + return { + restrict: 'ECA', + priority: -400, + require: 'ngInclude', + link: function(scope, $element, $attr, ctrl) { + $element.html(ctrl.template); + $compile($element.contents())(scope); + } + }; + }]; diff --git a/test/ng/directive/ngIncludeSpec.js b/test/ng/directive/ngIncludeSpec.js index 2d115e8b683f..9e0a47b0af32 100644 --- a/test/ng/directive/ngIncludeSpec.js +++ b/test/ng/directive/ngIncludeSpec.js @@ -524,6 +524,46 @@ describe('ngInclude and transcludes', function() { }); }); + + it('should link directives on the same element after the content has been loaded', function() { + var contentOnLink; + module(function() { + directive('test', function() { + return { + link: function(scope, element) { + contentOnLink = element.text(); + } + }; + }); + }); + inject(function($compile, $rootScope, $httpBackend) { + $httpBackend.expectGET('include.html').respond('someContent'); + element = $compile('
')($rootScope); + $rootScope.$apply(); + $httpBackend.flush(); + expect(contentOnLink).toBe('someContent'); + }); + }); + + it('should add the content to the element before compiling it', function() { + var root; + module(function() { + directive('test', function() { + return { + link: function(scope, element) { + root = element.parent().parent(); + } + }; + }); + }); + inject(function($compile, $rootScope, $httpBackend) { + $httpBackend.expectGET('include.html').respond(''); + element = $compile('
')($rootScope); + $rootScope.$apply(); + $httpBackend.flush(); + expect(root[0]).toBe(element[0]); + }); + }); }); describe('ngInclude animations', function() {