fix(ivy): use the root view injector when resolving dependencies (#27090)

PR Close #27090
This commit is contained in:
Marc Laval 2018-11-14 17:04:22 +01:00 committed by Andrew Kushnir
parent 8a626288a6
commit 1c9e526a83
7 changed files with 221 additions and 26 deletions

View File

@ -72,6 +72,25 @@ export const SCHEDULER = new InjectionToken<((fn: () => void) => void)>('SCHEDUL
export const WRAP_RENDERER_FACTORY2 =
new InjectionToken<(rf: RendererFactory2) => RendererFactory2>('WRAP_RENDERER_FACTORY2');
const NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR = {};
function createChainedInjector(rootViewInjector: Injector, moduleInjector: Injector): Injector {
return {
get: <T>(token: Type<T>| InjectionToken<T>, notFoundValue?: T): T => {
const value = rootViewInjector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);
if (value !== 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)
return value;
}
return moduleInjector.get(token, notFoundValue);
}
};
}
/**
* Render3 implementation of {@link viewEngine_ComponentFactory}.
*/
@ -122,7 +141,7 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
// Create the root view. Uses empty TView and ContentTemplate.
const rootView: LViewData = createLViewData(
renderer, createTView(-1, null, 1, 0, null, null, null), rootContext, rootFlags);
rootView[INJECTOR] = ngModule && ngModule.injector || null;
rootView[INJECTOR] = ngModule ? createChainedInjector(injector, ngModule.injector) : injector;
// rootView is the parent when bootstrapping
const oldView = enterView(rootView, null);

View File

@ -236,9 +236,12 @@ export function getParentInjectorTNode(
}
let viewOffset = getParentInjectorViewOffset(location);
// view offset is 1
let parentView = startView;
let parentTNode = startView[HOST_NODE] as TElementNode;
while (viewOffset > 0) {
// view offset is superior to 1
while (viewOffset > 1) {
parentView = parentView[DECLARATION_VIEW] !;
parentTNode = parentView[HOST_NODE] as TElementNode;
viewOffset--;

View File

@ -8,6 +8,7 @@
import {ChangeDetectorRef as ViewEngine_ChangeDetectorRef} from '../change_detection/change_detector_ref';
import {Injector, NullInjector} from '../di/injector';
import {InjectFlags} from '../di/injector_compatibility';
import {ComponentFactory as viewEngine_ComponentFactory, ComponentRef as viewEngine_ComponentRef} from '../linker/component_factory';
import {ElementRef as ViewEngine_ElementRef} from '../linker/element_ref';
import {NgModuleRef as viewEngine_NgModuleRef} from '../linker/ng_module_factory';
@ -159,7 +160,8 @@ export class NodeInjector implements Injector {
private _hostView: LViewData) {}
get(token: any, notFoundValue?: any): any {
return getOrCreateInjectable(this._tNode, this._hostView, token, notFoundValue);
return getOrCreateInjectable(
this._tNode, this._hostView, token, InjectFlags.Default, notFoundValue);
}
}

View File

@ -251,6 +251,9 @@
{
"name": "NOT_FOUND"
},
{
"name": "NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR"
},
{
"name": "NOT_YET"
},
@ -632,6 +635,9 @@
{
"name": "couldBeInjectableType"
},
{
"name": "createChainedInjector"
},
{
"name": "createElementRef"
},

View File

@ -503,6 +503,9 @@
{
"name": "NOT_FOUND"
},
{
"name": "NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR"
},
{
"name": "NOT_YET"
},
@ -1310,6 +1313,9 @@
{
"name": "couldBeInjectableType"
},
{
"name": "createChainedInjector"
},
{
"name": "createContainerRef"
},

View File

@ -6,12 +6,14 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component as _Component, ElementRef, InjectFlags, Injectable as _Injectable, InjectionToken, InjectorType, Provider, createInjector, defineInjectable, defineInjector, inject} from '../../src/core';
import {Component as _Component, ComponentFactoryResolver, ElementRef, InjectFlags, Injectable as _Injectable, InjectionToken, InjectorType, Provider, RendererFactory2, ViewContainerRef, createInjector, defineInjectable, defineInjector, inject, ɵNgModuleDef as NgModuleDef} from '../../src/core';
import {forwardRef} from '../../src/di/forward_ref';
import {ProvidersFeature, defineComponent, defineDirective, directiveInject} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, text, textBinding} from '../../src/render3/instructions';
import {ProvidersFeature, defineComponent, defineDirective, directiveInject, injectComponentFactoryResolver} from '../../src/render3/index';
import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementEnd, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, text, textBinding} from '../../src/render3/instructions';
import {RenderFlags} from '../../src/render3/interfaces/definition';
import {NgModuleFactory} from '../../src/render3/ng_module_ref';
import {getRendererFactory2} from './imported_renderer2';
import {ComponentFixture} from './render_util';
const Component: typeof _Component = function(...args: any[]): any {
@ -984,6 +986,137 @@ describe('providers', () => {
});
});
describe('- dynamic components dependency resolution', () => {
let hostComponent: HostComponent|null = null;
@Component({
template: `{{s}}`,
})
class EmbeddedComponent {
constructor(private s: String) {}
static ngComponentDef = defineComponent({
type: EmbeddedComponent,
selectors: [['embedded-cmp']],
factory: () => new EmbeddedComponent(directiveInject(String)),
consts: 1,
vars: 1,
template: (rf: RenderFlags, cmp: EmbeddedComponent) => {
if (rf & RenderFlags.Create) {
text(0);
}
if (rf & RenderFlags.Update) {
textBinding(0, interpolation1('', cmp.s, ''));
}
}
});
}
@Component({template: `foo`, providers: [{provide: String, useValue: 'From host component'}]})
class HostComponent {
constructor(public vcref: ViewContainerRef, public cfr: ComponentFactoryResolver) {}
static ngComponentDef = defineComponent({
type: HostComponent,
selectors: [['host-cmp']],
factory: () => hostComponent = new HostComponent(
directiveInject(ViewContainerRef as any), injectComponentFactoryResolver()),
consts: 1,
vars: 0,
template: (rf: RenderFlags, cmp: HostComponent) => {
if (rf & RenderFlags.Create) {
text(0, 'foo');
}
},
features: [
ProvidersFeature([{provide: String, useValue: 'From host component'}]),
],
});
}
@Component({
template: `<host-cmp></host-cmp>`,
providers: [{provide: String, useValue: 'From app component'}]
})
class AppComponent {
constructor() {}
static ngComponentDef = defineComponent({
type: AppComponent,
selectors: [['app-cmp']],
factory: () => new AppComponent(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, cmp: AppComponent) => {
if (rf & RenderFlags.Create) {
element(0, 'host-cmp');
}
},
features: [
ProvidersFeature([{provide: String, useValue: 'From app component'}]),
],
directives: [HostComponent]
});
}
it('should not cross the root view boundary, and use the root view injector', () => {
const fixture = new ComponentFixture(AppComponent);
expect(fixture.html).toEqual('<host-cmp>foo</host-cmp>');
hostComponent !.vcref.createComponent(
hostComponent !.cfr.resolveComponentFactory(EmbeddedComponent), undefined, {
get: (token: any, notFoundValue?: any) => {
return token === String ? 'From custom root view injector' : notFoundValue;
}
});
fixture.update();
expect(fixture.html)
.toEqual(
'<host-cmp>foo</host-cmp><embedded-cmp>From custom root view injector</embedded-cmp>');
});
it('should not cross the root view boundary, and use the module injector if no root view injector',
() => {
const fixture = new ComponentFixture(AppComponent);
expect(fixture.html).toEqual('<host-cmp>foo</host-cmp>');
class MyAppModule {
static ngInjectorDef = defineInjector({
factory: () => new MyAppModule(),
imports: [],
providers: [
{provide: RendererFactory2, useValue: getRendererFactory2(document)},
{provide: String, useValue: 'From module injector'}
]
});
static ngModuleDef: NgModuleDef<any> = { bootstrap: [] } as any;
}
const myAppModuleFactory = new NgModuleFactory(MyAppModule);
const ngModuleRef = myAppModuleFactory.create(null);
hostComponent !.vcref.createComponent(
hostComponent !.cfr.resolveComponentFactory(EmbeddedComponent), undefined,
{get: (token: any, notFoundValue?: any) => notFoundValue}, undefined, ngModuleRef);
fixture.update();
expect(fixture.html)
.toMatch(
/<host-cmp>foo<\/host-cmp><embedded-cmp _nghost-c(\d+)="">From module injector<\/embedded-cmp>/);
});
it('should cross the root view boundary to the parent of the host, thanks to the default root view injector',
() => {
const fixture = new ComponentFixture(AppComponent);
expect(fixture.html).toEqual('<host-cmp>foo</host-cmp>');
hostComponent !.vcref.createComponent(
hostComponent !.cfr.resolveComponentFactory(EmbeddedComponent));
fixture.update();
expect(fixture.html)
.toEqual('<host-cmp>foo</host-cmp><embedded-cmp>From app component</embedded-cmp>');
});
});
describe('deps boundary:', () => {
it('the deps of a token declared in providers should not be resolved with tokens from viewProviders',
() => {

View File

@ -923,24 +923,26 @@ describe('ViewContainerRef', () => {
describe('createComponent', () => {
let templateExecutionCounter = 0;
class EmbeddedComponent {
static ngComponentDef = defineComponent({
type: EmbeddedComponent,
encapsulation: ViewEncapsulation.None,
selectors: [['embedded-cmp']],
factory: () => new EmbeddedComponent(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, cmp: EmbeddedComponent) => {
templateExecutionCounter++;
if (rf & RenderFlags.Create) {
text(0, 'foo');
}
}
});
}
it('should work without Injector and NgModuleRef', () => {
class EmbeddedComponent {
constructor() {}
static ngComponentDef = defineComponent({
type: EmbeddedComponent,
encapsulation: ViewEncapsulation.None,
selectors: [['embedded-cmp']],
factory: () => new EmbeddedComponent(),
consts: 1,
vars: 0,
template: (rf: RenderFlags, cmp: EmbeddedComponent) => {
templateExecutionCounter++;
if (rf & RenderFlags.Create) {
text(0, 'foo');
}
}
});
}
templateExecutionCounter = 0;
const fixture =
new TemplateFixture(createTemplate, updateTemplate, 3, 1, [DirectiveWithVCRef]);
@ -965,13 +967,33 @@ describe('ViewContainerRef', () => {
});
it('should work with NgModuleRef and Injector', () => {
class EmbeddedComponent {
constructor(public s: String) {}
static ngComponentDef = defineComponent({
type: EmbeddedComponent,
encapsulation: ViewEncapsulation.None,
selectors: [['embedded-cmp']],
factory: () => new EmbeddedComponent(directiveInject(String)),
consts: 1,
vars: 0,
template: (rf: RenderFlags, cmp: EmbeddedComponent) => {
templateExecutionCounter++;
if (rf & RenderFlags.Create) {
text(0, 'foo');
}
}
});
}
class MyAppModule {
static ngInjectorDef = defineInjector({
factory: () => new MyAppModule(),
imports: [],
providers: [
{provide: APP_ROOT, useValue: true},
{provide: RendererFactory2, useValue: getRendererFactory2(document)}
{provide: RendererFactory2, useValue: getRendererFactory2(document)},
{provide: String, useValue: 'module'}
]
});
static ngModuleDef: NgModuleDef<any> = { bootstrap: [] } as any;
@ -982,7 +1004,10 @@ describe('ViewContainerRef', () => {
class SomeModule {
static ngInjectorDef = defineInjector({
factory: () => new SomeModule(),
providers: [{provide: NgModuleRef, useValue: ngModuleRef}]
providers: [
{provide: NgModuleRef, useValue: ngModuleRef},
{provide: String, useValue: 'injector'}
]
});
}
const injector = createInjector(SomeModule);
@ -993,11 +1018,12 @@ describe('ViewContainerRef', () => {
expect(fixture.html).toEqual('<p vcref=""></p>');
expect(templateExecutionCounter).toEqual(0);
directiveInstance !.vcref.createComponent(
const componentRef = directiveInstance !.vcref.createComponent(
directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent), 0, injector);
fixture.update();
expect(fixture.html).toEqual('<p vcref=""></p><embedded-cmp>foo</embedded-cmp>');
expect(templateExecutionCounter).toEqual(2);
expect(componentRef.instance.s).toEqual('injector');
directiveInstance !.vcref.createComponent(
directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent), 0, undefined,