fix(ivy): support projecting containers into containers (#24695)

PR Close #24695
This commit is contained in:
Kara Erickson 2018-06-27 16:05:23 -07:00 committed by Miško Hevery
parent 3db9d57de3
commit 99bdd257a6
3 changed files with 114 additions and 50 deletions

View File

@ -1831,48 +1831,11 @@ export function embeddedViewEnd(): void {
refreshView();
isParent = false;
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] !);
ngDevMode && assertEqual(isParent, false, 'isParent');
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:
// - there are selectors defined
// - a node has a tag name / attributes that can be matched
if (selectors && componentChild.tNode) {
if (selectors) {
const matchedIdx = matchingSelectorIndex(componentChild.tNode, selectors, textSelectors !);
distributedNodes[matchedIdx].push(componentChild);
} else {
@ -2037,10 +2000,14 @@ export function projection(
// process each node in the list of projected nodes:
let nodeToProject: LNode|null = node.data.head;
const lastNodeToProject = node.data.tail;
const renderParent = currentParent.tNode.type === TNodeType.View ?
(getParentLNode(currentParent) as LContainerNode).data[RENDER_PARENT] ! :
currentParent as LElementNode;
while (nodeToProject) {
appendProjectedNode(
nodeToProject as LTextNode | LElementNode | LContainerNode, currentParent as LElementNode,
viewData);
nodeToProject as LTextNode | LElementNode | LContainerNode, currentParent, viewData,
renderParent);
nodeToProject = nodeToProject === lastNodeToProject ? null : nodeToProject.pNextOrParent;
}
}

View File

@ -608,24 +608,24 @@ export function removeChild(parent: LNode, child: RNode | null, currentView: LVi
* @param currentView Current LView
*/
export function appendProjectedNode(
node: LElementNode | LTextNode | LContainerNode, currentParent: LElementNode,
currentView: LViewData): void {
node: LElementNode | LTextNode | LContainerNode, currentParent: LElementNode | LViewNode,
currentView: LViewData, renderParent: LElementNode): void {
appendChild(currentParent, node.native, currentView);
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).
// 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).
// Assignee the final projection location in those cases.
// Assign the final projection location in those cases.
const lContainer = (node as LContainerNode).data;
lContainer[RENDER_PARENT] = currentParent;
lContainer[RENDER_PARENT] = renderParent;
const views = lContainer[VIEWS];
for (let i = 0; i < views.length; i++) {
addRemoveViewFromContainer(node as LContainerNode, views[i], true, null);
}
}
if (node.dynamicLContainerNode) {
node.dynamicLContainerNode.data[RENDER_PARENT] = currentParent;
node.dynamicLContainerNode.data[RENDER_PARENT] = renderParent;
appendChild(currentParent, node.dynamicLContainerNode.native, currentView);
}
}

View File

@ -138,6 +138,7 @@ describe('content projection', () => {
});
it('should project content with container.', () => {
/** <div> <ng-content></ng-content></div> */
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
projectionDef(0);
@ -146,6 +147,16 @@ describe('content projection', () => {
elementEnd();
}
});
/**
* <child>
* (
* % if (value) {
* content
* % }
* )
* </child>
*/
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) {
if (rf & RenderFlags.Create) {
elementStart(0, 'child');
@ -182,12 +193,21 @@ describe('content projection', () => {
});
it('should project content with container into root', () => {
/** <ng-content></ng-content> */
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
projectionDef(0);
projection(1, 0);
}
});
/**
* <child>
* % if (value) {
* content
* % }
* </child>
*/
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) {
if (rf & RenderFlags.Create) {
elementStart(0, 'child');
@ -222,6 +242,7 @@ describe('content projection', () => {
});
it('should project content with container and if-else.', () => {
/** <div><ng-content></ng-content></div> */
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
projectionDef(0);
@ -230,6 +251,18 @@ describe('content projection', () => {
elementEnd();
}
});
/**
* <child>
* (
* % if (value) {
* content
* % } else {
* else
* % }
* )
* </child>
*/
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: {value: any}) {
if (rf & RenderFlags.Create) {
elementStart(0, 'child');
@ -270,7 +303,7 @@ describe('content projection', () => {
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;
/**
@ -306,9 +339,7 @@ describe('content projection', () => {
}
});
/**
* <child>content</child>
*/
/** <child>content</child> */
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'child');
@ -383,6 +414,72 @@ describe('content projection', () => {
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',
() => {
let childCmptInstance: any;