From c2c12e52fed38b3a10001ac90ecc4b789665d0a1 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Wed, 1 Aug 2018 15:19:27 +0200 Subject: [PATCH] feat(ivy): support ng-container as a child of an already inserted view (#25227) PR Close #25227 --- packages/core/src/render3/instructions.ts | 2 +- .../core/src/render3/node_manipulation.ts | 38 ++++- .../hello_world/bundle.golden_symbols.json | 6 + .../bundling/todo/bundle.golden_symbols.json | 6 + .../core/test/render3/integration_spec.ts | 151 +++++++++++++++++- 5 files changed, 194 insertions(+), 9 deletions(-) diff --git a/packages/core/src/render3/instructions.ts b/packages/core/src/render3/instructions.ts index 3a9d315d0e8..705cded3df4 100644 --- a/packages/core/src/render3/instructions.ts +++ b/packages/core/src/render3/instructions.ts @@ -706,7 +706,7 @@ export function element( * @param attrs Set of attributes to be used when matching directives. * @param localRefs A set of local reference bindings on the element. * - * Even if this instruction accepts a set of attributes no actual attribute values are propoagted to + * Even if this instruction accepts a set of attributes no actual attribute values are propagated to * the DOM (as a comment node can't have attributes). Attributes are here only for directive * matching purposes and setting initial inputs of directives. */ diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 24234f14255..17f28171f02 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -56,6 +56,15 @@ export function getParentLNode(node: LNode): LElementNode|LElementContainerNode| return readElementValue(parent ? node.view[parent.index] : node.view[HOST_NODE]); } +/** + * Retrieves render parent LElementNode for a given view. + * Might be null if a view is not yet attatched to any container. + */ +function getRenderParent(viewNode: LViewNode): LElementNode|null { + const container = getParentLNode(viewNode); + return container ? container.data[RENDER_PARENT] : null; +} + const enum WalkLNodeTreeAction { /** node insert in the native environment */ Insert = 0, @@ -565,6 +574,20 @@ export function canInsertNativeNode(parent: LNode, currentView: LViewData): bool } } +/** + * Inserts a native node before another native node for a given parent using {@link Renderer3}. + * This is a utility function that can be used when native nodes were determined - it abstracts an + * actual renderer being used. + */ +function nativeInsertBefore( + renderer: Renderer3, parent: RElement, child: RNode, beforeNode: RNode | null): void { + if (isProceduralRenderer(renderer)) { + renderer.insertBefore(parent, child, beforeNode); + } else { + parent.insertBefore(child, beforeNode, true); + } +} + /** * Appends the `child` element to the `parent`. * @@ -585,15 +608,16 @@ export function appendChild(parent: LNode, child: RNode | null, currentView: LVi const index = views.indexOf(parent as LViewNode); const beforeNode = index + 1 < views.length ? (getChildLNode(views[index + 1]) !).native : container.native; - isProceduralRenderer(renderer) ? - renderer.insertBefore(renderParent !.native, child, beforeNode) : - renderParent !.native.insertBefore(child, beforeNode, true); + nativeInsertBefore(renderer, renderParent !.native, child, beforeNode); } else if (parent.tNode.type === TNodeType.ElementContainer) { const beforeNode = parent.native; - const renderParent = getParentLNode(parent) as LElementNode; - isProceduralRenderer(renderer) ? - renderer.insertBefore(renderParent !.native, child, beforeNode) : - renderParent !.native.insertBefore(child, beforeNode, true); + const grandParent = getParentLNode(parent) as LElementNode | LViewNode; + if (grandParent.tNode.type === TNodeType.View) { + const renderParent = getRenderParent(grandParent as LViewNode); + nativeInsertBefore(renderer, renderParent !.native, child, beforeNode); + } else { + nativeInsertBefore(renderer, (grandParent as LElementNode).native, child, beforeNode); + } } else { isProceduralRenderer(renderer) ? renderer.appendChild(parent.native !as RElement, child) : parent.native !.appendChild(child); diff --git a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json index d4d1870f4cb..0188508d67d 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -176,6 +176,9 @@ { "name": "getRenderFlags" }, + { + "name": "getRenderParent" + }, { "name": "getRootView" }, @@ -200,6 +203,9 @@ { "name": "namespaceHTML" }, + { + "name": "nativeInsertBefore" + }, { "name": "readElementValue" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 2f713715326..757637d08bf 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -590,6 +590,9 @@ { "name": "getRenderFlags" }, + { + "name": "getRenderParent" + }, { "name": "getRenderer" }, @@ -728,6 +731,9 @@ { "name": "namespaceHTML" }, + { + "name": "nativeInsertBefore" + }, { "name": "nextContext" }, diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 7d2669b3238..66e9ce7dcd7 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -16,7 +16,8 @@ import {HEADER_OFFSET} from '../../src/render3/interfaces/view'; import {sanitizeUrl} from '../../src/sanitization/sanitization'; import {Sanitizer, SecurityContext} from '../../src/sanitization/security'; -import {ComponentFixture, TemplateFixture, containerEl, renderToHtml} from './render_util'; +import {NgIf} from './common_with_def'; +import {ComponentFixture, TemplateFixture, containerEl, createComponent, renderToHtml} from './render_util'; describe('render3 integration test', () => { @@ -445,6 +446,154 @@ describe('render3 integration test', () => { expect(fixture.html).toEqual('
before|Greetings|after
'); }); + it('should add and remove DOM nodes when ng-container is a child of a regular element', () => { + /** + * {% if (value) { %} + *
+ * content + *
+ * {% } %} + */ + const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) { + if (rf & RenderFlags.Create) { + container(0); + } + if (rf & RenderFlags.Update) { + containerRefreshStart(0); + if (ctx.value) { + let rf1 = embeddedViewStart(0); + { + if (rf1 & RenderFlags.Create) { + elementStart(0, 'div'); + { + elementContainerStart(1); + { text(2, 'content'); } + elementContainerEnd(); + } + elementEnd(); + } + } + embeddedViewEnd(); + } + containerRefreshEnd(); + } + }); + + const fixture = new ComponentFixture(TestCmpt); + expect(fixture.html).toEqual(''); + + fixture.component.value = true; + fixture.update(); + expect(fixture.html).toEqual('
content
'); + + fixture.component.value = false; + fixture.update(); + expect(fixture.html).toEqual(''); + }); + + it('should add and remove DOM nodes when ng-container is a child of an embedded view (JS block)', + () => { + /** + * {% if (value) { %} + * content + * {% } %} + */ + const TestCmpt = + createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) { + if (rf & RenderFlags.Create) { + container(0); + } + if (rf & RenderFlags.Update) { + containerRefreshStart(0); + if (ctx.value) { + let rf1 = embeddedViewStart(0); + { + if (rf1 & RenderFlags.Create) { + elementContainerStart(0); + { text(1, 'content'); } + elementContainerEnd(); + } + } + embeddedViewEnd(); + } + containerRefreshEnd(); + } + }); + + const fixture = new ComponentFixture(TestCmpt); + expect(fixture.html).toEqual(''); + + fixture.component.value = true; + fixture.update(); + expect(fixture.html).toEqual('content'); + + fixture.component.value = false; + fixture.update(); + expect(fixture.html).toEqual(''); + }); + + it('should add and remove DOM nodes when ng-container is a child of an embedded view (ViewContainerRef)', + () => { + + function ngIfTemplate(rf: RenderFlags, ctx: any) { + if (rf & RenderFlags.Create) { + elementContainerStart(0); + { text(1, 'content'); } + elementContainerEnd(); + } + } + + /** + * content + */ + // equivalent to: + /** + * + * + * content + * + * + */ + const TestCmpt = + createComponent('test-cmpt', function(rf: RenderFlags, ctx: {value: any}) { + if (rf & RenderFlags.Create) { + container(0, ngIfTemplate, null, [AttributeMarker.SelectOnly, 'ngIf']); + } + if (rf & RenderFlags.Update) { + elementProperty(0, 'ngIf', bind(ctx.value)); + } + }, [NgIf]); + + const fixture = new ComponentFixture(TestCmpt); + expect(fixture.html).toEqual(''); + + fixture.component.value = true; + fixture.update(); + expect(fixture.html).toEqual('content'); + + fixture.component.value = false; + fixture.update(); + expect(fixture.html).toEqual(''); + }); + + it('should render at the component view root', () => { + /** + * component template + */ + const TestCmpt = createComponent('test-cmpt', function(rf: RenderFlags) { + if (rf & RenderFlags.Create) { + elementContainerStart(0); + { text(1, 'component template'); } + elementContainerEnd(); + } + }); + + function App() { element(0, 'test-cmpt'); } + + const fixture = new TemplateFixture(App, () => {}, [TestCmpt]); + expect(fixture.html).toEqual('component template'); + }); + it('should support directives and inject ElementRef', () => { class Directive {