From ede65dbede75b89ef759df780fadb6c2769d1133 Mon Sep 17 00:00:00 2001 From: Pawel Kozlowski Date: Wed, 17 Oct 2018 11:01:35 +0200 Subject: [PATCH] fix(ivy): allow root components to inject ViewContainerRef (#26682) PR Close #26682 --- .../core/src/render3/interfaces/renderer.ts | 7 +++ .../core/src/render3/node_manipulation.ts | 27 +++++++++-- packages/core/src/render3/util.ts | 4 ++ .../src/render3/view_engine_compatibility.ts | 26 +++++++--- .../bundle.golden_symbols.json | 9 ++++ .../hello_world/bundle.golden_symbols.json | 6 +++ .../hello_world_r2/bundle.golden_symbols.json | 9 ++++ .../bundling/todo/bundle.golden_symbols.json | 9 ++++ .../todo_r2/bundle.golden_symbols.json | 9 ++++ .../core/test/render3/integration_spec.ts | 2 + packages/core/test/render3/render_util.ts | 33 +++++++++++-- .../test/render3/view_container_ref_spec.ts | 48 +++++++++++++++++++ 12 files changed, 175 insertions(+), 14 deletions(-) diff --git a/packages/core/src/render3/interfaces/renderer.ts b/packages/core/src/render3/interfaces/renderer.ts index 36fb6f802a..38a3247b49 100644 --- a/packages/core/src/render3/interfaces/renderer.ts +++ b/packages/core/src/render3/interfaces/renderer.ts @@ -71,6 +71,9 @@ export interface ProceduralRenderer3 { removeChild(parent: RElement, oldChild: RNode): void; selectRootElement(selectorOrNode: string|any): RElement; + parentNode(node: RNode): RElement|null; + nextSibling(node: RNode): RNode|null; + setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void; removeAttribute(el: RElement, name: string, namespace?: string|null): void; addClass(el: RElement, name: string): void; @@ -99,6 +102,10 @@ export const domRendererFactory3: RendererFactory3 = { /** Subset of API needed for appending elements and text nodes. */ export interface RNode { + parentNode: RNode|null; + + nextSibling: RNode|null; + removeChild(oldChild: RNode): void; /** diff --git a/packages/core/src/render3/node_manipulation.ts b/packages/core/src/render3/node_manipulation.ts index 4663868be5..ceb11abd7a 100644 --- a/packages/core/src/render3/node_manipulation.ts +++ b/packages/core/src/render3/node_manipulation.ts @@ -15,7 +15,7 @@ import {unusedValueExportToPlacateAjd as unused3} from './interfaces/projection' import {ProceduralRenderer3, RComment, RElement, RNode, RText, Renderer3, isProceduralRenderer, unusedValueExportToPlacateAjd as unused4} from './interfaces/renderer'; import {CLEANUP, CONTAINER_INDEX, FLAGS, HEADER_OFFSET, HOST_NODE, HookData, LViewData, LViewFlags, NEXT, PARENT, QUERIES, RENDERER, TVIEW, unusedValueExportToPlacateAjd as unused5} from './interfaces/view'; import {assertNodeType} from './node_assert'; -import {getNativeByTNode, isLContainer, readElementValue, stringify} from './util'; +import {getNativeByTNode, isLContainer, isRootView, readElementValue, stringify} from './util'; const unusedValueToPlacateAjd = unused1 + unused2 + unused3 + unused4 + unused5; @@ -501,6 +501,13 @@ function executePipeOnDestroys(viewData: LViewData): void { export function getRenderParent(tNode: TNode, currentView: LViewData): RElement|null { if (canInsertNativeNode(tNode, currentView)) { + // If we are asked for a render parent of the root component we need to do low-level DOM + // operation as LTree doesn't exist above the topmost host node. We might need to find a render + // parent of the topmost host node if the root component injects ViewContainerRef. + if (isRootView(currentView)) { + return nativeParentNode(currentView[RENDERER], getNativeByTNode(tNode, currentView)); + } + const hostTNode = currentView[HOST_NODE]; const tNodeParent = tNode.parent; @@ -598,7 +605,7 @@ export function canInsertNativeNode(tNode: TNode, currentView: LViewData): boole * This is a utility function that can be used when native nodes were determined - it abstracts an * actual renderer being used. */ -function nativeInsertBefore( +export function nativeInsertBefore( renderer: Renderer3, parent: RElement, child: RNode, beforeNode: RNode | null): void { if (isProceduralRenderer(renderer)) { renderer.insertBefore(parent, child, beforeNode); @@ -607,6 +614,20 @@ function nativeInsertBefore( } } +/** + * Returns a native parent of a given native node. + */ +export function nativeParentNode(renderer: Renderer3, node: RNode): RElement|null { + return (isProceduralRenderer(renderer) ? renderer.parentNode(node) : node.parentNode) as RElement; +} + +/** + * Returns a native sibling of a given native node. + */ +export function nativeNextSibling(renderer: Renderer3, node: RNode): RNode|null { + return isProceduralRenderer(renderer) ? renderer.nextSibling(node) : node.nextSibling; +} + /** * Appends the `child` element to the `parent`. * @@ -632,7 +653,7 @@ export function appendChild( renderer, lContainer[RENDER_PARENT] !, childEl, getBeforeNodeForView(index, views, lContainer[NATIVE])); } else if (parentTNode.type === TNodeType.ElementContainer) { - const renderParent: RElement = getRenderParent(childTNode, currentView) !; + const renderParent = getRenderParent(childTNode, currentView) !; nativeInsertBefore(renderer, renderParent, childEl, parentEl); } else { isProceduralRenderer(renderer) ? renderer.appendChild(parentEl !as RElement, childEl) : diff --git a/packages/core/src/render3/util.ts b/packages/core/src/render3/util.ts index 6805d7c14c..041d18fc05 100644 --- a/packages/core/src/render3/util.ts +++ b/packages/core/src/render3/util.ts @@ -134,6 +134,10 @@ export function isLContainer(value: RElement | RComment | LContainer | StylingCo return Array.isArray(value) && typeof value[ACTIVE_INDEX] === 'number'; } +export function isRootView(target: LViewData): boolean { + return (target[FLAGS] & LViewFlags.IsRoot) !== 0; +} + /** * Retrieve the root view from any component by walking the parent `LViewData` until * reaching the root `LViewData`. diff --git a/packages/core/src/render3/view_engine_compatibility.ts b/packages/core/src/render3/view_engine_compatibility.ts index 79c7e9fea4..6cd486baf3 100644 --- a/packages/core/src/render3/view_engine_compatibility.ts +++ b/packages/core/src/render3/view_engine_compatibility.ts @@ -26,9 +26,9 @@ import {LQueries} from './interfaces/query'; import {RComment, RElement, Renderer3, isProceduralRenderer} from './interfaces/renderer'; import {CONTEXT, HOST_NODE, LViewData, QUERIES, RENDERER, TView} from './interfaces/view'; import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert'; -import {addRemoveViewFromContainer, appendChild, detachView, findComponentView, getBeforeNodeForView, getRenderParent, insertView, removeView} from './node_manipulation'; +import {addRemoveViewFromContainer, appendChild, detachView, findComponentView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode, removeView} from './node_manipulation'; import {getPreviousOrParentTNode, getRenderer, getViewData} from './state'; -import {getComponentViewByIndex, getNativeByTNode, getParentInjectorTNode, getParentInjectorView, hasParentInjector, isComponent, isLContainer} from './util'; +import {getComponentViewByIndex, getNativeByTNode, getParentInjectorTNode, getParentInjectorView, hasParentInjector, isComponent, isLContainer, isRootView} from './util'; import {ViewRef} from './view_ref'; @@ -307,12 +307,26 @@ export function createContainerRef( lContainer = slotValue; lContainer[ACTIVE_INDEX] = -1; } else { - const comment = hostView[RENDERER].createComment(ngDevMode ? 'container' : ''); + const commentNode = hostView[RENDERER].createComment(ngDevMode ? 'container' : ''); ngDevMode && ngDevMode.rendererCreateComment++; - hostView[hostTNode.index] = lContainer = - createLContainer(slotValue, hostTNode, hostView, comment, true); - appendChild(comment, hostTNode, hostView); + // A container can be created on the root (topmost / bootstrapped) component and in this case we + // can't use LTree to insert container's marker node (both parent of a comment node and the + // commend node itself is located outside of elements hold by LTree). In this specific case we + // use low-level DOM manipulation to insert container's marker (comment) node. + if (isRootView(hostView)) { + const renderer = hostView[RENDERER]; + const hostNative = getNativeByTNode(hostTNode, hostView) !; + const parentOfHostNative = nativeParentNode(renderer, hostNative); + nativeInsertBefore( + renderer, parentOfHostNative !, commentNode, nativeNextSibling(renderer, hostNative)); + } else { + appendChild(commentNode, hostTNode, hostView); + } + + hostView[hostTNode.index] = lContainer = + createLContainer(slotValue, hostTNode, hostView, commentNode, true); + addToViewTree(hostView, hostTNode.index as number, lContainer); } diff --git a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json index 34a28a5ca3..bc9465b06f 100644 --- a/packages/core/test/bundling/animation_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/animation_world/bundle.golden_symbols.json @@ -872,6 +872,9 @@ { "name": "isProceduralRenderer" }, + { + "name": "isRootView" + }, { "name": "isSanitizable" }, @@ -920,6 +923,12 @@ { "name": "nativeInsertBefore" }, + { + "name": "nativeNextSibling" + }, + { + "name": "nativeParentNode" + }, { "name": "nextContext" }, 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 1a90289269..c2b73322a4 100644 --- a/packages/core/test/bundling/hello_world/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world/bundle.golden_symbols.json @@ -341,6 +341,9 @@ { "name": "isProceduralRenderer" }, + { + "name": "isRootView" + }, { "name": "leaveView" }, @@ -353,6 +356,9 @@ { "name": "nativeInsertBefore" }, + { + "name": "nativeParentNode" + }, { "name": "nextNgElementId" }, diff --git a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json index d21b5399f3..a053553523 100644 --- a/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/hello_world_r2/bundle.golden_symbols.json @@ -3803,6 +3803,9 @@ { "name": "isQuote" }, + { + "name": "isRootView" + }, { "name": "isScheduler" }, @@ -3956,6 +3959,12 @@ { "name": "nativeInsertBefore" }, + { + "name": "nativeNextSibling" + }, + { + "name": "nativeParentNode" + }, { "name": "needsAdditionalRootNode" }, diff --git a/packages/core/test/bundling/todo/bundle.golden_symbols.json b/packages/core/test/bundling/todo/bundle.golden_symbols.json index 0363026971..21ecf48c5c 100644 --- a/packages/core/test/bundling/todo/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo/bundle.golden_symbols.json @@ -893,6 +893,9 @@ { "name": "isProceduralRenderer" }, + { + "name": "isRootView" + }, { "name": "isStylingContext" }, @@ -938,6 +941,12 @@ { "name": "nativeInsertBefore" }, + { + "name": "nativeNextSibling" + }, + { + "name": "nativeParentNode" + }, { "name": "nextContext" }, diff --git a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json index e1e3954ece..35900a82fc 100644 --- a/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json +++ b/packages/core/test/bundling/todo_r2/bundle.golden_symbols.json @@ -2123,6 +2123,9 @@ { "name": "isPromise$2" }, + { + "name": "isRootView" + }, { "name": "isScheduler" }, @@ -2219,6 +2222,12 @@ { "name": "nativeInsertBefore" }, + { + "name": "nativeNextSibling" + }, + { + "name": "nativeParentNode" + }, { "name": "nextContext" }, diff --git a/packages/core/test/render3/integration_spec.ts b/packages/core/test/render3/integration_spec.ts index 44efa5ed83..906bd65ec2 100644 --- a/packages/core/test/render3/integration_spec.ts +++ b/packages/core/test/render3/integration_spec.ts @@ -2677,6 +2677,8 @@ class MockRenderer implements ProceduralRenderer3 { selectRootElement(selectorOrNode: string|any): RElement { return ({} as any); } + parentNode(node: RNode): RElement|null { return node.parentNode as RElement; } + nextSibling(node: RNode): RNode|null { return node.nextSibling; } setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void {} removeAttribute(el: RElement, name: string, namespace?: string|null): void {} addClass(el: RElement, name: string): void {} diff --git a/packages/core/test/render3/render_util.ts b/packages/core/test/render3/render_util.ts index 44d442a595..01241b0f47 100644 --- a/packages/core/test/render3/render_util.ts +++ b/packages/core/test/render3/render_util.ts @@ -36,17 +36,40 @@ import {Type} from '../../src/type'; import {getRendererFactory2} from './imported_renderer2'; export abstract class BaseFixture { + /** + * Each fixture creates the following initial DOM structure: + *
+ *
+ *
+ * + * Components are bootstrapped into the
. + * The
is there for cases where the root component creates DOM node _outside_ + * of its host element (for example when the root component injectes ViewContainerRef or does + * low-level DOM manipulation). + * + * The
is _not_ attached to the document body. + */ + containerElement: HTMLElement; hostElement: HTMLElement; constructor() { + this.containerElement = document.createElement('div'); + this.containerElement.setAttribute('fixture', 'mark'); this.hostElement = document.createElement('div'); - this.hostElement.setAttribute('fixture', 'mark'); + this.hostElement.setAttribute('host', 'mark'); + this.containerElement.appendChild(this.hostElement); } /** - * Current state of rendered HTML. + * Current state of HTML rendered by the bootstrapped component. */ get html(): string { return toHtml(this.hostElement as any as Element); } + + /** + * Current state of HTML rendered by the fixture (will include HTML rendered by the bootstrapped + * component as well as any elements outside of the component's host). + */ + get outerHtml(): string { return toHtml(this.containerElement as any as Element); } } function noop() {} @@ -251,9 +274,9 @@ export function toHtml(componentOrElement: T | RElement): string { if (element) { return stringifyElement(element) - .replace(/^
/, '') - .replace(/^
/, '') - .replace(/<\/div>$/, '') + .replace(/^
(.*)<\/div>$/, '$1') + .replace(/^
(.*)<\/div>$/, '$1') + .replace(/^
(.*)<\/div>$/, '$1') .replace(' style=""', '') .replace(//g, '') .replace(//g, ''); diff --git a/packages/core/test/render3/view_container_ref_spec.ts b/packages/core/test/render3/view_container_ref_spec.ts index f234806735..7df73d2830 100644 --- a/packages/core/test/render3/view_container_ref_spec.ts +++ b/packages/core/test/render3/view_container_ref_spec.ts @@ -1768,4 +1768,52 @@ describe('ViewContainerRef', () => { }); }); + + describe('view engine compatibility', () => { + + // https://stackblitz.com/edit/angular-xxpffd?file=src%2Findex.html + it('should allow injecting VCRef into the root (bootstrapped) component', () => { + + const DynamicComponent = + createComponent('dynamic-cmpt', function(rf: RenderFlags, parent: any) { + if (rf & RenderFlags.Create) { + text(0, 'inserted dynamically'); + } + }, 1, 0); + + @Component({selector: 'app', template: ''}) + class AppCmpt { + static ngComponentDef = defineComponent({ + type: AppCmpt, + selectors: [['app']], + factory: () => new AppCmpt( + directiveInject(ViewContainerRef as any), injectComponentFactoryResolver()), + consts: 0, + vars: 0, + template: (rf: RenderFlags, cmp: AppCmpt) => {} + }); + + constructor( + private _vcRef: ViewContainerRef, private _cfResolver: ComponentFactoryResolver) {} + + insert() { + this._vcRef.createComponent(this._cfResolver.resolveComponentFactory(DynamicComponent)); + } + + clear() { this._vcRef.clear(); } + } + + const fixture = new ComponentFixture(AppCmpt); + expect(fixture.outerHtml).toBe('
'); + + fixture.component.insert(); + fixture.update(); + expect(fixture.outerHtml) + .toBe('
inserted dynamically'); + + fixture.component.clear(); + fixture.update(); + expect(fixture.outerHtml).toBe('
'); + }); + }); });