Skip to content

Commit a4915d8

Browse files
committed
fix(core): set current tnode in foreign component instruction on reuse
Previously, the `ɵɵforeignComponent` instruction set the `currentTNode` state during the first template creation pass (via `getOrCreateTNode`), but failed to do so on subsequent instantiations when the `TNode` was accessed from cache. This resulted in the global `currentTNode` state remaining unchanged from the previous instruction. When closing a parent element (e.g., via `ɵɵelementEnd`), this mismatched state caused assertion failures because the framework attempted to close the wrong parent node. This change fixes the issue by calling `setCurrentTNode(tNode, false)` when the foreign component's `TNode` is retrieved from the cache.
1 parent d98cc32 commit a4915d8

2 files changed

Lines changed: 61 additions & 3 deletions

File tree

packages/core/src/render3/instructions/foreign_component.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {createForeignView} from '../foreign_view';
1212
import {TContainerNode, TNodeType} from '../interfaces/node';
1313
import {HEADER_OFFSET, RENDERER} from '../interfaces/view';
1414
import {appendChild} from '../node_manipulation';
15-
import {getLView, getTView, setCurrentTNodeAsNotParent} from '../state';
15+
import {getLView, getTView, setCurrentTNode, setCurrentTNodeAsNotParent} from '../state';
1616
import {getOrCreateTNode} from '../tnode_manipulation';
1717
import {addToEndOfViewTree} from '../view/construction';
1818
import {createLContainer} from '../view/container';
@@ -40,11 +40,12 @@ export function ɵɵforeignComponent(
4040
let tNode: TContainerNode;
4141
if (tView.firstCreatePass) {
4242
tNode = getOrCreateTNode(tView, adjustedIndex, TNodeType.Container, null, null);
43+
// `getOrCreateTNode` unconditionally sets the current node as a parent node, which it is not.
44+
setCurrentTNodeAsNotParent();
4345
} else {
4446
tNode = tView.data[adjustedIndex] as TContainerNode;
47+
setCurrentTNode(tNode, false);
4548
}
46-
// `getOrCreateTNode` unconditionally sets the current node as a parent node, which it is not.
47-
setCurrentTNodeAsNotParent();
4849

4950
// 2. Create the anchor node in the DOM
5051
const renderer = lView[RENDERER];

packages/core/test/render3/foreign_component_spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,9 @@ import {ɵɵelement, ɵɵelementEnd, ɵɵelementStart} from '../../src/render3/i
1414
import {inject, InjectionToken} from '../../src/di';
1515
import {ɵɵdefineDirective} from '../../src/render3/definition';
1616
import {ɵɵProvidersFeature} from '../../src/render3/features/providers_feature';
17+
import {createLView} from '../../src/render3/view/construction';
18+
import {renderView} from '../../src/render3/instructions/render';
19+
import {LView, LViewFlags, PARENT, RENDERER, T_HOST} from '../../src/render3/interfaces/view';
1720

1821
describe('ɵɵforeignComponent', () => {
1922
afterEach(ViewFixture.cleanUp);
@@ -171,4 +174,58 @@ describe('ɵɵforeignComponent', () => {
171174

172175
expect(fixture.host.innerHTML).toContain('<div id="foreign-el">templated-value</div>');
173176
});
177+
178+
it('should support reusing the same template between multiple view instances', () => {
179+
const foreignComp1 = foreignImport(() => {
180+
return [[document.createTextNode('foreign content')]];
181+
});
182+
183+
const createFn = () => {
184+
ɵɵelementStart(0, 'div');
185+
ɵɵforeignComponent(1, foreignComp1);
186+
ɵɵelementEnd();
187+
};
188+
const expectedHtml =
189+
'' +
190+
'<div>' +
191+
'<!--foreign-view-head-->foreign content<!--foreign-view-tail-->' +
192+
'<!--foreign-component-->' +
193+
'</div>';
194+
195+
const fixture = new ViewFixture({
196+
decls: 2,
197+
vars: 0,
198+
create: createFn,
199+
});
200+
expect(fixture.host.innerHTML).toContain(expectedHtml);
201+
202+
// Create second instance reusing the TView
203+
const host2 = renderSecondInstance(fixture);
204+
expect(fixture.host.innerHTML).toContain(expectedHtml);
205+
expect(host2.innerHTML).toContain(expectedHtml);
206+
});
174207
});
208+
209+
function renderSecondInstance(fixture: ViewFixture): HTMLElement {
210+
const hostLView = fixture.lView[PARENT] as LView;
211+
const hostTNode = fixture.lView[T_HOST];
212+
const hostRenderer = hostLView[RENDERER];
213+
const host = hostRenderer.createElement('host-element') as HTMLElement;
214+
215+
const lView = createLView(
216+
hostLView,
217+
fixture.tView,
218+
{},
219+
LViewFlags.CheckAlways,
220+
host,
221+
hostTNode,
222+
null,
223+
null,
224+
null,
225+
null,
226+
null,
227+
);
228+
229+
renderView(fixture.tView, lView, {});
230+
return host;
231+
}

0 commit comments

Comments
 (0)