From 13686bb518a79083d7fa668331bba3cb32cc8515 Mon Sep 17 00:00:00 2001 From: Victor Berchet Date: Tue, 14 Mar 2017 16:26:17 -0700 Subject: [PATCH] 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` --- modules/benchmarks/src/tree/ng2_next/tree.ts | 16 +- .../src/directives/ng_component_outlet.ts | 27 ++- .../directives/ng_component_outlet_spec.ts | 10 +- packages/compiler/src/identifiers.ts | 3 +- packages/compiler/src/ng_module_compiler.ts | 12 +- .../src/view_compiler/view_compiler.ts | 7 +- packages/core/src/application_ref.ts | 17 +- packages/core/src/linker/component_factory.ts | 6 +- .../src/linker/component_factory_resolver.ts | 32 ++- packages/core/src/linker/ng_module_factory.ts | 34 +-- packages/core/src/view/provider.ts | 35 +++- packages/core/src/view/refs.ts | 11 +- packages/core/src/view/services.ts | 31 +-- packages/core/src/view/types.ts | 4 +- packages/core/test/application_ref_spec.ts | 29 ++- packages/core/test/linker/integration_spec.ts | 5 +- .../test/linker/ng_module_integration_spec.ts | 9 +- .../linker/view_injector_integration_spec.ts | 24 ++- packages/core/test/view/helper.ts | 5 +- packages/core/testing/src/test_bed.ts | 8 +- packages/router/src/apply_redirects.ts | 100 +++++---- .../router/src/directives/router_outlet.ts | 45 ++++ packages/router/src/router.ts | 37 ++-- packages/router/src/router_config_loader.ts | 11 +- packages/router/src/router_preloader.ts | 21 +- packages/router/test/apply_redirects.spec.ts | 124 ++++++----- packages/router/test/integration.spec.ts | 197 +++++++++++++++++- tools/public_api_guard/core/typings/core.d.ts | 2 +- .../router/typings/router.d.ts | 7 +- 29 files changed, 627 insertions(+), 242 deletions(-) diff --git a/modules/benchmarks/src/tree/ng2_next/tree.ts b/modules/benchmarks/src/tree/ng2_next/tree.ts index f9353d44fe..8c326313bb 100644 --- a/modules/benchmarks/src/tree/ng2_next/tree.ts +++ b/modules/benchmarks/src/tree/ng2_next/tree.ts @@ -7,7 +7,7 @@ */ import {NgIf} from '@angular/common'; -import {ComponentFactory, ComponentRef, Injector, RendererFactory2, RootRenderer, Sanitizer, TemplateRef, ViewContainerRef} from '@angular/core'; +import {ComponentFactory, ComponentFactoryResolver, ComponentRef, Injector, NgModuleRef, RendererFactory2, RootRenderer, Sanitizer, TemplateRef, ViewContainerRef} from '@angular/core'; import {ArgumentType, BindingType, NodeFlags, ViewDefinition, ViewFlags, anchorDef, createComponentFactory, directiveDef, elementDef, initServicesIfNeeded, textDef, viewDef} from '@angular/core/src/view/index'; import {DomRendererFactory2} from '@angular/platform-browser/src/dom/dom_renderer'; import {DomSanitizerImpl, SafeStyle} from '@angular/platform-browser/src/security/dom_sanitization_service'; @@ -84,7 +84,7 @@ function TreeComponent_0(): ViewDefinition { }); } -export class AppModule implements Injector { +export class AppModule implements Injector, NgModuleRef { private sanitizer: DomSanitizerImpl; private componentFactory: ComponentFactory; private renderer2: RendererFactory2; @@ -108,12 +108,22 @@ export class AppModule implements Injector { return this.sanitizer; case RootRenderer: return null; + case NgModuleRef: + return this; } return Injector.NULL.get(token, notFoundValue); } bootstrap() { - this.componentRef = this.componentFactory.create(this, [], this.componentFactory.selector); + this.componentRef = + this.componentFactory.create(Injector.NULL, [], this.componentFactory.selector, this); } + tick() { this.componentRef.changeDetectorRef.detectChanges(); } + + get injector() { return this; } + get componentFactoryResolver(): ComponentFactoryResolver { return null; } + get instance() { return this; } + destroy() {} + onDestroy(callback: () => void) {} } diff --git a/packages/common/src/directives/ng_component_outlet.ts b/packages/common/src/directives/ng_component_outlet.ts index d20dacbc8b..ae8aa0421c 100644 --- a/packages/common/src/directives/ng_component_outlet.ts +++ b/packages/common/src/directives/ng_component_outlet.ts @@ -8,8 +8,6 @@ import {ComponentFactoryResolver, ComponentRef, Directive, Injector, Input, NgModuleFactory, NgModuleRef, OnChanges, OnDestroy, Provider, SimpleChanges, Type, ViewContainerRef} from '@angular/core'; - - /** * Instantiates a single {@link Component} type and inserts its Host View into current View. * `NgComponentOutlet` provides a declarative approach for dynamic component creation. @@ -81,34 +79,35 @@ export class NgComponentOutlet implements OnChanges, OnDestroy { constructor(private _viewContainerRef: ViewContainerRef) {} ngOnChanges(changes: SimpleChanges) { - if (this._componentRef) { - this._viewContainerRef.remove(this._viewContainerRef.indexOf(this._componentRef.hostView)); - } this._viewContainerRef.clear(); this._componentRef = null; if (this.ngComponentOutlet) { - let injector = this.ngComponentOutletInjector || this._viewContainerRef.parentInjector; + const elInjector = this.ngComponentOutletInjector || this._viewContainerRef.parentInjector; - if ((changes as any).ngComponentOutletNgModuleFactory) { + if (changes['ngComponentOutletNgModuleFactory']) { if (this._moduleRef) this._moduleRef.destroy(); + if (this.ngComponentOutletNgModuleFactory) { - this._moduleRef = this.ngComponentOutletNgModuleFactory.create(injector); + const parentModule = elInjector.get(NgModuleRef); + this._moduleRef = this.ngComponentOutletNgModuleFactory.create(parentModule.injector); } else { this._moduleRef = null; } } - if (this._moduleRef) { - injector = this._moduleRef.injector; - } - let componentFactory = - injector.get(ComponentFactoryResolver).resolveComponentFactory(this.ngComponentOutlet); + const componentFactoryResolver = this._moduleRef ? this._moduleRef.componentFactoryResolver : + elInjector.get(ComponentFactoryResolver); + + const componentFactory = + componentFactoryResolver.resolveComponentFactory(this.ngComponentOutlet); this._componentRef = this._viewContainerRef.createComponent( - componentFactory, this._viewContainerRef.length, injector, this.ngComponentOutletContent); + componentFactory, this._viewContainerRef.length, elInjector, + this.ngComponentOutletContent); } } + ngOnDestroy() { if (this._moduleRef) this._moduleRef.destroy(); } diff --git a/packages/common/test/directives/ng_component_outlet_spec.ts b/packages/common/test/directives/ng_component_outlet_spec.ts index 8c0b487d40..736b5c922c 100644 --- a/packages/common/test/directives/ng_component_outlet_spec.ts +++ b/packages/common/test/directives/ng_component_outlet_spec.ts @@ -177,13 +177,13 @@ export function main() { it('should not re-create moduleRef when it didn\'t actually change', async(() => { const compiler = TestBed.get(Compiler) as Compiler; const fixture = TestBed.createComponent(TestComponent); + fixture.componentInstance.module = compiler.compileModuleSync(TestModule2); fixture.componentInstance.currentComponent = Module2InjectedComponent; fixture.detectChanges(); - expect(fixture.nativeElement).toHaveText('baz'); - const moduleRef = fixture.componentInstance.ngComponentOutlet['_moduleRef']; + fixture.componentInstance.currentComponent = Module2InjectedComponent2; fixture.detectChanges(); @@ -247,11 +247,11 @@ class TestComponent { export class TestModule { } -@Component({selector: 'mdoule-2-injected-component', template: 'baz'}) +@Component({selector: 'module-2-injected-component', template: 'baz'}) class Module2InjectedComponent { } -@Component({selector: 'mdoule-2-injected-component-2', template: 'baz2'}) +@Component({selector: 'module-2-injected-component-2', template: 'baz2'}) class Module2InjectedComponent2 { } @@ -264,7 +264,7 @@ class Module2InjectedComponent2 { export class TestModule2 { } -@Component({selector: 'mdoule-3-injected-component', template: 'bat'}) +@Component({selector: 'module-3-injected-component', template: 'bat'}) class Module3InjectedComponent { } diff --git a/packages/compiler/src/identifiers.ts b/packages/compiler/src/identifiers.ts index 5bb1ebe896..7ae97d0623 100644 --- a/packages/compiler/src/identifiers.ts +++ b/packages/compiler/src/identifiers.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {ANALYZE_FOR_ENTRY_COMPONENTS, ChangeDetectionStrategy, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, LOCALE_ID, NgModuleFactory, QueryList, RenderComponentType, Renderer, SecurityContext, SimpleChange, TRANSLATIONS_FORMAT, TemplateRef, ViewContainerRef, ViewEncapsulation, ɵChangeDetectorStatus, ɵCodegenComponentFactoryResolver, ɵEMPTY_ARRAY, ɵEMPTY_MAP, ɵNgModuleInjector, ɵValueUnwrapper, ɵand, ɵccf, ɵcrt, ɵdevModeEqual, ɵdid, ɵeld, ɵinlineInterpolate, ɵinterpolate, ɵncd, ɵnov, ɵpad, ɵpid, ɵpod, ɵppd, ɵprd, ɵqud, ɵreflector, ɵregisterModuleFactory, ɵted, ɵunv, ɵvid} from '@angular/core'; +import {ANALYZE_FOR_ENTRY_COMPONENTS, ChangeDetectionStrategy, ChangeDetectorRef, ComponentFactory, ComponentFactoryResolver, ComponentRef, ElementRef, Injector, LOCALE_ID, NgModuleFactory, NgModuleRef, QueryList, RenderComponentType, Renderer, SecurityContext, SimpleChange, TRANSLATIONS_FORMAT, TemplateRef, ViewContainerRef, ViewEncapsulation, ɵChangeDetectorStatus, ɵCodegenComponentFactoryResolver, ɵEMPTY_ARRAY, ɵEMPTY_MAP, ɵNgModuleInjector, ɵValueUnwrapper, ɵand, ɵccf, ɵcrt, ɵdevModeEqual, ɵdid, ɵeld, ɵinlineInterpolate, ɵinterpolate, ɵncd, ɵnov, ɵpad, ɵpid, ɵpod, ɵppd, ɵprd, ɵqud, ɵreflector, ɵregisterModuleFactory, ɵted, ɵunv, ɵvid} from '@angular/core'; import {CompileIdentifierMetadata, CompileTokenMetadata} from './compile_metadata'; @@ -26,6 +26,7 @@ export class Identifiers { runtime: ANALYZE_FOR_ENTRY_COMPONENTS }; static ElementRef: IdentifierSpec = {name: 'ElementRef', moduleUrl: CORE, runtime: ElementRef}; + static NgModuleRef: IdentifierSpec = {name: 'NgModuleRef', moduleUrl: CORE, runtime: NgModuleRef}; static ViewContainerRef: IdentifierSpec = {name: 'ViewContainerRef', moduleUrl: CORE, runtime: ViewContainerRef}; static ChangeDetectorRef: diff --git a/packages/compiler/src/ng_module_compiler.ts b/packages/compiler/src/ng_module_compiler.ts index 94d25a5a21..3235e53855 100644 --- a/packages/compiler/src/ng_module_compiler.ts +++ b/packages/compiler/src/ng_module_compiler.ts @@ -216,11 +216,15 @@ class _InjectorBuilder implements ClassBuilder { result = o.literal(dep.value); } if (!dep.isSkipSelf) { - if (dep.token && - (tokenReference(dep.token) === resolveIdentifier(Identifiers.Injector) || - tokenReference(dep.token) === resolveIdentifier(Identifiers.ComponentFactoryResolver))) { - result = o.THIS_EXPR; + if (dep.token) { + if (tokenReference(dep.token) === resolveIdentifier(Identifiers.Injector)) { + result = o.THIS_EXPR; + } else if ( + tokenReference(dep.token) === resolveIdentifier(Identifiers.ComponentFactoryResolver)) { + result = o.THIS_EXPR.prop('componentFactoryResolver'); + } } + if (!result) { result = this._instances.get(tokenReference(dep.token)); } diff --git a/packages/compiler/src/view_compiler/view_compiler.ts b/packages/compiler/src/view_compiler/view_compiler.ts index 5b5987a9ba..9db0332da4 100644 --- a/packages/compiler/src/view_compiler/view_compiler.ts +++ b/packages/compiler/src/view_compiler/view_compiler.ts @@ -1125,13 +1125,14 @@ function createComponentFactoryResolver(directives: DirectiveAst[]): ProviderAst if (componentDirMeta && componentDirMeta.directive.entryComponents.length) { const entryComponentFactories = componentDirMeta.directive.entryComponents.map( (entryComponent) => o.importExpr({reference: entryComponent.componentFactory})); - const cfrExpr = o.importExpr(createIdentifier(Identifiers.CodegenComponentFactoryResolver)) - .instantiate([o.literalArr(entryComponentFactories)]); + const token = createIdentifierToken(Identifiers.ComponentFactoryResolver); + const classMeta: CompileTypeMetadata = { diDeps: [ {isValue: true, value: o.literalArr(entryComponentFactories)}, - {token: token, isSkipSelf: true, isOptional: true} + {token: token, isSkipSelf: true, isOptional: true}, + {token: createIdentifierToken(Identifiers.NgModuleRef)}, ], lifecycleHooks: [], reference: resolveIdentifier(Identifiers.CodegenComponentFactoryResolver) diff --git a/packages/core/src/application_ref.ts b/packages/core/src/application_ref.ts index 3d9c06c03a..4bb6441dda 100644 --- a/packages/core/src/application_ref.ts +++ b/packages/core/src/application_ref.ts @@ -8,20 +8,21 @@ import {Observable} from 'rxjs/Observable'; import {Observer} from 'rxjs/Observer'; -import {Subject} from 'rxjs/Subject'; import {Subscription} from 'rxjs/Subscription'; import {merge} from 'rxjs/observable/merge'; import {share} from 'rxjs/operator/share'; + import {ErrorHandler} from '../src/error_handler'; import {scheduleMicroTask, stringify} from '../src/util'; import {isPromise} from '../src/util/lang'; + import {ApplicationInitStatus} from './application_init'; import {APP_BOOTSTRAP_LISTENER, PLATFORM_INITIALIZER} from './application_tokens'; import {Console} from './console'; -import {Injectable, InjectionToken, Injector, Optional, Provider, ReflectiveInjector} from './di'; +import {Injectable, InjectionToken, Injector, Provider, ReflectiveInjector} from './di'; import {CompilerFactory, CompilerOptions} from './linker/compiler'; import {ComponentFactory, ComponentRef} from './linker/component_factory'; -import {ComponentFactoryResolver} from './linker/component_factory_resolver'; +import {ComponentFactoryBoundToModule, ComponentFactoryResolver} from './linker/component_factory_resolver'; import {NgModuleFactory, NgModuleInjector, NgModuleRef} from './linker/ng_module_factory'; import {InternalViewRef, ViewRef} from './linker/view_ref'; import {WtfScopeFn, wtfCreateScope, wtfLeave} from './profile/profile'; @@ -328,7 +329,7 @@ export class PlatformRef_ extends PlatformRef { private _moduleDoBootstrap(moduleRef: NgModuleInjector): void { const appRef = moduleRef.injector.get(ApplicationRef); if (moduleRef.bootstrapFactories.length > 0) { - moduleRef.bootstrapFactories.forEach((compFactory) => appRef.bootstrap(compFactory)); + moduleRef.bootstrapFactories.forEach(f => appRef.bootstrap(f)); } else if (moduleRef.instance.ngDoBootstrap) { moduleRef.instance.ngDoBootstrap(appRef); } else { @@ -502,7 +503,13 @@ export class ApplicationRef_ extends ApplicationRef { componentFactory = this._componentFactoryResolver.resolveComponentFactory(componentOrFactory); } this._rootComponentTypes.push(componentFactory.componentType); - const compRef = componentFactory.create(this._injector, [], componentFactory.selector); + + // Create a factory associated with the current module if it's not bound to some other + const ngModule = componentFactory instanceof ComponentFactoryBoundToModule ? + null : + this._injector.get(NgModuleRef); + const compRef = componentFactory.create(Injector.NULL, [], componentFactory.selector, ngModule); + compRef.onDestroy(() => { this._unloadComponent(compRef); }); const testability = compRef.injector.get(Testability, null); if (testability) { diff --git a/packages/core/src/linker/component_factory.ts b/packages/core/src/linker/component_factory.ts index efc66e8d11..a993802fa3 100644 --- a/packages/core/src/linker/component_factory.ts +++ b/packages/core/src/linker/component_factory.ts @@ -11,6 +11,7 @@ import {Injector} from '../di/injector'; import {Type} from '../type'; import {ElementRef} from './element_ref'; +import {NgModuleRef} from './ng_module_factory'; import {ViewRef} from './view_ref'; /** @@ -72,6 +73,7 @@ export abstract class ComponentFactory { /** * Creates a new component. */ - abstract create(injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any): - ComponentRef; + abstract create( + injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any, + ngModule?: NgModuleRef): ComponentRef; } diff --git a/packages/core/src/linker/component_factory_resolver.ts b/packages/core/src/linker/component_factory_resolver.ts index 97b62d1b44..fad59116f4 100644 --- a/packages/core/src/linker/component_factory_resolver.ts +++ b/packages/core/src/linker/component_factory_resolver.ts @@ -6,12 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ +import {Injector} from '../di/injector'; import {Type} from '../type'; import {stringify} from '../util'; -import {ComponentFactory} from './component_factory'; - - +import {ComponentFactory, ComponentRef} from './component_factory'; +import {NgModuleRef} from './ng_module_factory'; export function noComponentFactoryError(component: Function) { const error = Error( @@ -44,7 +44,9 @@ export abstract class ComponentFactoryResolver { export class CodegenComponentFactoryResolver implements ComponentFactoryResolver { private _factories = new Map>(); - constructor(factories: ComponentFactory[], private _parent: ComponentFactoryResolver) { + constructor( + factories: ComponentFactory[], private _parent: ComponentFactoryResolver, + private _ngModule: NgModuleRef) { for (let i = 0; i < factories.length; i++) { const factory = factories[i]; this._factories.set(factory.componentType, factory); @@ -52,10 +54,22 @@ export class CodegenComponentFactoryResolver implements ComponentFactoryResolver } resolveComponentFactory(component: {new (...args: any[]): T}): ComponentFactory { - let result = this._factories.get(component); - if (!result) { - result = this._parent.resolveComponentFactory(component); - } - return result; + let factory = this._factories.get(component) || this._parent.resolveComponentFactory(component); + + return factory ? new ComponentFactoryBoundToModule(factory, this._ngModule) : null; + } +} + +export class ComponentFactoryBoundToModule extends ComponentFactory { + constructor(private factory: ComponentFactory, private ngModule: NgModuleRef) { super(); } + + get selector() { return this.factory.selector; } + get componentType() { return this.factory.componentType; } + + create( + injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any, + ngModule?: NgModuleRef): ComponentRef { + return this.factory.create( + injector, projectableNodes, rootSelectorOrNode, ngModule || this.ngModule); } } diff --git a/packages/core/src/linker/ng_module_factory.ts b/packages/core/src/linker/ng_module_factory.ts index 97c8e90b11..c51aed137f 100644 --- a/packages/core/src/linker/ng_module_factory.ts +++ b/packages/core/src/linker/ng_module_factory.ts @@ -11,8 +11,7 @@ import {Type} from '../type'; import {stringify} from '../util'; import {ComponentFactory} from './component_factory'; -import {CodegenComponentFactoryResolver, ComponentFactoryResolver} from './component_factory_resolver'; - +import {CodegenComponentFactoryResolver, ComponentFactoryBoundToModule, ComponentFactoryResolver} from './component_factory_resolver'; /** @@ -62,10 +61,7 @@ export class NgModuleFactory { get moduleType(): Type { return this._moduleType; } create(parentInjector: Injector): NgModuleRef { - if (!parentInjector) { - parentInjector = Injector.NULL; - } - const instance = new this._injectorClass(parentInjector); + const instance = new this._injectorClass(parentInjector || Injector.NULL); instance.create(); return instance; } @@ -73,18 +69,21 @@ export class NgModuleFactory { const _UNDEFINED = new Object(); -export abstract class NgModuleInjector extends CodegenComponentFactoryResolver implements - Injector, - NgModuleRef { +export abstract class NgModuleInjector implements Injector, NgModuleRef { + bootstrapFactories: ComponentFactory[]; + instance: T; + private _destroyListeners: (() => void)[] = []; private _destroyed: boolean = false; - - public instance: T; + private _cmpFactoryResolver: CodegenComponentFactoryResolver; constructor( public parent: Injector, factories: ComponentFactory[], - public bootstrapFactories: ComponentFactory[]) { - super(factories, parent.get(ComponentFactoryResolver, ComponentFactoryResolver.NULL)); + bootstrapFactories: ComponentFactory[]) { + this.bootstrapFactories = + bootstrapFactories.map(f => new ComponentFactoryBoundToModule(f, this)); + this._cmpFactoryResolver = new CodegenComponentFactoryResolver( + factories, parent.get(ComponentFactoryResolver, ComponentFactoryResolver.NULL), this); } create() { this.instance = this.createInternal(); } @@ -92,9 +91,14 @@ export abstract class NgModuleInjector extends CodegenComponentFactoryResolve abstract createInternal(): T; get(token: any, notFoundValue: any = THROW_IF_NOT_FOUND): any { - if (token === Injector || token === ComponentFactoryResolver) { + if (token === Injector || token === NgModuleRef) { return this; } + + if (token === ComponentFactoryResolver) { + return this._cmpFactoryResolver; + } + const result = this.getInternal(token, _UNDEFINED); return result === _UNDEFINED ? this.parent.get(token, notFoundValue) : result; } @@ -103,7 +107,7 @@ export abstract class NgModuleInjector extends CodegenComponentFactoryResolve get injector(): Injector { return this; } - get componentFactoryResolver(): ComponentFactoryResolver { return this; } + get componentFactoryResolver(): ComponentFactoryResolver { return this._cmpFactoryResolver; } destroy(): void { if (this._destroyed) { diff --git a/packages/core/src/view/provider.ts b/packages/core/src/view/provider.ts index d33fd98cbe..0d728af7ab 100644 --- a/packages/core/src/view/provider.ts +++ b/packages/core/src/view/provider.ts @@ -133,7 +133,6 @@ export function createPipeInstance(view: ViewData, def: NodeDef): any { export function createDirectiveInstance(view: ViewData, def: NodeDef): any { // components can see other private services, other directives can't. const allowPrivateServices = (def.flags & NodeFlags.Component) > 0; - const providerDef = def.provider; // directives are always eager and classes! const instance = createClass(view, def.parent, allowPrivateServices, def.provider.value, def.provider.deps); @@ -325,6 +324,25 @@ function callFactory( return injectable; } +// This default value is when checking the hierarchy for a token. +// +// It means both: +// - the token is not provided by the current injector, +// - only the element injectors should be checked (ie do not check module injectors +// +// mod1 +// / +// el1 mod2 +// \ / +// el2 +// +// When requesting el2.injector.get(token), we should check in the following order and return the +// first found value: +// - el2.injector.get(token, default) +// - el1.injector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) -> do not check the module +// - mod2.injector.get(token, default) +const NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR = {}; + export function resolveDep( view: ViewData, elDef: NodeDef, allowPrivateServices: boolean, depDef: DepDef, notFoundValue = Injector.THROW_IF_NOT_FOUND): any { @@ -386,7 +404,20 @@ export function resolveDep( elDef = viewParentEl(view); view = view.parent; } - return startView.root.injector.get(depDef.token, notFoundValue); + + const value = startView.root.injector.get(depDef.token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR); + + if (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR || + notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) { + // Return the value from the root element injector when + // - it provides it + // (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) + // - the module injector should not be checked + // (notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) + return value; + } + + return startView.root.ngModule.injector.get(depDef.token, notFoundValue); } function findCompView(view: ViewData, elDef: NodeDef, allowPrivateServices: boolean) { diff --git a/packages/core/src/view/refs.ts b/packages/core/src/view/refs.ts index 1cc13a002b..defbcf1b6f 100644 --- a/packages/core/src/view/refs.ts +++ b/packages/core/src/view/refs.ts @@ -11,6 +11,7 @@ import {ChangeDetectorRef} from '../change_detection/change_detection'; import {Injector} from '../di'; import {ComponentFactory, ComponentRef} from '../linker/component_factory'; import {ElementRef} from '../linker/element_ref'; +import {NgModuleRef} from '../linker/ng_module_factory'; import {TemplateRef} from '../linker/template_ref'; import {ViewContainerRef} from '../linker/view_container_ref'; import {EmbeddedViewRef, InternalViewRef, ViewRef} from '../linker/view_ref'; @@ -52,12 +53,15 @@ class ComponentFactory_ extends ComponentFactory { * Creates a new component. */ create( - injector: Injector, projectableNodes: any[][] = null, - rootSelectorOrNode: string|any = null): ComponentRef { + injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string|any, + ngModule?: NgModuleRef): ComponentRef { + if (!ngModule) { + throw new Error('ngModule should be provided'); + } const viewDef = resolveViewDefinition(this.viewDefFactory); const componentNodeIndex = viewDef.nodes[0].element.componentProvider.index; const view = Services.createRootView( - injector, projectableNodes || [], rootSelectorOrNode, viewDef, EMPTY_CONTEXT); + injector, projectableNodes || [], rootSelectorOrNode, viewDef, ngModule, EMPTY_CONTEXT); const component = asProviderData(view, componentNodeIndex).instance; view.renderer.setAttribute(asElementData(view, 0).renderElement, 'ng-version', VERSION.full); @@ -107,6 +111,7 @@ class ViewContainerRef_ implements ViewContainerData { elDef = viewParentEl(view); view = view.parent; } + return view ? new Injector_(view, elDef) : this._view.root.injector; } diff --git a/packages/core/src/view/services.ts b/packages/core/src/view/services.ts index d42af7dd2f..caa07cd426 100644 --- a/packages/core/src/view/services.ts +++ b/packages/core/src/view/services.ts @@ -9,6 +9,7 @@ import {isDevMode} from '../application_ref'; import {DebugElement, DebugNode, EventListener, getDebugNode, indexDebugNode, removeDebugNodeFromIndex} from '../debug/debug_node'; import {Injector} from '../di'; +import {NgModuleRef} from '../linker/ng_module_factory'; import {Renderer2, RendererFactory2, RendererStyleFlags2, RendererType2} from '../render/api'; import {Sanitizer, SecurityContext} from '../security'; @@ -20,6 +21,7 @@ import {ArgumentType, BindingType, CheckType, DebugContext, DepFlags, ElementDat import {NOOP, checkBinding, isComponentView, renderNode, viewParentEl} from './util'; import {checkAndUpdateNode, checkAndUpdateView, checkNoChangesNode, checkNoChangesView, createEmbeddedView, createRootView, destroyView} from './view'; + let initialized = false; export function initServicesIfNeeded() { @@ -80,31 +82,32 @@ function createDebugServices() { } function createProdRootView( - injector: Injector, projectableNodes: any[][], rootSelectorOrNode: string | any, - def: ViewDefinition, context?: any): ViewData { - const rendererFactory: RendererFactory2 = injector.get(RendererFactory2); + elInjector: Injector, projectableNodes: any[][], rootSelectorOrNode: string | any, + def: ViewDefinition, ngModule: NgModuleRef, context?: any): ViewData { + const rendererFactory: RendererFactory2 = ngModule.injector.get(RendererFactory2); return createRootView( - createRootData(injector, rendererFactory, projectableNodes, rootSelectorOrNode), def, - context); + createRootData(elInjector, ngModule, rendererFactory, projectableNodes, rootSelectorOrNode), + def, context); } function debugCreateRootView( - injector: Injector, projectableNodes: any[][], rootSelectorOrNode: string | any, - def: ViewDefinition, context?: any): ViewData { - const rendererFactory: RendererFactory2 = injector.get(RendererFactory2); + elInjector: Injector, projectableNodes: any[][], rootSelectorOrNode: string | any, + def: ViewDefinition, ngModule: NgModuleRef, context?: any): ViewData { + const rendererFactory: RendererFactory2 = ngModule.injector.get(RendererFactory2); const root = createRootData( - injector, new DebugRendererFactory2(rendererFactory), projectableNodes, rootSelectorOrNode); + elInjector, ngModule, new DebugRendererFactory2(rendererFactory), projectableNodes, + rootSelectorOrNode); return callWithDebugContext(DebugAction.create, createRootView, null, [root, def, context]); } function createRootData( - injector: Injector, rendererFactory: RendererFactory2, projectableNodes: any[][], - rootSelectorOrNode: any): RootData { - const sanitizer = injector.get(Sanitizer); + elInjector: Injector, ngModule: NgModuleRef, rendererFactory: RendererFactory2, + projectableNodes: any[][], rootSelectorOrNode: any): RootData { + const sanitizer = ngModule.injector.get(Sanitizer); const renderer = rendererFactory.createRenderer(null, null); return { - injector, - projectableNodes, + ngModule, + injector: elInjector, projectableNodes, selectorOrNode: rootSelectorOrNode, sanitizer, rendererFactory, renderer }; } diff --git a/packages/core/src/view/types.ts b/packages/core/src/view/types.ts index ba98e333ce..2426ca3288 100644 --- a/packages/core/src/view/types.ts +++ b/packages/core/src/view/types.ts @@ -9,6 +9,7 @@ import {PipeTransform} from '../change_detection/change_detection'; import {Injector} from '../di'; import {ComponentRef} from '../linker/component_factory'; +import {NgModuleRef} from '../linker/ng_module_factory'; import {QueryList} from '../linker/query_list'; import {TemplateRef} from '../linker/template_ref'; import {ViewContainerRef} from '../linker/view_container_ref'; @@ -424,6 +425,7 @@ export function asQueryList(view: ViewData, index: number): QueryList { export interface RootData { injector: Injector; + ngModule: NgModuleRef; projectableNodes: any[][]; selectorOrNode: any; renderer: Renderer2; @@ -454,7 +456,7 @@ export interface Services { setCurrentNode(view: ViewData, nodeIndex: number): void; createRootView( injector: Injector, projectableNodes: any[][], rootSelectorOrNode: string|any, - def: ViewDefinition, context?: any): ViewData; + def: ViewDefinition, ngModule: NgModuleRef, context?: any): ViewData; createEmbeddedView(parent: ViewData, anchorDef: NodeDef, context?: any): ViewData; checkAndUpdateView(view: ViewData): void; checkNoChangesView(view: ViewData): void; diff --git a/packages/core/test/application_ref_spec.ts b/packages/core/test/application_ref_spec.ts index bd00965a65..7fdfaf8442 100644 --- a/packages/core/test/application_ref_spec.ts +++ b/packages/core/test/application_ref_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, CompilerFactory, Component, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; +import {APP_BOOTSTRAP_LISTENER, APP_INITIALIZER, Compiler, CompilerFactory, Component, NgModule, NgZone, PlatformRef, TemplateRef, Type, ViewChild, ViewContainerRef} from '@angular/core'; import {ApplicationRef, ApplicationRef_} from '@angular/core/src/application_ref'; import {ErrorHandler} from '@angular/core/src/error_handler'; import {ComponentRef} from '@angular/core/src/linker/component_factory'; @@ -72,6 +72,33 @@ export function main() { return MyModule; } + it('should bootstrap a component from a child module', + async(inject([ApplicationRef, Compiler], (app: ApplicationRef, compiler: Compiler) => { + @Component({ + selector: 'bootstrap-app', + template: '', + }) + class SomeComponent { + } + + @NgModule({ + providers: [{provide: 'hello', useValue: 'component'}], + declarations: [SomeComponent], + entryComponents: [SomeComponent], + }) + class SomeModule { + } + + createRootEl(); + const modFactory = compiler.compileModuleSync(SomeModule); + const module = modFactory.create(TestBed); + const cmpFactory = module.componentFactoryResolver.resolveComponentFactory(SomeComponent); + const component = app.bootstrap(cmpFactory); + + // The component should see the child module providers + expect(component.injector.get('hello')).toEqual('component'); + }))); + describe('ApplicationRef', () => { beforeEach(() => { TestBed.configureTestingModule({imports: [createModule()]}); }); diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 026ad9aa8d..e828043a57 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -1264,13 +1264,12 @@ function declareTests({useJit}: {useJit: boolean}) { TestBed.createComponent(SomeComponent); expect(noSelectorComponentFactory.selector).toBe('ng-component'); + expect( getDOM() - .nodeName( - noSelectorComponentFactory.create(TestBed.get(Injector)).location.nativeElement) + .nodeName(noSelectorComponentFactory.create(Injector.NULL).location.nativeElement) .toLowerCase()) .toEqual('ng-component'); - }); }); diff --git a/packages/core/test/linker/ng_module_integration_spec.ts b/packages/core/test/linker/ng_module_integration_spec.ts index a6755f38dd..f3cabc0eb1 100644 --- a/packages/core/test/linker/ng_module_integration_spec.ts +++ b/packages/core/test/linker/ng_module_integration_spec.ts @@ -130,9 +130,13 @@ function declareTests({useJit}: {useJit: boolean}) { } function createComp(compType: Type, moduleType: Type): ComponentFixture { - const ngModule = createModule(moduleType); + const ngModule = createModule(moduleType, injector); + const cf = ngModule.componentFactoryResolver.resolveComponentFactory(compType); - return new ComponentFixture(cf.create(injector), null, false); + + const comp = cf.create(Injector.NULL); + + return new ComponentFixture(comp, null, false); } describe('errors', () => { @@ -417,6 +421,7 @@ function declareTests({useJit}: {useJit: boolean}) { } const compFixture = createComp(CompUsingModuleDirectiveAndPipe, SomeModule); + compFixture.detectChanges(); expect(compFixture.debugElement.children[0].properties['title']) .toBe('transformed someValue'); diff --git a/packages/core/test/linker/view_injector_integration_spec.ts b/packages/core/test/linker/view_injector_integration_spec.ts index fb42c9d254..a148fbf0b2 100644 --- a/packages/core/test/linker/view_injector_integration_spec.ts +++ b/packages/core/test/linker/view_injector_integration_spec.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, DebugElement, Directive, ElementRef, Host, Inject, InjectionToken, Input, Optional, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewContainerRef} from '@angular/core'; +import {Attribute, ChangeDetectionStrategy, ChangeDetectorRef, Component, ComponentFactoryResolver, DebugElement, Directive, ElementRef, Host, Inject, InjectionToken, Injector, Input, NgModule, Optional, Pipe, PipeTransform, Provider, Self, SkipSelf, TemplateRef, Type, ViewContainerRef} from '@angular/core'; import {ComponentFixture, TestBed, fakeAsync} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {expect} from '@angular/platform-browser/testing/src/matchers'; @@ -642,6 +642,28 @@ export function main() { .toBe(el.children[0].nativeElement); }); + it('should inject ViewContainerRef', () => { + @Component({template: ''}) + class TestComp { + constructor(public vcr: ViewContainerRef) {} + } + + @NgModule({ + declarations: [TestComp], + entryComponents: [TestComp], + }) + class TestModule { + } + + const testInjector = {}; + + const compFactory = TestBed.configureTestingModule({imports: [TestModule]}) + .get(ComponentFactoryResolver) + .resolveComponentFactory(TestComp); + const component = compFactory.create(testInjector); + expect(component.instance.vcr.parentInjector).toBe(testInjector); + }); + it('should inject TemplateRef', () => { TestBed.configureTestingModule({declarations: [NeedsViewContainerRef, NeedsTemplateRef]}); const el = diff --git a/packages/core/test/view/helper.ts b/packages/core/test/view/helper.ts index bb4906ba6b..5c90a3a4f0 100644 --- a/packages/core/test/view/helper.ts +++ b/packages/core/test/view/helper.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {Injector, RootRenderer, Sanitizer} from '@angular/core'; +import {Injector, NgModuleRef, RootRenderer, Sanitizer} from '@angular/core'; import {ArgumentType, NodeCheckFn, RootData, Services, ViewData, ViewDefinition, initServicesIfNeeded} from '@angular/core/src/view/index'; import {TestBed} from '@angular/core/testing'; import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; @@ -33,7 +33,8 @@ export function createRootView( rootSelectorOrNode?: any): ViewData { initServicesIfNeeded(); return Services.createRootView( - TestBed.get(Injector), projectableNodes || [], rootSelectorOrNode, def, context); + TestBed.get(Injector), projectableNodes || [], rootSelectorOrNode, def, + TestBed.get(NgModuleRef), context); } export let removeNodes: Node[]; diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 7318b31d0a..6c7959c71b 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -292,8 +292,7 @@ export class TestBed implements Injector { const imports = [this.ngModule, this._imports]; const schemas = this._schemas; - @NgModule( - {providers: providers, declarations: declarations, imports: imports, schemas: schemas}) + @NgModule({providers, declarations, imports, schemas}) class DynamicTestModule { } @@ -359,10 +358,12 @@ export class TestBed implements Injector { this._initIfNeeded(); const componentFactory = this._moduleWithComponentFactories.componentFactories.find( (compFactory) => compFactory.componentType === component); + if (!componentFactory) { throw new Error( `Cannot create the component ${stringify(component)} as it was not imported into the testing module!`); } + const noNgZone = this.get(ComponentFixtureNoNgZone, false); const autoDetect: boolean = this.get(ComponentFixtureAutoDetect, false); const ngZone: NgZone = noNgZone ? null : this.get(NgZone, null); @@ -371,7 +372,8 @@ export class TestBed implements Injector { testComponentRenderer.insertRootElement(rootElId); const initComponent = () => { - const componentRef = componentFactory.create(this, [], `#${rootElId}`); + const componentRef = + componentFactory.create(Injector.NULL, [], `#${rootElId}`, this._moduleRef); return new ComponentFixture(componentRef, ngZone, autoDetect); }; diff --git a/packages/router/src/apply_redirects.ts b/packages/router/src/apply_redirects.ts index c32253f46c..a08f1fea73 100644 --- a/packages/router/src/apply_redirects.ts +++ b/packages/router/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/packages/router/src/directives/router_outlet.ts b/packages/router/src/directives/router_outlet.ts index 49ba9db661..435590b2e9 100644 --- a/packages/router/src/directives/router_outlet.ts +++ b/packages/router/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/packages/router/src/router.ts b/packages/router/src/router.ts index 3c13e92d82..8fbd39045b 100644 --- a/packages/router/src/router.ts +++ b/packages/router/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/packages/router/src/router_config_loader.ts b/packages/router/src/router_config_loader.ts index 0e598b8c54..943ea62111 100644 --- a/packages/router/src/router_config_loader.ts +++ b/packages/router/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/packages/router/src/router_preloader.ts b/packages/router/src/router_preloader.ts index 15af8a87be..ddc9b9dc29 100644 --- a/packages/router/src/router_preloader.ts +++ b/packages/router/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/packages/router/test/apply_redirects.spec.ts b/packages/router/test/apply_redirects.spec.ts index 3e7afb8b38..4212f203f8 100644 --- a/packages/router/test/apply_redirects.spec.ts +++ b/packages/router/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/packages/router/test/integration.spec.ts b/packages/router/test/integration.spec.ts index 9d459421cb..7c509f878f 100644 --- a/packages/router/test/integration.spec.ts +++ b/packages/router/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( diff --git a/tools/public_api_guard/core/typings/core.d.ts b/tools/public_api_guard/core/typings/core.d.ts index 5a239296d8..1868d6001c 100644 --- a/tools/public_api_guard/core/typings/core.d.ts +++ b/tools/public_api_guard/core/typings/core.d.ts @@ -227,7 +227,7 @@ export interface ComponentDecorator { export declare abstract class ComponentFactory { readonly abstract componentType: Type; readonly abstract selector: string; - abstract create(injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string | any): ComponentRef; + abstract create(injector: Injector, projectableNodes?: any[][], rootSelectorOrNode?: string | any, ngModule?: NgModuleRef): ComponentRef; } /** @stable */ diff --git a/tools/public_api_guard/router/typings/router.d.ts b/tools/public_api_guard/router/typings/router.d.ts index 514fbddc64..08bc632212 100644 --- a/tools/public_api_guard/router/typings/router.d.ts +++ b/tools/public_api_guard/router/typings/router.d.ts @@ -320,11 +320,12 @@ export declare class RouterOutlet implements OnDestroy { readonly component: Object; deactivateEvents: EventEmitter; readonly isActivated: boolean; - readonly locationFactoryResolver: ComponentFactoryResolver; - readonly locationInjector: Injector; + /** @deprecated */ readonly locationFactoryResolver: ComponentFactoryResolver; + /** @deprecated */ readonly locationInjector: Injector; outletMap: RouterOutletMap; constructor(parentOutletMap: RouterOutletMap, location: ViewContainerRef, resolver: ComponentFactoryResolver, name: string); - activate(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void; + /** @deprecated */ activate(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver, injector: Injector, providers: ResolvedReflectiveProvider[], outletMap: RouterOutletMap): void; + activateWith(activatedRoute: ActivatedRoute, resolver: ComponentFactoryResolver | null, outletMap: RouterOutletMap): void; attach(ref: ComponentRef, activatedRoute: ActivatedRoute): void; deactivate(): void; detach(): ComponentRef;