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

Commit c13c666

Browse files
committed
feat(ngRoute): allow ngView to be included in an asynchronously loaded template
During its linking phase, `ngView` relies on the info provided in `$route.current` for instantiating the initial view. `$route.current` is set in the callback of a listener to `$locationChangeSuccess`, which is registered during the instantiation of the `$route` service. Thus, it is crucial that the `$route` service is instantiated _before_ the initial `$locationChangeSuccess` event is fired. Since `ngView` declares `$route` as a dependency, the service is instantiated in time, if `ngView` is present during the initial load of the page. Yet, in cases where `ngView` is included in a template that is loaded asynchronously (e.g. in another directive's template), the directive factory might not be called soon enough for `$route` to be instantiated before the initial `$locationChangeSuccess` event is fired. This commit fixes it, by enabling eager instantiation of `$route` (during the initialization phase). Eager instantiation can be disabled (restoring the old behavior), but is on by default. Fixes #1213 Closes #14893 BREAKING CHANGE: In cases where `ngView` was loaded asynchronously, `$route` (and its dependencies; e.g. `$location`) might also have been instantiated asynchronously. After this change, `$route` (and its dependencies) will - by default - be instantiated early on. Although this is not expected to have unwanted side-effects in normal application bebavior, it may affect your unit tests: When testing a module that (directly or indirectly) depends on `ngRoute`, a request will be made for the default route's template. If not properly "trained", `$httpBackend` will complain about this unexpected request. You can restore the previous behavior (and avoid unexpected requests in tests), by using `$routeProvider.eagerInstantiationEnabled(false)`.
1 parent 47583d9 commit c13c666

File tree

3 files changed

+147
-5
lines changed

3 files changed

+147
-5
lines changed

Diff for: src/ngRoute/route.js

+63-5
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
/* global shallowCopy: false */
44

5-
// There are necessary for `shallowCopy()` (included via `src/shallowCopy.js`).
5+
// `isArray` and `isObject` are necessary for `shallowCopy()` (included via `src/shallowCopy.js`).
66
// They are initialized inside the `$RouteProvider`, to ensure `window.angular` is available.
77
var isArray;
88
var isObject;
9+
var isDefined;
910

1011
/**
1112
* @ngdoc module
@@ -22,10 +23,17 @@ var isObject;
2223
*
2324
* <div doc-module-components="ngRoute"></div>
2425
*/
25-
/* global -ngRouteModule */
26-
var ngRouteModule = angular.module('ngRoute', ['ng']).
27-
provider('$route', $RouteProvider),
28-
$routeMinErr = angular.$$minErr('ngRoute');
26+
/* global -ngRouteModule */
27+
var ngRouteModule = angular.
28+
module('ngRoute', []).
29+
provider('$route', $RouteProvider).
30+
// Ensure `$route` will be instantiated in time to capture the initial `$locationChangeSuccess`
31+
// event (unless explicitly disabled). This is necessary in case `ngView` is included in an
32+
// asynchronously loaded template.
33+
run(instantiateRoute);
34+
var $routeMinErr = angular.$$minErr('ngRoute');
35+
var isEagerInstantiationEnabled;
36+
2937

3038
/**
3139
* @ngdoc provider
@@ -44,6 +52,7 @@ var ngRouteModule = angular.module('ngRoute', ['ng']).
4452
function $RouteProvider() {
4553
isArray = angular.isArray;
4654
isObject = angular.isObject;
55+
isDefined = angular.isDefined;
4756

4857
function inherit(parent, extra) {
4958
return angular.extend(Object.create(parent), extra);
@@ -287,6 +296,47 @@ function $RouteProvider() {
287296
return this;
288297
};
289298

299+
/**
300+
* @ngdoc method
301+
* @name $routeProvider#eagerInstantiationEnabled
302+
* @kind function
303+
*
304+
* @description
305+
* Call this method as a setter to enable/disable eager instantiation of the
306+
* {@link ngRoute.$route $route} service upon application bootstrap. You can also call it as a
307+
* getter (i.e. without any arguments) to get the current value of the
308+
* `eagerInstantiationEnabled` flag.
309+
*
310+
* Instantiating `$route` early is necessary for capturing the initial
311+
* {@link ng.$location#$locationChangeStart $locationChangeStart} event and navigating to the
312+
* appropriate route. Usually, `$route` is instantiated in time by the
313+
* {@link ngRoute.ngView ngView} directive. Yet, in cases where `ngView` is included in an
314+
* asynchronously loaded template (e.g. in another directive's template), the directive factory
315+
* might not be called soon enough for `$route` to be instantiated _before_ the initial
316+
* `$locationChangeSuccess` event is fired. Eager instantiation ensures that `$route` is always
317+
* instantiated in time, regardless of when `ngView` will be loaded.
318+
*
319+
* The default value is true.
320+
*
321+
* **Note**:<br />
322+
* You may want to disable the default behavior when unit-testing modules that depend on
323+
* `ngRoute`, in order to avoid an unexpected request for the default route's template.
324+
*
325+
* @param {boolean=} enabled - If provided, update the internal `eagerInstantiationEnabled` flag.
326+
*
327+
* @returns {*} The current value of the `eagerInstantiationEnabled` flag if used as a getter or
328+
* itself (for chaining) if used as a setter.
329+
*/
330+
isEagerInstantiationEnabled = true;
331+
this.eagerInstantiationEnabled = function eagerInstantiationEnabled(enabled) {
332+
if (isDefined(enabled)) {
333+
isEagerInstantiationEnabled = enabled;
334+
return this;
335+
}
336+
337+
return isEagerInstantiationEnabled;
338+
};
339+
290340

291341
this.$get = ['$rootScope',
292342
'$location',
@@ -791,3 +841,11 @@ function $RouteProvider() {
791841
}
792842
}];
793843
}
844+
845+
instantiateRoute.$inject = ['$injector'];
846+
function instantiateRoute($injector) {
847+
if (isEagerInstantiationEnabled) {
848+
// Instantiate `$route`
849+
$injector.get('$route');
850+
}
851+
}

Diff for: test/ngRoute/directive/ngViewSpec.js

+31
Original file line numberDiff line numberDiff line change
@@ -1029,4 +1029,35 @@ describe('ngView', function() {
10291029
));
10301030
});
10311031
});
1032+
1033+
describe('in async template', function() {
1034+
beforeEach(module('ngRoute'));
1035+
beforeEach(module(function($compileProvider, $provide, $routeProvider) {
1036+
$compileProvider.directive('asyncView', function() {
1037+
return {templateUrl: 'async-view.html'};
1038+
});
1039+
1040+
$provide.decorator('$templateRequest', function($timeout) {
1041+
return function() {
1042+
return $timeout(angular.identity, 500, false, '<ng-view></ng-view>');
1043+
};
1044+
});
1045+
1046+
$routeProvider.when('/', {template: 'Hello, world!'});
1047+
}));
1048+
1049+
1050+
it('should work correctly upon initial page load',
1051+
// Injecting `$location` here is necessary, so that it gets instantiated early
1052+
inject(function($compile, $location, $rootScope, $timeout) {
1053+
var elem = $compile('<async-view></async-view>')($rootScope);
1054+
$rootScope.$digest();
1055+
$timeout.flush(500);
1056+
1057+
expect(elem.text()).toBe('Hello, world!');
1058+
1059+
dealoc(elem);
1060+
})
1061+
);
1062+
});
10321063
});

Diff for: test/ngRoute/routeSpec.js

+53
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,58 @@
11
'use strict';
22

3+
describe('$routeProvider', function() {
4+
var $routeProvider;
5+
6+
beforeEach(module('ngRoute'));
7+
beforeEach(module(function(_$routeProvider_) {
8+
$routeProvider = _$routeProvider_;
9+
$routeProvider.when('/foo', {template: 'Hello, world!'});
10+
}));
11+
12+
13+
it('should support enabling/disabling automatic instantiation upon initial load',
14+
inject(function() {
15+
expect($routeProvider.eagerInstantiationEnabled(true)).toBe($routeProvider);
16+
expect($routeProvider.eagerInstantiationEnabled()).toBe(true);
17+
18+
expect($routeProvider.eagerInstantiationEnabled(false)).toBe($routeProvider);
19+
expect($routeProvider.eagerInstantiationEnabled()).toBe(false);
20+
21+
expect($routeProvider.eagerInstantiationEnabled(true)).toBe($routeProvider);
22+
expect($routeProvider.eagerInstantiationEnabled()).toBe(true);
23+
})
24+
);
25+
26+
27+
it('should automatically instantiate `$route` upon initial load', function() {
28+
inject(function($location, $rootScope) {
29+
$location.path('/foo');
30+
$rootScope.$digest();
31+
});
32+
33+
inject(function($route) {
34+
expect($route.current).toBeDefined();
35+
});
36+
});
37+
38+
39+
it('should not automatically instantiate `$route` if disabled', function() {
40+
module(function($routeProvider) {
41+
$routeProvider.eagerInstantiationEnabled(false);
42+
});
43+
44+
inject(function($location, $rootScope) {
45+
$location.path('/foo');
46+
$rootScope.$digest();
47+
});
48+
49+
inject(function($route) {
50+
expect($route.current).toBeUndefined();
51+
});
52+
});
53+
});
54+
55+
356
describe('$route', function() {
457
var $httpBackend,
558
element;

0 commit comments

Comments
 (0)