diff --git a/src/apply_redirects.ts b/src/apply_redirects.ts index c32253f46c5de..a08f1fea730f3 100644 --- a/src/apply_redirects.ts +++ b/src/apply_redirects.ts @@ -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 { } export function applyRedirects( - injector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer, + moduleInjector: Injector, configLoader: RouterConfigLoader, urlSerializer: UrlSerializer, urlTree: UrlTree, config: Routes): Observable { - 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; 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 { 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 { 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, routes: Route[], segmentGroup: UrlSegmentGroup, outlet: string): Observable { 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, 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 { + ngModule: NgModuleRef, segmentGroup: UrlSegmentGroup, routes: Route[], + segments: UrlSegment[], outlet: string, + allowRedirects: boolean): Observable { 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, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route, paths: UrlSegment[], outlet: string, allowRedirects: boolean): Observable { 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, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route, segments: UrlSegment[], outlet: string): Observable { 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, routes: Route[], route: Route, outlet: string): Observable { 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, segmentGroup: UrlSegmentGroup, routes: Route[], route: Route, segments: UrlSegment[], outlet: string): Observable { 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, rawSegmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): Observable { if (route.path === '**') { if (route.loadChildren) { - return map.call(this.configLoader.load(injector, route), (cfg: LoadedRouterConfig) => { - (route)._loadedConfig = cfg; - return new UrlSegmentGroup(segments, {}); - }); + return map.call( + this.configLoader.load(ngModule.injector, route), (cfg: LoadedRouterConfig) => { + (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 { + private getChildConfig(ngModule: NgModuleRef, route: Route): Observable { 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 (route)._loadedConfig ? of ((route)._loadedConfig) : - map.call(this.configLoader.load(injector, route), (cfg: LoadedRouterConfig) => { - (route)._loadedConfig = cfg; - return cfg; - }); + map.call( + this.configLoader.load(ngModule.injector, route), (cfg: LoadedRouterConfig) => { + (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 { @@ -386,12 +396,12 @@ class ApplyRedirects { } } -function runGuards(injector: Injector, route: Route): Observable { +function runGuards(moduleInjector: Injector, route: Route): Observable { 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)); }); diff --git a/src/directives/router_outlet.ts b/src/directives/router_outlet.ts index 49ba9db6616c5..435590b2e9aff 100644 --- a/src/directives/router_outlet.ts +++ b/src/directives/router_outlet.ts @@ -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 = 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); + } +} \ No newline at end of file diff --git a/src/router.ts b/src/router.ts index 3c13e92d82292..8fbd39045b6b6 100644 --- a/src/router.ts +++ b/src/router.ts @@ -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; /** * 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, 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 = []; 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 = [{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( diff --git a/src/router_config_loader.ts b/src/router_config_loader.ts index 0e598b8c54300..943ea6211131a 100644 --- a/src/router_config_loader.ts +++ b/src/router_config_loader.ts @@ -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('ROUTES'); export class LoadedRouterConfig { - constructor( - public routes: Route[], public injector: Injector, - public factoryResolver: ComponentFactoryResolver, public injectorFactory: Function) {} + constructor(public routes: Route[], public module: NgModuleRef) {} } 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); }); } diff --git a/src/router_preloader.ts b/src/router_preloader.ts index 15af8a87be6b3..ddc9b9dc2989d 100644 --- a/src/router_preloader.ts +++ b/src/router_preloader.ts @@ -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 { return this.processRoutes(this.injector, this.router.config); } + preload(): Observable { + 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 { + private processRoutes(ngModule: NgModuleRef, routes: Routes): Observable { const res: Observable[] = []; for (const c of routes) { // we already have the config loaded, just recurse if (c.loadChildren && !c.canLoad && (c)._loadedConfig) { const childConfig = (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 { + private preloadConfig(ngModule: NgModuleRef, route: Route): Observable { 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); }); }); } diff --git a/test/apply_redirects.spec.ts b/test/apply_redirects.spec.ts index 3e7afb8b387eb..4212f203f8d92 100644 --- a/test/apply_redirects.spec.ts +++ b/test/apply_redirects.spec.ts @@ -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; + + 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}], 'stubInjector', 'stubFactoryResolver', - '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('providedInjector', loader, serializer, tree('a/b'), config) + applyRedirects(testModule.injector, loader, serializer, tree('a/b'), config) .forEach(r => { compareTrees(r, tree('/a/b')); expect((config[0])._loadedConfig).toBe(loadedConfig); @@ -167,18 +178,18 @@ describe('applyRedirects', () => { }; const config = [{path: 'a', component: ComponentA, loadChildren: 'children'}]; - applyRedirects(null, loader, serializer, tree('a/b'), config) + applyRedirects(testModule.injector, 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}], 'stubInjector', 'stubFactoryResolver', - '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}], 'stubInjector', 'stubFactoryResolver', - '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}], 'stubInjector', 'stubFactoryResolver', - '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}], 'stubInjector', 'stubFactoryResolver', - '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}], 'stubInjector', 'stubFactoryResolver', - '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('providedInjector', loader, serializer, tree(''), config) - .forEach(r => { - compareTrees(r, tree('a')); - expect((config[1])._loadedConfig).toBe(loadedConfig); - }); + applyRedirects(testModule.injector, loader, serializer, tree(''), config).forEach(r => { + compareTrees(r, tree('a')); + expect((config[1])._loadedConfig).toBe(loadedConfig); + }); }); it('should load the configuration only once', () => { - const loadedConfig = new LoadedRouterConfig( - [{path: '', component: ComponentB}], 'stubInjector', 'stubFactoryResolver', - '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('providedInjector', loader, serializer, tree('a?k1'), config) + applyRedirects(testModule.injector, loader, serializer, tree('a?k1'), config) .subscribe(r => {}); - applyRedirects('providedInjector', loader, serializer, tree('a?k2'), config) + applyRedirects(testModule.injector, 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}], 'stubInjector', 'stubFactoryResolver', - 'injectorFactory'); + const loadedConfig = new LoadedRouterConfig([{path: '', component: ComponentB}], testModule); const loader = {load: (injector: any, p: any) => of (loadedConfig)}; const config = [{path: '**', loadChildren: 'children'}]; - applyRedirects('providedInjector', loader, serializer, tree('xyz'), config) + applyRedirects(testModule.injector, loader, serializer, tree('xyz'), config) .forEach(r => { expect((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}], 'stubInjector', 'stubFactoryResolver', - '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('providedInjector', loader, serializer, tree('xyz'), config) + applyRedirects(testModule.injector, loader, serializer, tree('xyz'), config) .forEach(r => { expect((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}], 'stubInjector', 'stubFactoryResolver', - '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('providedInjector', loader, serializer, tree('xyz'), config) + applyRedirects(testModule.injector, loader, serializer, tree('xyz'), config) .forEach(r => { expect((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; }); } diff --git a/test/integration.spec.ts b/test/integration.spec.ts index 9d459421cb5eb..7c509f878f5e3 100644 --- a/test/integration.spec.ts +++ b/test/integration.spec.ts @@ -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[]', + 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 []' + }) + 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 { + 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 '}) + @Component({ + selector: 'lazy-parent', + template: 'lazy-parent ', + }) 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(