Skip to content

Commit d80e930

Browse files
pkozlowski-opensourceIgorMinar
authored andcommitted
fix(ivy): properly find RNode (angular#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 angular#23193
1 parent 5cd36c7 commit d80e930

File tree

6 files changed

+189
-23
lines changed

6 files changed

+189
-23
lines changed

packages/core/src/render3/di.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -556,10 +556,12 @@ export function getOrCreateContainerRef(di: LInjector): viewEngine_ViewContainer
556556

557557
ngDevMode && assertNodeOfPossibleTypes(vcRefHost, LNodeType.Container, LNodeType.Element);
558558

559-
const lContainer = createLContainer(vcRefHost.parent !, vcRefHost.view, undefined, vcRefHost);
559+
const lContainer = createLContainer(vcRefHost.parent !, vcRefHost.view);
560560
const lContainerNode: LContainerNode = createLNodeObject(
561561
LNodeType.Container, vcRefHost.view, vcRefHost.parent !, undefined, lContainer, null);
562562

563+
vcRefHost.dynamicLContainerNode = lContainerNode;
564+
563565
addToViewTree(vcRefHost.view, lContainer);
564566

565567
di.viewContainerRef = new ViewContainerRef(lContainerNode);
@@ -608,6 +610,10 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
608610
const adjustedIdx = this._adjustAndAssertIndex(index);
609611

610612
insertView(this._lContainerNode, lViewNode, adjustedIdx);
613+
// invalidate cache of next sibling RNode (we do similar operation in the containerRefreshEnd
614+
// instruction)
615+
this._lContainerNode.native = undefined;
616+
611617
this._viewRefs.splice(adjustedIdx, 0, viewRef);
612618

613619
(lViewNode as{parent: LNode}).parent = this._lContainerNode;

packages/core/src/render3/instructions.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,8 @@ export function createLNodeObject(
318318
data: state,
319319
queries: queries,
320320
tNode: null,
321-
pNextOrParent: null
321+
pNextOrParent: null,
322+
dynamicLContainerNode: null
322323
};
323324
}
324325

@@ -386,6 +387,9 @@ export function createLNode(
386387
previousOrParentNode.next,
387388
`previousOrParentNode's next property should not have been set ${index}.`);
388389
previousOrParentNode.next = node;
390+
if (previousOrParentNode.dynamicLContainerNode) {
391+
previousOrParentNode.dynamicLContainerNode.next = node;
392+
}
389393
}
390394
}
391395
previousOrParentNode = node;
@@ -452,9 +456,10 @@ export function renderEmbeddedTemplate<T>(
452456
const directives = currentView && currentView.tView.directiveRegistry;
453457
const pipes = currentView && currentView.tView.pipeRegistry;
454458

455-
const view = createLView(
456-
-1, renderer, createTView(directives, pipes), template, context, LViewFlags.CheckAlways);
457-
viewNode = createLNode(null, LNodeType.View, null, view);
459+
const tView = getOrCreateTView(template, directives, pipes);
460+
const lView = createLView(-1, renderer, tView, template, context, LViewFlags.CheckAlways);
461+
462+
viewNode = createLNode(null, LNodeType.View, null, lView);
458463
cm = true;
459464
}
460465
oldView = enterView(viewNode.data, viewNode);
@@ -1311,8 +1316,7 @@ function generateInitialInputs(
13111316

13121317

13131318
export function createLContainer(
1314-
parentLNode: LNode, currentView: LView, template?: ComponentTemplate<any>,
1315-
host?: LContainerNode | LElementNode): LContainer {
1319+
parentLNode: LNode, currentView: LView, template?: ComponentTemplate<any>): LContainer {
13161320
ngDevMode && assertNotNull(parentLNode, 'containers should have a parent');
13171321
return <LContainer>{
13181322
views: [],
@@ -1324,8 +1328,7 @@ export function createLContainer(
13241328
next: null,
13251329
parent: currentView,
13261330
dynamicViewCount: 0,
1327-
queries: null,
1328-
host: host == null ? null : host
1331+
queries: null
13291332
};
13301333
}
13311334

packages/core/src/render3/interfaces/container.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,6 @@ export interface LContainer {
8080
* this container are reported to queries referenced here.
8181
*/
8282
queries: LQueries|null;
83-
84-
/**
85-
* If a LContainer is created dynamically (by a directive requesting ViewContainerRef) this fields
86-
* keeps a reference to a node on which a ViewContainerRef was requested. We need to store this
87-
* information to find a next render sibling node.
88-
*/
89-
host: LContainerNode|LElementNode|null;
9083
}
9184

9285
/**

packages/core/src/render3/interfaces/node.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,11 @@ export interface LNode {
130130
* data about this node.
131131
*/
132132
tNode: TNode|null;
133+
134+
/**
135+
* A pointer to a LContainerNode created by directives requesting ViewContainerRef
136+
*/
137+
dynamicLContainerNode: LContainerNode|null;
133138
}
134139

135140

@@ -158,6 +163,7 @@ export interface LTextNode extends LNode {
158163
/** LTextNodes can be inside LElementNodes or inside LViewNodes. */
159164
readonly parent: LElementNode|LViewNode;
160165
readonly data: null;
166+
dynamicLContainerNode: null;
161167
}
162168

163169
/** Abstract node which contains root nodes of a view. */
@@ -169,6 +175,7 @@ export interface LViewNode extends LNode {
169175
/** LViewNodes can only be added to LContainerNodes. */
170176
readonly parent: LContainerNode|null;
171177
readonly data: LView;
178+
dynamicLContainerNode: null;
172179
}
173180

174181
/** Abstract node container which contains other views. */
@@ -199,6 +206,7 @@ export interface LProjectionNode extends LNode {
199206

200207
/** Projections can be added to elements or views. */
201208
readonly parent: LElementNode|LViewNode;
209+
dynamicLContainerNode: null;
202210
}
203211

204212
/**

packages/core/src/render3/node_manipulation.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -125,8 +125,10 @@ function findFirstRNode(rootNode: LNode): RElement|RText|null {
125125
// A LElementNode has a matching RNode in LElementNode.native
126126
return (node as LElementNode).native;
127127
} else if (node.type === LNodeType.Container) {
128-
// For container look at the first node of the view next
129-
const childContainerData: LContainer = (node as LContainerNode).data;
128+
const lContainerNode: LContainerNode = (node as LContainerNode);
129+
const childContainerData: LContainer = lContainerNode.dynamicLContainerNode ?
130+
lContainerNode.dynamicLContainerNode.data :
131+
lContainerNode.data;
130132
nextNode = childContainerData.views.length ? childContainerData.views[0].child : null;
131133
} else if (node.type === LNodeType.Projection) {
132134
// For Projection look at the first projected node
@@ -281,10 +283,7 @@ export function insertView(
281283
if (!beforeNode) {
282284
let containerNextNativeNode = container.native;
283285
if (containerNextNativeNode === undefined) {
284-
// TODO(pk): this is probably too simplistic, add more tests for various host placements
285-
// (dynamic view, projection, ...)
286-
containerNextNativeNode = container.native =
287-
findNextRNodeSibling(container.data.host ? container.data.host : container, null);
286+
containerNextNativeNode = container.native = findNextRNodeSibling(container, null);
288287
}
289288
beforeNode = containerNextNativeNode;
290289
}

packages/core/test/render3/view_container_ref_spec.ts

Lines changed: 158 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import {TemplateRef, ViewContainerRef} from '../../src/core';
1010
import {getOrCreateNodeInjectorForNode, getOrCreateTemplateRef} from '../../src/render3/di';
1111
import {defineComponent, defineDirective, injectTemplateRef, injectViewContainerRef} from '../../src/render3/index';
12-
import {bind, container, elementEnd, elementProperty, elementStart, interpolation1, load, loadDirective, text, textBinding} from '../../src/render3/instructions';
12+
import {bind, container, containerRefreshEnd, containerRefreshStart, elementEnd, elementProperty, elementStart, embeddedViewEnd, embeddedViewStart, interpolation1, load, loadDirective, text, textBinding} from '../../src/render3/instructions';
1313

1414
import {ComponentFixture} from './render_util';
1515

@@ -190,4 +190,161 @@ describe('ViewContainerRef', () => {
190190
expect(fixture.html).toEqual('before||after');
191191
});
192192

193+
it('should add embedded views at the right position in the DOM tree (ng-template next to other ng-template)',
194+
() => {
195+
let directiveInstances: TestDirective[] = [];
196+
197+
class TestDirective {
198+
static ngDirectiveDef = defineDirective({
199+
type: TestDirective,
200+
selectors: [['', 'testdir', '']],
201+
factory: () => {
202+
const instance = new TestDirective(injectViewContainerRef(), injectTemplateRef());
203+
204+
directiveInstances.push(instance);
205+
206+
return instance;
207+
}
208+
});
209+
210+
constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<{}>) {}
211+
212+
insertTpl(ctx: {}) { this._vcRef.createEmbeddedView(this._tplRef, ctx); }
213+
214+
remove(index?: number) { this._vcRef.remove(index); }
215+
}
216+
217+
function EmbeddedTemplateA(ctx: any, cm: boolean) {
218+
if (cm) {
219+
text(0, 'A');
220+
}
221+
}
222+
223+
function EmbeddedTemplateB(ctx: any, cm: boolean) {
224+
if (cm) {
225+
text(0, 'B');
226+
}
227+
}
228+
229+
/**
230+
* before|
231+
* <ng-template directive>A<ng-template>
232+
* <ng-template directive>B<ng-template>
233+
* |after
234+
*/
235+
class TestComponent {
236+
testDir: TestDirective;
237+
static ngComponentDef = defineComponent({
238+
type: TestComponent,
239+
selectors: [['test-cmp']],
240+
factory: () => new TestComponent(),
241+
template: (cmp: TestComponent, cm: boolean) => {
242+
if (cm) {
243+
text(0, 'before|');
244+
container(1, EmbeddedTemplateA, undefined, ['testdir', '']);
245+
container(2, EmbeddedTemplateB, undefined, ['testdir', '']);
246+
text(3, '|after');
247+
}
248+
},
249+
directives: [TestDirective]
250+
});
251+
}
252+
253+
const fixture = new ComponentFixture(TestComponent);
254+
expect(fixture.html).toEqual('before||after');
255+
256+
directiveInstances ![1].insertTpl({});
257+
expect(fixture.html).toEqual('before|B|after');
258+
259+
directiveInstances ![0].insertTpl({});
260+
expect(fixture.html).toEqual('before|AB|after');
261+
});
262+
263+
264+
it('should add embedded views at the right position in the DOM tree (ng-template next to a JS block)',
265+
() => {
266+
let directiveInstance: TestDirective;
267+
268+
class TestDirective {
269+
static ngDirectiveDef = defineDirective({
270+
type: TestDirective,
271+
selectors: [['', 'testdir', '']],
272+
factory: () => directiveInstance =
273+
new TestDirective(injectViewContainerRef(), injectTemplateRef())
274+
});
275+
276+
constructor(private _vcRef: ViewContainerRef, private _tplRef: TemplateRef<{}>) {}
277+
278+
insertTpl(ctx: {}) { this._vcRef.createEmbeddedView(this._tplRef, ctx); }
279+
280+
remove(index?: number) { this._vcRef.remove(index); }
281+
}
282+
283+
function EmbeddedTemplateA(ctx: any, cm: boolean) {
284+
if (cm) {
285+
text(0, 'A');
286+
}
287+
}
288+
289+
/**
290+
* before|
291+
* <ng-template directive>A<ng-template>
292+
* % if (condition) {
293+
* B
294+
* }
295+
* |after
296+
*/
297+
class TestComponent {
298+
condition = false;
299+
testDir: TestDirective;
300+
static ngComponentDef = defineComponent({
301+
type: TestComponent,
302+
selectors: [['test-cmp']],
303+
factory: () => new TestComponent(),
304+
template: (cmp: TestComponent, cm: boolean) => {
305+
if (cm) {
306+
text(0, 'before|');
307+
container(1, EmbeddedTemplateA, undefined, ['testdir', '']);
308+
container(2);
309+
text(3, '|after');
310+
}
311+
containerRefreshStart(2);
312+
{
313+
if (cmp.condition) {
314+
let cm1 = embeddedViewStart(0);
315+
{
316+
if (cm1) {
317+
text(0, 'B');
318+
}
319+
}
320+
embeddedViewEnd();
321+
}
322+
}
323+
containerRefreshEnd();
324+
},
325+
directives: [TestDirective]
326+
});
327+
}
328+
329+
const fixture = new ComponentFixture(TestComponent);
330+
expect(fixture.html).toEqual('before||after');
331+
332+
fixture.component.condition = true;
333+
fixture.update();
334+
expect(fixture.html).toEqual('before|B|after');
335+
336+
directiveInstance !.insertTpl({});
337+
expect(fixture.html).toEqual('before|AB|after');
338+
339+
fixture.component.condition = false;
340+
fixture.update();
341+
expect(fixture.html).toEqual('before|A|after');
342+
343+
directiveInstance !.insertTpl({});
344+
expect(fixture.html).toEqual('before|AA|after');
345+
346+
fixture.component.condition = true;
347+
fixture.update();
348+
expect(fixture.html).toEqual('before|AAB|after');
349+
});
193350
});

0 commit comments

Comments
 (0)