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 {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<C>(
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`.

View File

@ -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;
}

View File

@ -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<C>(
componentFactory: ComponentFactory<C>, index?: number, injector?: Injector,
projectableNodes?: any[][]): ComponentRef<C> {
projectableNodes?: any[][], ngModuleRef?: NgModuleRef<any>): ComponentRef<C> {
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);

View File

@ -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: '<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]})
.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 = <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', () => {

View File

@ -564,6 +564,24 @@ export function main() {
.toThrowError(
/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', () => {
@ -655,13 +673,16 @@ export function main() {
class TestModule {
}
const testInjector = {};
const testInjector = <Injector>{
get: (token: any, notFoundValue: any) =>
token === 'someToken' ? 'someNewValue' : notFoundValue
};
const compFactory = TestBed.configureTestingModule({imports: [TestModule]})
.get(ComponentFactoryResolver)
.resolveComponentFactory(TestComp);
const component = compFactory.create(<Injector>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', () => {

View File

@ -1086,7 +1086,7 @@ export declare abstract class ViewContainerRef {
readonly abstract length: number;
readonly abstract parentInjector: Injector;
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 detach(index?: number): ViewRef;
abstract get(index: number): ViewRef;