Skip to content

Commit c09bd67

Browse files
pkozlowski-opensourcekara
authored andcommitted
fix(ivy): fix views manipulation logic (angular#22656)
This commit fixes a bug that would result in views insert / remove even if a view needed only refresh operation. The crux of the bug was that we were looking for a view to update only in the LContainer.nextIndex position. This is incorrect as a view with a given block id could be present later in the views array (this happens if we about to remove a view in the middle of the views array). The code in this fix searches for a view to update in the views array and can remove views in the middle of the views collection. Previously we would remove views at the end of the collection only. PR Close angular#22656
1 parent 9df13ad commit c09bd67

File tree

4 files changed

+114
-29
lines changed

4 files changed

+114
-29
lines changed

packages/core/src/render3/di.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -582,6 +582,15 @@ class ViewContainerRef implements viewEngine_ViewContainerRef {
582582
const lView = (viewRef as EmbeddedViewRef<any>)._lViewNode;
583583
insertView(this._node, lView, index);
584584

585+
// TODO(pk): this is a temporary index adjustment so imperativelly inserted (through
586+
// ViewContainerRef) views
587+
// are not removed in the containerRefreshEnd instruction.
588+
// The final fix will consist of creating a dedicated container node for views inserted through
589+
// ViewContainerRef.
590+
// Such container should not be trimmed as it is the case in the containerRefreshEnd
591+
// instruction.
592+
this._node.data.nextIndex = this._node.data.views.length;
593+
585594
// If the view is dynamic (has a template), it needs to be counted both at the container
586595
// level and at the node above the container.
587596
if (lView.data.template !== null) {

packages/core/src/render3/instructions.ts

Lines changed: 46 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1204,8 +1204,9 @@ export function containerRefreshEnd(): void {
12041204
container.native = undefined;
12051205
ngDevMode && assertNodeType(container, LNodeFlags.Container);
12061206
const nextIndex = container.data.nextIndex;
1207+
1208+
// remove extra views at the end of the container
12071209
while (nextIndex < container.data.views.length) {
1208-
// remove extra view.
12091210
removeView(container, nextIndex);
12101211
}
12111212
}
@@ -1222,6 +1223,35 @@ function refreshDynamicChildren() {
12221223
}
12231224
}
12241225

1226+
/**
1227+
* Looks for a view with a given view block id inside a provided LContainer.
1228+
* Removes views that need to be deleted in the process.
1229+
*
1230+
* @param containerNode where to search for views
1231+
* @param startIdx starting index in the views array to search from
1232+
* @param viewBlockId exact view block id to look for
1233+
* @returns index of a found view or -1 if not found
1234+
*/
1235+
function scanForView(
1236+
containerNode: LContainerNode, startIdx: number, viewBlockId: number): LViewNode|null {
1237+
const views = containerNode.data.views;
1238+
for (let i = startIdx; i < views.length; i++) {
1239+
const viewAtPositionId = views[i].data.id;
1240+
if (viewAtPositionId === viewBlockId) {
1241+
return views[i];
1242+
} else if (viewAtPositionId < viewBlockId) {
1243+
// found a view that should not be at this position - remove
1244+
removeView(containerNode, i);
1245+
} else {
1246+
// found a view with id grater than the one we are searching for
1247+
// which means that required view doesn't exist and can't be found at
1248+
// later positions in the views array - stop the search here
1249+
break;
1250+
}
1251+
}
1252+
return null;
1253+
}
1254+
12251255
/**
12261256
* Marks the start of an embedded view.
12271257
*
@@ -1233,17 +1263,13 @@ export function embeddedViewStart(viewBlockId: number): boolean {
12331263
(isParent ? previousOrParentNode : previousOrParentNode.parent !) as LContainerNode;
12341264
ngDevMode && assertNodeType(container, LNodeFlags.Container);
12351265
const lContainer = container.data;
1236-
const views = lContainer.views;
1266+
const existingViewNode = scanForView(container, lContainer.nextIndex, viewBlockId);
12371267

1238-
const existingView: LViewNode|false =
1239-
!creationMode && lContainer.nextIndex < views.length && views[lContainer.nextIndex];
1240-
let viewUpdateMode = existingView && viewBlockId === (existingView as LViewNode).data.id;
1241-
1242-
if (viewUpdateMode) {
1243-
previousOrParentNode = views[lContainer.nextIndex++];
1268+
if (existingViewNode) {
1269+
previousOrParentNode = existingViewNode;
12441270
ngDevMode && assertNodeType(previousOrParentNode, LNodeFlags.View);
12451271
isParent = true;
1246-
enterView((existingView as LViewNode).data, previousOrParentNode as LViewNode);
1272+
enterView((existingViewNode as LViewNode).data, existingViewNode as LViewNode);
12471273
} else {
12481274
// When we create a new LView, we always reset the state of the instructions.
12491275
const newView = createLView(
@@ -1254,10 +1280,9 @@ export function embeddedViewStart(viewBlockId: number): boolean {
12541280
}
12551281

12561282
enterView(newView, createLNode(null, LNodeFlags.View, null, newView));
1257-
lContainer.nextIndex++;
12581283
}
12591284

1260-
return !viewUpdateMode;
1285+
return !existingViewNode;
12611286
}
12621287

12631288
/**
@@ -1286,19 +1311,18 @@ export function embeddedViewEnd(): void {
12861311
refreshChildComponents();
12871312
isParent = false;
12881313
const viewNode = previousOrParentNode = currentView.node as LViewNode;
1289-
const container = previousOrParentNode.parent as LContainerNode;
1290-
if (container) {
1314+
const containerNode = previousOrParentNode.parent as LContainerNode;
1315+
if (containerNode) {
12911316
ngDevMode && assertNodeType(viewNode, LNodeFlags.View);
1292-
ngDevMode && assertNodeType(container, LNodeFlags.Container);
1293-
const containerState = container.data;
1294-
const previousView = containerState.nextIndex <= containerState.views.length ?
1295-
containerState.views[containerState.nextIndex - 1] as LViewNode :
1296-
null;
1297-
const viewIdChanged = previousView == null ? true : previousView.data.id !== viewNode.data.id;
1298-
1299-
if (viewIdChanged) {
1300-
insertView(container, viewNode, containerState.nextIndex - 1);
1317+
ngDevMode && assertNodeType(containerNode, LNodeFlags.Container);
1318+
const lContainer = containerNode.data;
1319+
1320+
if (creationMode) {
1321+
// it is a new view, insert it into collection of views for a given container
1322+
insertView(containerNode, viewNode, lContainer.nextIndex);
13011323
}
1324+
1325+
lContainer.nextIndex++;
13021326
}
13031327
leaveView(currentView !.parent !);
13041328
ngDevMode && assertEqual(isParent, false, 'isParent');

packages/core/src/render3/node_manipulation.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -270,18 +270,13 @@ export function insertView(
270270
setViewNext(views[index - 1], newView);
271271
}
272272

273-
if (index < views.length && views[index].data.id !== newView.data.id) {
274-
// View ID change replace the view.
273+
if (index < views.length) {
275274
setViewNext(newView, views[index]);
276275
views.splice(index, 0, newView);
277-
} else if (index >= views.length) {
276+
} else {
278277
views.push(newView);
279278
}
280279

281-
if (state.nextIndex <= index) {
282-
state.nextIndex++;
283-
}
284-
285280
// If the container's renderParent is null, we know that it is a root node of its own parent view
286281
// and we should wait until that parent processes its nodes (otherwise, we will insert this view's
287282
// nodes twice - once now and once when its parent inserts its views).

packages/core/test/render3/control_flow_spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,54 @@ describe('JS control flow', () => {
125125
expect(renderToHtml(Template, ctx)).toEqual('<div><span>Hello</span></div>');
126126
});
127127

128+
it('should work with adjacent if blocks managing views in the same container', () => {
129+
130+
const ctx = {condition1: true, condition2: true, condition3: true};
131+
132+
/**
133+
* % if(ctx.condition1) {
134+
* 1
135+
* % }; if(ctx.condition2) {
136+
* 2
137+
* % }; if(ctx.condition3) {
138+
* 3
139+
* % }
140+
*/
141+
function Template(ctx: any, cm: boolean) {
142+
if (cm) {
143+
container(0);
144+
}
145+
containerRefreshStart(0);
146+
if (ctx.condition1) {
147+
const cm1 = embeddedViewStart(1);
148+
if (cm1) {
149+
text(0, '1');
150+
}
151+
embeddedViewEnd();
152+
} // can't have ; here due linting rules
153+
if (ctx.condition2) {
154+
const cm2 = embeddedViewStart(2);
155+
if (cm2) {
156+
text(0, '2');
157+
}
158+
embeddedViewEnd();
159+
} // can't have ; here due linting rules
160+
if (ctx.condition3) {
161+
const cm3 = embeddedViewStart(3);
162+
if (cm3) {
163+
text(0, '3');
164+
}
165+
embeddedViewEnd();
166+
}
167+
containerRefreshEnd();
168+
}
169+
170+
expect(renderToHtml(Template, ctx)).toEqual('123');
171+
172+
ctx.condition2 = false;
173+
expect(renderToHtml(Template, ctx)).toEqual('13');
174+
});
175+
128176
it('should work with containers with views as parents', () => {
129177
function Template(ctx: any, cm: boolean) {
130178
if (cm) {
@@ -490,6 +538,15 @@ describe('JS for loop', () => {
490538
const ctx: {data1: string[] | null,
491539
data2: number[] | null} = {data1: ['a', 'b', 'c'], data2: [1, 2]};
492540

541+
/**
542+
* <div>
543+
* % for (let i = 0; i < ctx.data1.length; i++) {
544+
* {{data1[i]}}
545+
* % } for (let j = 0; j < ctx.data2.length; j++) {
546+
* {{data1[j]}}
547+
* % }
548+
* </div>
549+
*/
493550
function Template(ctx: any, cm: boolean) {
494551
if (cm) {
495552
elementStart(0, 'div');

0 commit comments

Comments
 (0)