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)`.
This commit is contained in:
+63
-5
@@ -2,10 +2,11 @@
|
||||
|
||||
/* global shallowCopy: false */
|
||||
|
||||
// There are necessary for `shallowCopy()` (included via `src/shallowCopy.js`).
|
||||
// `isArray` and `isObject` are necessary for `shallowCopy()` (included via `src/shallowCopy.js`).
|
||||
// They are initialized inside the `$RouteProvider`, to ensure `window.angular` is available.
|
||||
var isArray;
|
||||
var isObject;
|
||||
var isDefined;
|
||||
|
||||
/**
|
||||
* @ngdoc module
|
||||
@@ -22,10 +23,17 @@ var isObject;
|
||||
*
|
||||
* <div doc-module-components="ngRoute"></div>
|
||||
*/
|
||||
/* global -ngRouteModule */
|
||||
var ngRouteModule = angular.module('ngRoute', ['ng']).
|
||||
provider('$route', $RouteProvider),
|
||||
$routeMinErr = angular.$$minErr('ngRoute');
|
||||
/* global -ngRouteModule */
|
||||
var ngRouteModule = angular.
|
||||
module('ngRoute', []).
|
||||
provider('$route', $RouteProvider).
|
||||
// Ensure `$route` will be instantiated in time to capture the initial `$locationChangeSuccess`
|
||||
// event (unless explicitly disabled). This is necessary in case `ngView` is included in an
|
||||
// asynchronously loaded template.
|
||||
run(instantiateRoute);
|
||||
var $routeMinErr = angular.$$minErr('ngRoute');
|
||||
var isEagerInstantiationEnabled;
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc provider
|
||||
@@ -44,6 +52,7 @@ var ngRouteModule = angular.module('ngRoute', ['ng']).
|
||||
function $RouteProvider() {
|
||||
isArray = angular.isArray;
|
||||
isObject = angular.isObject;
|
||||
isDefined = angular.isDefined;
|
||||
|
||||
function inherit(parent, extra) {
|
||||
return angular.extend(Object.create(parent), extra);
|
||||
@@ -287,6 +296,47 @@ function $RouteProvider() {
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* @ngdoc method
|
||||
* @name $routeProvider#eagerInstantiationEnabled
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Call this method as a setter to enable/disable eager instantiation of the
|
||||
* {@link ngRoute.$route $route} service upon application bootstrap. You can also call it as a
|
||||
* getter (i.e. without any arguments) to get the current value of the
|
||||
* `eagerInstantiationEnabled` flag.
|
||||
*
|
||||
* Instantiating `$route` early is necessary for capturing the initial
|
||||
* {@link ng.$location#$locationChangeStart $locationChangeStart} event and navigating to the
|
||||
* appropriate route. Usually, `$route` is instantiated in time by the
|
||||
* {@link ngRoute.ngView ngView} directive. Yet, in cases where `ngView` is included in an
|
||||
* asynchronously loaded template (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. Eager instantiation ensures that `$route` is always
|
||||
* instantiated in time, regardless of when `ngView` will be loaded.
|
||||
*
|
||||
* The default value is true.
|
||||
*
|
||||
* **Note**:<br />
|
||||
* You may want to disable the default behavior when unit-testing modules that depend on
|
||||
* `ngRoute`, in order to avoid an unexpected request for the default route's template.
|
||||
*
|
||||
* @param {boolean=} enabled - If provided, update the internal `eagerInstantiationEnabled` flag.
|
||||
*
|
||||
* @returns {*} The current value of the `eagerInstantiationEnabled` flag if used as a getter or
|
||||
* itself (for chaining) if used as a setter.
|
||||
*/
|
||||
isEagerInstantiationEnabled = true;
|
||||
this.eagerInstantiationEnabled = function eagerInstantiationEnabled(enabled) {
|
||||
if (isDefined(enabled)) {
|
||||
isEagerInstantiationEnabled = enabled;
|
||||
return this;
|
||||
}
|
||||
|
||||
return isEagerInstantiationEnabled;
|
||||
};
|
||||
|
||||
|
||||
this.$get = ['$rootScope',
|
||||
'$location',
|
||||
@@ -791,3 +841,11 @@ function $RouteProvider() {
|
||||
}
|
||||
}];
|
||||
}
|
||||
|
||||
instantiateRoute.$inject = ['$injector'];
|
||||
function instantiateRoute($injector) {
|
||||
if (isEagerInstantiationEnabled) {
|
||||
// Instantiate `$route`
|
||||
$injector.get('$route');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1029,4 +1029,35 @@ describe('ngView', function() {
|
||||
));
|
||||
});
|
||||
});
|
||||
|
||||
describe('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, '<ng-view></ng-view>');
|
||||
};
|
||||
});
|
||||
|
||||
$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('<async-view></async-view>')($rootScope);
|
||||
$rootScope.$digest();
|
||||
$timeout.flush(500);
|
||||
|
||||
expect(elem.text()).toBe('Hello, world!');
|
||||
|
||||
dealoc(elem);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,58 @@
|
||||
'use strict';
|
||||
|
||||
describe('$routeProvider', function() {
|
||||
var $routeProvider;
|
||||
|
||||
beforeEach(module('ngRoute'));
|
||||
beforeEach(module(function(_$routeProvider_) {
|
||||
$routeProvider = _$routeProvider_;
|
||||
$routeProvider.when('/foo', {template: 'Hello, world!'});
|
||||
}));
|
||||
|
||||
|
||||
it('should support enabling/disabling automatic instantiation upon initial load',
|
||||
inject(function() {
|
||||
expect($routeProvider.eagerInstantiationEnabled(true)).toBe($routeProvider);
|
||||
expect($routeProvider.eagerInstantiationEnabled()).toBe(true);
|
||||
|
||||
expect($routeProvider.eagerInstantiationEnabled(false)).toBe($routeProvider);
|
||||
expect($routeProvider.eagerInstantiationEnabled()).toBe(false);
|
||||
|
||||
expect($routeProvider.eagerInstantiationEnabled(true)).toBe($routeProvider);
|
||||
expect($routeProvider.eagerInstantiationEnabled()).toBe(true);
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
it('should automatically instantiate `$route` upon initial load', function() {
|
||||
inject(function($location, $rootScope) {
|
||||
$location.path('/foo');
|
||||
$rootScope.$digest();
|
||||
});
|
||||
|
||||
inject(function($route) {
|
||||
expect($route.current).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('should not automatically instantiate `$route` if disabled', function() {
|
||||
module(function($routeProvider) {
|
||||
$routeProvider.eagerInstantiationEnabled(false);
|
||||
});
|
||||
|
||||
inject(function($location, $rootScope) {
|
||||
$location.path('/foo');
|
||||
$rootScope.$digest();
|
||||
});
|
||||
|
||||
inject(function($route) {
|
||||
expect($route.current).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe('$route', function() {
|
||||
var $httpBackend,
|
||||
element;
|
||||
|
||||
Reference in New Issue
Block a user