fix(ivy): correct position for re-projected containers (#24721)

PR Close #24721
This commit is contained in:
Kara Erickson 2018-06-29 16:44:54 -07:00 committed by Miško Hevery
parent 3553977bd7
commit a294e0dd79
5 changed files with 356 additions and 53 deletions

View File

@ -9,7 +9,7 @@
import {assertEqual, assertLessThan} from './assert'; import {assertEqual, assertLessThan} from './assert';
import {NO_CHANGE, bindingUpdated, createLNode, getPreviousOrParentNode, getRenderer, getViewData, load, resetApplicationState} from './instructions'; import {NO_CHANGE, bindingUpdated, createLNode, getPreviousOrParentNode, getRenderer, getViewData, load, resetApplicationState} from './instructions';
import {RENDER_PARENT} from './interfaces/container'; import {RENDER_PARENT} from './interfaces/container';
import {LContainerNode, LElementNode, LNode, TNodeType} from './interfaces/node'; import {LContainerNode, LElementNode, LNode, TContainerNode, TNodeType} from './interfaces/node';
import {BINDING_INDEX, HEADER_OFFSET, TVIEW} from './interfaces/view'; import {BINDING_INDEX, HEADER_OFFSET, TVIEW} from './interfaces/view';
import {appendChild, createTextNode, getParentLNode, removeChild} from './node_manipulation'; import {appendChild, createTextNode, getParentLNode, removeChild} from './node_manipulation';
import {stringify} from './util'; import {stringify} from './util';
@ -241,7 +241,8 @@ function appendI18nNode(node: LNode, parentNode: LNode, previousNode: LNode) {
appendChild(parentNode, node.native || null, viewData); appendChild(parentNode, node.native || null, viewData);
// On first pass, re-organize node tree to put this node in the correct position. // On first pass, re-organize node tree to put this node in the correct position.
if (node.view[TVIEW].firstTemplatePass) { const firstTemplatePass = node.view[TVIEW].firstTemplatePass;
if (firstTemplatePass) {
node.tNode.next = null; node.tNode.next = null;
if (previousNode === parentNode && node.tNode !== parentNode.tNode.child) { if (previousNode === parentNode && node.tNode !== parentNode.tNode.child) {
node.tNode.next = parentNode.tNode.child; node.tNode.next = parentNode.tNode.child;
@ -257,7 +258,10 @@ function appendI18nNode(node: LNode, parentNode: LNode, previousNode: LNode) {
// (node.native as RComment).textContent = 'test'; // (node.native as RComment).textContent = 'test';
// console.log(node.native); // console.log(node.native);
appendChild(parentNode, node.dynamicLContainerNode.native || null, viewData); appendChild(parentNode, node.dynamicLContainerNode.native || null, viewData);
node.pNextOrParent = node.dynamicLContainerNode; if (firstTemplatePass) {
node.tNode.dynamicContainerNode = node.dynamicLContainerNode.tNode;
node.dynamicLContainerNode.tNode.parent = node.tNode as TContainerNode;
}
return node.dynamicLContainerNode; return node.dynamicLContainerNode;
} }

View File

@ -1920,7 +1920,7 @@ export function projectionDef(
* @param appendedFirst First node of the linked list to append. * @param appendedFirst First node of the linked list to append.
* @param appendedLast Last node of the linked list to append. * @param appendedLast Last node of the linked list to append.
*/ */
function appendToProjectionNode( function addToProjectionList(
projectionNode: LProjectionNode, projectionNode: LProjectionNode,
appendedFirst: LElementNode | LTextNode | LContainerNode | null, appendedFirst: LElementNode | LTextNode | LContainerNode | null,
appendedLast: LElementNode | LTextNode | LContainerNode | null) { appendedLast: LElementNode | LTextNode | LContainerNode | null) {
@ -1965,36 +1965,33 @@ export function projection(
const distributedNodes = loadInternal(localIndex, componentLView) as Array<LNode[]>; const distributedNodes = loadInternal(localIndex, componentLView) as Array<LNode[]>;
const nodesForSelector = distributedNodes[selectorIndex]; const nodesForSelector = distributedNodes[selectorIndex];
// build the linked list of projected nodes:
for (let i = 0; i < nodesForSelector.length; i++) {
const nodeToProject = nodesForSelector[i];
if (nodeToProject.tNode.type === TNodeType.Projection) {
// Reprojecting a projection -> append the list of previously projected nodes
const previouslyProjected = (nodeToProject as LProjectionNode).data;
appendToProjectionNode(node, previouslyProjected.head, previouslyProjected.tail);
} else {
// Projecting a single node
appendToProjectionNode(
node, nodeToProject as LTextNode | LElementNode | LContainerNode,
nodeToProject as LTextNode | LElementNode | LContainerNode);
}
}
const currentParent = getParentLNode(node); const currentParent = getParentLNode(node);
if (canInsertNativeNode(currentParent, viewData)) { const canInsert = canInsertNativeNode(currentParent, viewData);
ngDevMode && assertNodeOfPossibleTypes(currentParent, TNodeType.Element, TNodeType.View);
// 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 ? const renderParent = currentParent.tNode.type === TNodeType.View ?
(getParentLNode(currentParent) as LContainerNode).data[RENDER_PARENT] ! : (getParentLNode(currentParent) as LContainerNode).data[RENDER_PARENT] ! :
currentParent as LElementNode; currentParent as LElementNode;
while (nodeToProject) { for (let i = 0; i < nodesForSelector.length; i++) {
const nodeToProject = nodesForSelector[i];
let head = nodeToProject as LTextNode | LElementNode | LContainerNode | null;
let tail = nodeToProject as LTextNode | LElementNode | LContainerNode | null;
if (nodeToProject.tNode.type === TNodeType.Projection) {
const previouslyProjected = (nodeToProject as LProjectionNode).data;
head = previouslyProjected.head;
tail = previouslyProjected.tail;
}
addToProjectionList(node, head, tail);
if (canInsert) {
let currentNode: LNode|null = head;
while (currentNode) {
appendProjectedNode( appendProjectedNode(
nodeToProject as LTextNode | LElementNode | LContainerNode, currentParent, viewData, currentNode as LTextNode | LElementNode | LContainerNode, currentParent, viewData,
renderParent); renderParent);
nodeToProject = nodeToProject === lastNodeToProject ? null : nodeToProject.pNextOrParent; currentNode = currentNode === tail ? null : currentNode.pNextOrParent;
}
} }
} }
} }

View File

@ -618,7 +618,7 @@ export function appendProjectedNode(
lContainer[RENDER_PARENT] = renderParent; 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, node.native);
} }
} }
if (node.dynamicLContainerNode) { if (node.dynamicLContainerNode) {

View File

@ -45,12 +45,15 @@ describe('content projection', () => {
}); });
it('should project content when root.', () => { it('should project content when 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>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');
@ -64,6 +67,7 @@ describe('content projection', () => {
}); });
it('should re-project content when root.', () => { it('should re-project content when root.', () => {
/** <div><ng-content></ng-content></div> */
const GrandChild = createComponent('grand-child', function(rf: RenderFlags, ctx: any) { const GrandChild = createComponent('grand-child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
projectionDef(0); projectionDef(0);
@ -72,6 +76,8 @@ describe('content projection', () => {
elementEnd(); elementEnd();
} }
}); });
/** <grand-child><ng-content></ng-content></grand-child> */
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);
@ -80,6 +86,8 @@ describe('content projection', () => {
elementEnd(); elementEnd();
} }
}, [GrandChild]); }, [GrandChild]);
/** <child><b>Hello</b>World!</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');
@ -137,6 +145,59 @@ describe('content projection', () => {
.toEqual('<child><div><projected-comp>content</projected-comp></div></child>'); .toEqual('<child><div><projected-comp>content</projected-comp></div></child>');
}); });
it('should project components that have their own projection', () => {
/** <div><ng-content></ng-content></div> */
const Child = createComponent('child', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
projectionDef(0);
elementStart(1, 'div');
{ projection(2, 0); }
elementEnd();
}
});
/** <p><ng-content></ng-content></p> */
const ProjectedComp = createComponent('projected-comp', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
projectionDef(0);
elementStart(1, 'p');
projection(2, 0);
elementEnd();
}
});
/**
* <child>
* <projected-comp>
* <div> Some content </div>
* Other content
* </projected-comp>
* </child>
*/
const Parent = createComponent('parent', (rf: RenderFlags, ctx: any) => {
if (rf & RenderFlags.Create) {
elementStart(0, 'child');
{
elementStart(1, 'projected-comp');
{
elementStart(2, 'div');
text(3, 'Some content');
elementEnd();
text(4, 'Other content');
}
elementEnd();
}
elementEnd();
}
}, [Child, ProjectedComp]);
const parent = renderComponent(Parent);
expect(toHtml(parent))
.toEqual(
'<child><div><projected-comp><p><div>Some content</div>Other content</p></projected-comp></div></child>');
});
it('should project content with container.', () => { it('should project content with container.', () => {
/** <div> <ng-content></ng-content></div> */ /** <div> <ng-content></ng-content></div> */
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) { const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
@ -414,7 +475,89 @@ 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', () => { it('should project containers into containers', () => {
/**
* <div>
* Before (inside)
* % if (!skipContent) {
* <ng-content></ng-content>
* % }
* After (inside)
* </div>
*/
const Child = createComponent('child', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
projectionDef(0);
elementStart(1, 'div');
{
text(2, 'Before (inside)-');
container(3);
text(4, '-After (inside)');
}
elementEnd();
}
if (rf & RenderFlags.Update) {
containerRefreshStart(3);
{
if (!ctx.skipContent) {
let rf0 = embeddedViewStart(0);
if (rf0 & RenderFlags.Create) {
projection(0, 0);
}
embeddedViewEnd();
}
}
containerRefreshEnd();
}
});
/**
* <child>
* Before text-
* % if (!skipContent) {
* content
* % }
* -After text
* </child>
*/
const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'child');
{
text(1, 'Before text-');
container(2);
text(3, '-After text');
}
elementEnd();
}
if (rf & RenderFlags.Update) {
containerRefreshStart(2);
{
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>Before (inside)-Before text-content-After text-After (inside)</div></child>');
fixture.component.skipContent = true;
fixture.update();
expect(fixture.html)
.toEqual(
'<child><div>Before (inside)-Before text--After text-After (inside)</div></child>');
});
it('should re-project containers into containers', () => {
/** /**
* <div> * <div>
* % if (!skipContent) { * % if (!skipContent) {
@ -446,24 +589,31 @@ describe('content projection', () => {
/** /**
* <child> * <child>
* Before text
* % if (!skipContent) { * % if (!skipContent) {
* content * <ng-content></ng-content>
* % } * % }
* -After text
* </child> * </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'); projectionDef(0);
{ container(1); } elementStart(1, 'child');
{
text(2, 'Before text');
container(3);
text(4, '-After text');
}
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
containerRefreshStart(1); containerRefreshStart(3);
{ {
if (!ctx.skipContent) { if (!ctx.skipContent) {
let rf0 = embeddedViewStart(0); let rf0 = embeddedViewStart(0);
if (rf0 & RenderFlags.Create) { if (rf0 & RenderFlags.Create) {
text(0, 'content'); projection(0, 0);
} }
embeddedViewEnd(); embeddedViewEnd();
} }
@ -472,14 +622,33 @@ describe('content projection', () => {
} }
}, [Child]); }, [Child]);
const fixture = new ComponentFixture(Parent); let parent: any;
expect(fixture.html).toEqual('<child><div>content</div></child>'); /** <parent><p>text</p></parent> */
const App = createComponent('app', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'parent');
{
elementStart(1, 'p');
{ text(2, 'text'); }
elementEnd();
}
elementEnd();
// testing
parent = loadDirective(0);
}
}, [Parent]);
fixture.component.skipContent = true; const fixture = new ComponentFixture(App);
expect(fixture.html)
.toEqual('<parent><child><div>Before text<p>text</p>-After text</div></child></parent>');
parent.skipContent = true;
fixture.update(); fixture.update();
expect(fixture.html).toEqual('<child><div></div></child>'); expect(fixture.html)
.toEqual('<parent><child><div>Before text-After text</div></child></parent>');
}); });
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;
@ -633,6 +802,129 @@ describe('content projection', () => {
expect(toHtml(parent)).toEqual('<child><div>content</div></child>'); expect(toHtml(parent)).toEqual('<child><div>content</div></child>');
}); });
it('should project with multiple instances of a component with projection', () => {
const ProjectionComp = createComponent('projection-comp', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
projectionDef(0);
text(1, 'Before');
projection(2, 0);
text(3, 'After');
}
});
/**
* <projection-comp>
* <div>A</div>
* <p>123</p>
* </projection-comp>
* <projection-comp>
* <div>B</div>
* <p>456</p>
* </projection-comp>
*/
const AppComp = createComponent('app-comp', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'projection-comp');
elementStart(1, 'div');
text(2, 'A');
elementEnd();
elementStart(3, 'p');
text(4, '123');
elementEnd();
elementEnd();
elementStart(5, 'projection-comp');
elementStart(6, 'div');
text(7, 'B');
elementEnd();
elementStart(8, 'p');
text(9, '456');
elementEnd();
elementEnd();
}
}, [ProjectionComp]);
const fixture = new ComponentFixture(AppComp);
fixture.update();
expect(fixture.html)
.toEqual(
'<projection-comp>Before<div>A</div><p>123</p>After</projection-comp><projection-comp>Before<div>B</div><p>456</p>After</projection-comp>');
});
it('should re-project with multiple instances of a component with projection', () => {
/**
* Before
* <ng-content></ng-content>
* After
*/
const ProjectionComp = createComponent('projection-comp', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
projectionDef(0);
text(1, 'Before');
projection(2, 0);
text(3, 'After');
}
});
/**
* <projection-comp>
* <div>A</div>
* <ng-content></ng-content>
* <p>123</p>
* </projection-comp>
* <projection-comp>
* <div>B</div>
* <p>456</p>
* </projection-comp>
*/
const ProjectionParent = createComponent('parent-comp', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
projectionDef(0);
elementStart(1, 'projection-comp');
elementStart(2, 'div');
text(3, 'A');
elementEnd();
projection(4, 0);
elementStart(5, 'p');
text(6, '123');
elementEnd();
elementEnd();
elementStart(7, 'projection-comp');
elementStart(8, 'div');
text(9, 'B');
elementEnd();
elementStart(10, 'p');
text(11, '456');
elementEnd();
elementEnd();
}
}, [ProjectionComp]);
/**
* <parent-comp>
* **ABC**
* </parent-comp>
* <parent-comp>
* **DEF**
* </parent-comp>
*/
const AppComp = createComponent('app-comp', function(rf: RenderFlags, ctx: any) {
if (rf & RenderFlags.Create) {
elementStart(0, 'parent-comp');
text(1, '**ABC**');
elementEnd();
elementStart(2, 'parent-comp');
text(3, '**DEF**');
elementEnd();
}
}, [ProjectionParent]);
const fixture = new ComponentFixture(AppComp);
fixture.update();
expect(fixture.html)
.toEqual(
'<parent-comp><projection-comp>Before<div>A</div>**ABC**<p>123</p>After</projection-comp><projection-comp>Before<div>B</div><p>456</p>After</projection-comp></parent-comp><parent-comp><projection-comp>Before<div>A</div>**DEF**<p>123</p>After</projection-comp><projection-comp>Before<div>B</div><p>456</p>After</projection-comp></parent-comp>');
});
describe('with selectors', () => { describe('with selectors', () => {
it('should project nodes using attribute selectors', () => { it('should project nodes using attribute selectors', () => {

View File

@ -727,9 +727,12 @@ describe('ViewContainerRef', () => {
@Component({ @Component({
selector: 'child-with-view', selector: 'child-with-view',
template: ` template: `
Before (inside)-
% if (show) { % if (show) {
<ng-content></ng-content> <ng-content></ng-content>
% }` % }
After (inside)
`
}) })
class ChildWithView { class ChildWithView {
show: boolean = true; show: boolean = true;
@ -740,10 +743,12 @@ describe('ViewContainerRef', () => {
template: (rf: RenderFlags, cmp: ChildWithView) => { template: (rf: RenderFlags, cmp: ChildWithView) => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
projectionDef(0); projectionDef(0);
container(1); text(1, 'Before (inside)-');
container(2);
text(3, 'After (inside)');
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
containerRefreshStart(1); containerRefreshStart(2);
if (cmp.show) { if (cmp.show) {
let rf0 = embeddedViewStart(0); let rf0 = embeddedViewStart(0);
if (rf0 & RenderFlags.Create) { if (rf0 & RenderFlags.Create) {
@ -764,7 +769,9 @@ describe('ViewContainerRef', () => {
<span>{{name}}</span> <span>{{name}}</span>
</ng-template> </ng-template>
<child-with-view> <child-with-view>
Before projected
<header vcref [tplRef]="foo" [name]="name">blah</header> <header vcref [tplRef]="foo" [name]="name">blah</header>
After projected
</child-with-view>` </child-with-view>`
}) })
class Parent { class Parent {
@ -777,15 +784,17 @@ describe('ViewContainerRef', () => {
if (rf & RenderFlags.Create) { if (rf & RenderFlags.Create) {
container(0, embeddedTemplate); container(0, embeddedTemplate);
elementStart(1, 'child-with-view'); elementStart(1, 'child-with-view');
elementStart(2, 'header', ['vcref', '']); text(2, 'Before projected');
text(3, 'blah'); elementStart(3, 'header', ['vcref', '']);
text(4, 'blah');
elementEnd(); elementEnd();
text(5, 'After projected-');
elementEnd(); elementEnd();
} }
if (rf & RenderFlags.Update) { if (rf & RenderFlags.Update) {
const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0))); const tplRef = getOrCreateTemplateRef(getOrCreateNodeInjectorForNode(load(0)));
elementProperty(2, 'tplRef', bind(tplRef)); elementProperty(3, 'tplRef', bind(tplRef));
elementProperty(2, 'name', bind(cmp.name)); elementProperty(3, 'name', bind(cmp.name));
} }
}, },
directives: [ChildWithView, DirectiveWithVCRef] directives: [ChildWithView, DirectiveWithVCRef]
@ -794,13 +803,14 @@ describe('ViewContainerRef', () => {
const fixture = new ComponentFixture(Parent); const fixture = new ComponentFixture(Parent);
expect(fixture.html) expect(fixture.html)
.toEqual('<child-with-view><header vcref="">blah</header></child-with-view>'); .toEqual(
'<child-with-view>Before (inside)-Before projected<header vcref="">blah</header>After projected-After (inside)</child-with-view>');
directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component); directiveInstance !.vcref.createEmbeddedView(directiveInstance !.tplRef, fixture.component);
fixture.update(); fixture.update();
expect(fixture.html) expect(fixture.html)
.toEqual( .toEqual(
'<child-with-view><header vcref="">blah</header><span>bar</span></child-with-view>'); '<child-with-view>Before (inside)-Before projected<header vcref="">blah</header><span>bar</span>After projected-After (inside)</child-with-view>');
}); });
describe('with select', () => { describe('with select', () => {