fix(ivy): support projecting containers into containers (#24695)
PR Close #24695
This commit is contained in:

committed by
Miško Hevery

parent
3db9d57de3
commit
99bdd257a6
@ -1831,48 +1831,11 @@ export function embeddedViewEnd(): void {
|
|||||||
refreshView();
|
refreshView();
|
||||||
isParent = false;
|
isParent = false;
|
||||||
previousOrParentNode = viewData[HOST_NODE] as LViewNode;
|
previousOrParentNode = viewData[HOST_NODE] as LViewNode;
|
||||||
if (creationMode) {
|
|
||||||
const containerNode = getParentLNode(previousOrParentNode) as LContainerNode;
|
|
||||||
if (containerNode) {
|
|
||||||
ngDevMode && assertNodeType(previousOrParentNode, TNodeType.View);
|
|
||||||
ngDevMode && assertNodeType(containerNode, TNodeType.Container);
|
|
||||||
// When projected nodes are going to be inserted, the renderParent of the dynamic container
|
|
||||||
// used by the ViewContainerRef must be set.
|
|
||||||
setRenderParentInProjectedNodes(
|
|
||||||
containerNode.data[RENDER_PARENT], previousOrParentNode as LViewNode);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
leaveView(viewData[PARENT] !);
|
leaveView(viewData[PARENT] !);
|
||||||
ngDevMode && assertEqual(isParent, false, 'isParent');
|
ngDevMode && assertEqual(isParent, false, 'isParent');
|
||||||
ngDevMode && assertNodeType(previousOrParentNode, TNodeType.View);
|
ngDevMode && assertNodeType(previousOrParentNode, TNodeType.View);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* For nodes which are projected inside an embedded view, this function sets the renderParent
|
|
||||||
* of their dynamic LContainerNode.
|
|
||||||
* @param renderParent the renderParent of the LContainer which contains the embedded view.
|
|
||||||
* @param viewNode the embedded view.
|
|
||||||
*/
|
|
||||||
function setRenderParentInProjectedNodes(
|
|
||||||
renderParent: LElementNode | null, viewNode: LViewNode): void {
|
|
||||||
if (renderParent != null) {
|
|
||||||
let node: LNode|null = getChildLNode(viewNode);
|
|
||||||
while (node) {
|
|
||||||
if (node.tNode.type === TNodeType.Projection) {
|
|
||||||
let nodeToProject: LNode|null = (node as LProjectionNode).data.head;
|
|
||||||
const lastNodeToProject = (node as LProjectionNode).data.tail;
|
|
||||||
while (nodeToProject) {
|
|
||||||
if (nodeToProject.dynamicLContainerNode) {
|
|
||||||
nodeToProject.dynamicLContainerNode.data[RENDER_PARENT] = renderParent;
|
|
||||||
}
|
|
||||||
nodeToProject = nodeToProject === lastNodeToProject ? null : nodeToProject.pNextOrParent;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
node = getNextLNode(node);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/////////////
|
/////////////
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -1946,7 +1909,7 @@ export function projectionDef(
|
|||||||
// execute selector matching logic if and only if:
|
// execute selector matching logic if and only if:
|
||||||
// - there are selectors defined
|
// - there are selectors defined
|
||||||
// - a node has a tag name / attributes that can be matched
|
// - a node has a tag name / attributes that can be matched
|
||||||
if (selectors && componentChild.tNode) {
|
if (selectors) {
|
||||||
const matchedIdx = matchingSelectorIndex(componentChild.tNode, selectors, textSelectors !);
|
const matchedIdx = matchingSelectorIndex(componentChild.tNode, selectors, textSelectors !);
|
||||||
distributedNodes[matchedIdx].push(componentChild);
|
distributedNodes[matchedIdx].push(componentChild);
|
||||||
} else {
|
} else {
|
||||||
@ -2037,10 +2000,14 @@ export function projection(
|
|||||||
// process each node in the list of projected nodes:
|
// process each node in the list of projected nodes:
|
||||||
let nodeToProject: LNode|null = node.data.head;
|
let nodeToProject: LNode|null = node.data.head;
|
||||||
const lastNodeToProject = node.data.tail;
|
const lastNodeToProject = node.data.tail;
|
||||||
|
const renderParent = currentParent.tNode.type === TNodeType.View ?
|
||||||
|
(getParentLNode(currentParent) as LContainerNode).data[RENDER_PARENT] ! :
|
||||||
|
currentParent as LElementNode;
|
||||||
|
|
||||||
while (nodeToProject) {
|
while (nodeToProject) {
|
||||||
appendProjectedNode(
|
appendProjectedNode(
|
||||||
nodeToProject as LTextNode | LElementNode | LContainerNode, currentParent as LElementNode,
|
nodeToProject as LTextNode | LElementNode | LContainerNode, currentParent, viewData,
|
||||||
viewData);
|
renderParent);
|
||||||
nodeToProject = nodeToProject === lastNodeToProject ? null : nodeToProject.pNextOrParent;
|
nodeToProject = nodeToProject === lastNodeToProject ? null : nodeToProject.pNextOrParent;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -608,24 +608,24 @@ export function removeChild(parent: LNode, child: RNode | null, currentView: LVi
|
|||||||
* @param currentView Current LView
|
* @param currentView Current LView
|
||||||
*/
|
*/
|
||||||
export function appendProjectedNode(
|
export function appendProjectedNode(
|
||||||
node: LElementNode | LTextNode | LContainerNode, currentParent: LElementNode,
|
node: LElementNode | LTextNode | LContainerNode, currentParent: LElementNode | LViewNode,
|
||||||
currentView: LViewData): void {
|
currentView: LViewData, renderParent: LElementNode): void {
|
||||||
appendChild(currentParent, node.native, currentView);
|
appendChild(currentParent, node.native, currentView);
|
||||||
if (node.tNode.type === TNodeType.Container) {
|
if (node.tNode.type === TNodeType.Container) {
|
||||||
// The node we are adding is a Container and we are adding it to Element which
|
// The node we are adding is a container and we are adding it to an element which
|
||||||
// is not a component (no more re-projection).
|
// is not a component (no more re-projection).
|
||||||
// Alternatively a container is projected at the root of a component's template
|
// Alternatively a container is projected at the root of a component's template
|
||||||
// and can't be re-projected (as not content of any component).
|
// and can't be re-projected (as not content of any component).
|
||||||
// Assignee the final projection location in those cases.
|
// Assign the final projection location in those cases.
|
||||||
const lContainer = (node as LContainerNode).data;
|
const lContainer = (node as LContainerNode).data;
|
||||||
lContainer[RENDER_PARENT] = currentParent;
|
lContainer[RENDER_PARENT] = renderParent;
|
||||||
const views = lContainer[VIEWS];
|
const views = lContainer[VIEWS];
|
||||||
for (let i = 0; i < views.length; i++) {
|
for (let i = 0; i < views.length; i++) {
|
||||||
addRemoveViewFromContainer(node as LContainerNode, views[i], true, null);
|
addRemoveViewFromContainer(node as LContainerNode, views[i], true, null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (node.dynamicLContainerNode) {
|
if (node.dynamicLContainerNode) {
|
||||||
node.dynamicLContainerNode.data[RENDER_PARENT] = currentParent;
|
node.dynamicLContainerNode.data[RENDER_PARENT] = renderParent;
|
||||||
appendChild(currentParent, node.dynamicLContainerNode.native, currentView);
|
appendChild(currentParent, node.dynamicLContainerNode.native, currentView);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -138,6 +138,7 @@ describe('content projection', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should project content with container.', () => {
|
it('should project content with container.', () => {
|
||||||
|
/** <div> <ng-content></ng-content></div> */
|
||||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
projectionDef(0);
|
projectionDef(0);
|
||||||
@ -146,6 +147,16 @@ describe('content projection', () => {
|
|||||||
elementEnd();
|
elementEnd();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <child>
|
||||||
|
* (
|
||||||
|
* % if (value) {
|
||||||
|
* content
|
||||||
|
* % }
|
||||||
|
* )
|
||||||
|
* </child>
|
||||||
|
*/
|
||||||
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) {
|
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
elementStart(0, 'child');
|
elementStart(0, 'child');
|
||||||
@ -182,12 +193,21 @@ describe('content projection', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should project content with container into root', () => {
|
it('should project content with container into root', () => {
|
||||||
|
/** <ng-content></ng-content> */
|
||||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
projectionDef(0);
|
projectionDef(0);
|
||||||
projection(1, 0);
|
projection(1, 0);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <child>
|
||||||
|
* % if (value) {
|
||||||
|
* content
|
||||||
|
* % }
|
||||||
|
* </child>
|
||||||
|
*/
|
||||||
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) {
|
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
elementStart(0, 'child');
|
elementStart(0, 'child');
|
||||||
@ -222,6 +242,7 @@ describe('content projection', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should project content with container and if-else.', () => {
|
it('should project content with container and if-else.', () => {
|
||||||
|
/** <div><ng-content></ng-content></div> */
|
||||||
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
projectionDef(0);
|
projectionDef(0);
|
||||||
@ -230,6 +251,18 @@ describe('content projection', () => {
|
|||||||
elementEnd();
|
elementEnd();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <child>
|
||||||
|
* (
|
||||||
|
* % if (value) {
|
||||||
|
* content
|
||||||
|
* % } else {
|
||||||
|
* else
|
||||||
|
* % }
|
||||||
|
* )
|
||||||
|
* </child>
|
||||||
|
*/
|
||||||
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) {
|
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
elementStart(0, 'child');
|
elementStart(0, 'child');
|
||||||
@ -270,7 +303,7 @@ describe('content projection', () => {
|
|||||||
expect(toHtml(parent)).toEqual('<child><div>(else)</div></child>');
|
expect(toHtml(parent)).toEqual('<child><div>(else)</div></child>');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should support projection in embedded views', () => {
|
it('should support projection into embedded views', () => {
|
||||||
let childCmptInstance: any;
|
let childCmptInstance: any;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -306,9 +339,7 @@ describe('content projection', () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/** <child>content</child> */
|
||||||
* <child>content</child>
|
|
||||||
*/
|
|
||||||
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) {
|
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) {
|
||||||
if (rf & RenderFlags.Create) {
|
if (rf & RenderFlags.Create) {
|
||||||
elementStart(0, 'child');
|
elementStart(0, 'child');
|
||||||
@ -383,6 +414,72 @@ describe('content projection', () => {
|
|||||||
expect(toHtml(parent)).toEqual('<child><div></div></child>');
|
expect(toHtml(parent)).toEqual('<child><div></div></child>');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should project containers into embedded views', () => {
|
||||||
|
/**
|
||||||
|
* <div>
|
||||||
|
* % if (!skipContent) {
|
||||||
|
* <ng-content></ng-content>
|
||||||
|
* % }
|
||||||
|
* </div>
|
||||||
|
*/
|
||||||
|
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
projectionDef(0);
|
||||||
|
elementStart(1, 'div');
|
||||||
|
{ container(2); }
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
containerRefreshStart(2);
|
||||||
|
{
|
||||||
|
if (!ctx.skipContent) {
|
||||||
|
let rf0 = embeddedViewStart(0);
|
||||||
|
if (rf0 & RenderFlags.Create) {
|
||||||
|
projection(0, 0);
|
||||||
|
}
|
||||||
|
embeddedViewEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
containerRefreshEnd();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* <child>
|
||||||
|
* % if (!skipContent) {
|
||||||
|
* content
|
||||||
|
* % }
|
||||||
|
* </child>
|
||||||
|
*/
|
||||||
|
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) {
|
||||||
|
if (rf & RenderFlags.Create) {
|
||||||
|
elementStart(0, 'child');
|
||||||
|
{ container(1); }
|
||||||
|
elementEnd();
|
||||||
|
}
|
||||||
|
if (rf & RenderFlags.Update) {
|
||||||
|
containerRefreshStart(1);
|
||||||
|
{
|
||||||
|
if (!ctx.skipContent) {
|
||||||
|
let rf0 = embeddedViewStart(0);
|
||||||
|
if (rf0 & RenderFlags.Create) {
|
||||||
|
text(0, 'content');
|
||||||
|
}
|
||||||
|
embeddedViewEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
containerRefreshEnd();
|
||||||
|
}
|
||||||
|
}, [Child]);
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(Parent);
|
||||||
|
expect(fixture.html).toEqual('<child><div>content</div></child>');
|
||||||
|
|
||||||
|
fixture.component.skipContent = true;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('<child><div></div></child>');
|
||||||
|
});
|
||||||
|
|
||||||
it('should support projection in embedded views when ng-content is a root node of an embedded view, with other nodes after',
|
it('should support projection in embedded views when ng-content is a root node of an embedded view, with other nodes after',
|
||||||
() => {
|
() => {
|
||||||
let childCmptInstance: any;
|
let childCmptInstance: any;
|
||||||
|
Reference in New Issue
Block a user