fix(ivy): properly find RNode (#23193)
As we no longer create native (RNode) comment nodes for containers, we need to execute logic for finding a next sibiling node with RNode when inserting a view. The mentioned logic need to be updated for the case of dynamically created containers (LContainerNode). Indeed, we need to be able to descend into dynamically inserted views while looking for a RNode. To achieve this we need to have a pointer from a host LNode to a dynamically created LContainerNode). PR Close #23193
This commit is contained in:
parent
5cd36c7764
commit
d80e9304c6
|
@ -556,10 +556,12 @@ export function getOrCreateContainerRef(di: LInjector): viewEngine_ViewContainer
|
||||||
|
|
||||||
ngDevMode && assertNodeOfPossibleTypes(vcRefHost, LNodeType.Container, LNodeType.Element);
|
ngDevMode && assertNodeOfPossibleTypes(vcRefHost, LNodeType.Container, LNodeType.Element);
|
||||||
|
|
||||||
const lContainer = createLContainer(vcRefHost.parent !, vcRefHost.view, undefined, vcRefHost);
|
const lContainer = createLContainer(vcRefHost.parent !, vcRefHost.view);
|
||||||
const lContainerNode: LContainerNode = createLNodeObject(
|
const lContainerNode: LContainerNode = createLNodeObject(
|
||||||
LNodeType.Container, vcRefHost.view, vcRefHost.parent !, undefined, lContainer, null);
|
LNodeType.Container, vcRefHost.view, vcRefHost.parent !, undefined, lContainer, null);
|
||||||
|
|
||||||
|
vcRefHost.dynamicLContainerNode = lContainerNode;
|
||||||
|
|
||||||
addToViewTree(vcRefHost.view, lContainer);
|
addToViewTree(vcRefHost.view, lContainer);
|
||||||
|
|
||||||
di.viewContainerRef = new ViewContainerRef(lContainerNode);
|
di.viewContainerRef = new ViewContainerRef(lContainerNode);
|
||||||
|
@ -608,6 +610,10 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
|
||||||
const adjustedIdx = this._adjustAndAssertIndex(index);
|
const adjustedIdx = this._adjustAndAssertIndex(index);
|
||||||
|
|
||||||
insertView(this._lContainerNode, lViewNode, adjustedIdx);
|
insertView(this._lContainerNode, lViewNode, adjustedIdx);
|
||||||
|
// invalidate cache of next sibling RNode (we do similar operation in the containerRefreshEnd
|
||||||
|
// instruction)
|
||||||
|
this._lContainerNode.native = undefined;
|
||||||
|
|
||||||
this._viewRefs.splice(adjustedIdx, 0, viewRef);
|
this._viewRefs.splice(adjustedIdx, 0, viewRef);
|
||||||
|
|
||||||
(lViewNode as{parent: LNode}).parent = this._lContainerNode;
|
(lViewNode as{parent: LNode}).parent = this._lContainerNode;
|
||||||
|
|
|
@ -318,7 +318,8 @@ export function createLNodeObject(
|
||||||
data: state,
|
data: state,
|
||||||
queries: queries,
|
queries: queries,
|
||||||
tNode: null,
|
tNode: null,
|
||||||
pNextOrParent: null
|
pNextOrParent: null,
|
||||||
|
dynamicLContainerNode: null
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -386,6 +387,9 @@ export function createLNode(
|
||||||
previousOrParentNode.next,
|
previousOrParentNode.next,
|
||||||
`previousOrParentNode's next property should not have been set ${index}.`);
|
`previousOrParentNode's next property should not have been set ${index}.`);
|
||||||
previousOrParentNode.next = node;
|
previousOrParentNode.next = node;
|
||||||
|
if (previousOrParentNode.dynamicLContainerNode) {
|
||||||
|
previousOrParentNode.dynamicLContainerNode.next = node;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
previousOrParentNode = node;
|
previousOrParentNode = node;
|
||||||
|
@ -452,9 +456,10 @@ export function renderEmbeddedTemplate<T>(
|
||||||
const directives = currentView && currentView.tView.directiveRegistry;
|
const directives = currentView && currentView.tView.directiveRegistry;
|
||||||
const pipes = currentView && currentView.tView.pipeRegistry;
|
const pipes = currentView && currentView.tView.pipeRegistry;
|
||||||
|
|
||||||
const view = createLView(
|
const tView = getOrCreateTView(template, directives, pipes);
|
||||||
-1, renderer, createTView(directives, pipes), template, context, LViewFlags.CheckAlways);
|
const lView = createLView(-1, renderer, tView, template, context, LViewFlags.CheckAlways);
|
||||||
viewNode = createLNode(null, LNodeType.View, null, view);
|
|
||||||
|
viewNode = createLNode(null, LNodeType.View, null, lView);
|
||||||
cm = true;
|
cm = true;
|
||||||
}
|
}
|
||||||
oldView = enterView(viewNode.data, viewNode);
|
oldView = enterView(viewNode.data, viewNode);
|
||||||
|
@ -1311,8 +1316,7 @@ function generateInitialInputs(
|
||||||
|
|
||||||
|
|
||||||
export function createLContainer(
|
export function createLContainer(
|
||||||
parentLNode: LNode, currentView: LView, template?: ComponentTemplate<any>,
|
parentLNode: LNode, currentView: LView, template?: ComponentTemplate<any>): LContainer {
|
||||||
host?: LContainerNode | LElementNode): LContainer {
|
|
||||||
ngDevMode && assertNotNull(parentLNode, 'containers should have a parent');
|
ngDevMode && assertNotNull(parentLNode, 'containers should have a parent');
|
||||||
return <LContainer>{
|
return <LContainer>{
|
||||||
views: [],
|
views: [],
|
||||||
|
@ -1324,8 +1328,7 @@ export function createLContainer(
|
||||||
next: null,
|
next: null,
|
||||||
parent: currentView,
|
parent: currentView,
|
||||||
dynamicViewCount: 0,
|
dynamicViewCount: 0,
|
||||||
queries: null,
|
queries: null
|
||||||
host: host == null ? null : host
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -80,13 +80,6 @@ export interface LContainer {
|
||||||
* this container are reported to queries referenced here.
|
* this container are reported to queries referenced here.
|
||||||
*/
|
*/
|
||||||
queries: LQueries|null;
|
queries: LQueries|null;
|
||||||
|
|
||||||
/**
|
|
||||||
* If a LContainer is created dynamically (by a directive requesting ViewContainerRef) this fields
|
|
||||||
* keeps a reference to a node on which a ViewContainerRef was requested. We need to store this
|
|
||||||
* information to find a next render sibling node.
|
|
||||||
*/
|
|
||||||
host: LContainerNode|LElementNode|null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -130,6 +130,11 @@ export interface LNode {
|
||||||
* data about this node.
|
* data about this node.
|
||||||
*/
|
*/
|
||||||
tNode: TNode|null;
|
tNode: TNode|null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A pointer to a LContainerNode created by directives requesting ViewContainerRef
|
||||||
|
*/
|
||||||
|
dynamicLContainerNode: LContainerNode|null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -158,6 +163,7 @@ export interface LTextNode extends LNode {
|
||||||
/** LTextNodes can be inside LElementNodes or inside LViewNodes. */
|
/** LTextNodes can be inside LElementNodes or inside LViewNodes. */
|
||||||
readonly parent: LElementNode|LViewNode;
|
readonly parent: LElementNode|LViewNode;
|
||||||
readonly data: null;
|
readonly data: null;
|
||||||
|
dynamicLContainerNode: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Abstract node which contains root nodes of a view. */
|
/** Abstract node which contains root nodes of a view. */
|
||||||
|
@ -169,6 +175,7 @@ export interface LViewNode extends LNode {
|
||||||
/** LViewNodes can only be added to LContainerNodes. */
|
/** LViewNodes can only be added to LContainerNodes. */
|
||||||
readonly parent: LContainerNode|null;
|
readonly parent: LContainerNode|null;
|
||||||
readonly data: LView;
|
readonly data: LView;
|
||||||
|
dynamicLContainerNode: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Abstract node container which contains other views. */
|
/** Abstract node container which contains other views. */
|
||||||
|
@ -199,6 +206,7 @@ export interface LProjectionNode extends LNode {
|
||||||
|
|
||||||
/** Projections can be added to elements or views. */
|
/** Projections can be added to elements or views. */
|
||||||
readonly parent: LElementNode|LViewNode;
|
readonly parent: LElementNode|LViewNode;
|
||||||
|
dynamicLContainerNode: null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -125,8 +125,10 @@ function findFirstRNode(rootNode: LNode): RElement|RText|null {
|
||||||
// A LElementNode has a matching RNode in LElementNode.native
|
// A LElementNode has a matching RNode in LElementNode.native
|
||||||
return (node as LElementNode).native;
|
return (node as LElementNode).native;
|
||||||
} else if (node.type === LNodeType.Container) {
|
} else if (node.type === LNodeType.Container) {
|
||||||
// For container look at the first node of the view next
|
const lContainerNode: LContainerNode = (node as LContainerNode);
|
||||||
const childContainerData: LContainer = (node as LContainerNode).data;
|
const childContainerData: LContainer = lContainerNode.dynamicLContainerNode ?
|
||||||
|
lContainerNode.dynamicLContainerNode.data :
|
||||||
|
lContainerNode.data;
|
||||||
nextNode = childContainerData.views.length ? childContainerData.views[0].child : null;
|
nextNode = childContainerData.views.length ? childContainerData.views[0].child : null;
|
||||||
} else if (node.type === LNodeType.Projection) {
|
} else if (node.type === LNodeType.Projection) {
|
||||||
// For Projection look at the first projected node
|
// For Projection look at the first projected node
|
||||||
|
@ -281,10 +283,7 @@ export function insertView(
|
||||||
if (!beforeNode) {
|
if (!beforeNode) {
|
||||||
let containerNextNativeNode = container.native;
|
let containerNextNativeNode = container.native;
|
||||||
if (containerNextNativeNode === undefined) {
|
if (containerNextNativeNode === undefined) {
|
||||||
// TODO(pk): this is probably too simplistic, add more tests for various host placements
|
containerNextNativeNode = container.native = findNextRNodeSibling(container, null);
|
||||||
// (dynamic view, projection, ...)
|
|
||||||
containerNextNativeNode = container.native =
|
|
||||||
findNextRNodeSibling(container.data.host ? container.data.host : container, null);
|
|
||||||
}
|
}
|
||||||
beforeNode = containerNextNativeNode;
|
beforeNode = containerNextNativeNode;
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,7 @@
|
||||||
import {TemplateRef, ViewContainerRef} from '../../src/core';
|
import {TemplateRef, ViewContainerRef} from '../../src/core';
|
||||||
import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
|
import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
|
||||||
import {defineComponent, defineDirective, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
|
import {defineComponent, defineDirective, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
|
||||||
import {bind, container, elementEnd, elementProperty, elementStart, interpolation1, load, loadDirective, text, textBinding} from '../../src/render3/instructions';
|
import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, text, textBinding} from '../../src/render3/instructions';
|
||||||
|
|
||||||
import {ComponentFixture} from './render_util';
|
import {ComponentFixture} from './render_util';
|
||||||
|
|
||||||
|
@ -190,4 +190,161 @@ describe('ViewContainerRef', () => {
|
||||||
expect(fixture.html).toEqual('before||after');
|
expect(fixture.html).toEqual('before||after');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should add embedded views at the right position in the DOM tree (ng-template next to other ng-template)',
|
||||||
|
() => {
|
||||||
|
let directiveInstances: TestDirective[] = [];
|
||||||
|
|
||||||
|
class TestDirective {
|
||||||
|
static ngDirectiveDef = defineDirective({
|
||||||
|
type: TestDirective,
|
||||||
|
selectors: [['', 'testdir', '']],
|
||||||
|
factory: () => {
|
||||||
|
const instance = new TestDirective(injectViewContainerRef(), injectTemplateRef());
|
||||||
|
|
||||||
|
directiveInstances.push(instance);
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<{}>) {}
|
||||||
|
|
||||||
|
insertTpl(ctx: {}) { this._vcRef.createEmbeddedView(this._tplRef, ctx); }
|
||||||
|
|
||||||
|
remove(index?: number) { this._vcRef.remove(index); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmbeddedTemplateA(ctx: any, cm: boolean) {
|
||||||
|
if (cm) {
|
||||||
|
text(0, 'A');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmbeddedTemplateB(ctx: any, cm: boolean) {
|
||||||
|
if (cm) {
|
||||||
|
text(0, 'B');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* before|
|
||||||
|
* <ng-template directive>A<ng-template>
|
||||||
|
* <ng-template directive>B<ng-template>
|
||||||
|
* |after
|
||||||
|
*/
|
||||||
|
class TestComponent {
|
||||||
|
testDir: TestDirective;
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: TestComponent,
|
||||||
|
selectors: [['test-cmp']],
|
||||||
|
factory: () => new TestComponent(),
|
||||||
|
template: (cmp: TestComponent, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
text(0, 'before|');
|
||||||
|
container(1, EmbeddedTemplateA, undefined, ['testdir', '']);
|
||||||
|
container(2, EmbeddedTemplateB, undefined, ['testdir', '']);
|
||||||
|
text(3, '|after');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
directives: [TestDirective]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(TestComponent);
|
||||||
|
expect(fixture.html).toEqual('before||after');
|
||||||
|
|
||||||
|
directiveInstances ![1].insertTpl({});
|
||||||
|
expect(fixture.html).toEqual('before|B|after');
|
||||||
|
|
||||||
|
directiveInstances ![0].insertTpl({});
|
||||||
|
expect(fixture.html).toEqual('before|AB|after');
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
it('should add embedded views at the right position in the DOM tree (ng-template next to a JS block)',
|
||||||
|
() => {
|
||||||
|
let directiveInstance: TestDirective;
|
||||||
|
|
||||||
|
class TestDirective {
|
||||||
|
static ngDirectiveDef = defineDirective({
|
||||||
|
type: TestDirective,
|
||||||
|
selectors: [['', 'testdir', '']],
|
||||||
|
factory: () => directiveInstance =
|
||||||
|
new TestDirective(injectViewContainerRef(), injectTemplateRef())
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<{}>) {}
|
||||||
|
|
||||||
|
insertTpl(ctx: {}) { this._vcRef.createEmbeddedView(this._tplRef, ctx); }
|
||||||
|
|
||||||
|
remove(index?: number) { this._vcRef.remove(index); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function EmbeddedTemplateA(ctx: any, cm: boolean) {
|
||||||
|
if (cm) {
|
||||||
|
text(0, 'A');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* before|
|
||||||
|
* <ng-template directive>A<ng-template>
|
||||||
|
* % if (condition) {
|
||||||
|
* B
|
||||||
|
* }
|
||||||
|
* |after
|
||||||
|
*/
|
||||||
|
class TestComponent {
|
||||||
|
condition = false;
|
||||||
|
testDir: TestDirective;
|
||||||
|
static ngComponentDef = defineComponent({
|
||||||
|
type: TestComponent,
|
||||||
|
selectors: [['test-cmp']],
|
||||||
|
factory: () => new TestComponent(),
|
||||||
|
template: (cmp: TestComponent, cm: boolean) => {
|
||||||
|
if (cm) {
|
||||||
|
text(0, 'before|');
|
||||||
|
container(1, EmbeddedTemplateA, undefined, ['testdir', '']);
|
||||||
|
container(2);
|
||||||
|
text(3, '|after');
|
||||||
|
}
|
||||||
|
containerRefreshStart(2);
|
||||||
|
{
|
||||||
|
if (cmp.condition) {
|
||||||
|
let cm1 = embeddedViewStart(0);
|
||||||
|
{
|
||||||
|
if (cm1) {
|
||||||
|
text(0, 'B');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
embeddedViewEnd();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
containerRefreshEnd();
|
||||||
|
},
|
||||||
|
directives: [TestDirective]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const fixture = new ComponentFixture(TestComponent);
|
||||||
|
expect(fixture.html).toEqual('before||after');
|
||||||
|
|
||||||
|
fixture.component.condition = true;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('before|B|after');
|
||||||
|
|
||||||
|
directiveInstance !.insertTpl({});
|
||||||
|
expect(fixture.html).toEqual('before|AB|after');
|
||||||
|
|
||||||
|
fixture.component.condition = false;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('before|A|after');
|
||||||
|
|
||||||
|
directiveInstance !.insertTpl({});
|
||||||
|
expect(fixture.html).toEqual('before|AA|after');
|
||||||
|
|
||||||
|
fixture.component.condition = true;
|
||||||
|
fixture.update();
|
||||||
|
expect(fixture.html).toEqual('before|AAB|after');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue