From 1c9e526a839788ed650bf85288f6e4fa5fdd143a Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Wed, 14 Nov 2018 17:04:22 +0100 Subject: [PATCH] fix(ivy): use the root view injector when resolving dependencies (#27090) PR Close #27090 --- packages/core/src/render3/component_ref.ts | 21 ++- packages/core/src/render3/util.ts | 5 +- .../src/render3/view_engine_compatibility.ts | 4 +- .../hello_world_r2/bundle.golden_symbols.json | 6 + .../todo_r2/bundle.golden_symbols.json | 6 + packages/core/test/render3/providers_spec.ts | 139 +++++++++++++++++- .../test/render3/view_container_ref_spec.ts | 66 ++++++--- 7 files changed, 221 insertions(+), 26 deletions(-) diff --git a/packages/core/src/render3/component_ref.ts b/packages/core/src/render3/component_ref.ts index 240a84cb0e..b7cda56062 100644 --- a/packages/core/src/render3/component_ref.ts +++ b/packages/core/src/render3/component_ref.ts @@ -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: (token: Type| InjectionToken, 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 extends viewEngine_ComponentFactory { // 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); diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index 9c0a9599ab..2ae48084dc 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -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--; diff --git a/packages/core/src/render3/view_engine_compatibility.ts b/packages/core/src/render3/view_engine_compatibility.ts index 6cd486baf3..e1bec3c745 100644 --- a/packages/core/src/render3/view_engine_compatibility.ts +++ b/packages/core/src/render3/view_engine_compatibility.ts @@ -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); } } diff --git a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json index 3efa3d8617..79e0992226 100644 --- a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json @@ -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" }, diff --git a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json index cecfc71b4e..26914e538d 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -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" }, diff --git a/packages/core/test/render3/providers_spec.ts b/packages/core/test/render3/providers_spec.ts index 0f7caf5e0c..5519374930 100644 --- a/packages/core/test/render3/providers_spec.ts +++ b/packages/core/test/render3/providers_spec.ts @@ -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: ``, + 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('foo'); + + 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( + 'fooFrom custom root view injector'); + }); + + 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('foo'); + + class MyAppModule { + static ngInjectorDef = defineInjector({ + factory: () => new MyAppModule(), + imports: [], + providers: [ + {provide: RendererFactory2, useValue: getRendererFactory2(document)}, + {provide: String, useValue: 'From module injector'} + ] + }); + static ngModuleDef: NgModuleDef = { 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( + /foo<\/host-cmp>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('foo'); + + hostComponent !.vcref.createComponent( + hostComponent !.cfr.resolveComponentFactory(EmbeddedComponent)); + fixture.update(); + expect(fixture.html) + .toEqual('fooFrom app component'); + }); + }); + describe('deps boundary:', () => { it('the deps of a token declared in providers should not be resolved with tokens from viewProviders', () => { diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index 3f8653d8e8..009396bdae 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -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 = { 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('

'); expect(templateExecutionCounter).toEqual(0); - directiveInstance !.vcref.createComponent( + const componentRef = directiveInstance !.vcref.createComponent( directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent), 0, injector); fixture.update(); expect(fixture.html).toEqual('

foo'); expect(templateExecutionCounter).toEqual(2); + expect(componentRef.instance.s).toEqual('injector'); directiveInstance !.vcref.createComponent( directiveInstance !.cfr.resolveComponentFactory(EmbeddedComponent), 0, undefined,