fix(ivy): ng-content tags in re-inserted templates should walk declaration tree (#27783)
This PR assures that content projection works if an <ng-content> tag is placed inside an <ng-template> in one component and that <ng-template> 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
This commit is contained in:
parent
3f2ebbd7ab
commit
7eb2c41fb2
|
@ -21,7 +21,7 @@ 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 {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<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;
|
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
|
// If we should skip this injector, or if there is no injector on this node, start by searching
|
||||||
// the parent injector.
|
// the parent injector.
|
||||||
|
|
|
@ -263,28 +263,16 @@ export function addAllToArray(items: any[], arr: any[]) {
|
||||||
* Given a current view, finds the nearest component's host (LElement).
|
* Given a current view, finds the nearest component's host (LElement).
|
||||||
*
|
*
|
||||||
* @param lView LView for which we want a host element node
|
* @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
|
* @returns The host node
|
||||||
*/
|
*/
|
||||||
export function findComponentView(lView: LView, declarationMode?: boolean): LView {
|
export function findComponentView(lView: LView): LView {
|
||||||
let rootTNode = lView[HOST_NODE];
|
let rootTNode = lView[HOST_NODE];
|
||||||
|
|
||||||
while (rootTNode && rootTNode.type === TNodeType.View) {
|
while (rootTNode && rootTNode.type === TNodeType.View) {
|
||||||
ngDevMode && assertDefined(
|
ngDevMode && assertDefined(lView[DECLARATION_VIEW], 'lView[DECLARATION_VIEW]');
|
||||||
lView[declarationMode ? DECLARATION_VIEW : PARENT],
|
lView = lView[DECLARATION_VIEW] !;
|
||||||
declarationMode ? 'lView.declarationView' : 'lView.parent');
|
|
||||||
lView = lView[declarationMode ? DECLARATION_VIEW : PARENT] !;
|
|
||||||
rootTNode = lView[HOST_NODE];
|
rootTNode = lView[HOST_NODE];
|
||||||
}
|
}
|
||||||
|
|
||||||
return lView;
|
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;
|
|
||||||
}
|
|
||||||
|
|
|
@ -689,9 +689,6 @@
|
||||||
{
|
{
|
||||||
"name": "getHostNative"
|
"name": "getHostNative"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "getHostTElementNode"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "getInitialClassNameValue"
|
"name": "getInitialClassNameValue"
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,10 +8,12 @@
|
||||||
|
|
||||||
import {SelectorFlags} from '@angular/core/src/render3/interfaces/projection';
|
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 {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 {RenderFlags} from '../../src/render3/interfaces/definition';
|
||||||
|
|
||||||
|
import {TemplateRef, ViewContainerRef, QueryList} from '@angular/core';
|
||||||
import {NgIf, NgForOf} from './common_with_def';
|
import {NgIf, NgForOf} from './common_with_def';
|
||||||
import {ComponentFixture, createComponent, getDirectiveOnNode, renderComponent, toHtml} from './render_util';
|
import {ComponentFixture, createComponent, getDirectiveOnNode, renderComponent, toHtml} from './render_util';
|
||||||
|
|
||||||
|
@ -997,6 +999,83 @@ describe('content projection', () => {
|
||||||
expect(fixture.html).toEqual('<child><span>B</span>Before-<div>A</div>-After</child>');
|
expect(fixture.html).toEqual('<child><span>B</span>Before-<div>A</div>-After</child>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should project if <ng-content> 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <ng-template>
|
||||||
|
* <ng-content></ng-content>
|
||||||
|
* </ng-template>
|
||||||
|
*/
|
||||||
|
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<any> */
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
query(0, TemplateRef as any, true);
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
let tmp: any;
|
||||||
|
queryRefresh(tmp = load<QueryList<any>>(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'}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <button [trigger]="comp"></button>
|
||||||
|
* <comp #comp>
|
||||||
|
* Some content
|
||||||
|
* </comp>
|
||||||
|
*/
|
||||||
|
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(`<button></button><comp></comp>`);
|
||||||
|
|
||||||
|
triggerDir.open();
|
||||||
|
expect(fixture.html).toEqual(`<button></button>Some content<comp></comp>`);
|
||||||
|
});
|
||||||
|
|
||||||
it('should project nodes into the last ng-content', () => {
|
it('should project nodes into the last ng-content', () => {
|
||||||
/**
|
/**
|
||||||
* <div><ng-content></ng-content></div>
|
* <div><ng-content></ng-content></div>
|
||||||
|
|
Loading…
Reference in New Issue