fix: element injector vs module injector (#15044)

fixes #12869
fixes #12889
fixes #13885
fixes #13870

Before this change there was a single injector tree.
Now we have 2 injector trees, one for the modules and one for the components.
This fixes lazy loading modules.

See the design docs for details:
https://docs.google.com/document/d/1OEUIwc-s69l1o97K0wBd_-Lth5BBxir1KuCRWklTlI4

BREAKING CHANGES

`ComponentFactory.create()` takes an extra optional `NgModuleRef` parameter.
No change should be required in user code as the correct module will be used
when none is provided

DEPRECATIONS

The following methods were used internally and are no more required:
- `RouterOutlet.locationFactoryResolver`
- `RouterOutlet.locationInjector`
This commit is contained in:
Victor Berchet
2017-03-14 16:26:17 -07:00
committed by Chuck Jazdzewski
parent f093501501
commit 13686bb518
29 changed files with 627 additions and 242 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Injector} from '@angular/core';
import {Injector, NgModuleRef} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Observer} from 'rxjs/Observer';
import {from} from 'rxjs/observable/from';
@ -55,21 +55,24 @@ function canLoadFails(route: Route): Observable<LoadedRouterConfig> {
}
export function applyRedirects(
injector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer,
moduleInjector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer,
urlTree: UrlTree, config: Routes): Observable<UrlTree> {
return new ApplyRedirects(injector, configLoader, urlSerializer, urlTree, config).apply();
return new ApplyRedirects(moduleInjector, configLoader, urlSerializer, urlTree, config).apply();
}
class ApplyRedirects {
private allowRedirects: boolean = true;
private ngModule: NgModuleRef<any>;
constructor(
private injector: Injector, private configLoader: RouterConfigLoader,
private urlSerializer: UrlSerializer, private urlTree: UrlTree, private config: Routes) {}
moduleInjector: Injector, private configLoader: RouterConfigLoader,
private urlSerializer: UrlSerializer, private urlTree: UrlTree, private config: Routes) {
this.ngModule = moduleInjector.get(NgModuleRef);
}
apply(): Observable<UrlTree> {
const expanded$ =
this.expandSegmentGroup(this.injector, this.config, this.urlTree.root, PRIMARY_OUTLET);
this.expandSegmentGroup(this.ngModule, this.config, this.urlTree.root, PRIMARY_OUTLET);
const urlTrees$ = map.call(
expanded$, (rootSegmentGroup: UrlSegmentGroup) => this.createUrlTree(
rootSegmentGroup, this.urlTree.queryParams, this.urlTree.fragment));
@ -91,7 +94,7 @@ class ApplyRedirects {
private match(tree: UrlTree): Observable<UrlTree> {
const expanded$ =
this.expandSegmentGroup(this.injector, this.config, tree.root, PRIMARY_OUTLET);
this.expandSegmentGroup(this.ngModule, this.config, tree.root, PRIMARY_OUTLET);
const mapped$ = map.call(
expanded$, (rootSegmentGroup: UrlSegmentGroup) =>
this.createUrlTree(rootSegmentGroup, tree.queryParams, tree.fragment));
@ -117,31 +120,33 @@ class ApplyRedirects {
}
private expandSegmentGroup(
injector: Injector, routes: Route[], segmentGroup: UrlSegmentGroup,
ngModule: NgModuleRef<any>, routes: Route[], segmentGroup: UrlSegmentGroup,
outlet: string): Observable<UrlSegmentGroup> {
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
return map.call(
this.expandChildren(injector, routes, segmentGroup),
this.expandChildren(ngModule, routes, segmentGroup),
(children: any) => new UrlSegmentGroup([], children));
}
return this.expandSegment(injector, segmentGroup, routes, segmentGroup.segments, outlet, true);
return this.expandSegment(ngModule, segmentGroup, routes, segmentGroup.segments, outlet, true);
}
private expandChildren(injector: Injector, routes: Route[], segmentGroup: UrlSegmentGroup):
Observable<{[name: string]: UrlSegmentGroup}> {
private expandChildren(
ngModule: NgModuleRef<any>, routes: Route[],
segmentGroup: UrlSegmentGroup): Observable<{[name: string]: UrlSegmentGroup}> {
return waitForMap(
segmentGroup.children,
(childOutlet, child) => this.expandSegmentGroup(injector, routes, child, childOutlet));
(childOutlet, child) => this.expandSegmentGroup(ngModule, routes, child, childOutlet));
}
private expandSegment(
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], segments: UrlSegment[],
outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
ngModule: NgModuleRef<any>, segmentGroup: UrlSegmentGroup, routes: Route[],
segments: UrlSegment[], outlet: string,
allowRedirects: boolean): Observable<UrlSegmentGroup> {
const routes$ = of (...routes);
const processedRoutes$ = map.call(routes$, (r: any) => {
const expanded$ = this.expandSegmentAgainstRoute(
injector, segmentGroup, routes, r, segments, outlet, allowRedirects);
ngModule, segmentGroup, routes, r, segments, outlet, allowRedirects);
return _catch.call(expanded$, (e: any) => {
if (e instanceof NoMatch) {
return of (null);
@ -171,7 +176,7 @@ class ApplyRedirects {
}
private expandSegmentAgainstRoute(
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
ngModule: NgModuleRef<any>, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
paths: UrlSegment[], outlet: string, allowRedirects: boolean): Observable<UrlSegmentGroup> {
if (getOutlet(route) !== outlet) {
return noMatch(segmentGroup);
@ -182,27 +187,27 @@ class ApplyRedirects {
}
if (route.redirectTo === undefined) {
return this.matchSegmentAgainstRoute(injector, segmentGroup, route, paths);
return this.matchSegmentAgainstRoute(ngModule, segmentGroup, route, paths);
}
return this.expandSegmentAgainstRouteUsingRedirect(
injector, segmentGroup, routes, route, paths, outlet);
ngModule, segmentGroup, routes, route, paths, outlet);
}
private expandSegmentAgainstRouteUsingRedirect(
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
ngModule: NgModuleRef<any>, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
if (route.path === '**') {
return this.expandWildCardWithParamsAgainstRouteUsingRedirect(
injector, routes, route, outlet);
ngModule, routes, route, outlet);
}
return this.expandRegularSegmentAgainstRouteUsingRedirect(
injector, segmentGroup, routes, route, segments, outlet);
ngModule, segmentGroup, routes, route, segments, outlet);
}
private expandWildCardWithParamsAgainstRouteUsingRedirect(
injector: Injector, routes: Route[], route: Route,
ngModule: NgModuleRef<any>, routes: Route[], route: Route,
outlet: string): Observable<UrlSegmentGroup> {
const newTree = this.applyRedirectCommands([], route.redirectTo, {});
if (route.redirectTo.startsWith('/')) {
@ -211,12 +216,12 @@ class ApplyRedirects {
return mergeMap.call(this.lineralizeSegments(route, newTree), (newSegments: UrlSegment[]) => {
const group = new UrlSegmentGroup(newSegments, {});
return this.expandSegment(injector, group, routes, newSegments, outlet, false);
return this.expandSegment(ngModule, group, routes, newSegments, outlet, false);
});
}
private expandRegularSegmentAgainstRouteUsingRedirect(
injector: Injector, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
ngModule: NgModuleRef<any>, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route,
segments: UrlSegment[], outlet: string): Observable<UrlSegmentGroup> {
const {matched, consumedSegments, lastChild, positionalParamSegments} =
match(segmentGroup, route, segments);
@ -230,20 +235,21 @@ class ApplyRedirects {
return mergeMap.call(this.lineralizeSegments(route, newTree), (newSegments: UrlSegment[]) => {
return this.expandSegment(
injector, segmentGroup, routes, newSegments.concat(segments.slice(lastChild)), outlet,
ngModule, segmentGroup, routes, newSegments.concat(segments.slice(lastChild)), outlet,
false);
});
}
private matchSegmentAgainstRoute(
injector: Injector, rawSegmentGroup: UrlSegmentGroup, route: Route,
ngModule: NgModuleRef<any>, rawSegmentGroup: UrlSegmentGroup, route: Route,
segments: UrlSegment[]): Observable<UrlSegmentGroup> {
if (route.path === '**') {
if (route.loadChildren) {
return map.call(this.configLoader.load(injector, route), (cfg: LoadedRouterConfig) => {
(<any>route)._loadedConfig = cfg;
return new UrlSegmentGroup(segments, {});
});
return map.call(
this.configLoader.load(ngModule.injector, route), (cfg: LoadedRouterConfig) => {
(<any>route)._loadedConfig = cfg;
return new UrlSegmentGroup(segments, {});
});
}
return of (new UrlSegmentGroup(segments, {}));
@ -253,15 +259,17 @@ class ApplyRedirects {
if (!matched) return noMatch(rawSegmentGroup);
const rawSlicedSegments = segments.slice(lastChild);
const childConfig$ = this.getChildConfig(injector, route);
const childConfig$ = this.getChildConfig(ngModule, route);
return mergeMap.call(childConfig$, (routerConfig: LoadedRouterConfig) => {
const childInjector = routerConfig.injector;
const childModule = routerConfig.module;
const childConfig = routerConfig.routes;
const {segmentGroup, slicedSegments} =
split(rawSegmentGroup, consumedSegments, rawSlicedSegments, childConfig);
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
const expanded$ = this.expandChildren(childInjector, childConfig, segmentGroup);
const expanded$ = this.expandChildren(childModule, childConfig, segmentGroup);
return map.call(
expanded$, (children: any) => new UrlSegmentGroup(consumedSegments, children));
}
@ -271,35 +279,37 @@ class ApplyRedirects {
}
const expanded$ = this.expandSegment(
childInjector, segmentGroup, childConfig, slicedSegments, PRIMARY_OUTLET, true);
childModule, segmentGroup, childConfig, slicedSegments, PRIMARY_OUTLET, true);
return map.call(
expanded$, (cs: UrlSegmentGroup) =>
new UrlSegmentGroup(consumedSegments.concat(cs.segments), cs.children));
});
}
private getChildConfig(injector: Injector, route: Route): Observable<LoadedRouterConfig> {
private getChildConfig(ngModule: NgModuleRef<any>, route: Route): Observable<LoadedRouterConfig> {
if (route.children) {
return of (new LoadedRouterConfig(route.children, injector, null, null));
// The children belong to the same module
return of (new LoadedRouterConfig(route.children, ngModule));
}
if (route.loadChildren) {
return mergeMap.call(runGuards(injector, route), (shouldLoad: any) => {
return mergeMap.call(runGuards(ngModule.injector, route), (shouldLoad: any) => {
if (shouldLoad) {
return (<any>route)._loadedConfig ?
of ((<any>route)._loadedConfig) :
map.call(this.configLoader.load(injector, route), (cfg: LoadedRouterConfig) => {
(<any>route)._loadedConfig = cfg;
return cfg;
});
map.call(
this.configLoader.load(ngModule.injector, route), (cfg: LoadedRouterConfig) => {
(<any>route)._loadedConfig = cfg;
return cfg;
});
}
return canLoadFails(route);
});
}
return of (new LoadedRouterConfig([], injector, null, null));
return of (new LoadedRouterConfig([], ngModule));
}
private lineralizeSegments(route: Route, urlTree: UrlTree): Observable<UrlSegment[]> {
@ -386,12 +396,12 @@ class ApplyRedirects {
}
}
function runGuards(injector: Injector, route: Route): Observable<boolean> {
function runGuards(moduleInjector: Injector, route: Route): Observable<boolean> {
const canLoad = route.canLoad;
if (!canLoad || canLoad.length === 0) return of (true);
const obs = map.call(from(canLoad), (c: any) => {
const guard = injector.get(c);
const guard = moduleInjector.get(c);
return wrapIntoObservable(guard.canLoad ? guard.canLoad(route) : guard(route));
});

View File

@ -52,7 +52,9 @@ export class RouterOutlet implements OnDestroy {
ngOnDestroy(): void { this.parentOutletMap.removeOutlet(this.name ? this.name : PRIMARY_OUTLET); }
/** @deprecated since v4 **/
get locationInjector(): Injector { return this.location.injector; }
/** @deprecated since v4 **/
get locationFactoryResolver(): ComponentFactoryResolver { return this.resolver; }
get isActivated(): boolean { return !!this.activated; }
@ -90,6 +92,7 @@ export class RouterOutlet implements OnDestroy {
}
}
/** @deprecated since v4, use {@link activateWith} */
activate(
activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector,
providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void {
@ -105,9 +108,51 @@ export class RouterOutlet implements OnDestroy {
const factory = resolver.resolveComponentFactory(component);
const inj = ReflectiveInjector.fromResolvedProviders(providers, injector);
this.activated = this.location.createComponent(factory, this.location.length, inj, []);
this.activated.changeDetectorRef.detectChanges();
this.activateEvents.emit(this.activated.instance);
}
activateWith(
activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver|null,
outletMap: RouterOutletMap) {
if (this.isActivated) {
throw new Error('Cannot activate an already activated outlet');
}
this.outletMap = outletMap;
this._activatedRoute = activatedRoute;
const snapshot = activatedRoute._futureSnapshot;
const component = <any>snapshot._routeConfig.component;
resolver = resolver || this.resolver;
const factory = resolver.resolveComponentFactory(component);
const injector = new OutletInjector(activatedRoute, outletMap, this.location.injector);
this.activated = this.location.createComponent(factory, this.location.length, injector, []);
this.activated.changeDetectorRef.detectChanges();
this.activateEvents.emit(this.activated.instance);
}
}
class OutletInjector implements Injector {
constructor(
private route: ActivatedRoute, private map: RouterOutletMap, private parent: Injector) {}
get(token: any, notFoundValue?: any): any {
if (token === ActivatedRoute) {
return this.route;
}
if (token === RouterOutletMap) {
return this.map;
}
return this.parent.get(token, notFoundValue);
}
}

View File

@ -7,7 +7,7 @@
*/
import {Location} from '@angular/common';
import {Compiler, ComponentFactoryResolver, Injector, NgModuleFactoryLoader, ReflectiveInjector, Type, isDevMode} from '@angular/core';
import {Compiler, Injector, NgModuleFactoryLoader, NgModuleRef, Type, isDevMode} from '@angular/core';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {Observable} from 'rxjs/Observable';
import {Subject} from 'rxjs/Subject';
@ -225,6 +225,7 @@ export class Router {
private locationSubscription: Subscription;
private navigationId: number = 0;
private configLoader: RouterConfigLoader;
private ngModule: NgModuleRef<any>;
/**
* Error handler that is invoked when a navigation errors.
@ -263,11 +264,13 @@ export class Router {
// TODO: vsavkin make internal after the final is out.
constructor(
private rootComponentType: Type<any>, private urlSerializer: UrlSerializer,
private outletMap: RouterOutletMap, private location: Location, private injector: Injector,
private outletMap: RouterOutletMap, private location: Location, injector: Injector,
loader: NgModuleFactoryLoader, compiler: Compiler, public config: Routes) {
const onLoadStart = (r: Route) => this.triggerEvent(new RouteConfigLoadStart(r));
const onLoadEnd = (r: Route) => this.triggerEvent(new RouteConfigLoadEnd(r));
this.ngModule = injector.get(NgModuleRef);
this.resetConfig(config);
this.currentUrlTree = createEmptyUrlTree();
this.rawUrlTree = this.currentUrlTree;
@ -607,8 +610,9 @@ export class Router {
// this operation do not result in any side effects
let urlAndSnapshot$: Observable<{appliedUrl: UrlTree, snapshot: RouterStateSnapshot}>;
if (!precreatedState) {
const moduleInjector = this.ngModule.injector;
const redirectsApplied$ =
applyRedirects(this.injector, this.configLoader, this.urlSerializer, url, this.config);
applyRedirects(moduleInjector, this.configLoader, this.urlSerializer, url, this.config);
urlAndSnapshot$ = mergeMap.call(redirectsApplied$, (appliedUrl: UrlTree) => {
return map.call(
@ -636,8 +640,9 @@ export class Router {
const preactivationTraverse$ = map.call(
beforePreactivationDone$,
({appliedUrl, snapshot}: {appliedUrl: string, snapshot: RouterStateSnapshot}) => {
const moduleInjector = this.ngModule.injector;
preActivation =
new PreActivation(snapshot, this.currentRouterState.snapshot, this.injector);
new PreActivation(snapshot, this.currentRouterState.snapshot, moduleInjector);
preActivation.traverse(this.outletMap);
return {appliedUrl, snapshot};
});
@ -771,7 +776,7 @@ export class PreActivation {
private checks: Array<CanActivate|CanDeactivate> = [];
constructor(
private future: RouterStateSnapshot, private curr: RouterStateSnapshot,
private injector: Injector) {}
private moduleInjector: Injector) {}
traverse(parentOutletMap: RouterOutletMap): void {
const futureRoot = this.future._root;
@ -991,7 +996,7 @@ export class PreActivation {
private getToken(token: any, snapshot: ActivatedRouteSnapshot): any {
const config = closestLoadedConfig(snapshot);
const injector = config ? config.injector : this.injector;
const injector = config ? config.module.injector : this.moduleInjector;
return injector.get(token);
}
}
@ -1102,26 +1107,10 @@ class ActivateRoutes {
private placeComponentIntoOutlet(
outletMap: RouterOutletMap, future: ActivatedRoute, outlet: RouterOutlet): void {
const resolved = <any[]>[{provide: ActivatedRoute, useValue: future}, {
provide: RouterOutletMap,
useValue: outletMap
}];
const config = parentLoadedConfig(future.snapshot);
const cmpFactoryResolver = config ? config.module.componentFactoryResolver : null;
let resolver: ComponentFactoryResolver = null;
let injector: Injector = null;
if (config) {
injector = config.injectorFactory(outlet.locationInjector);
resolver = config.factoryResolver;
resolved.push({provide: ComponentFactoryResolver, useValue: resolver});
} else {
injector = outlet.locationInjector;
resolver = outlet.locationFactoryResolver;
}
outlet.activate(future, resolver, injector, ReflectiveInjector.resolve(resolved), outletMap);
outlet.activateWith(future, cmpFactoryResolver, outletMap);
}
private deactiveRouteAndItsChildren(

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Compiler, ComponentFactoryResolver, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoader} from '@angular/core';
import {Compiler, InjectionToken, Injector, NgModuleFactory, NgModuleFactoryLoader, NgModuleRef} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {fromPromise} from 'rxjs/observable/fromPromise';
import {of } from 'rxjs/observable/of';
@ -22,9 +22,7 @@ import {flatten, wrapIntoObservable} from './utils/collection';
export const ROUTES = new InjectionToken<Route[][]>('ROUTES');
export class LoadedRouterConfig {
constructor(
public routes: Route[], public injector: Injector,
public factoryResolver: ComponentFactoryResolver, public injectorFactory: Function) {}
constructor(public routes: Route[], public module: NgModuleRef<any>) {}
}
export class RouterConfigLoader {
@ -46,11 +44,8 @@ export class RouterConfigLoader {
}
const module = factory.create(parentInjector);
const injectorFactory = (parent: Injector) => factory.create(parent).injector;
return new LoadedRouterConfig(
flatten(module.injector.get(ROUTES)), module.injector, module.componentFactoryResolver,
injectorFactory);
return new LoadedRouterConfig(flatten(module.injector.get(ROUTES)), module);
});
}

View File

@ -6,7 +6,7 @@
*found in the LICENSE file at https://angular.io/license
*/
import {Compiler, Injectable, Injector, NgModuleFactoryLoader} from '@angular/core';
import {Compiler, Injectable, Injector, NgModuleFactoryLoader, NgModuleRef} from '@angular/core';
import {Observable} from 'rxjs/Observable';
import {Subscription} from 'rxjs/Subscription';
import {from} from 'rxjs/observable/from';
@ -91,37 +91,40 @@ export class RouterPreloader {
this.subscription = concatMap.call(navigations, () => this.preload()).subscribe(() => {});
}
preload(): Observable<any> { return this.processRoutes(this.injector, this.router.config); }
preload(): Observable<any> {
const ngModule = this.injector.get(NgModuleRef);
return this.processRoutes(ngModule, this.router.config);
}
ngOnDestroy(): void { this.subscription.unsubscribe(); }
private processRoutes(injector: Injector, routes: Routes): Observable<void> {
private processRoutes(ngModule: NgModuleRef<any>, routes: Routes): Observable<void> {
const res: Observable<any>[] = [];
for (const c of routes) {
// we already have the config loaded, just recurse
if (c.loadChildren && !c.canLoad && (<any>c)._loadedConfig) {
const childConfig = (<any>c)._loadedConfig;
res.push(this.processRoutes(childConfig.injector, childConfig.routes));
res.push(this.processRoutes(childConfig.module, childConfig.routes));
// no config loaded, fetch the config
} else if (c.loadChildren && !c.canLoad) {
res.push(this.preloadConfig(injector, c));
res.push(this.preloadConfig(ngModule, c));
// recurse into children
} else if (c.children) {
res.push(this.processRoutes(injector, c.children));
res.push(this.processRoutes(ngModule, c.children));
}
}
return mergeAll.call(from(res));
}
private preloadConfig(injector: Injector, route: Route): Observable<void> {
private preloadConfig(ngModule: NgModuleRef<any>, route: Route): Observable<void> {
return this.preloadingStrategy.preload(route, () => {
const loaded = this.loader.load(injector, route);
const loaded = this.loader.load(ngModule.injector, route);
return mergeMap.call(loaded, (config: any): any => {
const c: any = route;
c._loadedConfig = config;
return this.processRoutes(config.injector, config.routes);
return this.processRoutes(config.module, config.routes);
});
});
}

View File

@ -6,6 +6,8 @@
* found in the LICENSE file at https://angular.io/license
*/
import {NgModuleRef} from '@angular/core';
import {TestBed} from '@angular/core/testing';
import {Observable} from 'rxjs/Observable';
import {of } from 'rxjs/observable/of';
@ -16,10 +18,21 @@ import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree, equalSegments} from '../
describe('applyRedirects', () => {
const serializer = new DefaultUrlSerializer();
let testModule: NgModuleRef<any>;
beforeEach(() => { testModule = TestBed.get(NgModuleRef); });
it('should return the same url tree when no redirects', () => {
checkRedirect(
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}],
[
{
path: 'a',
component: ComponentA,
children: [
{path: 'b', component: ComponentB},
],
},
],
'/a/b', (t: UrlTree) => { compareTrees(t, tree('/a/b')); });
});
@ -39,7 +52,7 @@ describe('applyRedirects', () => {
});
it('should throw when cannot handle a positional parameter', () => {
applyRedirects(null, null, serializer, tree('/a/1'), [
applyRedirects(testModule.injector, null, serializer, tree('/a/1'), [
{path: 'a/:id', redirectTo: 'a/:other'}
]).subscribe(() => {}, (e) => {
expect(e.message).toEqual('Cannot redirect to \'a/:other\'. Cannot find \':other\'.');
@ -143,18 +156,16 @@ describe('applyRedirects', () => {
describe('lazy loading', () => {
it('should load config on demand', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loadedConfig = new LoadedRouterConfig([{path: 'b', component: ComponentB}], testModule);
const loader = {
load: (injector: any, p: any) => {
if (injector !== 'providedInjector') throw 'Invalid Injector';
if (injector !== testModule.injector) throw 'Invalid Injector';
return of (loadedConfig);
}
};
const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a/b'), config)
applyRedirects(testModule.injector, <any>loader, serializer, tree('a/b'), config)
.forEach(r => {
compareTrees(r, tree('/a/b'));
expect((<any>config[0])._loadedConfig).toBe(loadedConfig);
@ -167,18 +178,18 @@ describe('applyRedirects', () => {
};
const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}];
applyRedirects(null, <any>loader, serializer, tree('a/b'), config)
applyRedirects(testModule.injector, <any>loader, serializer, tree('a/b'), config)
.subscribe(() => {}, (e) => { expect(e.message).toEqual('Loading Error'); });
});
it('should load when all canLoad guards return true', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loadedConfig = new LoadedRouterConfig([{path: 'b', component: ComponentB}], testModule);
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const guard = () => true;
const injector = {get: () => guard};
const injector = {
get: (token: any) => token === 'guard1' || token === 'guard2' ? guard : {injector}
};
const config = [{
path: 'a',
@ -193,14 +204,23 @@ describe('applyRedirects', () => {
});
it('should not load when any canLoad guards return false', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loadedConfig = new LoadedRouterConfig([{path: 'b', component: ComponentB}], testModule);
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const trueGuard = () => true;
const falseGuard = () => false;
const injector = {get: (guardName: any) => guardName === 'guard1' ? trueGuard : falseGuard};
const injector = {
get: (token: any) => {
switch (token) {
case 'guard1':
return trueGuard;
case 'guard2':
return falseGuard;
case NgModuleRef:
return {injector};
}
}
};
const config = [{
path: 'a',
@ -219,14 +239,23 @@ describe('applyRedirects', () => {
});
it('should not load when any canLoad guards is rejected (promises)', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loadedConfig = new LoadedRouterConfig([{path: 'b', component: ComponentB}], testModule);
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const trueGuard = () => Promise.resolve(true);
const falseGuard = () => Promise.reject('someError');
const injector = {get: (guardName: any) => guardName === 'guard1' ? trueGuard : falseGuard};
const injector = {
get: (token: any) => {
switch (token) {
case 'guard1':
return trueGuard;
case 'guard2':
return falseGuard;
case NgModuleRef:
return {injector};
}
}
};
const config = [{
path: 'a',
@ -241,13 +270,11 @@ describe('applyRedirects', () => {
});
it('should work with objects implementing the CanLoad interface', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: 'b', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loadedConfig = new LoadedRouterConfig([{path: 'b', component: ComponentB}], testModule);
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const guard = {canLoad: () => Promise.resolve(true)};
const injector = {get: () => guard};
const injector = {get: (token: any) => token === 'guard' ? guard : {injector}};
const config =
[{path: 'a', component: ComponentA, canLoad: ['guard'], loadChildren: 'children'}];
@ -259,26 +286,21 @@ describe('applyRedirects', () => {
});
it('should work with absolute redirects', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loadedConfig = new LoadedRouterConfig([{path: '', component: ComponentB}], testModule);
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const config =
[{path: '', pathMatch: 'full', redirectTo: '/a'}, {path: 'a', loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree(''), config)
.forEach(r => {
compareTrees(r, tree('a'));
expect((<any>config[1])._loadedConfig).toBe(loadedConfig);
});
applyRedirects(testModule.injector, <any>loader, serializer, tree(''), config).forEach(r => {
compareTrees(r, tree('a'));
expect((<any>config[1])._loadedConfig).toBe(loadedConfig);
});
});
it('should load the configuration only once', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loadedConfig = new LoadedRouterConfig([{path: '', component: ComponentB}], testModule);
let called = false;
const loader = {
@ -291,10 +313,10 @@ describe('applyRedirects', () => {
const config = [{path: 'a', loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a?k1'), config)
applyRedirects(testModule.injector, <any>loader, serializer, tree('a?k1'), config)
.subscribe(r => {});
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('a?k2'), config)
applyRedirects(testModule.injector, <any>loader, serializer, tree('a?k2'), config)
.subscribe(
r => {
compareTrees(r, tree('a?k2'));
@ -304,43 +326,37 @@ describe('applyRedirects', () => {
});
it('should load the configuration of a wildcard route', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loadedConfig = new LoadedRouterConfig([{path: '', component: ComponentB}], testModule);
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const config = [{path: '**', loadChildren: 'children'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
applyRedirects(testModule.injector, <any>loader, serializer, tree('xyz'), config)
.forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
});
it('should load the configuration after a local redirect from a wildcard route', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loadedConfig = new LoadedRouterConfig([{path: '', component: ComponentB}], testModule);
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const config =
[{path: 'not-found', loadChildren: 'children'}, {path: '**', redirectTo: 'not-found'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
applyRedirects(testModule.injector, <any>loader, serializer, tree('xyz'), config)
.forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
});
it('should load the configuration after an absolute redirect from a wildcard route', () => {
const loadedConfig = new LoadedRouterConfig(
[{path: '', component: ComponentB}], <any>'stubInjector', <any>'stubFactoryResolver',
<any>'injectorFactory');
const loadedConfig = new LoadedRouterConfig([{path: '', component: ComponentB}], testModule);
const loader = {load: (injector: any, p: any) => of (loadedConfig)};
const config =
[{path: 'not-found', loadChildren: 'children'}, {path: '**', redirectTo: '/not-found'}];
applyRedirects(<any>'providedInjector', <any>loader, serializer, tree('xyz'), config)
applyRedirects(testModule.injector, <any>loader, serializer, tree('xyz'), config)
.forEach(r => { expect((<any>config[0])._loadedConfig).toBe(loadedConfig); });
});
});
@ -388,7 +404,7 @@ describe('applyRedirects', () => {
{path: '', redirectTo: 'a', pathMatch: 'full'}
];
applyRedirects(null, null, serializer, tree('b'), config)
applyRedirects(testModule.injector, null, serializer, tree('b'), config)
.subscribe(
(_) => { throw 'Should not be reached'; },
e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'b\''); });
@ -518,7 +534,7 @@ describe('applyRedirects', () => {
]
}];
applyRedirects(null, null, serializer, tree('a/(d//aux:e)'), config)
applyRedirects(testModule.injector, null, serializer, tree('a/(d//aux:e)'), config)
.subscribe(
(_) => { throw 'Should not be reached'; },
e => { expect(e.message).toEqual('Cannot match any routes. URL Segment: \'a\''); });
@ -549,7 +565,7 @@ describe('applyRedirects', () => {
it('should error when no children matching and some url is left', () => {
applyRedirects(
null, null, serializer, tree('/a/c'),
testModule.injector, null, serializer, tree('/a/c'),
[{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}])
.subscribe(
(_) => { throw 'Should not be reached'; },
@ -599,7 +615,7 @@ describe('applyRedirects', () => {
it('should throw when using non-absolute redirects', () => {
applyRedirects(
null, null, serializer, tree('a'),
testModule.injector, null, serializer, tree('a'),
[
{path: 'a', redirectTo: 'b(aux:c)'},
])
@ -614,7 +630,7 @@ describe('applyRedirects', () => {
});
function checkRedirect(config: Routes, url: string, callback: any): void {
applyRedirects(null, null, new DefaultUrlSerializer(), tree(url), config)
applyRedirects(TestBed, null, new DefaultUrlSerializer(), tree(url), config)
.subscribe(callback, e => { throw e; });
}

View File

@ -7,7 +7,7 @@
*/
import {CommonModule, Location} from '@angular/common';
import {Component, NgModule, NgModuleFactoryLoader} from '@angular/core';
import {Component, Inject, Injectable, NgModule, NgModuleFactoryLoader, NgModuleRef} from '@angular/core';
import {ComponentFixture, TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
import {By} from '@angular/platform-browser/src/dom/debug/by';
import {expect} from '@angular/platform-browser/testing/src/matchers';
@ -2422,6 +2422,184 @@ describe('Integration', () => {
expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]');
})));
it('should have 2 injector trees: module and element',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
@Component({
selector: 'lazy',
template: 'parent[<router-outlet></router-outlet>]',
viewProviders: [
{provide: 'shadow', useValue: 'from parent component'},
],
})
class Parent {
}
@Component({selector: 'lazy', template: 'child'})
class Child {
}
@NgModule({
declarations: [Parent],
imports: [RouterModule.forChild([{
path: 'parent',
component: Parent,
children: [
{path: 'child', loadChildren: 'child'},
]
}])],
providers: [
{provide: 'moduleName', useValue: 'parent'},
{provide: 'fromParent', useValue: 'from parent'},
],
})
class ParentModule {
}
@NgModule({
declarations: [Child],
imports: [RouterModule.forChild([{path: '', component: Child}])],
providers: [
{provide: 'moduleName', useValue: 'child'},
{provide: 'fromChild', useValue: 'from child'},
{provide: 'shadow', useValue: 'from child module'},
],
})
class ChildModule {
}
loader.stubbedModules = {
parent: ParentModule,
child: ChildModule,
};
const fixture = createRoot(router, RootCmp);
router.resetConfig([{path: 'lazy', loadChildren: 'parent'}]);
router.navigateByUrl('/lazy/parent/child');
advance(fixture);
expect(location.path()).toEqual('/lazy/parent/child');
expect(fixture.nativeElement).toHaveText('parent[child]');
const pInj = fixture.debugElement.query(By.directive(Parent)).injector;
const cInj = fixture.debugElement.query(By.directive(Child)).injector;
expect(pInj.get('moduleName')).toEqual('parent');
expect(pInj.get('fromParent')).toEqual('from parent');
expect(pInj.get(Parent)).toBeAnInstanceOf(Parent);
expect(pInj.get('fromChild', null)).toEqual(null);
expect(pInj.get(Child, null)).toEqual(null);
expect(cInj.get('moduleName')).toEqual('child');
expect(cInj.get('fromParent')).toEqual('from parent');
expect(cInj.get('fromChild')).toEqual('from child');
expect(cInj.get(Parent)).toBeAnInstanceOf(Parent);
expect(cInj.get(Child)).toBeAnInstanceOf(Child);
// The child module can not shadow the parent component
expect(cInj.get('shadow')).toEqual('from parent component');
const pmInj = pInj.get(NgModuleRef).injector;
const cmInj = cInj.get(NgModuleRef).injector;
expect(pmInj.get('moduleName')).toEqual('parent');
expect(cmInj.get('moduleName')).toEqual('child');
expect(pmInj.get(Parent, '-')).toEqual('-');
expect(cmInj.get(Parent, '-')).toEqual('-');
expect(pmInj.get(Child, '-')).toEqual('-');
expect(cmInj.get(Child, '-')).toEqual('-');
})));
// https://github.com/angular/angular/issues/12889
it('should create a single instance of lazy-loaded modules',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
@Component({
selector: 'lazy',
template: 'lazy-loaded-parent [<router-outlet></router-outlet>]'
})
class ParentLazyLoadedComponent {
}
@Component({selector: 'lazy', template: 'lazy-loaded-child'})
class ChildLazyLoadedComponent {
}
@NgModule({
declarations: [ParentLazyLoadedComponent, ChildLazyLoadedComponent],
imports: [RouterModule.forChild([{
path: 'loaded',
component: ParentLazyLoadedComponent,
children: [{path: 'child', component: ChildLazyLoadedComponent}]
}])]
})
class LoadedModule {
static instances = 0;
constructor() { LoadedModule.instances++; }
}
loader.stubbedModules = {expected: LoadedModule};
const fixture = createRoot(router, RootCmp);
router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]);
router.navigateByUrl('/lazy/loaded/child');
advance(fixture);
expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]');
expect(LoadedModule.instances).toEqual(1);
})));
// https://github.com/angular/angular/issues/13870
it('should create a single instance of guards for lazy-loaded modules',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
@Injectable()
class Service {
}
@Injectable()
class Resolver implements Resolve<Service> {
constructor(public service: Service) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.service;
}
}
@Component({selector: 'lazy', template: 'lazy'})
class LazyLoadedComponent {
resolvedService: Service;
constructor(public injectedService: Service, route: ActivatedRoute) {
this.resolvedService = route.snapshot.data['service'];
}
}
@NgModule({
declarations: [LazyLoadedComponent],
providers: [Service, Resolver],
imports: [
RouterModule.forChild([{
path: 'loaded',
component: LazyLoadedComponent,
resolve: {'service': Resolver},
}]),
]
})
class LoadedModule {
}
loader.stubbedModules = {expected: LoadedModule};
const fixture = createRoot(router, RootCmp);
router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]);
router.navigateByUrl('/lazy/loaded');
advance(fixture);
expect(fixture.nativeElement).toHaveText('lazy');
const lzc =
fixture.debugElement.query(By.directive(LazyLoadedComponent)).componentInstance;
expect(lzc.injectedService).toBe(lzc.resolvedService);
})));
it('should emit RouteConfigLoadStart and RouteConfigLoadEnd event when route is lazy loaded',
fakeAsync(inject(
[Router, Location, NgModuleFactoryLoader],
@ -2547,7 +2725,6 @@ describe('Integration', () => {
describe('should use the injector of the lazily-loaded configuration', () => {
class LazyLoadedServiceDefinedInModule {}
class LazyLoadedServiceDefinedInCmp {}
@Component({
selector: 'eager-parent',
@ -2556,11 +2733,17 @@ describe('Integration', () => {
class EagerParentComponent {
}
@Component({selector: 'lazy-parent', template: 'lazy-parent <router-outlet></router-outlet>'})
@Component({
selector: 'lazy-parent',
template: 'lazy-parent <router-outlet></router-outlet>',
})
class LazyParentComponent {
}
@Component({selector: 'lazy-child', template: 'lazy-child'})
@Component({
selector: 'lazy-child',
template: 'lazy-child',
})
class LazyChildComponent {
constructor(
lazy: LazyParentComponent, // should be able to inject lazy/direct parent
@ -2593,7 +2776,11 @@ describe('Integration', () => {
class TestModule {
}
beforeEach(() => { TestBed.configureTestingModule({imports: [TestModule]}); });
beforeEach(() => {
TestBed.configureTestingModule({
imports: [TestModule],
});
});
it('should use the injector of the lazily-loaded configuration',
fakeAsync(inject(