From 431eb309f3648dbc4e04c465fb619804b391f9e2 Mon Sep 17 00:00:00 2001 From: Tobias Bosch Date: Tue, 21 Mar 2017 08:15:10 -0700 Subject: [PATCH] fix(core): provide `NgModuleRef` in `ViewContainerRef.createComponent`. (#15350) This is needed to support the corner cases: - usage of a `ComponentFactory` that was created on the fly via `Compiler` - overwriting of the `NgModuleRef` that is associated to a `ComponentFactory` by the `ComponentFactoryResolver` from which it was read. Fixes #15241 --- .../core/src/linker/view_container_ref.ts | 3 +- packages/core/src/view/provider.ts | 2 +- packages/core/src/view/refs.ts | 16 ++- packages/core/test/linker/integration_spec.ts | 125 +++++++++++++++++- .../linker/view_injector_integration_spec.ts | 27 +++- tools/public_api_guard/core/core.d.ts | 2 +- 6 files changed, 160 insertions(+), 15 deletions(-) diff --git a/packages/core/src/linker/view_container_ref.ts b/packages/core/src/linker/view_container_ref.ts index 476e6e438c..4024f376cf 100644 --- a/packages/core/src/linker/view_container_ref.ts +++ b/packages/core/src/linker/view_container_ref.ts @@ -9,6 +9,7 @@ import {Injector} from '../di/injector'; import {ComponentFactory, ComponentRef} from './component_factory'; import {ElementRef} from './element_ref'; +import {NgModuleRef} from './ng_module_factory'; import {TemplateRef} from './template_ref'; import {EmbeddedViewRef, ViewRef} from './view_ref'; @@ -83,7 +84,7 @@ export abstract class ViewContainerRef { */ abstract createComponent( componentFactory: ComponentFactory, index?: number, injector?: Injector, - projectableNodes?: any[][]): ComponentRef; + projectableNodes?: any[][], ngModule?: NgModuleRef): ComponentRef; /** * Inserts a View identified by a {@link ViewRef} into the container at the specified `index`. diff --git a/packages/core/src/view/provider.ts b/packages/core/src/view/provider.ts index a4f0c4619b..1bc402a789 100644 --- a/packages/core/src/view/provider.ts +++ b/packages/core/src/view/provider.ts @@ -356,7 +356,7 @@ export function resolveDep( } const tokenKey = depDef.tokenKey; - if (depDef.flags & DepFlags.SkipSelf) { + if (elDef && (depDef.flags & DepFlags.SkipSelf)) { allowPrivateServices = false; elDef = elDef.parent; } diff --git a/packages/core/src/view/refs.ts b/packages/core/src/view/refs.ts index eb39e946a7..f3b8d5cb82 100644 --- a/packages/core/src/view/refs.ts +++ b/packages/core/src/view/refs.ts @@ -10,6 +10,7 @@ import {ApplicationRef} from '../application_ref'; import {ChangeDetectorRef} from '../change_detection/change_detection'; import {Injector} from '../di'; import {ComponentFactory, ComponentRef} from '../linker/component_factory'; +import {ComponentFactoryBoundToModule} from '../linker/component_factory_resolver'; import {ElementRef} from '../linker/element_ref'; import {NgModuleRef} from '../linker/ng_module_factory'; import {TemplateRef} from '../linker/template_ref'; @@ -137,7 +138,7 @@ class ViewContainerRef_ implements ViewContainerData { view = view.parent; } - return view ? new Injector_(view, elDef) : this._view.root.injector; + return view ? new Injector_(view, elDef) : new Injector_(this._view, null); } clear(): void { @@ -169,9 +170,13 @@ class ViewContainerRef_ implements ViewContainerData { createComponent( componentFactory: ComponentFactory, index?: number, injector?: Injector, - projectableNodes?: any[][]): ComponentRef { + projectableNodes?: any[][], ngModuleRef?: NgModuleRef): ComponentRef { const contextInjector = injector || this.parentInjector; - const componentRef = componentFactory.create(contextInjector, projectableNodes); + if (!ngModuleRef && !(componentFactory instanceof ComponentFactoryBoundToModule)) { + ngModuleRef = contextInjector.get(NgModuleRef); + } + const componentRef = + componentFactory.create(contextInjector, projectableNodes, undefined, ngModuleRef); this.insert(componentRef.hostView, index); return componentRef; } @@ -298,9 +303,10 @@ export function createInjector(view: ViewData, elDef: NodeDef): Injector { } class Injector_ implements Injector { - constructor(private view: ViewData, private elDef: NodeDef) {} + constructor(private view: ViewData, private elDef: NodeDef|null) {} get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any { - const allowPrivateServices = (this.elDef.flags & NodeFlags.ComponentView) !== 0; + const allowPrivateServices = + this.elDef ? (this.elDef.flags & NodeFlags.ComponentView) !== 0 : false; return Services.resolveDep( this.view, this.elDef, allowPrivateServices, {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue); diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 604b0eb770..d0a44f2a9d 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -7,7 +7,7 @@ */ import {CommonModule} from '@angular/common'; -import {ComponentFactory, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, OnDestroy, ReflectiveInjector, SkipSelf} from '@angular/core'; +import {Compiler, ComponentFactory, EventEmitter, Host, Inject, Injectable, InjectionToken, Injector, NO_ERRORS_SCHEMA, NgModule, NgModuleRef, OnDestroy, ReflectiveInjector, SkipSelf} from '@angular/core'; import {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection'; import {getDebugContext} from '@angular/core/src/errors'; import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver'; @@ -22,6 +22,7 @@ import {getDOM} from '@angular/platform-browser/src/dom/dom_adapter'; import {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens'; import {dispatchEvent, el} from '@angular/platform-browser/testing/src/browser_util'; import {expect} from '@angular/platform-browser/testing/src/matchers'; + import {stringify} from '../../src/util'; const ANCHOR_ELEMENT = new InjectionToken('AnchorElement'); @@ -1019,7 +1020,7 @@ function declareTests({useJit}: {useJit: boolean}) { fixture.destroy(); }); - describe('dynamic ViewContainers', () => { + describe('ViewContainerRef.createComponent', () => { beforeEach(() => { // we need a module to declarate ChildCompUsingService as an entryComponent otherwise the // factory doesn't get created @@ -1036,7 +1037,7 @@ function declareTests({useJit}: {useJit: boolean}) { MyComp, {add: {template: '
'}}); }); - it('should allow to create a ViewContainerRef at any bound location', async(() => { + it('should allow to create a component at any bound location', async(() => { const fixture = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}) .createComponent(MyComp); const tc = fixture.debugElement.children[0].children[0]; @@ -1047,7 +1048,7 @@ function declareTests({useJit}: {useJit: boolean}) { .toHaveText('dynamic greet'); })); - it('should allow to create multiple ViewContainerRef at a location', async(() => { + it('should allow to create multiple components at a location', async(() => { const fixture = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]}) .createComponent(MyComp); const tc = fixture.debugElement.children[0].children[0]; @@ -1060,6 +1061,122 @@ function declareTests({useJit}: {useJit: boolean}) { expect(fixture.debugElement.children[0].children[2].nativeElement) .toHaveText('dynamic greet'); })); + + it('should create a component that has been freshly compiled', () => { + @Component({template: ''}) + class RootComp { + constructor(public vc: ViewContainerRef) {} + } + + @NgModule({ + declarations: [RootComp], + providers: [{provide: 'someToken', useValue: 'someRootValue'}], + }) + class RootModule { + } + + @Component({template: ''}) + class MyComp { + constructor(@Inject('someToken') public someToken: string) {} + } + + @NgModule({ + declarations: [MyComp], + providers: [{provide: 'someToken', useValue: 'someValue'}], + }) + class MyModule { + } + + const compFixture = + TestBed.configureTestingModule({imports: [RootModule]}).createComponent(RootComp); + const compiler = TestBed.get(Compiler); + const myCompFactory = + >compiler.compileModuleAndAllComponentsSync(MyModule) + .componentFactories[0]; + + // Note: the ComponentFactory was created directly via the compiler, i.e. it + // does not have an association to an NgModuleRef. + // -> expect the providers of the module that the view container belongs to. + const compRef = compFixture.componentInstance.vc.createComponent(myCompFactory); + expect(compRef.instance.someToken).toBe('someRootValue'); + }); + + it('should create a component with the passed NgModuleRef', () => { + @Component({template: ''}) + class RootComp { + constructor(public vc: ViewContainerRef) {} + } + + @Component({template: ''}) + class MyComp { + constructor(@Inject('someToken') public someToken: string) {} + } + + @NgModule({ + declarations: [RootComp, MyComp], + entryComponents: [MyComp], + providers: [{provide: 'someToken', useValue: 'someRootValue'}], + }) + class RootModule { + } + + @NgModule({providers: [{provide: 'someToken', useValue: 'someValue'}]}) + class MyModule { + } + + const compFixture = + TestBed.configureTestingModule({imports: [RootModule]}).createComponent(RootComp); + const compiler = TestBed.get(Compiler); + const myModule = compiler.compileModuleSync(MyModule).create(TestBed.get(NgModuleRef)); + const myCompFactory = (TestBed.get(ComponentFactoryResolver)) + .resolveComponentFactory(MyComp); + + // Note: MyComp was declared as entryComponent in the RootModule, + // but we pass MyModule to the createComponent call. + // -> expect the providers of MyModule! + const compRef = compFixture.componentInstance.vc.createComponent( + myCompFactory, undefined, undefined, undefined, myModule); + expect(compRef.instance.someToken).toBe('someValue'); + }); + + it('should create a component with the NgModuleRef of the ComponentFactoryResolver', () => { + @Component({template: ''}) + class RootComp { + constructor(public vc: ViewContainerRef) {} + } + + @NgModule({ + declarations: [RootComp], + providers: [{provide: 'someToken', useValue: 'someRootValue'}], + }) + class RootModule { + } + + @Component({template: ''}) + class MyComp { + constructor(@Inject('someToken') public someToken: string) {} + } + + @NgModule({ + declarations: [MyComp], + entryComponents: [MyComp], + providers: [{provide: 'someToken', useValue: 'someValue'}], + }) + class MyModule { + } + + const compFixture = + TestBed.configureTestingModule({imports: [RootModule]}).createComponent(RootComp); + const compiler = TestBed.get(Compiler); + const myModule = compiler.compileModuleSync(MyModule).create(TestBed.get(NgModuleRef)); + const myCompFactory = myModule.componentFactoryResolver.resolveComponentFactory(MyComp); + + // Note: MyComp was declared as entryComponent in MyModule, + // and we don't pass an explicit ModuleRef to the createComponent call. + // -> expect the providers of MyModule! + const compRef = compFixture.componentInstance.vc.createComponent(myCompFactory); + expect(compRef.instance.someToken).toBe('someValue'); + }); }); it('should support static attributes', () => { diff --git a/packages/core/test/linker/view_injector_integration_spec.ts b/packages/core/test/linker/view_injector_integration_spec.ts index a148fbf0b2..f99605b094 100644 --- a/packages/core/test/linker/view_injector_integration_spec.ts +++ b/packages/core/test/linker/view_injector_integration_spec.ts @@ -564,6 +564,24 @@ export function main() { .toThrowError( /Template parse errors:\nNo provider for SimpleDirective \("\[ERROR ->\]
<\/div>"\): .*SimpleComponent.html@0:0/); }); + + it('should allow to use the NgModule injector from a root ViewContainerRef.parentInjector', + () => { + @Component({template: ''}) + class MyComp { + constructor(public vc: ViewContainerRef) {} + } + + const compFixture = TestBed + .configureTestingModule({ + declarations: [MyComp], + providers: [{provide: 'someToken', useValue: 'someValue'}] + }) + .createComponent(MyComp); + + expect(compFixture.componentInstance.vc.parentInjector.get('someToken')) + .toBe('someValue'); + }); }); describe('static attributes', () => { @@ -655,13 +673,16 @@ export function main() { class TestModule { } - const testInjector = {}; + const testInjector = { + get: (token: any, notFoundValue: any) => + token === 'someToken' ? 'someNewValue' : notFoundValue + }; const compFactory = TestBed.configureTestingModule({imports: [TestModule]}) .get(ComponentFactoryResolver) .resolveComponentFactory(TestComp); - const component = compFactory.create(testInjector); - expect(component.instance.vcr.parentInjector).toBe(testInjector); + const component = compFactory.create(testInjector); + expect(component.instance.vcr.parentInjector.get('someToken')).toBe('someNewValue'); }); it('should inject TemplateRef', () => { diff --git a/tools/public_api_guard/core/core.d.ts b/tools/public_api_guard/core/core.d.ts index f5d65d42e0..5ecf430899 100644 --- a/tools/public_api_guard/core/core.d.ts +++ b/tools/public_api_guard/core/core.d.ts @@ -1086,7 +1086,7 @@ export declare abstract class ViewContainerRef { readonly abstract length: number; readonly abstract parentInjector: Injector; abstract clear(): void; - abstract createComponent(componentFactory: ComponentFactory, index?: number, injector?: Injector, projectableNodes?: any[][]): ComponentRef; + abstract createComponent(componentFactory: ComponentFactory, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef): ComponentRef; abstract createEmbeddedView(templateRef: TemplateRef, context?: C, index?: number): EmbeddedViewRef; abstract detach(index?: number): ViewRef; abstract get(index: number): ViewRef;