From 8f8572fd3e3c988ef38e1293cbe7b1403188742e Mon Sep 17 00:00:00 2001 From: Marc Laval Date: Thu, 13 Dec 2018 11:14:33 +0100 Subject: [PATCH] fix(ivy): @Host should behave as in View Engine (#27646) PR Close #27646 --- packages/core/src/render3/di.ts | 54 ++-- packages/core/src/render3/instructions.ts | 4 +- .../core/src/render3/interfaces/injector.ts | 1 - .../core/src/render3/node_manipulation.ts | 20 +- packages/core/src/render3/query.ts | 5 +- packages/core/src/render3/util.ts | 32 ++- .../src/render3/view_engine_compatibility.ts | 4 +- .../bundling/todo/bundle.golden_symbols.json | 3 + packages/core/test/linker/integration_spec.ts | 25 +- .../linker/view_injector_integration_spec.ts | 18 +- packages/core/test/render3/di_spec.ts | 244 +++++++++++++++++- packages/core/test/render3/render_util.ts | 10 +- 12 files changed, 337 insertions(+), 83 deletions(-) diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 74ee5aa5ef..7bd723aa79 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -21,7 +21,8 @@ import {AttributeMarker, TContainerNode, TElementContainerNode, TElementNode, TN import {DECLARATION_VIEW, HOST_NODE, INJECTOR, LView, TData, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes} from './node_assert'; import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state'; -import {getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, stringify} from './util'; +import {getHostTElementNode, getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, isComponentDef, stringify} from './util'; + /** * Defines if the call to `inject` should include `viewProviders` in its resolution. @@ -197,7 +198,7 @@ export function getInjectorIndex(tNode: TNode, hostView: LView): number { */ export function getParentInjectorLocation(tNode: TNode, view: LView): RelativeInjectorLocation { if (tNode.parent && tNode.parent.injectorIndex !== -1) { - return tNode.parent.injectorIndex as any; // ViewOffset is 0, AcrossHostBoundary is 0 + return tNode.parent.injectorIndex as any; // ViewOffset is 0 } // For most cases, the parent injector index can be found on the host node (e.g. for component @@ -210,13 +211,9 @@ export function getParentInjectorLocation(tNode: TNode, view: LView): RelativeIn hostTNode = view[HOST_NODE] !; viewOffset++; } - const acrossHostBoundary = hostTNode && hostTNode.type === TNodeType.Element ? - RelativeInjectorLocationFlags.AcrossHostBoundary : - 0; return hostTNode ? - hostTNode.injectorIndex | (viewOffset << RelativeInjectorLocationFlags.ViewOffsetShift) | - acrossHostBoundary : + hostTNode.injectorIndex | (viewOffset << RelativeInjectorLocationFlags.ViewOffsetShift) : -1 as any; } @@ -323,6 +320,7 @@ export function getOrCreateInjectable( let previousTView: TView|null = null; let injectorIndex = getInjectorIndex(tNode, lView); let parentLocation: RelativeInjectorLocation = NO_PARENT_INJECTOR; + let hostTElementNode: TNode|null = flags & InjectFlags.Host ? getHostTElementNode(lView) : null; // If we should skip this injector, or if there is no injector on this node, start by searching // the parent injector. @@ -330,7 +328,7 @@ export function getOrCreateInjectable( parentLocation = injectorIndex === -1 ? getParentInjectorLocation(tNode, lView) : lView[injectorIndex + PARENT_INJECTOR]; - if (!shouldSearchParent(flags, parentLocation)) { + if (!shouldSearchParent(flags, false)) { injectorIndex = -1; } else { previousTView = lView[TVIEW]; @@ -350,13 +348,14 @@ export function getOrCreateInjectable( // At this point, we have an injector which *may* contain the token, so we step through // the providers and directives associated with the injector's corresponding node to get // the instance. - const instance: T|null = - searchTokensOnInjector(injectorIndex, lView, token, previousTView); + const instance: T|null = searchTokensOnInjector( + injectorIndex, lView, token, previousTView, flags, hostTElementNode); if (instance !== NOT_FOUND) { return instance; } } - if (shouldSearchParent(flags, parentLocation) && + if (shouldSearchParent( + flags, lView[TVIEW].data[injectorIndex + TNODE] === hostTElementNode) && bloomHasToken(bloomHash, injectorIndex, lView)) { // The def wasn't found anywhere on this node, so it was a false positive. // Traverse up the tree and continue searching. @@ -396,7 +395,7 @@ const NOT_FOUND = {}; function searchTokensOnInjector( injectorIndex: number, lView: LView, token: Type| InjectionToken, - previousTView: TView | null) { + previousTView: TView | null, flags: InjectFlags, hostTElementNode: TNode | null) { const currentTView = lView[TVIEW]; const tNode = currentTView.data[injectorIndex + TNODE] as TNode; // First, we need to determine if view providers can be accessed by the starting element. @@ -418,7 +417,12 @@ function searchTokensOnInjector( // into the ViewProviders. (previousTView != currentTView && (tNode.type === TNodeType.Element)); - const injectableIdx = locateDirectiveOrProvider(tNode, lView, token, canAccessViewProviders); + // This special case happens when there is a @host on the inject and when we are searching + // on the host element node. + const isHostSpecialCase = (flags & InjectFlags.Host) && hostTElementNode === tNode; + + const injectableIdx = + locateDirectiveOrProvider(tNode, lView, token, canAccessViewProviders, isHostSpecialCase); if (injectableIdx !== null) { return getNodeInjectable(currentTView.data, lView, injectableIdx, tNode as TElementNode); } else { @@ -433,13 +437,13 @@ function searchTokensOnInjector( * @param lView The view we are currently processing * @param token Provider token or type of a directive to look for. * @param canAccessViewProviders Whether view providers should be considered. + * @param isHostSpecialCase Whether the host special case applies. * @returns Index of a found directive or provider, or null when none found. */ export function locateDirectiveOrProvider( - tNode: TNode, lView: LView, token: Type| InjectionToken, - canAccessViewProviders: boolean): number|null { + tNode: TNode, lView: LView, token: Type| InjectionToken, canAccessViewProviders: boolean, + isHostSpecialCase: boolean | number): number|null { const tView = lView[TVIEW]; - const nodeFlags = tNode.flags; const nodeProviderIndexes = tNode.providerIndexes; const tInjectables = tView.data; @@ -450,13 +454,21 @@ export function locateDirectiveOrProvider( nodeProviderIndexes >> TNodeProviderIndexes.CptViewProvidersCountShift; const startingIndex = canAccessViewProviders ? injectablesStart : injectablesStart + cptViewProvidersCount; - for (let i = startingIndex; i < directiveEnd; i++) { + // When the host special case applies, only the viewProviders and the component are visible + const endIndex = isHostSpecialCase ? injectablesStart + cptViewProvidersCount : directiveEnd; + for (let i = startingIndex; i < endIndex; i++) { const providerTokenOrDef = tInjectables[i] as InjectionToken| Type| DirectiveDef; if (i < directivesStart && token === providerTokenOrDef || i >= directivesStart && (providerTokenOrDef as DirectiveDef).type === token) { return i; } } + if (isHostSpecialCase) { + const dirDef = tInjectables[directivesStart] as DirectiveDef; + if (dirDef && isComponentDef(dirDef) && dirDef.type === token) { + return directivesStart; + } + } return null; } @@ -546,12 +558,8 @@ export function bloomHasToken( } /** Returns true if flags prevent parent injector from being searched for tokens */ -function shouldSearchParent(flags: InjectFlags, parentLocation: RelativeInjectorLocation): boolean| - number { - return !( - flags & InjectFlags.Self || - (flags & InjectFlags.Host && - ((parentLocation as any as number) & RelativeInjectorLocationFlags.AcrossHostBoundary))); +function shouldSearchParent(flags: InjectFlags, isFirstHostTNode: boolean): boolean|number { + return !(flags & InjectFlags.Self) && !(flags & InjectFlags.Host && isFirstHostTNode); } export function injectInjector() { diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 2f18c17bc4..4ca0b1aae1 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -33,14 +33,14 @@ import {SanitizerFn} from './interfaces/sanitization'; import {StylingIndex} from './interfaces/styling'; import {BINDING_INDEX, CLEANUP, CONTAINER_INDEX, CONTENT_QUERIES, CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, INJECTOR, LView, LViewFlags, NEXT, OpaqueViewState, PARENT, QUERIES, RENDERER, RENDERER_FACTORY, RootContext, RootContextFlags, SANITIZER, TAIL, TVIEW, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; -import {appendChild, appendProjectedNode, createTextNode, findComponentView, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation'; +import {appendChild, appendProjectedNode, createTextNode, getLViewChild, getRenderParent, insertView, removeView} from './node_manipulation'; import {isNodeMatchingSelectorList, matchingSelectorIndex} from './node_selector_matcher'; import {decreaseElementDepthCount, enterView, getBindingsEnabled, getCheckNoChangesMode, getContextLView, getCurrentDirectiveDef, getElementDepthCount, getFirstTemplatePass, getIsParent, getLView, getPreviousOrParentTNode, increaseElementDepthCount, isCreationMode, leaveView, nextContextImpl, resetComponentState, setBindingRoot, setCheckNoChangesMode, setCurrentDirectiveDef, setFirstTemplatePass, setIsParent, setPreviousOrParentTNode} from './state'; import {createStylingContextTemplate, renderStyleAndClassBindings, setStyle, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings'; import {BoundPlayerFactory} from './styling/player_factory'; import {getStylingContext, isAnimationProp} from './styling/util'; import {NO_CHANGE} from './tokens'; -import {getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, loadInternal, readElementValue, readPatchedLView, stringify} from './util'; +import {findComponentView, getComponentViewByIndex, getNativeByIndex, getNativeByTNode, getRootContext, getRootView, getTNode, isComponent, isComponentDef, loadInternal, readElementValue, readPatchedLView, stringify} from './util'; /** diff --git a/packages/core/src/render3/interfaces/injector.ts b/packages/core/src/render3/interfaces/injector.ts index 043cd39d8f..199b90b0de 100644 --- a/packages/core/src/render3/interfaces/injector.ts +++ b/packages/core/src/render3/interfaces/injector.ts @@ -26,7 +26,6 @@ export interface RelativeInjectorLocation { __brand__: 'RelativeInjectorLocation export const enum RelativeInjectorLocationFlags { InjectorIndexMask = 0b111111111111111, - AcrossHostBoundary = 0b1000000000000000, ViewOffsetShift = 16, NO_PARENT = -1, } diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 47c590a770..efa5f1d50f 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -15,7 +15,7 @@ import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection' import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {CLEANUP, CONTAINER_INDEX, FLAGS, HEADER_OFFSET, HOST_NODE, HookData, LView, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; import {assertNodeType} from './node_assert'; -import {getNativeByTNode, isLContainer, isRootView, readElementValue, stringify} from './util'; +import {findComponentView, getNativeByTNode, isLContainer, isRootView, readElementValue, stringify} from './util'; const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4 + unused5; @@ -195,24 +195,6 @@ function walkTNodeTree( } } -/** - * Given a current view, finds the nearest component's host (LElement). - * - * @param lView LView for which we want a host element node - * @returns The host node - */ -export function findComponentView(lView: LView): LView { - let rootTNode = lView[HOST_NODE]; - - while (rootTNode && rootTNode.type === TNodeType.View) { - ngDevMode && assertDefined(lView[PARENT], 'lView.parent'); - lView = lView[PARENT] !; - rootTNode = lView[HOST_NODE]; - } - - return lView; -} - /** * NOTE: for performance reasons, the possible actions are inlined within the function instead of * being passed as an argument. diff --git a/packages/core/src/render3/query.ts b/packages/core/src/render3/query.ts index d0ea61c539..4066f151a2 100644 --- a/packages/core/src/render3/query.ts +++ b/packages/core/src/render3/query.ts @@ -250,7 +250,8 @@ function queryByReadToken(read: any, tNode: TNode, currentView: LView): any { if (typeof factoryFn === 'function') { return factoryFn(); } else { - const matchingIdx = locateDirectiveOrProvider(tNode, currentView, read as Type, false); + const matchingIdx = + locateDirectiveOrProvider(tNode, currentView, read as Type, false, false); if (matchingIdx !== null) { return getNodeInjectable( currentView[TVIEW].data, currentView, matchingIdx, tNode as TElementNode); @@ -304,7 +305,7 @@ function add( if (type === ViewEngine_TemplateRef) { result = queryByTemplateRef(type, tNode, currentView, predicate.read); } else { - const matchingIdx = locateDirectiveOrProvider(tNode, currentView, type, false); + const matchingIdx = locateDirectiveOrProvider(tNode, currentView, type, false, false); if (matchingIdx !== null) { result = queryRead(tNode, currentView, predicate.read, matchingIdx); } diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index 03c6fb29d4..9cdd42d69b 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -13,7 +13,7 @@ import {ACTIVE_INDEX, LContainer} from './interfaces/container'; import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context'; import {ComponentDef, DirectiveDef} from './interfaces/definition'; import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector'; -import {TContainerNode, TElementNode, TNode, TNodeFlags} from './interfaces/node'; +import {TContainerNode, TElementNode, TNode, TNodeFlags, TNodeType} from './interfaces/node'; import {RComment, RElement, RText} from './interfaces/renderer'; import {StylingContext} from './interfaces/styling'; import {CONTEXT, DECLARATION_VIEW, FLAGS, HEADER_OFFSET, HOST, HOST_NODE, LView, LViewFlags, PARENT, RootContext, TData, TVIEW, TView} from './interfaces/view'; @@ -260,3 +260,33 @@ export function addAllToArray(items: any[], arr: any[]) { arr.push(items[i]); } } + +/** + * Given a current view, finds the nearest component's host (LElement). + * + * @param lView LView for which we want a host element node + * @param declarationMode indicates whether DECLARATION_VIEW or PARENT should be used to climb the + * tree. + * @returns The host node + */ +export function findComponentView(lView: LView, declarationMode?: boolean): LView { + let rootTNode = lView[HOST_NODE]; + + while (rootTNode && rootTNode.type === TNodeType.View) { + ngDevMode && assertDefined( + lView[declarationMode ? DECLARATION_VIEW : PARENT], + declarationMode ? 'lView.declarationView' : 'lView.parent'); + lView = lView[declarationMode ? DECLARATION_VIEW : PARENT] !; + rootTNode = lView[HOST_NODE]; + } + + return lView; +} + +/** + * Return the host TElementNode of the starting LView + * @param lView the starting LView. + */ +export function getHostTElementNode(lView: LView): TElementNode|null { + return findComponentView(lView, true)[HOST_NODE] as TElementNode; +} diff --git a/packages/core/src/render3/view_engine_compatibility.ts b/packages/core/src/render3/view_engine_compatibility.ts index d38319b354..0f924ba8c9 100644 --- a/packages/core/src/render3/view_engine_compatibility.ts +++ b/packages/core/src/render3/view_engine_compatibility.ts @@ -25,9 +25,9 @@ import {LQueries} from './interfaces/query'; import {RComment, RElement, Renderer3, isProceduralRenderer} from './interfaces/renderer'; import {CONTAINER_INDEX, CONTEXT, HOST_NODE, LView, QUERIES, RENDERER, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes} from './node_assert'; -import {addRemoveViewFromContainer, appendChild, detachView, findComponentView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode, removeView} from './node_manipulation'; +import {addRemoveViewFromContainer, appendChild, detachView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode, removeView} from './node_manipulation'; import {getLView, getPreviousOrParentTNode} from './state'; -import {getComponentViewByIndex, getNativeByTNode, getParentInjectorTNode, getParentInjectorView, hasParentInjector, isComponent, isLContainer, isRootView} from './util'; +import {findComponentView, getComponentViewByIndex, getNativeByTNode, getParentInjectorTNode, getParentInjectorView, hasParentInjector, isComponent, isLContainer, isRootView} from './util'; import {ViewRef} from './view_ref'; diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 4a0a88740c..4ea9b4f786 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -683,6 +683,9 @@ { "name": "getHostNative" }, + { + "name": "getHostTElementNode" + }, { "name": "getInitialIndex" }, diff --git a/packages/core/test/linker/integration_spec.ts b/packages/core/test/linker/integration_spec.ts index 8313c73f81..9e661038df 100644 --- a/packages/core/test/linker/integration_spec.ts +++ b/packages/core/test/linker/integration_spec.ts @@ -1327,24 +1327,21 @@ function declareTests(config?: {useJit: boolean}) { expect(comp.injectable).toBeAnInstanceOf(InjectableService); }); - fixmeIvy( - 'FW-804: Injection of view providers with the @Host annotation works differently in ivy') - .it('should support viewProviders', () => { - TestBed.configureTestingModule({ - declarations: - [MyComp, DirectiveProvidingInjectableInView, DirectiveConsumingInjectable], - schemas: [NO_ERRORS_SCHEMA], - }); - const template = ` + it('should support viewProviders', () => { + TestBed.configureTestingModule({ + declarations: [MyComp, DirectiveProvidingInjectableInView, DirectiveConsumingInjectable], + schemas: [NO_ERRORS_SCHEMA], + }); + const template = ` `; - TestBed.overrideComponent(DirectiveProvidingInjectableInView, {set: {template}}); - const fixture = TestBed.createComponent(DirectiveProvidingInjectableInView); + TestBed.overrideComponent(DirectiveProvidingInjectableInView, {set: {template}}); + const fixture = TestBed.createComponent(DirectiveProvidingInjectableInView); - const comp = fixture.debugElement.children[0].references !['consuming']; - expect(comp.injectable).toBeAnInstanceOf(InjectableService); - }); + const comp = fixture.debugElement.children[0].references !['consuming']; + expect(comp.injectable).toBeAnInstanceOf(InjectableService); + }); it('should support unbounded lookup', () => { TestBed.configureTestingModule({ diff --git a/packages/core/test/linker/view_injector_integration_spec.ts b/packages/core/test/linker/view_injector_integration_spec.ts index 7748b3642e..d9c5e6070c 100644 --- a/packages/core/test/linker/view_injector_integration_spec.ts +++ b/packages/core/test/linker/view_injector_integration_spec.ts @@ -725,16 +725,14 @@ class TestComp { expect(d.dependency).toBeNull(); }); - fixmeIvy('unknown').it( - 'should instantiate directives that depends on the host component', () => { - TestBed.configureTestingModule( - {declarations: [SimpleComponent, NeedsComponentFromHost]}); - TestBed.overrideComponent( - SimpleComponent, {set: {template: '
'}}); - const el = createComponent('
'); - const d = el.children[0].children[0].injector.get(NeedsComponentFromHost); - expect(d.dependency).toBeAnInstanceOf(SimpleComponent); - }); + it('should instantiate directives that depends on the host component', () => { + TestBed.configureTestingModule({declarations: [SimpleComponent, NeedsComponentFromHost]}); + TestBed.overrideComponent( + SimpleComponent, {set: {template: '
'}}); + const el = createComponent('
'); + const d = el.children[0].children[0].injector.get(NeedsComponentFromHost); + expect(d.dependency).toBeAnInstanceOf(SimpleComponent); + }); fixmeIvy('unknown').it( 'should instantiate host views for components that have a @Host dependency ', () => { diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 41701f6fd4..3ac7c644d2 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -6,8 +6,8 @@ * found in the LICENSE file at https://angular.io/license */ -import {Attribute, ChangeDetectorRef, ElementRef, Host, InjectFlags, Injector, Optional, Renderer2, Self, SkipSelf, TemplateRef, ViewContainerRef, createInjector, defineInjectable, defineInjector} from '@angular/core'; -import {RenderFlags} from '@angular/core/src/render3/interfaces/definition'; +import {Attribute, ChangeDetectorRef, ElementRef, Host, Inject, InjectFlags, Injector, Optional, Renderer2, Self, SkipSelf, TemplateRef, ViewContainerRef, createInjector, defineInjectable, defineInjector} from '@angular/core'; +import {ComponentType, RenderFlags} from '@angular/core/src/render3/interfaces/definition'; import {defineComponent} from '../../src/render3/definition'; import {bloomAdd, bloomHasToken, bloomHashBitOrFactory as bloomHash, getOrCreateNodeInjectorForNode} from '../../src/render3/di'; @@ -1066,8 +1066,12 @@ describe('di', () => { describe('@Host', () => { let dirA: DirA|null = null; + let dirString: DirString|null = null; - beforeEach(() => dirA = null); + beforeEach(() => { + dirA = null; + dirString = null; + }); class DirA { constructor(@Host() public dirB: DirB) {} @@ -1079,13 +1083,93 @@ describe('di', () => { }); } - it('should not find providers across component boundaries', () => { + class DirString { + constructor(@Host() public s: String) {} + + static ngDirectiveDef = defineDirective({ + type: DirString, + selectors: [['', 'dirString', '']], + factory: () => dirString = new DirString(directiveInject(String, InjectFlags.Host)) + }); + } + + it('should find viewProviders on the host itself', () => { + /**
*/ + const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'div', ['dirString', '']); + } + }, 1, 0, [DirString], [], null, [], [{provide: String, useValue: 'Foo'}]); + + /* */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'comp'); + } + }, 1, 0, [Comp]); + + new ComponentFixture(App); + expect(dirString !.s).toEqual('Foo'); + }); + + it('should find host component on the host itself', () => { + let dirComp: DirComp|null = null; + + class DirComp { + constructor(@Host() public comp: any) {} + + static ngDirectiveDef = defineDirective({ + type: DirComp, + selectors: [['', 'dirCmp', '']], + factory: () => dirComp = new DirComp(directiveInject(Comp, InjectFlags.Host)) + }); + } + + /**
*/ + const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'div', ['dirCmp', '']); + } + }, 1, 0, [DirComp]); + + /* */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'comp'); + } + }, 1, 0, [Comp]); + + new ComponentFixture(App); + expect(dirComp !.comp instanceof Comp).toBeTruthy(); + }); + + it('should not find providers on the host itself', () => { + /**
*/ + const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'div', ['dirString', '']); + } + }, 1, 0, [DirString], [], null, [{provide: String, useValue: 'Foo'}]); + + /* */ + const App = createComponent('app', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'comp'); + } + }, 1, 0, [Comp]); + + expect(() => { + new ComponentFixture(App); + }).toThrowError(/NodeInjector: NOT_FOUND \[String\]/); + }); + + it('should not find other directives on the host itself', () => { /**
*/ const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) { if (rf & RenderFlags.Create) { element(0, 'div', ['dirA', '']); } - }, 1, 0, [DirA, DirB]); + }, 1, 0, [DirA]); /* */ const App = createComponent('app', function(rf: RenderFlags, ctx: any) { @@ -1099,7 +1183,7 @@ describe('di', () => { }).toThrowError(/NodeInjector: NOT_FOUND \[DirB\]/); }); - it('should not find providers across component boundaries if in inline view', () => { + it('should not find providers on the host itself if in inline view', () => { let comp !: any; /** @@ -1177,6 +1261,154 @@ describe('di', () => { expect(dirA !.dirB).toEqual(dirB); }); + + it('should not find component above the host', () => { + let dirComp: DirComp|null = null; + + class DirComp { + constructor(@Host() public comp: any) {} + + static ngDirectiveDef = defineDirective({ + type: DirComp, + selectors: [['', 'dirCmp', '']], + factory: () => dirComp = new DirComp(directiveInject(App, InjectFlags.Host)) + }); + } + + /**
*/ + const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'div', ['dirCmp', '']); + } + }, 1, 0, [DirComp]); + + /* */ + class App { + static ngComponentDef = defineComponent({ + type: App, + selectors: [['app']], + consts: 1, + vars: 0, + factory: () => new App, + template: function(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + element(0, 'comp'); + } + }, + directives: [Comp], + }); + } + + expect(() => { + new ComponentFixture(App); + }).toThrowError(/NodeInjector: NOT_FOUND \[App\]/); + }); + + describe('regression', () => { + // based on https://stackblitz.com/edit/angular-riss8k?file=src/app/app.component.ts + it('should allow directives with Host flag to inject view providers from containing component', + () => { + let controlContainers: ControlContainer[] = []; + let injectedControlContainer: ControlContainer|null = null; + + class ControlContainer {} + + /* + @Directive({ + selector: '[group]', + providers: [{provide: ControlContainer, useExisting: GroupDirective}] + }) + */ + class GroupDirective { + constructor() { controlContainers.push(this); } + + static ngDirectiveDef = defineDirective({ + type: GroupDirective, + selectors: [['', 'group', '']], + factory: () => new GroupDirective(), + features: [ProvidersFeature( + [{provide: ControlContainer, useExisting: GroupDirective}])], + }); + } + + // @Directive({selector: '[controlName]'}) + class ControlNameDirective { + constructor(@Host() @SkipSelf() @Inject(ControlContainer) parent: + ControlContainer) { + injectedControlContainer = parent; + } + + static ngDirectiveDef = defineDirective({ + type: ControlNameDirective, + selectors: [['', 'controlName', '']], + factory: () => new ControlNameDirective(directiveInject( + ControlContainer, InjectFlags.Host|InjectFlags.SkipSelf)) + }); + } + + /* + @Component({ + selector: 'child', + template: ` + + `, + viewProviders: [{provide: ControlContainer, useExisting: GroupDirective}] + }) + */ + class ChildComponent { + static ngComponentDef = defineComponent({ + type: ChildComponent, + selectors: [['child']], + consts: 1, + vars: 0, + factory: () => new ChildComponent(), + template: function(rf: RenderFlags, ctx: ChildComponent) { + if (rf & RenderFlags.Create) { + element(0, 'input', ['controlName', '', 'type', 'text']); + } + }, + directives: [ControlNameDirective], + features: [ProvidersFeature( + [], [{provide: ControlContainer, useExisting: GroupDirective}])], + }); + } + /* + @Component({ + selector: 'my-app', + template: ` +
+ +
+ ` + }) + */ + class AppComponent { + static ngComponentDef = defineComponent({ + type: AppComponent, + selectors: [['my-app']], + consts: 2, + vars: 0, + factory: () => new AppComponent(), + template: function(rf: RenderFlags, ctx: AppComponent) { + if (rf & RenderFlags.Create) { + elementStart(0, 'div', ['group', '']); + element(1, 'child'); + elementEnd(); + } + }, + directives: [ChildComponent, GroupDirective] + }); + } + + const fixture = new ComponentFixture(AppComponent as ComponentType); + expect(fixture.html) + .toEqual( + '
'); + + expect(controlContainers).toEqual([injectedControlContainer !]); + + }); + }); }); }); }); diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 226ff1e078..2ec2ecfb50 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -7,6 +7,7 @@ */ import {ChangeDetectorRef} from '@angular/core/src/change_detection/change_detector_ref'; +import {Provider} from '@angular/core/src/di/provider'; import {ElementRef} from '@angular/core/src/linker/element_ref'; import {TemplateRef} from '@angular/core/src/linker/template_ref'; import {ViewContainerRef} from '@angular/core/src/linker/view_container_ref'; @@ -24,7 +25,7 @@ import {CreateComponentOptions} from '../../src/render3/component'; import {getDirectivesAtNodeIndex, getLContext, isComponentInstance} from '../../src/render3/context_discovery'; import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition'; import {NG_ELEMENT_ID} from '../../src/render3/fields'; -import {ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index'; +import {ComponentTemplate, ComponentType, DirectiveDef, DirectiveType, ProvidersFeature, RenderFlags, defineComponent, defineDirective, renderComponent as _renderComponent, tick} from '../../src/render3/index'; import {renderTemplate} from '../../src/render3/instructions'; import {DirectiveDefList, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeTypesOrFactory} from '../../src/render3/interfaces/definition'; import {PlayerHandler} from '../../src/render3/interfaces/player'; @@ -295,7 +296,8 @@ export function toHtml(componentOrElement: T | RElement, keepNgReflect = fals export function createComponent( name: string, template: ComponentTemplate, consts: number = 0, vars: number = 0, directives: DirectiveTypesOrFactory = [], pipes: PipeTypesOrFactory = [], - viewQuery: ComponentTemplate| null = null): ComponentType { + viewQuery: ComponentTemplate| null = null, providers: Provider[] = [], + viewProviders: Provider[] = []): ComponentType { return class Component { value: any; static ngComponentDef = defineComponent({ @@ -307,7 +309,9 @@ export function createComponent( template: template, viewQuery: viewQuery, directives: directives, - pipes: pipes + pipes: pipes, + features: (providers.length > 0 || viewProviders.length > 0)? + [ProvidersFeature(providers || [], viewProviders || [])]: [] }); }; }