fix(ivy): allow root components to inject ViewContainerRef (#26682)
PR Close #26682
This commit is contained in:
parent
578e4c7642
commit
ede65dbede
|
@ -71,6 +71,9 @@ export interface ProceduralRenderer3 {
|
||||||
removeChild(parent: RElement, oldChild: RNode): void;
|
removeChild(parent: RElement, oldChild: RNode): void;
|
||||||
selectRootElement(selectorOrNode: string|any): RElement;
|
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;
|
setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void;
|
||||||
removeAttribute(el: RElement, name: string, namespace?: string|null): void;
|
removeAttribute(el: RElement, name: string, namespace?: string|null): void;
|
||||||
addClass(el: RElement, name: string): 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. */
|
/** Subset of API needed for appending elements and text nodes. */
|
||||||
export interface RNode {
|
export interface RNode {
|
||||||
|
parentNode: RNode|null;
|
||||||
|
|
||||||
|
nextSibling: RNode|null;
|
||||||
|
|
||||||
removeChild(oldChild: RNode): void;
|
removeChild(oldChild: RNode): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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 {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 {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 {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;
|
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 {
|
export function getRenderParent(tNode: TNode, currentView: LViewData): RElement|null {
|
||||||
if (canInsertNativeNode(tNode, currentView)) {
|
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 hostTNode = currentView[HOST_NODE];
|
||||||
|
|
||||||
const tNodeParent = tNode.parent;
|
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
|
* This is a utility function that can be used when native nodes were determined - it abstracts an
|
||||||
* actual renderer being used.
|
* actual renderer being used.
|
||||||
*/
|
*/
|
||||||
function nativeInsertBefore(
|
export function nativeInsertBefore(
|
||||||
renderer: Renderer3, parent: RElement, child: RNode, beforeNode: RNode | null): void {
|
renderer: Renderer3, parent: RElement, child: RNode, beforeNode: RNode | null): void {
|
||||||
if (isProceduralRenderer(renderer)) {
|
if (isProceduralRenderer(renderer)) {
|
||||||
renderer.insertBefore(parent, child, beforeNode);
|
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`.
|
* Appends the `child` element to the `parent`.
|
||||||
*
|
*
|
||||||
|
@ -632,7 +653,7 @@ export function appendChild(
|
||||||
renderer, lContainer[RENDER_PARENT] !, childEl,
|
renderer, lContainer[RENDER_PARENT] !, childEl,
|
||||||
getBeforeNodeForView(index, views, lContainer[NATIVE]));
|
getBeforeNodeForView(index, views, lContainer[NATIVE]));
|
||||||
} else if (parentTNode.type === TNodeType.ElementContainer) {
|
} else if (parentTNode.type === TNodeType.ElementContainer) {
|
||||||
const renderParent: RElement = getRenderParent(childTNode, currentView) !;
|
const renderParent = getRenderParent(childTNode, currentView) !;
|
||||||
nativeInsertBefore(renderer, renderParent, childEl, parentEl);
|
nativeInsertBefore(renderer, renderParent, childEl, parentEl);
|
||||||
} else {
|
} else {
|
||||||
isProceduralRenderer(renderer) ? renderer.appendChild(parentEl !as RElement, childEl) :
|
isProceduralRenderer(renderer) ? renderer.appendChild(parentEl !as RElement, childEl) :
|
||||||
|
|
|
@ -134,6 +134,10 @@ export function isLContainer(value: RElement | RComment | LContainer | StylingCo
|
||||||
return Array.isArray(value) && typeof value[ACTIVE_INDEX] === 'number';
|
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
|
* Retrieve the root view from any component by walking the parent `LViewData` until
|
||||||
* reaching the root `LViewData`.
|
* reaching the root `LViewData`.
|
||||||
|
|
|
@ -26,9 +26,9 @@ import {LQueries} from './interfaces/query';
|
||||||
import {RComment, RElement, Renderer3, isProceduralRenderer} from './interfaces/renderer';
|
import {RComment, RElement, Renderer3, isProceduralRenderer} from './interfaces/renderer';
|
||||||
import {CONTEXT, HOST_NODE, LViewData, QUERIES, RENDERER, TView} from './interfaces/view';
|
import {CONTEXT, HOST_NODE, LViewData, QUERIES, RENDERER, TView} from './interfaces/view';
|
||||||
import {assertNodeOfPossibleTypes, assertNodeType} from './node_assert';
|
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 {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';
|
import {ViewRef} from './view_ref';
|
||||||
|
|
||||||
|
|
||||||
|
@ -307,12 +307,26 @@ export function createContainerRef(
|
||||||
lContainer = slotValue;
|
lContainer = slotValue;
|
||||||
lContainer[ACTIVE_INDEX] = -1;
|
lContainer[ACTIVE_INDEX] = -1;
|
||||||
} else {
|
} else {
|
||||||
const comment = hostView[RENDERER].createComment(ngDevMode ? 'container' : '');
|
const commentNode = hostView[RENDERER].createComment(ngDevMode ? 'container' : '');
|
||||||
ngDevMode && ngDevMode.rendererCreateComment++;
|
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);
|
addToViewTree(hostView, hostTNode.index as number, lContainer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -872,6 +872,9 @@
|
||||||
{
|
{
|
||||||
"name": "isProceduralRenderer"
|
"name": "isProceduralRenderer"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "isRootView"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "isSanitizable"
|
"name": "isSanitizable"
|
||||||
},
|
},
|
||||||
|
@ -920,6 +923,12 @@
|
||||||
{
|
{
|
||||||
"name": "nativeInsertBefore"
|
"name": "nativeInsertBefore"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "nativeNextSibling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nativeParentNode"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nextContext"
|
"name": "nextContext"
|
||||||
},
|
},
|
||||||
|
|
|
@ -341,6 +341,9 @@
|
||||||
{
|
{
|
||||||
"name": "isProceduralRenderer"
|
"name": "isProceduralRenderer"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "isRootView"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "leaveView"
|
"name": "leaveView"
|
||||||
},
|
},
|
||||||
|
@ -353,6 +356,9 @@
|
||||||
{
|
{
|
||||||
"name": "nativeInsertBefore"
|
"name": "nativeInsertBefore"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "nativeParentNode"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nextNgElementId"
|
"name": "nextNgElementId"
|
||||||
},
|
},
|
||||||
|
|
|
@ -3803,6 +3803,9 @@
|
||||||
{
|
{
|
||||||
"name": "isQuote"
|
"name": "isQuote"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "isRootView"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "isScheduler"
|
"name": "isScheduler"
|
||||||
},
|
},
|
||||||
|
@ -3956,6 +3959,12 @@
|
||||||
{
|
{
|
||||||
"name": "nativeInsertBefore"
|
"name": "nativeInsertBefore"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "nativeNextSibling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nativeParentNode"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "needsAdditionalRootNode"
|
"name": "needsAdditionalRootNode"
|
||||||
},
|
},
|
||||||
|
|
|
@ -893,6 +893,9 @@
|
||||||
{
|
{
|
||||||
"name": "isProceduralRenderer"
|
"name": "isProceduralRenderer"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "isRootView"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "isStylingContext"
|
"name": "isStylingContext"
|
||||||
},
|
},
|
||||||
|
@ -938,6 +941,12 @@
|
||||||
{
|
{
|
||||||
"name": "nativeInsertBefore"
|
"name": "nativeInsertBefore"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "nativeNextSibling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nativeParentNode"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nextContext"
|
"name": "nextContext"
|
||||||
},
|
},
|
||||||
|
|
|
@ -2123,6 +2123,9 @@
|
||||||
{
|
{
|
||||||
"name": "isPromise$2"
|
"name": "isPromise$2"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "isRootView"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "isScheduler"
|
"name": "isScheduler"
|
||||||
},
|
},
|
||||||
|
@ -2219,6 +2222,12 @@
|
||||||
{
|
{
|
||||||
"name": "nativeInsertBefore"
|
"name": "nativeInsertBefore"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "nativeNextSibling"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "nativeParentNode"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nextContext"
|
"name": "nextContext"
|
||||||
},
|
},
|
||||||
|
|
|
@ -2677,6 +2677,8 @@ class MockRenderer implements ProceduralRenderer3 {
|
||||||
selectRootElement(selectorOrNode: string|any): RElement {
|
selectRootElement(selectorOrNode: string|any): RElement {
|
||||||
return ({} as any);
|
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 {}
|
setAttribute(el: RElement, name: string, value: string, namespace?: string|null): void {}
|
||||||
removeAttribute(el: RElement, name: string, namespace?: string|null): void {}
|
removeAttribute(el: RElement, name: string, namespace?: string|null): void {}
|
||||||
addClass(el: RElement, name: string): void {}
|
addClass(el: RElement, name: string): void {}
|
||||||
|
|
|
@ -36,17 +36,40 @@ import {Type} from '../../src/type';
|
||||||
import {getRendererFactory2} from './imported_renderer2';
|
import {getRendererFactory2} from './imported_renderer2';
|
||||||
|
|
||||||
export abstract class BaseFixture {
|
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;
|
hostElement: HTMLElement;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
|
this.containerElement = document.createElement('div');
|
||||||
|
this.containerElement.setAttribute('fixture', 'mark');
|
||||||
this.hostElement = document.createElement('div');
|
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); }
|
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() {}
|
function noop() {}
|
||||||
|
@ -251,9 +274,9 @@ export function toHtml<T>(componentOrElement: T | RElement): string {
|
||||||
|
|
||||||
if (element) {
|
if (element) {
|
||||||
return stringifyElement(element)
|
return stringifyElement(element)
|
||||||
.replace(/^<div host="">/, '')
|
.replace(/^<div host="">(.*)<\/div>$/, '$1')
|
||||||
.replace(/^<div fixture="mark">/, '')
|
.replace(/^<div fixture="mark">(.*)<\/div>$/, '$1')
|
||||||
.replace(/<\/div>$/, '')
|
.replace(/^<div host="mark">(.*)<\/div>$/, '$1')
|
||||||
.replace(' style=""', '')
|
.replace(' style=""', '')
|
||||||
.replace(/<!--container-->/g, '')
|
.replace(/<!--container-->/g, '')
|
||||||
.replace(/<!--ng-container-->/g, '');
|
.replace(/<!--ng-container-->/g, '');
|
||||||
|
|
|
@ -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('<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>');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue