Skip to content

Commit

Permalink
fix(ivy): allow root components to inject ViewContainerRef
Browse files Browse the repository at this point in the history
  • Loading branch information
pkozlowski-opensource committed Oct 23, 2018
1 parent 6737e91 commit e4743d9
Show file tree
Hide file tree
Showing 8 changed files with 144 additions and 24 deletions.
6 changes: 3 additions & 3 deletions packages/core/src/render3/interfaces/container.ts
Expand Up @@ -7,7 +7,7 @@
*/

import {LQueries} from './query';
import {RComment, RElement} from './renderer';
import {RComment, RElement, RNode} from './renderer';
import {StylingContext} from './styling';
import {HOST, LViewData, NEXT, PARENT, QUERIES} from './view';

Expand Down Expand Up @@ -100,10 +100,10 @@ export interface LContainer extends Array<any> {
* When views are inserted into `LContainer` then `renderParent` is:
* - `null`, we are in a view, keep going up a hierarchy until actual
* `renderParent` is found.
* - not `null`, then use the `projectedParent.native` as the `RElement` to insert
* - not `null`, then use the `projectedParent.native` as the `RNode` to insert
* views into.
*/
[RENDER_PARENT]: RElement|null;
[RENDER_PARENT]: RNode|null;
}

// Note: This hack is necessary so we don't erroneously get a circular dependency
Expand Down
11 changes: 9 additions & 2 deletions packages/core/src/render3/interfaces/renderer.ts
Expand Up @@ -68,9 +68,12 @@ export interface ProceduralRenderer3 {
destroyNode?: ((node: RNode) => void)|null;
appendChild(parent: RElement, newChild: RNode): void;
insertBefore(parent: RNode, newChild: RNode, refChild: RNode|null): void;
removeChild(parent: RElement, oldChild: RNode): void;
removeChild(parent: RNode, oldChild: RNode): void;
selectRootElement(selectorOrNode: string|any): RElement;

parentNode(node: RNode): RNode|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;
Expand All @@ -94,11 +97,15 @@ export interface RendererFactory3 {

export const domRendererFactory3: RendererFactory3 = {
createRenderer: (hostElement: RElement | null, rendererType: RendererType2 | null):
Renderer3 => { return document;}
Renderer3 => { return document as ObjectOrientedRenderer3;}
};

/** Subset of API needed for appending elements and text nodes. */
export interface RNode {
parentNode: RNode|null;

nextSibling: RNode|null;

removeChild(oldChild: RNode): void;

/**
Expand Down
37 changes: 29 additions & 8 deletions packages/core/src/render3/node_manipulation.ts
Expand Up @@ -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;

Expand Down Expand Up @@ -53,7 +53,7 @@ export function getLContainer(tNode: TViewNode, embeddedView: LViewData): LConta
* Retrieves render parent for a given view.
* Might be null if a view is not yet attached to any container.
*/
export function getContainerRenderParent(tViewNode: TViewNode, view: LViewData): RElement|null {
export function getContainerRenderParent(tViewNode: TViewNode, view: LViewData): RNode|null {
const container = getLContainer(tViewNode, view);
return container ? container[RENDER_PARENT] : null;
}
Expand Down Expand Up @@ -93,7 +93,7 @@ const projectionNodeStack: (LViewData | TNode)[] = [];
*/
function walkTNodeTree(
viewToWalk: LViewData, action: WalkTNodeTreeAction, renderer: Renderer3,
renderParent: RElement | null, beforeNode?: RNode | null) {
renderParent: RNode | null, beforeNode?: RNode | null) {
const rootTNode = viewToWalk[TVIEW].node as TViewNode;
let projectionNodeIndex = -1;
let currentView = viewToWalk;
Expand Down Expand Up @@ -203,7 +203,7 @@ export function findComponentView(lViewData: LViewData): LViewData {
* being passed as an argument.
*/
function executeNodeAction(
action: WalkTNodeTreeAction, renderer: Renderer3, parent: RElement | null,
action: WalkTNodeTreeAction, renderer: Renderer3, parent: RNode | null,
node: RComment | RElement | RText, beforeNode?: RNode | null) {
if (action === WalkTNodeTreeAction.Insert) {
isProceduralRenderer(renderer !) ?
Expand Down Expand Up @@ -499,8 +499,15 @@ function executePipeOnDestroys(viewData: LViewData): void {
}
}

export function getRenderParent(tNode: TNode, currentView: LViewData): RElement|null {
export function getRenderParent(tNode: TNode, currentView: LViewData): RNode|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];
return tNode.parent == null && hostTNode !.type === TNodeType.View ?
getContainerRenderParent(hostTNode as TViewNode, currentView) :
Expand Down Expand Up @@ -592,15 +599,29 @@ 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(
renderer: Renderer3, parent: RElement, child: RNode, beforeNode: RNode | null): void {
export function nativeInsertBefore(
renderer: Renderer3, parent: RNode, child: RNode, beforeNode: RNode | null): void {
if (isProceduralRenderer(renderer)) {
renderer.insertBefore(parent, child, beforeNode);
} else {
parent.insertBefore(child, beforeNode, true);
}
}

/**
* Returns a native parent of a given native node.
*/
export function nativeParentNode(renderer: Renderer3, node: RNode): RNode|null {
return isProceduralRenderer(renderer) ? renderer.parentNode(node) : node.parentNode;
}

/**
* 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`.
*
Expand All @@ -627,7 +648,7 @@ export function appendChild(
getBeforeNodeForView(index, views, lContainer[NATIVE]));
} else if (parentTNode.type === TNodeType.ElementContainer) {
let elementContainer = getHighestElementContainer(childTNode);
let renderParent: RElement = getRenderParent(elementContainer, currentView) !;
const renderParent = getRenderParent(elementContainer, currentView) !;
nativeInsertBefore(renderer, renderParent, childEl, parentEl);
} else {
isProceduralRenderer(renderer) ? renderer.appendChild(parentEl !as RElement, childEl) :
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/render3/util.ts
Expand Up @@ -127,6 +127,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`.
Expand Down
26 changes: 20 additions & 6 deletions packages/core/src/render3/view_engine_compatibility.ts
Expand Up @@ -19,16 +19,16 @@ import {Renderer2} from '../render/api';
import {assertDefined, assertGreaterThan, assertLessThan} from './assert';
import {NodeInjector, getParentInjectorLocation, getParentInjectorView} from './di';
import {_getViewData, addToViewTree, createEmbeddedViewAndNode, createLContainer, getPreviousOrParentTNode, getRenderer, renderEmbeddedTemplate} from './instructions';
import {ACTIVE_INDEX, LContainer, NATIVE, RENDER_PARENT, VIEWS} from './interfaces/container';
import {ACTIVE_INDEX, LContainer, NATIVE, VIEWS} from './interfaces/container';
import {RenderFlags} from './interfaces/definition';
import {InjectorLocationFlags} from './interfaces/injector';
import {TContainerNode, TElementContainerNode, TElementNode, TNode, TNodeFlags, TNodeType, TViewNode} from './interfaces/node';
import {LQueries} from './interfaces/query';
import {RComment, RElement, Renderer3, isProceduralRenderer} from './interfaces/renderer';
import {CONTEXT, HOST_NODE, LViewData, QUERIES, RENDERER, TVIEW, TView} from './interfaces/view';
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
import {addRemoveViewFromContainer, appendChild, detachView, findComponentView, getBeforeNodeForView, getRenderParent, insertView, removeView} from './node_manipulation';
import {getComponentViewByIndex, getNativeByTNode, isComponent, isLContainer} from './util';
import {addRemoveViewFromContainer, appendChild, detachView, findComponentView, getBeforeNodeForView, insertView, nativeInsertBefore, nativeNextSibling, nativeParentNode, removeView} from './node_manipulation';
import {getComponentViewByIndex, getNativeByTNode, isComponent, isLContainer, isRootView} from './util';
import {ViewRef} from './view_ref';


Expand Down Expand Up @@ -295,12 +295,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++;

// 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, comment, true);
createLContainer(slotValue, hostTNode, hostView, commentNode, true);

appendChild(comment, hostTNode, hostView);
addToViewTree(hostView, hostTNode.index as number, lContainer);
}

Expand Down
2 changes: 2 additions & 0 deletions packages/core/test/render3/integration_spec.ts
Expand Up @@ -2511,6 +2511,8 @@ class MockRenderer implements ProceduralRenderer3 {
selectRootElement(selectorOrNode: string|any): RElement {
return ({} as any);
}
parentNode(node: RNode): RNode|null { return node.parentNode; }
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 {}
Expand Down
33 changes: 28 additions & 5 deletions packages/core/test/render3/render_util.ts
Expand Up @@ -35,17 +35,40 @@ import {Type} from '../../src/type';
import {getRendererFactory2} from './imported_renderer2';

export abstract class BaseFixture {
/**
* Each fixture creates the following initial DOM structure:
* <div fixture="mark">
* <div host="mark"></div>
* </div>
*
* Components are bootstrapped into the <div host="mark"></div>.
* The <div fixture="mark"> 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 <div fixture="mark"> 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() {}
Expand Down Expand Up @@ -250,9 +273,9 @@ export function toHtml<T>(componentOrElement: T | RElement): string {

if (element) {
return stringifyElement(element)
.replace(/^<div host="">/, '')
.replace(/^<div fixture="mark">/, '')
.replace(/<\/div>$/, '')
.replace(/^<div host="">(.*)<\/div>$/, '$1')
.replace(/^<div fixture="mark">(.*)<\/div>$/, '$1')
.replace(/^<div host="mark">(.*)<\/div>$/, '$1')
.replace(' style=""', '')
.replace(/<!--container-->/g, '')
.replace(/<!--ng-container-->/g, '');
Expand Down
49 changes: 49 additions & 0 deletions packages/core/test/render3/view_container_ref_spec.ts
Expand Up @@ -1769,4 +1769,53 @@ 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) {
console.log('Creating text node')
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('<div host="mark"></div>');

fixture.component.insert();
fixture.update();
expect(fixture.outerHtml)
.toBe('<div host="mark"></div><dynamic-cmpt>inserted dynamically</dynamic-cmpt>');

fixture.component.clear();
fixture.update();
expect(fixture.outerHtml).toBe('<div host="mark"></div>');
});
});
});

0 comments on commit e4743d9

Please sign in to comment.