From dc954f25885144e5193961ba673c1f0585b15ce0 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 14 Jul 2016 13:22:06 +0100 Subject: [PATCH 1/2] fix(): allow to be included in an asynchronously loaded template --- src/ngRoute/route.js | 45 ++++++++++++++++++++++++++-- test/ngRoute/directive/ngViewSpec.js | 31 +++++++++++++++++++ 2 files changed, 73 insertions(+), 3 deletions(-) diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js index 4ead323e8baf..56c5333a31b1 100644 --- a/src/ngRoute/route.js +++ b/src/ngRoute/route.js @@ -23,10 +23,40 @@ var isObject; *
*/ /* global -ngRouteModule */ -var ngRouteModule = angular.module('ngRoute', ['ng']). - provider('$route', $RouteProvider), +var ngRouteModule = angular.module('ngRoute', ['ng']) + .provider('$route', $RouteProvider) + .factory('$$trackLocationChanges', ['$rootScope', $$trackLocationChanges]) + .run(['$$trackLocationChanges', function($$trackLocationChanges) { + $$trackLocationChanges.start(); + }]), + $routeMinErr = angular.$$minErr('ngRoute'); +function $$trackLocationChanges($rootScope) { + var removeStartHandler, removeSuccessHandler; + var service = { + start: function() { + removeStartHandler = $rootScope.$on('$locationChangeStart', storeStartEvent); + removeSuccessHandler = $rootScope.$on('$locationChangeSuccess', storeSuccessEvent); + }, + stop: function() { + removeStartHandler(); + removeSuccessHandler(); + } + }; + + return service; + + function storeStartEvent(e) { + service.startEvent = e; + delete service.successEvent; + } + + function storeSuccessEvent(e) { + service.successEvent = e; + } +} + /** * @ngdoc provider * @name $routeProvider @@ -295,7 +325,9 @@ function $RouteProvider() { '$injector', '$templateRequest', '$sce', - function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce) { + '$$trackLocationChanges', + function($rootScope, $location, $routeParams, $q, $injector, $templateRequest, $sce, $$trackLocationChanges) { + /** * @ngdoc service @@ -555,6 +587,12 @@ function $RouteProvider() { } }; + if ($$trackLocationChanges.successEvent) { + prepareRoute($$trackLocationChanges.startEvent); + commitRoute($$trackLocationChanges.successEvent); + } + $$trackLocationChanges.stop(); + $rootScope.$on('$locationChangeStart', prepareRoute); $rootScope.$on('$locationChangeSuccess', commitRoute); @@ -612,6 +650,7 @@ function $RouteProvider() { } function commitRoute() { + $route.initialized = true; var lastRoute = $route.current; var nextRoute = preparedRoute; diff --git a/test/ngRoute/directive/ngViewSpec.js b/test/ngRoute/directive/ngViewSpec.js index 7c11b9a95670..b4bd0b526045 100644 --- a/test/ngRoute/directive/ngViewSpec.js +++ b/test/ngRoute/directive/ngViewSpec.js @@ -695,6 +695,37 @@ describe('ngView', function() { }); }); + describe('ngView in async template', function() { + beforeEach(module('ngRoute')); + beforeEach(module(function($compileProvider, $provide, $routeProvider) { + $compileProvider.directive('asyncView', function() { + return {templateUrl: 'async-view.html'}; + }); + + $provide.decorator('$templateRequest', function($timeout) { + return function() { + return $timeout(angular.identity, 500, false, ''); + }; + }); + + $routeProvider.when('/', {template: 'Hello, world!'}); + })); + + + it('should work correctly upon initial page load', + // Injecting `$location` here is necessary, so that it gets instantiated early + inject(function($compile, $location, $rootScope, $timeout) { + var elem = $compile('')($rootScope); + $rootScope.$digest(); + $timeout.flush(500); + + expect(elem.text()).toBe('Hello, world!'); + + dealoc(elem); + }) + ); + }); + describe('animations', function() { var body, element, $rootElement; From 003ac6aa6def7761e9225c314acd2060fca52ca1 Mon Sep 17 00:00:00 2001 From: Peter Bacon Darwin Date: Thu, 14 Jul 2016 17:44:21 +0100 Subject: [PATCH 2/2] fix(): allow to be included in an asynchronously loaded template --- src/ngRoute/route.js | 41 ++++++++----- test/ngRoute/directive/ngViewSpec.js | 89 +++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 18 deletions(-) diff --git a/src/ngRoute/route.js b/src/ngRoute/route.js index 56c5333a31b1..388d80f36c7e 100644 --- a/src/ngRoute/route.js +++ b/src/ngRoute/route.js @@ -25,16 +25,17 @@ var isObject; /* global -ngRouteModule */ var ngRouteModule = angular.module('ngRoute', ['ng']) .provider('$route', $RouteProvider) - .factory('$$trackLocationChanges', ['$rootScope', $$trackLocationChanges]) + .factory('$$trackLocationChanges', ['$rootScope', '$location', $$trackLocationChanges]) .run(['$$trackLocationChanges', function($$trackLocationChanges) { $$trackLocationChanges.start(); }]), $routeMinErr = angular.$$minErr('ngRoute'); -function $$trackLocationChanges($rootScope) { +function $$trackLocationChanges($rootScope, $location) { var removeStartHandler, removeSuccessHandler; var service = { + events: [], start: function() { removeStartHandler = $rootScope.$on('$locationChangeStart', storeStartEvent); removeSuccessHandler = $rootScope.$on('$locationChangeSuccess', storeSuccessEvent); @@ -47,13 +48,12 @@ function $$trackLocationChanges($rootScope) { return service; - function storeStartEvent(e) { - service.startEvent = e; - delete service.successEvent; + function storeStartEvent(e, url) { + service.events.push({ startEvent: e, locationPath: $location.path(), locationSearch: $location.search() }); } function storeSuccessEvent(e) { - service.successEvent = e; + service.events[service.events.length-1].successEvent = e; } } @@ -557,7 +557,7 @@ function $RouteProvider() { }; $rootScope.$evalAsync(function() { - prepareRoute(fakeLocationEvent); + prepareRoute(fakeLocationEvent, $location.path(), $location.search()); if (!fakeLocationEvent.defaultPrevented) commitRoute(); }); }, @@ -587,13 +587,22 @@ function $RouteProvider() { } }; - if ($$trackLocationChanges.successEvent) { - prepareRoute($$trackLocationChanges.startEvent); - commitRoute($$trackLocationChanges.successEvent); + var eventPair; + while(eventPair = $$trackLocationChanges.events.pop()) { + // find the most recent success event + if (eventPair.successEvent) { + prepareRoute(eventPair.startEvent, eventPair.locationPath, eventPair.locationSearch); + // if the start event is not prevented then commit the change and escape + // otherwise try the previous location change + if (!eventPair.startEvent.defaultPrevented) { + commitRoute(eventPair.successEvent); + break; + } + } } $$trackLocationChanges.stop(); - $rootScope.$on('$locationChangeStart', prepareRoute); + $rootScope.$on('$locationChangeStart', function(e) { return prepareRoute(e, $location.path(), $location.search()); }); $rootScope.$on('$locationChangeSuccess', commitRoute); return $route; @@ -632,10 +641,10 @@ function $RouteProvider() { return params; } - function prepareRoute($locationEvent) { + function prepareRoute($locationEvent, locationPath, locationSearch) { var lastRoute = $route.current; - preparedRoute = parseRoute(); + preparedRoute = parseRoute(locationPath, locationSearch); preparedRouteIsUpdateOnly = preparedRoute && lastRoute && preparedRoute.$$route === lastRoute.$$route && angular.equals(preparedRoute.pathParams, lastRoute.pathParams) && !preparedRoute.reloadOnSearch && !forceReload; @@ -795,13 +804,13 @@ function $RouteProvider() { /** * @returns {Object} the current active route, by matching it against the URL */ - function parseRoute() { + function parseRoute(locationPath, locationSearch) { // Match a route var params, match; angular.forEach(routes, function(route, path) { - if (!match && (params = switchRouteMatcher($location.path(), route))) { + if (!match && (params = switchRouteMatcher(locationPath, route))) { match = inherit(route, { - params: angular.extend({}, $location.search(), params), + params: angular.extend({}, locationSearch, params), pathParams: params}); match.$$route = route; } diff --git a/test/ngRoute/directive/ngViewSpec.js b/test/ngRoute/directive/ngViewSpec.js index b4bd0b526045..29ec8a8a26c9 100644 --- a/test/ngRoute/directive/ngViewSpec.js +++ b/test/ngRoute/directive/ngViewSpec.js @@ -696,6 +696,7 @@ describe('ngView', function() { }); describe('ngView in async template', function() { + beforeEach(module('ngRoute')); beforeEach(module(function($compileProvider, $provide, $routeProvider) { $compileProvider.directive('asyncView', function() { @@ -709,6 +710,8 @@ describe('ngView', function() { }); $routeProvider.when('/', {template: 'Hello, world!'}); + $routeProvider.when('/one', {template: 'One'}); + $routeProvider.when('/two', {template: 'Two'}); })); @@ -716,15 +719,97 @@ describe('ngView', function() { // Injecting `$location` here is necessary, so that it gets instantiated early inject(function($compile, $location, $rootScope, $timeout) { var elem = $compile('')($rootScope); + $rootScope.$digest(); - $timeout.flush(500); + expect(elem.text()).toBe(''); + $timeout.flush(500); expect(elem.text()).toBe('Hello, world!'); dealoc(elem); }) ); - }); + + it('should cope with multiple location changes before the template arrives', function() { + inject(function($compile, $location, $rootScope, $timeout) { + var elem = $compile('')($rootScope); + + $rootScope.$digest(); + expect(elem.text()).toBe(''); + + $location.path('one'); + $rootScope.$digest(); + expect(elem.text()).toBe(''); + + $location.path('two'); + $rootScope.$digest(); + expect(elem.text()).toBe(''); + + $timeout.flush(500); + expect(elem.text()).toBe('Two'); + + dealoc(elem); + }); + }); + + it('should use the previous location change if the latest is prevented via $location event', function() { + inject(function($compile, $location, $rootScope, $timeout) { + var preventDefault; + + $rootScope.$on('$locationChangeStart', function(e) { + if (preventDefault) e.preventDefault(); + }); + + var elem = $compile('')($rootScope); + + $rootScope.$digest(); + expect(elem.text()).toBe(''); + + preventDefault = false; + $location.path('one'); + $rootScope.$digest(); + expect(elem.text()).toBe(''); + + + preventDefault = true; + $location.path('two'); + $rootScope.$digest(); + expect(elem.text()).toBe(''); + + $timeout.flush(500); + expect(elem.text()).toBe('One'); + + dealoc(elem); + }); + }); + + it('should use the previous location change if the latest is prevented via $route event', function() { + inject(function($compile, $location, $rootScope, $timeout) { + + $rootScope.$on('$routeChangeStart', function(e, next, current) { + if (next.$$route.originalPath == '/two') e.preventDefault(); + }); + + var elem = $compile('')($rootScope); + + $rootScope.$digest(); + expect(elem.text()).toBe(''); + + $location.path('one'); + $rootScope.$digest(); + expect(elem.text()).toBe(''); + + + $location.path('two'); + $rootScope.$digest(); + expect(elem.text()).toBe(''); + + $timeout.flush(500); + expect(elem.text()).toBe('One'); + + dealoc(elem); + }); + }); }); describe('animations', function() { var body, element, $rootElement;