fix(ivy): @Host should behave as in View Engine (#27646)

PR Close #27646
This commit is contained in:
Marc Laval 2018-12-13 11:14:33 +01:00 committed by Matias Niemelä
parent e8f7241366
commit 8f8572fd3e
12 changed files with 337 additions and 83 deletions

View File

@ -21,7 +21,8 @@ import {AttributeMarker, TContainerNode, TElementContainerNode, TElementNode, TN
import {DECLARATION_VIEW, HOST_NODE, INJECTOR, LView, TData, TVIEW, TView} from './interfaces/view'; import {DECLARATION_VIEW, HOST_NODE, INJECTOR, LView, TData, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes} from './node_assert'; import {assertNodeOfPossibleTypes} from './node_assert';
import {getLView, getPreviousOrParentTNode, setTNodeAndViewData} from './state'; 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. * 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 { export function getParentInjectorLocation(tNode: TNode, view: LView): RelativeInjectorLocation {
if (tNode.parent && tNode.parent.injectorIndex !== -1) { 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 // 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] !; hostTNode = view[HOST_NODE] !;
viewOffset++; viewOffset++;
} }
const acrossHostBoundary = hostTNode && hostTNode.type === TNodeType.Element ?
RelativeInjectorLocationFlags.AcrossHostBoundary :
0;
return hostTNode ? return hostTNode ?
hostTNode.injectorIndex | (viewOffset << RelativeInjectorLocationFlags.ViewOffsetShift) | hostTNode.injectorIndex | (viewOffset << RelativeInjectorLocationFlags.ViewOffsetShift) :
acrossHostBoundary :
-1 as any; -1 as any;
} }
@ -323,6 +320,7 @@ export function getOrCreateInjectable<T>(
let previousTView: TView|null = null; let previousTView: TView|null = null;
let injectorIndex = getInjectorIndex(tNode, lView); let injectorIndex = getInjectorIndex(tNode, lView);
let parentLocation: RelativeInjectorLocation = NO_PARENT_INJECTOR; 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 // If we should skip this injector, or if there is no injector on this node, start by searching
// the parent injector. // the parent injector.
@ -330,7 +328,7 @@ export function getOrCreateInjectable<T>(
parentLocation = injectorIndex === -1 ? getParentInjectorLocation(tNode, lView) : parentLocation = injectorIndex === -1 ? getParentInjectorLocation(tNode, lView) :
lView[injectorIndex + PARENT_INJECTOR]; lView[injectorIndex + PARENT_INJECTOR];
if (!shouldSearchParent(flags, parentLocation)) { if (!shouldSearchParent(flags, false)) {
injectorIndex = -1; injectorIndex = -1;
} else { } else {
previousTView = lView[TVIEW]; previousTView = lView[TVIEW];
@ -350,13 +348,14 @@ export function getOrCreateInjectable<T>(
// At this point, we have an injector which *may* contain the token, so we step through // 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 providers and directives associated with the injector's corresponding node to get
// the instance. // the instance.
const instance: T|null = const instance: T|null = searchTokensOnInjector<T>(
searchTokensOnInjector<T>(injectorIndex, lView, token, previousTView); injectorIndex, lView, token, previousTView, flags, hostTElementNode);
if (instance !== NOT_FOUND) { if (instance !== NOT_FOUND) {
return instance; return instance;
} }
} }
if (shouldSearchParent(flags, parentLocation) && if (shouldSearchParent(
flags, lView[TVIEW].data[injectorIndex + TNODE] === hostTElementNode) &&
bloomHasToken(bloomHash, injectorIndex, lView)) { bloomHasToken(bloomHash, injectorIndex, lView)) {
// The def wasn't found anywhere on this node, so it was a false positive. // The def wasn't found anywhere on this node, so it was a false positive.
// Traverse up the tree and continue searching. // Traverse up the tree and continue searching.
@ -396,7 +395,7 @@ const NOT_FOUND = {};
function searchTokensOnInjector<T>( function searchTokensOnInjector<T>(
injectorIndex: number, lView: LView, token: Type<T>| InjectionToken<T>, injectorIndex: number, lView: LView, token: Type<T>| InjectionToken<T>,
previousTView: TView | null) { previousTView: TView | null, flags: InjectFlags, hostTElementNode: TNode | null) {
const currentTView = lView[TVIEW]; const currentTView = lView[TVIEW];
const tNode = currentTView.data[injectorIndex + TNODE] as TNode; const tNode = currentTView.data[injectorIndex + TNODE] as TNode;
// First, we need to determine if view providers can be accessed by the starting element. // First, we need to determine if view providers can be accessed by the starting element.
@ -418,7 +417,12 @@ function searchTokensOnInjector<T>(
// into the ViewProviders. // into the ViewProviders.
(previousTView != currentTView && (tNode.type === TNodeType.Element)); (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) { if (injectableIdx !== null) {
return getNodeInjectable(currentTView.data, lView, injectableIdx, tNode as TElementNode); return getNodeInjectable(currentTView.data, lView, injectableIdx, tNode as TElementNode);
} else { } else {
@ -433,13 +437,13 @@ function searchTokensOnInjector<T>(
* @param lView The view we are currently processing * @param lView The view we are currently processing
* @param token Provider token or type of a directive to look for. * @param token Provider token or type of a directive to look for.
* @param canAccessViewProviders Whether view providers should be considered. * @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. * @returns Index of a found directive or provider, or null when none found.
*/ */
export function locateDirectiveOrProvider<T>( export function locateDirectiveOrProvider<T>(
tNode: TNode, lView: LView, token: Type<T>| InjectionToken<T>, tNode: TNode, lView: LView, token: Type<T>| InjectionToken<T>, canAccessViewProviders: boolean,
canAccessViewProviders: boolean): number|null { isHostSpecialCase: boolean | number): number|null {
const tView = lView[TVIEW]; const tView = lView[TVIEW];
const nodeFlags = tNode.flags;
const nodeProviderIndexes = tNode.providerIndexes; const nodeProviderIndexes = tNode.providerIndexes;
const tInjectables = tView.data; const tInjectables = tView.data;
@ -450,13 +454,21 @@ export function locateDirectiveOrProvider<T>(
nodeProviderIndexes >> TNodeProviderIndexes.CptViewProvidersCountShift; nodeProviderIndexes >> TNodeProviderIndexes.CptViewProvidersCountShift;
const startingIndex = const startingIndex =
canAccessViewProviders ? injectablesStart : injectablesStart + cptViewProvidersCount; 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<any>| Type<any>| DirectiveDef<any>; const providerTokenOrDef = tInjectables[i] as InjectionToken<any>| Type<any>| DirectiveDef<any>;
if (i < directivesStart && token === providerTokenOrDef || if (i < directivesStart && token === providerTokenOrDef ||
i >= directivesStart && (providerTokenOrDef as DirectiveDef<any>).type === token) { i >= directivesStart && (providerTokenOrDef as DirectiveDef<any>).type === token) {
return i; return i;
} }
} }
if (isHostSpecialCase) {
const dirDef = tInjectables[directivesStart] as DirectiveDef<any>;
if (dirDef && isComponentDef(dirDef) && dirDef.type === token) {
return directivesStart;
}
}
return null; return null;
} }
@ -546,12 +558,8 @@ export function bloomHasToken(
} }
/** Returns true if flags prevent parent injector from being searched for tokens */ /** Returns true if flags prevent parent injector from being searched for tokens */
function shouldSearchParent(flags: InjectFlags, parentLocation: RelativeInjectorLocation): boolean| function shouldSearchParent(flags: InjectFlags, isFirstHostTNode: boolean): boolean|number {
number { return !(flags & InjectFlags.Self) && !(flags & InjectFlags.Host && isFirstHostTNode);
return !(
flags & InjectFlags.Self ||
(flags & InjectFlags.Host &&
((parentLocation as any as number) & RelativeInjectorLocationFlags.AcrossHostBoundary)));
} }
export function injectInjector() { export function injectInjector() {

View File

@ -33,14 +33,14 @@ import {SanitizerFn} from './interfaces/sanitization';
import {StylingIndex} from './interfaces/styling'; 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 {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 {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 {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 {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 {createStylingContextTemplate, renderStyleAndClassBindings, setStyle, updateClassProp as updateElementClassProp, updateStyleProp as updateElementStyleProp, updateStylingMap} from './styling/class_and_style_bindings';
import {BoundPlayerFactory} from './styling/player_factory'; import {BoundPlayerFactory} from './styling/player_factory';
import {getStylingContext, isAnimationProp} from './styling/util'; import {getStylingContext, isAnimationProp} from './styling/util';
import {NO_CHANGE} from './tokens'; 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';
/** /**

View File

@ -26,7 +26,6 @@ export interface RelativeInjectorLocation { __brand__: 'RelativeInjectorLocation
export const enum RelativeInjectorLocationFlags { export const enum RelativeInjectorLocationFlags {
InjectorIndexMask = 0b111111111111111, InjectorIndexMask = 0b111111111111111,
AcrossHostBoundary = 0b1000000000000000,
ViewOffsetShift = 16, ViewOffsetShift = 16,
NO_PARENT = -1, NO_PARENT = -1,
} }

View File

@ -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 {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 {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 {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; 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 * NOTE: for performance reasons, the possible actions are inlined within the function instead of
* being passed as an argument. * being passed as an argument.

View File

@ -250,7 +250,8 @@ function queryByReadToken(read: any, tNode: TNode, currentView: LView): any {
if (typeof factoryFn === 'function') { if (typeof factoryFn === 'function') {
return factoryFn(); return factoryFn();
} else { } else {
const matchingIdx = locateDirectiveOrProvider(tNode, currentView, read as Type<any>, false); const matchingIdx =
locateDirectiveOrProvider(tNode, currentView, read as Type<any>, false, false);
if (matchingIdx !== null) { if (matchingIdx !== null) {
return getNodeInjectable( return getNodeInjectable(
currentView[TVIEW].data, currentView, matchingIdx, tNode as TElementNode); currentView[TVIEW].data, currentView, matchingIdx, tNode as TElementNode);
@ -304,7 +305,7 @@ function add(
if (type === ViewEngine_TemplateRef) { if (type === ViewEngine_TemplateRef) {
result = queryByTemplateRef(type, tNode, currentView, predicate.read); result = queryByTemplateRef(type, tNode, currentView, predicate.read);
} else { } else {
const matchingIdx = locateDirectiveOrProvider(tNode, currentView, type, false); const matchingIdx = locateDirectiveOrProvider(tNode, currentView, type, false, false);
if (matchingIdx !== null) { if (matchingIdx !== null) {
result = queryRead(tNode, currentView, predicate.read, matchingIdx); result = queryRead(tNode, currentView, predicate.read, matchingIdx);
} }

View File

@ -13,7 +13,7 @@ import {ACTIVE_INDEX, LContainer} from './interfaces/container';
import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context'; import {LContext, MONKEY_PATCH_KEY_NAME} from './interfaces/context';
import {ComponentDef, DirectiveDef} from './interfaces/definition'; import {ComponentDef, DirectiveDef} from './interfaces/definition';
import {NO_PARENT_INJECTOR, RelativeInjectorLocation, RelativeInjectorLocationFlags} from './interfaces/injector'; 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 {RComment, RElement, RText} from './interfaces/renderer';
import {StylingContext} from './interfaces/styling'; 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'; 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]); 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;
}

View File

@ -25,9 +25,9 @@ import {LQueries} from './interfaces/query';
import {RComment, RElement, Renderer3, isProceduralRenderer} from './interfaces/renderer'; import {RComment, RElement, Renderer3, isProceduralRenderer} from './interfaces/renderer';
import {CONTAINER_INDEX, CONTEXT, HOST_NODE, LView, QUERIES, RENDERER, TView} from './interfaces/view'; import {CONTAINER_INDEX, CONTEXT, HOST_NODE, LView, QUERIES, RENDERER, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes} from './node_assert'; 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 {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'; import {ViewRef} from './view_ref';

View File

@ -683,6 +683,9 @@
{ {
"name": "getHostNative" "name": "getHostNative"
}, },
{
"name": "getHostTElementNode"
},
{ {
"name": "getInitialIndex" "name": "getInitialIndex"
}, },

View File

@ -1327,24 +1327,21 @@ function declareTests(config?: {useJit: boolean}) {
expect(comp.injectable).toBeAnInstanceOf(InjectableService); expect(comp.injectable).toBeAnInstanceOf(InjectableService);
}); });
fixmeIvy( it('should support viewProviders', () => {
'FW-804: Injection of view providers with the @Host annotation works differently in ivy') TestBed.configureTestingModule({
.it('should support viewProviders', () => { declarations: [MyComp, DirectiveProvidingInjectableInView, DirectiveConsumingInjectable],
TestBed.configureTestingModule({ schemas: [NO_ERRORS_SCHEMA],
declarations: });
[MyComp, DirectiveProvidingInjectableInView, DirectiveConsumingInjectable], const template = `
schemas: [NO_ERRORS_SCHEMA],
});
const template = `
<directive-consuming-injectable #consuming> <directive-consuming-injectable #consuming>
</directive-consuming-injectable> </directive-consuming-injectable>
`; `;
TestBed.overrideComponent(DirectiveProvidingInjectableInView, {set: {template}}); TestBed.overrideComponent(DirectiveProvidingInjectableInView, {set: {template}});
const fixture = TestBed.createComponent(DirectiveProvidingInjectableInView); const fixture = TestBed.createComponent(DirectiveProvidingInjectableInView);
const comp = fixture.debugElement.children[0].references !['consuming']; const comp = fixture.debugElement.children[0].references !['consuming'];
expect(comp.injectable).toBeAnInstanceOf(InjectableService); expect(comp.injectable).toBeAnInstanceOf(InjectableService);
}); });
it('should support unbounded lookup', () => { it('should support unbounded lookup', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({

View File

@ -725,16 +725,14 @@ class TestComp {
expect(d.dependency).toBeNull(); expect(d.dependency).toBeNull();
}); });
fixmeIvy('unknown').it( it('should instantiate directives that depends on the host component', () => {
'should instantiate directives that depends on the host component', () => { TestBed.configureTestingModule({declarations: [SimpleComponent, NeedsComponentFromHost]});
TestBed.configureTestingModule( TestBed.overrideComponent(
{declarations: [SimpleComponent, NeedsComponentFromHost]}); SimpleComponent, {set: {template: '<div needsComponentFromHost></div>'}});
TestBed.overrideComponent( const el = createComponent('<div simpleComponent></div>');
SimpleComponent, {set: {template: '<div needsComponentFromHost></div>'}}); const d = el.children[0].children[0].injector.get(NeedsComponentFromHost);
const el = createComponent('<div simpleComponent></div>'); expect(d.dependency).toBeAnInstanceOf(SimpleComponent);
const d = el.children[0].children[0].injector.get(NeedsComponentFromHost); });
expect(d.dependency).toBeAnInstanceOf(SimpleComponent);
});
fixmeIvy('unknown').it( fixmeIvy('unknown').it(
'should instantiate host views for components that have a @Host dependency ', () => { 'should instantiate host views for components that have a @Host dependency ', () => {

View File

@ -6,8 +6,8 @@
* found in the LICENSE file at https://angular.io/license * 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 {Attribute, ChangeDetectorRef, ElementRef, Host, Inject, InjectFlags, Injector, Optional, Renderer2, Self, SkipSelf, TemplateRef, ViewContainerRef, createInjector, defineInjectable, defineInjector} from '@angular/core';
import {RenderFlags} from '@angular/core/src/render3/interfaces/definition'; import {ComponentType, RenderFlags} from '@angular/core/src/render3/interfaces/definition';
import {defineComponent} from '../../src/render3/definition'; import {defineComponent} from '../../src/render3/definition';
import {bloomAdd, bloomHasToken, bloomHashBitOrFactory as bloomHash, getOrCreateNodeInjectorForNode} from '../../src/render3/di'; import {bloomAdd, bloomHasToken, bloomHashBitOrFactory as bloomHash, getOrCreateNodeInjectorForNode} from '../../src/render3/di';
@ -1066,8 +1066,12 @@ describe('di', () => {
describe('@Host', () => { describe('@Host', () => {
let dirA: DirA|null = null; let dirA: DirA|null = null;
let dirString: DirString|null = null;
beforeEach(() => dirA = null); beforeEach(() => {
dirA = null;
dirString = null;
});
class DirA { class DirA {
constructor(@Host() public dirB: DirB) {} 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', () => {
/** <div dirString></div> */
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'}]);
/* <comp></comp> */
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))
});
}
/** <div dirCmp></div> */
const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'div', ['dirCmp', '']);
}
}, 1, 0, [DirComp]);
/* <comp></comp> */
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', () => {
/** <div dirString></div> */
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'}]);
/* <comp></comp> */
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', () => {
/** <div dirA></div> */ /** <div dirA></div> */
const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) { const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
element(0, 'div', ['dirA', '']); element(0, 'div', ['dirA', '']);
} }
}, 1, 0, [DirA, DirB]); }, 1, 0, [DirA]);
/* <comp dirB></comp> */ /* <comp dirB></comp> */
const App = createComponent('app', function(rf: RenderFlags, ctx: any) { const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
@ -1099,7 +1183,7 @@ describe('di', () => {
}).toThrowError(/NodeInjector: NOT_FOUND \[DirB\]/); }).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; let comp !: any;
/** /**
@ -1177,6 +1261,154 @@ describe('di', () => {
expect(dirA !.dirB).toEqual(dirB); 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))
});
}
/** <div dirCmp></div> */
const Comp = createComponent('comp', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
element(0, 'div', ['dirCmp', '']);
}
}, 1, 0, [DirComp]);
/* <comp></comp> */
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: `
<input controlName type="text">
`,
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: `
<div group>
<child></child>
</div>
`
})
*/
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<AppComponent>);
expect(fixture.html)
.toEqual(
'<div group=""><child><input controlname="" type="text"></child></div>');
expect(controlContainers).toEqual([injectedControlContainer !]);
});
});
}); });
}); });
}); });

View File

@ -7,6 +7,7 @@
*/ */
import {ChangeDetectorRef} from '@angular/core/src/change_detection/change_detector_ref'; 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 {ElementRef} from '@angular/core/src/linker/element_ref';
import {TemplateRef} from '@angular/core/src/linker/template_ref'; import {TemplateRef} from '@angular/core/src/linker/template_ref';
import {ViewContainerRef} from '@angular/core/src/linker/view_container_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 {getDirectivesAtNodeIndex, getLContext, isComponentInstance} from '../../src/render3/context_discovery';
import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition'; import {extractDirectiveDef, extractPipeDef} from '../../src/render3/definition';
import {NG_ELEMENT_ID} from '../../src/render3/fields'; 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 {renderTemplate} from '../../src/render3/instructions';
import {DirectiveDefList, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeTypesOrFactory} from '../../src/render3/interfaces/definition'; import {DirectiveDefList, DirectiveTypesOrFactory, PipeDef, PipeDefList, PipeTypesOrFactory} from '../../src/render3/interfaces/definition';
import {PlayerHandler} from '../../src/render3/interfaces/player'; import {PlayerHandler} from '../../src/render3/interfaces/player';
@ -295,7 +296,8 @@ export function toHtml<T>(componentOrElement: T | RElement, keepNgReflect = fals
export function createComponent( export function createComponent(
name: string, template: ComponentTemplate<any>, consts: number = 0, vars: number = 0, name: string, template: ComponentTemplate<any>, consts: number = 0, vars: number = 0,
directives: DirectiveTypesOrFactory = [], pipes: PipeTypesOrFactory = [], directives: DirectiveTypesOrFactory = [], pipes: PipeTypesOrFactory = [],
viewQuery: ComponentTemplate<any>| null = null): ComponentType<any> { viewQuery: ComponentTemplate<any>| null = null, providers: Provider[] = [],
viewProviders: Provider[] = []): ComponentType<any> {
return class Component { return class Component {
value: any; value: any;
static ngComponentDef = defineComponent({ static ngComponentDef = defineComponent({
@ -307,7 +309,9 @@ export function createComponent(
template: template, template: template,
viewQuery: viewQuery, viewQuery: viewQuery,
directives: directives, directives: directives,
pipes: pipes pipes: pipes,
features: (providers.length > 0 || viewProviders.length > 0)?
[ProvidersFeature(providers || [], viewProviders || [])]: []
}); });
}; };
} }