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:
Pawel Kozlowski 2018-04-03 10:18:25 +02:00 committed by Igor Minar
parent 5cd36c7764
commit d80e9304c6
6 changed files with 189 additions and 23 deletions

View File

@ -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;

View File

@ -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
}; };
} }

View File

@ -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;
} }
/** /**

View File

@ -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;
} }
/** /**

View File

@ -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;
} }

View File

@ -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');
});
}); });