From 7eb2c41fb2060f0cbf1b90873add15a11e775505 Mon Sep 17 00:00:00 2001 From: Kara Erickson Date: Thu, 20 Dec 2018 13:30:08 -0800 Subject: [PATCH] fix(ivy): ng-content tags in re-inserted templates should walk declaration tree (#27783) This PR assures that content projection works if an tag is placed inside an in one component and that is inserted into a different component. It fixes a bug where the projection instruction code would walk up the insertion tree to find selector data instead of the declaration tree. PR Close #27783 --- packages/core/src/render3/di.ts | 5 +- packages/core/src/render3/util.ts | 18 +---- .../bundling/todo/bundle.golden_symbols.json | 3 - packages/core/test/render3/content_spec.ts | 81 ++++++++++++++++++- 4 files changed, 86 insertions(+), 21 deletions(-) diff --git a/packages/core/src/render3/di.ts b/packages/core/src/render3/di.ts index 7bd723aa79..87885c8436 100644 --- a/packages/core/src/render3/di.ts +++ b/packages/core/src/render3/di.ts @@ -21,7 +21,7 @@ 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 {getHostTElementNode, getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, isComponentDef, stringify} from './util'; +import {findComponentView, getParentInjectorIndex, getParentInjectorView, hasParentInjector, isComponent, isComponentDef, stringify} from './util'; /** @@ -320,7 +320,8 @@ 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; + let hostTElementNode: TNode|null = + flags & InjectFlags.Host ? findComponentView(lView)[HOST_NODE] : null; // If we should skip this injector, or if there is no injector on this node, start by searching // the parent injector. diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index c8c2d3132d..d4d269b3a8 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -263,28 +263,16 @@ export function addAllToArray(items: any[], arr: any[]) { * 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 { +export function findComponentView(lView: LView): 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] !; + ngDevMode && assertDefined(lView[DECLARATION_VIEW], 'lView[DECLARATION_VIEW]'); + lView = lView[DECLARATION_VIEW] !; 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/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 9b295d6b60..b9b56ee305 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -689,9 +689,6 @@ { "name": "getHostNative" }, - { - "name": "getHostTElementNode" - }, { "name": "getInitialClassNameValue" }, diff --git a/packages/core/test/render3/content_spec.ts b/packages/core/test/render3/content_spec.ts index 6625bd58ba..6180051949 100644 --- a/packages/core/test/render3/content_spec.ts +++ b/packages/core/test/render3/content_spec.ts @@ -8,10 +8,12 @@ import {SelectorFlags} from '@angular/core/src/render3/interfaces/projection'; -import {AttributeMarker, detectChanges} from '../../src/render3/index'; +import {AttributeMarker, defineDirective, detectChanges, directiveInject, load, query, queryRefresh, reference, templateRefExtractor} from '../../src/render3/index'; + import {bind, container, containerRefreshEnd, containerRefreshStart, element, elementContainerEnd, elementContainerStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, projection, projectionDef, template, text, textBinding, interpolation1} from '../../src/render3/instructions'; import {RenderFlags} from '../../src/render3/interfaces/definition'; +import {TemplateRef, ViewContainerRef, QueryList} from '@angular/core'; import {NgIf, NgForOf} from './common_with_def'; import {ComponentFixture, createComponent, getDirectiveOnNode, renderComponent, toHtml} from './render_util'; @@ -997,6 +999,83 @@ describe('content projection', () => { expect(fixture.html).toEqual('BBefore-
A
-After
'); }); + it('should project if is in a template that has different declaration/insertion points', + () => { + let triggerDir !: Trigger; + + function NgTemplate(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + projection(0); + } + } + + /** + * + * + * + */ + const Comp = createComponent( + 'comp', + (rf: RenderFlags, ctx: any) => { + if (rf & RenderFlags.Create) { + projectionDef(); + template(1, NgTemplate, 1, 0, 'ng-template', null, null, templateRefExtractor); + } + }, + 2, 0, [], [], + function(rf: RenderFlags, ctx: any) { + /** @ViewChild(TemplateRef) template: TemplateRef */ + if (rf & RenderFlags.Create) { + query(0, TemplateRef as any, true); + } + if (rf & RenderFlags.Update) { + let tmp: any; + queryRefresh(tmp = load>(0)) && (ctx.template = tmp.first); + } + }); + + class Trigger { + // @Input() + trigger: any; + + constructor(public vcr: ViewContainerRef) {} + + open() { this.vcr.createEmbeddedView(this.trigger.template); } + + static ngComponentDef = defineDirective({ + type: Trigger, + selectors: [['', 'trigger', '']], + factory: () => triggerDir = new Trigger(directiveInject(ViewContainerRef as any)), + inputs: {trigger: 'trigger'} + }); + } + + /** + * + * + * Some content + * + */ + const App = createComponent('app', (rf: RenderFlags, ctx: any) => { + if (rf & RenderFlags.Create) { + element(0, 'button', [AttributeMarker.SelectOnly, 'trigger']); + elementStart(1, 'comp', null, ['comp', '']); + { text(3, 'Some content'); } + elementEnd(); + } + if (rf & RenderFlags.Update) { + const comp = reference(2); + elementProperty(0, 'trigger', bind(comp)); + } + }, 4, 1, [Comp, Trigger]); + + const fixture = new ComponentFixture(App); + expect(fixture.html).toEqual(``); + + triggerDir.open(); + expect(fixture.html).toEqual(`Some content`); + }); + it('should project nodes into the last ng-content', () => { /** *