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
This commit is contained in:
Tobias Bosch 2017-03-21 08:15:10 -07:00 committed by Miško Hevery
parent 8e6995c91e
commit 431eb309f3
6 changed files with 160 additions and 15 deletions

View File

@ -9,6 +9,7 @@
import {Injector} from '../di/injector'; import {Injector} from '../di/injector';
import {ComponentFactory, ComponentRef} from './component_factory'; import {ComponentFactory, ComponentRef} from './component_factory';
import {ElementRef} from './element_ref'; import {ElementRef} from './element_ref';
import {NgModuleRef} from './ng_module_factory';
import {TemplateRef} from './template_ref'; import {TemplateRef} from './template_ref';
import {EmbeddedViewRef, ViewRef} from './view_ref'; import {EmbeddedViewRef, ViewRef} from './view_ref';
@ -83,7 +84,7 @@ export abstract class ViewContainerRef {
*/ */
abstract createComponent<C>( abstract createComponent<C>(
componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, componentFactory: ComponentFactory<C>, index?: number, injector?: Injector,
projectableNodes?: any[][]): ComponentRef<C>; projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>;
/** /**
* Inserts a View identified by a {@link ViewRef} into the container at the specified `index`. * Inserts a View identified by a {@link ViewRef} into the container at the specified `index`.

View File

@ -356,7 +356,7 @@ export function resolveDep(
} }
const tokenKey = depDef.tokenKey; const tokenKey = depDef.tokenKey;
if (depDef.flags & DepFlags.SkipSelf) { if (elDef && (depDef.flags & DepFlags.SkipSelf)) {
allowPrivateServices = false; allowPrivateServices = false;
elDef = elDef.parent; elDef = elDef.parent;
} }

View File

@ -10,6 +10,7 @@ import {ApplicationRef} from '../application_ref';
import {ChangeDetectorRef} from '../change_detection/change_detection'; import {ChangeDetectorRef} from '../change_detection/change_detection';
import {Injector} from '../di'; import {Injector} from '../di';
import {ComponentFactory, ComponentRef} from '../linker/component_factory'; import {ComponentFactory, ComponentRef} from '../linker/component_factory';
import {ComponentFactoryBoundToModule} from '../linker/component_factory_resolver';
import {ElementRef} from '../linker/element_ref'; import {ElementRef} from '../linker/element_ref';
import {NgModuleRef} from '../linker/ng_module_factory'; import {NgModuleRef} from '../linker/ng_module_factory';
import {TemplateRef} from '../linker/template_ref'; import {TemplateRef} from '../linker/template_ref';
@ -137,7 +138,7 @@ class ViewContainerRef_ implements ViewContainerData {
view = view.parent; 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 { clear(): void {
@ -169,9 +170,13 @@ class ViewContainerRef_ implements ViewContainerData {
createComponent<C>( createComponent<C>(
componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, componentFactory: ComponentFactory<C>, index?: number, injector?: Injector,
projectableNodes?: any[][]): ComponentRef<C> { projectableNodes?: any[][], ngModuleRef?: NgModuleRef<any>): ComponentRef<C> {
const contextInjector = injector || this.parentInjector; 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); this.insert(componentRef.hostView, index);
return componentRef; return componentRef;
} }
@ -298,9 +303,10 @@ export function createInjector(view: ViewData, elDef: NodeDef): Injector {
} }
class Injector_ implements 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 { 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( return Services.resolveDep(
this.view, this.elDef, allowPrivateServices, this.view, this.elDef, allowPrivateServices,
{flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue); {flags: DepFlags.None, token, tokenKey: tokenKey(token)}, notFoundValue);

View File

@ -7,7 +7,7 @@
*/ */
import {CommonModule} from '@angular/common'; 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 {ChangeDetectionStrategy, ChangeDetectorRef, PipeTransform} from '@angular/core/src/change_detection/change_detection';
import {getDebugContext} from '@angular/core/src/errors'; import {getDebugContext} from '@angular/core/src/errors';
import {ComponentFactoryResolver} from '@angular/core/src/linker/component_factory_resolver'; 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 {DOCUMENT} from '@angular/platform-browser/src/dom/dom_tokens';
import {dispatchEvent, el} from '@angular/platform-browser/testing/src/browser_util'; import {dispatchEvent, el} from '@angular/platform-browser/testing/src/browser_util';
import {expect} from '@angular/platform-browser/testing/src/matchers'; import {expect} from '@angular/platform-browser/testing/src/matchers';
import {stringify} from '../../src/util'; import {stringify} from '../../src/util';
const ANCHOR_ELEMENT = new InjectionToken('AnchorElement'); const ANCHOR_ELEMENT = new InjectionToken('AnchorElement');
@ -1019,7 +1020,7 @@ function declareTests({useJit}: {useJit: boolean}) {
fixture.destroy(); fixture.destroy();
}); });
describe('dynamic ViewContainers', () => { describe('ViewContainerRef.createComponent', () => {
beforeEach(() => { beforeEach(() => {
// we need a module to declarate ChildCompUsingService as an entryComponent otherwise the // we need a module to declarate ChildCompUsingService as an entryComponent otherwise the
// factory doesn't get created // factory doesn't get created
@ -1036,7 +1037,7 @@ function declareTests({useJit}: {useJit: boolean}) {
MyComp, {add: {template: '<div><dynamic-vp #dynamic></dynamic-vp></div>'}}); MyComp, {add: {template: '<div><dynamic-vp #dynamic></dynamic-vp></div>'}});
}); });
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]}) const fixture = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]})
.createComponent(MyComp); .createComponent(MyComp);
const tc = fixture.debugElement.children[0].children[0]; const tc = fixture.debugElement.children[0].children[0];
@ -1047,7 +1048,7 @@ function declareTests({useJit}: {useJit: boolean}) {
.toHaveText('dynamic greet'); .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]}) const fixture = TestBed.configureTestingModule({schemas: [NO_ERRORS_SCHEMA]})
.createComponent(MyComp); .createComponent(MyComp);
const tc = fixture.debugElement.children[0].children[0]; 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) expect(fixture.debugElement.children[0].children[2].nativeElement)
.toHaveText('dynamic greet'); .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 = <Compiler>TestBed.get(Compiler);
const myCompFactory =
<ComponentFactory<MyComp>>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 = <Compiler>TestBed.get(Compiler);
const myModule = compiler.compileModuleSync(MyModule).create(TestBed.get(NgModuleRef));
const myCompFactory = (<ComponentFactoryResolver>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 = <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', () => { it('should support static attributes', () => {

View File

@ -564,6 +564,24 @@ export function main() {
.toThrowError( .toThrowError(
/Template parse errors:\nNo provider for SimpleDirective \("\[ERROR ->\]<div needsDirectiveFromHost><\/div>"\): .*SimpleComponent.html@0:0/); /Template parse errors:\nNo provider for SimpleDirective \("\[ERROR ->\]<div needsDirectiveFromHost><\/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', () => { describe('static attributes', () => {
@ -655,13 +673,16 @@ export function main() {
class TestModule { class TestModule {
} }
const testInjector = {}; const testInjector = <Injector>{
get: (token: any, notFoundValue: any) =>
token === 'someToken' ? 'someNewValue' : notFoundValue
};
const compFactory = TestBed.configureTestingModule({imports: [TestModule]}) const compFactory = TestBed.configureTestingModule({imports: [TestModule]})
.get(ComponentFactoryResolver) .get(ComponentFactoryResolver)
.resolveComponentFactory(TestComp); .resolveComponentFactory(TestComp);
const component = compFactory.create(<Injector>testInjector); const component = compFactory.create(testInjector);
expect(component.instance.vcr.parentInjector).toBe(testInjector); expect(component.instance.vcr.parentInjector.get('someToken')).toBe('someNewValue');
}); });
it('should inject TemplateRef', () => { it('should inject TemplateRef', () => {

View File

@ -1086,7 +1086,7 @@ export declare abstract class ViewContainerRef {
readonly abstract length: number; readonly abstract length: number;
readonly abstract parentInjector: Injector; readonly abstract parentInjector: Injector;
abstract clear(): void; abstract clear(): void;
abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][]): ComponentRef<C>; abstract createComponent<C>(componentFactory: ComponentFactory<C>, index?: number, injector?: Injector, projectableNodes?: any[][], ngModule?: NgModuleRef<any>): ComponentRef<C>;
abstract createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number): EmbeddedViewRef<C>; abstract createEmbeddedView<C>(templateRef: TemplateRef<C>, context?: C, index?: number): EmbeddedViewRef<C>;
abstract detach(index?: number): ViewRef; abstract detach(index?: number): ViewRef;
abstract get(index: number): ViewRef; abstract get(index: number): ViewRef;