diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index b8ae8a2faf..3d2c023f1a 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -145,7 +145,7 @@ function walkLNodeTree( nextNode = head ? (componentHost.data as LViewData)[PARENT] ![head.index] : null; } else { // Otherwise look at the first child - nextNode = getChildLNode(node as LViewNode); + nextNode = getChildLNode(node as LViewNode | LElementContainerNode); } if (nextNode === null) { @@ -532,6 +532,16 @@ function canInsertNativeChildOfElement(parent: LElementNode, currentView: LViewD return false; } +/** + * We might delay insertion of children for a given view if it is disconnected. + * This might happen for 2 main reason: + * - view is not inserted into any container (view was created but not iserted yet) + * - view is inserted into a container but the container itself is not inserted into the DOM + * (container might be part of projection or child of a view that is not inserted yet). + * + * In other words we can insert children of a given view this view was inserted into a container and + * the container itself has it render parent determined. + */ function canInsertNativeChildOfView(parent: LViewNode): boolean { ngDevMode && assertNodeType(parent, TNodeType.View); @@ -635,7 +645,10 @@ export function appendChild(parent: LNode, child: RNode | null, currentView: LVi nativeInsertBefore(renderer, renderParent !.native, child, beforeNode); } else if (parent.tNode.type === TNodeType.ElementContainer) { const beforeNode = parent.native; - const grandParent = getParentLNode(parent) as LElementNode | LViewNode; + let grandParent = getParentLNode(parent as LElementContainerNode); + while (grandParent.tNode.type === TNodeType.ElementContainer) { + grandParent = getParentLNode(grandParent as LElementContainerNode); + } if (grandParent.tNode.type === TNodeType.View) { const renderParent = getRenderParent(grandParent as LViewNode); nativeInsertBefore(renderer, renderParent !.native, child, beforeNode); diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index c72b1c7953..7df1547fc5 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -651,6 +651,108 @@ describe('render3 integration test', () => { expect(fixture.html).toEqual('component template'); }); + it('should render inside another ng-container', () => { + /** + * + * + * + * content + * + * + * + */ + const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) { + if (rf & RenderFlags.Create) { + elementContainerStart(0); + { + elementContainerStart(1); + { + elementContainerStart(2); + { text(3, 'content'); } + elementContainerEnd(); + } + elementContainerEnd(); + } + elementContainerEnd(); + } + }); + + function App() { element(0, 'test-cmpt'); } + + const fixture = new TemplateFixture(App, () => {}, [TestCmpt]); + expect(fixture.html).toEqual('content'); + }); + + it('should render inside another ng-container at the root of a delayed view', () => { + + class TestDirective { + constructor(private _tplRef: TemplateRef, private _vcRef: ViewContainerRef) {} + + createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); } + + clear() { this._vcRef.clear(); } + + static ngDirectiveDef = defineDirective({ + type: TestDirective, + selectors: [['', 'testDirective', '']], + factory: () => new TestDirective(injectTemplateRef(), injectViewContainerRef()), + }); + } + + let testDirective: TestDirective; + + function embeddedTemplate(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementContainerStart(0); + { + elementContainerStart(1); + { + elementContainerStart(2); + { text(3, 'content'); } + elementContainerEnd(); + } + elementContainerEnd(); + } + elementContainerEnd(); + } + } + + ` + + + + content + + + + `; + const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) { + if (rf & RenderFlags.Create) { + container(0, embeddedTemplate, null, [AttributeMarker.SelectOnly, 'testDirective']); + } + if (rf & RenderFlags.Update) { + testDirective = loadDirective(0); + } + }, [TestDirective]); + + function App() { element(0, 'test-cmpt'); } + + const fixture = new ComponentFixture(TestCmpt); + expect(fixture.html).toEqual(''); + + testDirective !.createAndInsert(); + fixture.update(); + expect(fixture.html).toEqual('content'); + + testDirective !.createAndInsert(); + fixture.update(); + expect(fixture.html).toEqual('contentcontent'); + + testDirective !.clear(); + fixture.update(); + expect(fixture.html).toEqual(''); + }); + it('should support directives and inject ElementRef', () => { class Directive {