diff --git a/modules/@angular/router/src/apply_redirects.ts b/modules/@angular/router/src/apply_redirects.ts index e16b4abf4865b1..d9659ddb809a42 100644 --- a/modules/@angular/router/src/apply_redirects.ts +++ b/modules/@angular/router/src/apply_redirects.ts @@ -264,7 +264,7 @@ class ApplyRedirects { private getChildConfig(injector: Injector, route: Route): Observable { if (route.children) { - return of (new LoadedRouterConfig(route.children, injector, null)); + return of (new LoadedRouterConfig(route.children, injector, null, null)); } else if (route.loadChildren) { return mergeMap.call(runGuards(injector, route), (shouldLoad: any) => { if (shouldLoad) { @@ -281,7 +281,7 @@ class ApplyRedirects { } }); } else { - return of (new LoadedRouterConfig([], injector, null)); + return of (new LoadedRouterConfig([], injector, null, null)); } } } diff --git a/modules/@angular/router/src/directives/router_outlet.ts b/modules/@angular/router/src/directives/router_outlet.ts index 1ed1706d184b76..6ae5675c69f233 100644 --- a/modules/@angular/router/src/directives/router_outlet.ts +++ b/modules/@angular/router/src/directives/router_outlet.ts @@ -54,6 +54,9 @@ export class RouterOutlet implements OnDestroy { ngOnDestroy(): void { this.parentOutletMap.removeOutlet(this.name ? this.name : PRIMARY_OUTLET); } + get locationInjector(): Injector { return this.location.injector; } + get locationFactoryResolver(): ComponentFactoryResolver { return this.resolver; } + get isActivated(): boolean { return !!this.activated; } get component(): Object { if (!this.activated) throw new Error('Outlet is not activated'); @@ -74,9 +77,8 @@ export class RouterOutlet implements OnDestroy { } activate( - activatedRoute: ActivatedRoute, loadedResolver: ComponentFactoryResolver, - loadedInjector: Injector, providers: ResolvedReflectiveProvider[], - outletMap: RouterOutletMap): void { + activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, + providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void { if (this.isActivated) { throw new Error('Cannot activate an already activated outlet'); } @@ -86,15 +88,8 @@ export class RouterOutlet implements OnDestroy { const snapshot = activatedRoute._futureSnapshot; const component: any = snapshot._routeConfig.component; + const factory = resolver.resolveComponentFactory(component); - let factory: ComponentFactory; - if (loadedResolver) { - factory = loadedResolver.resolveComponentFactory(component); - } else { - factory = this.resolver.resolveComponentFactory(component); - } - - const injector = loadedInjector ? loadedInjector : this.location.parentInjector; const inj = ReflectiveInjector.fromResolvedProviders(providers, injector); this.activated = this.location.createComponent(factory, this.location.length, inj, []); this.activated.changeDetectorRef.detectChanges(); diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index fd99c2f06975d0..2892daf7fc0da9 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -1095,17 +1095,19 @@ class ActivateRoutes { const config = parentLoadedConfig(future.snapshot); - let loadedFactoryResolver: ComponentFactoryResolver = null; - let loadedInjector: Injector = null; + let resolver: ComponentFactoryResolver = null; + let injector: Injector = null; if (config) { - loadedFactoryResolver = config.factoryResolver; - loadedInjector = config.injector; - resolved.push({provide: ComponentFactoryResolver, useValue: loadedFactoryResolver}); + injector = config.injectorFactory(outlet.locationInjector); + resolver = config.factoryResolver; + resolved.push({provide: ComponentFactoryResolver, useValue: resolver}); + } else { + injector = outlet.locationInjector; + resolver = outlet.locationFactoryResolver; } - outlet.activate( - future, loadedFactoryResolver, loadedInjector, ReflectiveInjector.resolve(resolved), - outletMap); + + outlet.activate(future, resolver, injector, ReflectiveInjector.resolve(resolved), outletMap); } private deactiveRouteAndItsChildren( diff --git a/modules/@angular/router/src/router_config_loader.ts b/modules/@angular/router/src/router_config_loader.ts index fccf355443467f..1643068ab69c89 100644 --- a/modules/@angular/router/src/router_config_loader.ts +++ b/modules/@angular/router/src/router_config_loader.ts @@ -24,17 +24,19 @@ export const ROUTES = new OpaqueToken('ROUTES'); export class LoadedRouterConfig { constructor( public routes: Route[], public injector: Injector, - public factoryResolver: ComponentFactoryResolver) {} + public factoryResolver: ComponentFactoryResolver, public injectorFactory: Function) {} } export class RouterConfigLoader { constructor(private loader: NgModuleFactoryLoader, private compiler: Compiler) {} load(parentInjector: Injector, loadChildren: LoadChildren): Observable { - return map.call(this.loadModuleFactory(loadChildren), (r: any) => { + return map.call(this.loadModuleFactory(loadChildren), (r: NgModuleFactory) => { const ref = r.create(parentInjector); + const injectorFactory = (parent: Injector) => r.create(parent).injector; return new LoadedRouterConfig( - flatten(ref.injector.get(ROUTES)), ref.injector, ref.componentFactoryResolver); + flatten(ref.injector.get(ROUTES)), ref.injector, ref.componentFactoryResolver, + injectorFactory); }); } diff --git a/modules/@angular/router/test/apply_redirects.spec.ts b/modules/@angular/router/test/apply_redirects.spec.ts index f1bcb081aa77fa..4068de35044bb3 100644 --- a/modules/@angular/router/test/apply_redirects.spec.ts +++ b/modules/@angular/router/test/apply_redirects.spec.ts @@ -143,7 +143,8 @@ describe('applyRedirects', () => { describe('lazy loading', () => { it('should load config on demand', () => { const loadedConfig = new LoadedRouterConfig( - [{path: 'b', component: ComponentB}], 'stubInjector', 'stubFactoryResolver'); + [{path: 'b', component: ComponentB}], 'stubInjector', 'stubFactoryResolver', + 'injectorFactory'); const loader = { load: (injector: any, p: any) => { if (injector !== 'providedInjector') throw 'Invalid Injector'; @@ -171,7 +172,8 @@ describe('applyRedirects', () => { it('should load when all canLoad guards return true', () => { const loadedConfig = new LoadedRouterConfig( - [{path: 'b', component: ComponentB}], 'stubInjector', 'stubFactoryResolver'); + [{path: 'b', component: ComponentB}], 'stubInjector', 'stubFactoryResolver', + 'injectorFactory'); const loader = {load: (injector: any, p: any) => of (loadedConfig)}; const guard = () => true; @@ -191,7 +193,8 @@ describe('applyRedirects', () => { it('should not load when any canLoad guards return false', () => { const loadedConfig = new LoadedRouterConfig( - [{path: 'b', component: ComponentB}], 'stubInjector', 'stubFactoryResolver'); + [{path: 'b', component: ComponentB}], 'stubInjector', 'stubFactoryResolver', + 'injectorFactory'); const loader = {load: (injector: any, p: any) => of (loadedConfig)}; const trueGuard = () => true; @@ -216,7 +219,8 @@ describe('applyRedirects', () => { it('should not load when any canLoad guards is rejected (promises)', () => { const loadedConfig = new LoadedRouterConfig( - [{path: 'b', component: ComponentB}], 'stubInjector', 'stubFactoryResolver'); + [{path: 'b', component: ComponentB}], 'stubInjector', 'stubFactoryResolver', + 'injectorFactory'); const loader = {load: (injector: any, p: any) => of (loadedConfig)}; const trueGuard = () => Promise.resolve(true); @@ -237,7 +241,8 @@ describe('applyRedirects', () => { it('should work with objects implementing the CanLoad interface', () => { const loadedConfig = new LoadedRouterConfig( - [{path: 'b', component: ComponentB}], 'stubInjector', 'stubFactoryResolver'); + [{path: 'b', component: ComponentB}], 'stubInjector', 'stubFactoryResolver', + 'injectorFactory'); const loader = {load: (injector: any, p: any) => of (loadedConfig)}; const guard = {canLoad: () => Promise.resolve(true)}; @@ -254,7 +259,8 @@ describe('applyRedirects', () => { it('should work with absolute redirects', () => { const loadedConfig = new LoadedRouterConfig( - [{path: '', component: ComponentB}], 'stubInjector', 'stubFactoryResolver'); + [{path: '', component: ComponentB}], 'stubInjector', 'stubFactoryResolver', + 'injectorFactory'); const loader = {load: (injector: any, p: any) => of (loadedConfig)}; @@ -269,7 +275,8 @@ describe('applyRedirects', () => { it('should load the configuration only once', () => { const loadedConfig = new LoadedRouterConfig( - [{path: '', component: ComponentB}], 'stubInjector', 'stubFactoryResolver'); + [{path: '', component: ComponentB}], 'stubInjector', 'stubFactoryResolver', + 'injectorFactory'); let called = false; const loader = { @@ -295,7 +302,8 @@ describe('applyRedirects', () => { it('should load the configuration of a wildcard route', () => { const loadedConfig = new LoadedRouterConfig( - [{path: '', component: ComponentB}], 'stubInjector', 'stubFactoryResolver'); + [{path: '', component: ComponentB}], 'stubInjector', 'stubFactoryResolver', + 'injectorFactory'); const loader = {load: (injector: any, p: any) => of (loadedConfig)}; @@ -308,7 +316,8 @@ describe('applyRedirects', () => { it('should load the configuration after a local redirect from a wildcard route', () => { const loadedConfig = new LoadedRouterConfig( - [{path: '', component: ComponentB}], 'stubInjector', 'stubFactoryResolver'); + [{path: '', component: ComponentB}], 'stubInjector', 'stubFactoryResolver', + 'injectorFactory'); const loader = {load: (injector: any, p: any) => of (loadedConfig)}; @@ -322,7 +331,8 @@ describe('applyRedirects', () => { it('should load the configuration after an absolute redirect from a wildcard route', () => { const loadedConfig = new LoadedRouterConfig( - [{path: '', component: ComponentB}], 'stubInjector', 'stubFactoryResolver'); + [{path: '', component: ComponentB}], 'stubInjector', 'stubFactoryResolver', + 'injectorFactory'); const loader = {load: (injector: any, p: any) => of (loadedConfig)}; diff --git a/modules/@angular/router/test/integration.spec.ts b/modules/@angular/router/test/integration.spec.ts index d35d2c49e80a20..324fabbc994249 100644 --- a/modules/@angular/router/test/integration.spec.ts +++ b/modules/@angular/router/test/integration.spec.ts @@ -2024,54 +2024,78 @@ describe('Integration', () => { expect(location.path()).toEqual('/lazy2/loaded'); }))); - it('should use the injector of the lazily-loaded configuration', - fakeAsync(inject( - [Router, Location, NgModuleFactoryLoader], - (router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => { - class LazyLoadedServiceDefinedInModule {} - class LazyLoadedServiceDefinedInCmp {} - @Component({selector: 'lazy', template: 'lazy-loaded'}) - class LazyLoadedChildComponent { - constructor(service: LazyLoadedServiceDefinedInCmp) {} - } + describe('should use the injector of the lazily-loaded configuration', () => { + class LazyLoadedServiceDefinedInModule {} + class LazyLoadedServiceDefinedInCmp {} - @Component({ - selector: 'lazy', - template: '', - providers: [LazyLoadedServiceDefinedInCmp] - }) - class LazyLoadedParentComponent { - constructor(service: LazyLoadedServiceDefinedInModule) {} - } + @Component({ + selector: 'eager-parent', + template: 'eager-parent ', + }) + class EagerParentComponent { + } - @NgModule({ - declarations: [LazyLoadedParentComponent, LazyLoadedChildComponent], - imports: [RouterModule.forChild([{ - path: '', - children: [{ - path: 'loaded', - component: LazyLoadedParentComponent, - children: [{path: 'child', component: LazyLoadedChildComponent}] - }] - }])], - providers: [LazyLoadedServiceDefinedInModule] - }) - class LoadedModule { - } + @Component({selector: 'lazy-parent', template: 'lazy-parent '}) + class LazyParentComponent { + } - loader.stubbedModules = {expected: LoadedModule}; + @Component({selector: 'lazy-child', template: 'lazy-child'}) + class LazyChildComponent { + constructor( + lazy: LazyParentComponent, // should be able to inject lazy/direct parent + lazyService: LazyLoadedServiceDefinedInModule, // should be able to inject lazy service + eager: + EagerParentComponent // should use the injector of the location to create a parent + ) {} + } - const fixture = createRoot(router, RootCmp); + @NgModule({ + declarations: [LazyParentComponent, LazyChildComponent], + imports: [RouterModule.forChild([{ + path: '', + children: [{ + path: 'lazy-parent', + component: LazyParentComponent, + children: [{path: 'lazy-child', component: LazyChildComponent}] + }] + }])], + providers: [LazyLoadedServiceDefinedInModule] + }) + class LoadedModule { + } - router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]); + @NgModule({ + declarations: [EagerParentComponent], + entryComponents: [EagerParentComponent], + imports: [RouterModule] + }) + class TestModule { + } - router.navigateByUrl('/lazy/loaded/child'); - advance(fixture); + beforeEach(() => { TestBed.configureTestingModule({imports: [TestModule]}); }); - expect(location.path()).toEqual('/lazy/loaded/child'); - expect(fixture.nativeElement).toHaveText('lazy-loaded'); - }))); + it('should use the injector of the lazily-loaded configuration', + fakeAsync(inject( + [Router, Location, NgModuleFactoryLoader], + (router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => { + loader.stubbedModules = {expected: LoadedModule}; + + const fixture = createRoot(router, RootCmp); + + router.resetConfig([{ + path: 'eager-parent', + component: EagerParentComponent, + children: [{path: 'lazy', loadChildren: 'expected'}] + }]); + + router.navigateByUrl('/eager-parent/lazy/lazy-parent/lazy-child'); + advance(fixture); + + expect(location.path()).toEqual('/eager-parent/lazy/lazy-parent/lazy-child'); + expect(fixture.nativeElement).toHaveText('eager-parent lazy-parent lazy-child'); + }))); + }); it('works when given a callback', fakeAsync(inject( diff --git a/tools/public_api_guard/router/index.d.ts b/tools/public_api_guard/router/index.d.ts index 359c422f89415c..94298b6149ad2c 100644 --- a/tools/public_api_guard/router/index.d.ts +++ b/tools/public_api_guard/router/index.d.ts @@ -289,9 +289,11 @@ export declare class RouterOutlet implements OnDestroy { component: Object; deactivateEvents: EventEmitter; isActivated: boolean; + locationFactoryResolver: ComponentFactoryResolver; + locationInjector: Injector; outletMap: RouterOutletMap; constructor(parentOutletMap: RouterOutletMap, location: ViewContainerRef, resolver: ComponentFactoryResolver, name: string); - activate(activatedRoute: ActivatedRoute, loadedResolver: ComponentFactoryResolver, loadedInjector: Injector, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void; + activate(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void; deactivate(): void; ngOnDestroy(): void; }